1use anyhow::{Result, bail};
6use parking_lot::Mutex;
7use rusqlite::{Connection, params};
8use std::{path::Path, sync::Arc};
9
10use freezeout_core::{crypto::PeerId, poker::Chips};
11
12#[derive(Debug)]
14pub struct Player {
15 pub player_id: PeerId,
17 pub nickname: String,
19 pub chips: Chips,
21}
22
23#[derive(Debug, Clone)]
25pub struct Db {
26 conn: Arc<Mutex<Connection>>,
27}
28
29impl Db {
30 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
32 let conn = Connection::open(path)?;
33
34 Self::init_database(&conn)?;
35
36 Ok(Db {
37 conn: Arc::new(Mutex::new(conn)),
38 })
39 }
40
41 pub fn open_in_memory() -> Result<Self> {
43 let conn = Connection::open_in_memory()?;
44
45 Self::init_database(&conn)?;
46
47 Ok(Db {
48 conn: Arc::new(Mutex::new(conn)),
49 })
50 }
51
52 fn init_database(conn: &Connection) -> Result<()> {
53 conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")?;
54
55 conn.execute(
57 "CREATE TABLE IF NOT EXISTS players (
58 id TEXT PRIMARY KEY,
59 nickname TEXT NOT NULL,
60 chips INTEGER NOT NULL,
61 created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
62 last_update DATETIME DEFAULT CURRENT_TIMESTAMP
63 )",
64 (),
65 )?;
66
67 Ok(())
68 }
69
70 pub async fn join_server(
76 &self,
77 player_id: PeerId,
78 nickname: &str,
79 join_chips: Chips,
80 ) -> Result<Player> {
81 let conn = self.conn.clone();
82 let nickname = nickname.to_string();
83
84 tokio::task::spawn_blocking(move || {
85 let conn = conn.lock();
86
87 let mut stmt = conn.prepare(
88 "SELECT id, nickname, chips
89 FROM players
90 WHERE id = ?1",
91 )?;
92
93 let res = stmt.query_row(params![player_id.digits()], |row| {
94 Ok(Player {
95 player_id: player_id.clone(),
96 nickname: row.get(1)?,
97 chips: Chips::from(row.get::<usize, i32>(2)? as u32),
98 })
99 });
100
101 match res {
102 Ok(mut player) => {
103 let mut do_update = false;
104
105 if player.chips < join_chips {
107 player.chips = join_chips;
108 do_update = true;
109 }
110
111 if player.nickname != nickname {
113 player.nickname = nickname.to_string();
114 do_update = true;
115 }
116
117 if do_update {
118 conn.execute(
119 "UPDATE players SET
120 chips = ?2,
121 nickname = ?3,
122 last_update = CURRENT_TIMESTAMP
123 WHERE id = ?1",
124 params![
125 player.player_id.digits(),
126 player.chips.amount(),
127 player.nickname
128 ],
129 )?;
130 }
131
132 Ok(player)
133 }
134 Err(rusqlite::Error::QueryReturnedNoRows) => {
135 let player = Player {
137 player_id,
138 nickname: nickname.to_string(),
139 chips: join_chips,
140 };
141
142 conn.execute(
143 "INSERT INTO players (id, nickname, chips, last_update)
144 VALUES (?1, ?2, ?3, CURRENT_TIMESTAMP)",
145 params![player.player_id.digits(), nickname, player.chips.amount()],
146 )?;
147
148 Ok(player)
149 }
150 Err(e) => Err(e.into()),
151 }
152 })
153 .await?
154 }
155
156 pub async fn pay_from_player(&self, player_id: PeerId, amount: Chips) -> Result<bool> {
161 let conn = self.conn.clone();
162
163 tokio::task::spawn_blocking(move || {
164 let conn = conn.lock();
165
166 let mut stmt = conn.prepare("SELECT chips FROM players WHERE id = ?1")?;
167 let res = stmt.query_row(params![player_id.digits()], |row| {
168 Ok(Chips::from(row.get::<usize, i32>(0)? as u32))
169 });
170
171 match res {
172 Ok(chips) => {
173 if chips < amount {
174 return Ok(false);
175 }
176
177 let remaining_chips = chips - amount;
178
179 conn.execute(
181 "UPDATE players SET
182 chips = ?2,
183 last_update = CURRENT_TIMESTAMP
184 WHERE id = ?1",
185 params![player_id.digits(), remaining_chips.amount(),],
186 )?;
187
188 Ok(true)
189 }
190 Err(e) => Err(e.into()),
191 }
192 })
193 .await?
194 }
195
196 pub async fn pay_to_player(&self, player_id: PeerId, amount: Chips) -> Result<()> {
200 let conn = self.conn.clone();
201
202 tokio::task::spawn_blocking(move || {
203 let conn = conn.lock();
204
205 let num_rows = conn.execute(
206 "UPDATE players SET
207 chips = chips + ?2,
208 last_update = CURRENT_TIMESTAMP
209 WHERE id = ?1",
210 params![player_id.digits(), amount.amount(),],
211 )?;
212
213 if num_rows == 0 {
214 bail!("Player {player_id} not found");
215 } else {
216 Ok(())
217 }
218 })
219 .await?
220 }
221
222 pub async fn get_player(&self, player_id: PeerId) -> Result<Player> {
224 let conn = self.conn.clone();
225
226 tokio::task::spawn_blocking(move || {
227 let conn = conn.lock();
228
229 let mut stmt = conn.prepare(
230 "SELECT id, nickname, chips
231 FROM players
232 WHERE id = ?1",
233 )?;
234
235 stmt.query_row(params![player_id.digits()], |row| {
236 Ok(Player {
237 player_id: player_id.clone(),
238 nickname: row.get(1)?,
239 chips: Chips::from(row.get::<usize, i32>(2)? as u32),
240 })
241 })
242 .map_err(anyhow::Error::from)
243 })
244 .await?
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use freezeout_core::crypto::SigningKey;
252
253 #[tokio::test]
254 async fn join_server() {
255 const JOIN_CHIPS: Chips = Chips::new(1_000_000);
256 const NICKNAME: &str = "alice";
257
258 let db = Db::open_in_memory().unwrap();
259 let player_id = SigningKey::default().verifying_key().peer_id();
260
261 let player = db
263 .join_server(player_id.clone(), NICKNAME, JOIN_CHIPS)
264 .await
265 .unwrap();
266
267 assert_eq!(player.chips, JOIN_CHIPS);
268 assert_eq!(player.nickname, NICKNAME);
269
270 let player = db
272 .join_server(player_id.clone(), "bob", JOIN_CHIPS)
273 .await
274 .unwrap();
275 assert_eq!(player.nickname, "bob");
276
277 let player = db
279 .join_server(player_id.clone(), NICKNAME, JOIN_CHIPS * 2)
280 .await
281 .unwrap();
282 assert_eq!(player.chips, JOIN_CHIPS * 2);
283
284 let player = db.get_player(player_id).await.unwrap();
286 assert_eq!(player.chips, JOIN_CHIPS * 2);
287 assert_eq!(player.nickname, NICKNAME);
288 }
289
290 #[tokio::test]
291 async fn pay_player() {
292 const JOIN_CHIPS: Chips = Chips::new(1_000_000);
293 const NICKNAME: &str = "alice";
294
295 let db = Db::open_in_memory().unwrap();
296 let player_id = SigningKey::default().verifying_key().peer_id();
297
298 db.join_server(player_id.clone(), NICKNAME, JOIN_CHIPS)
300 .await
301 .unwrap();
302
303 db.pay_to_player(player_id.clone(), JOIN_CHIPS * 2)
305 .await
306 .unwrap();
307
308 let player = db.get_player(player_id.clone()).await.unwrap();
310 assert_eq!(player.chips, JOIN_CHIPS * 3);
312
313 let has_chips = db
315 .pay_from_player(player_id.clone(), JOIN_CHIPS)
316 .await
317 .unwrap();
318 assert!(has_chips);
319
320 let player = db.get_player(player_id.clone()).await.unwrap();
322 assert_eq!(player.chips, JOIN_CHIPS * 2);
323
324 let has_chips = db
326 .pay_from_player(player_id.clone(), JOIN_CHIPS * 2)
327 .await
328 .unwrap();
329 assert!(has_chips);
330
331 let has_chips = db
333 .pay_from_player(player_id.clone(), JOIN_CHIPS)
334 .await
335 .unwrap();
336 assert!(!has_chips);
337 }
338}