1mod scoring;
38mod game_state;
39mod cards;
40mod result;
41
42#[cfg(feature = "server")]
43pub mod game_manager;
44
45#[cfg(feature = "server")]
46pub mod matchmaking;
47
48#[cfg(feature = "server")]
49pub mod sqlite_store;
50
51#[cfg(feature = "server")]
52pub mod validation;
53
54#[cfg(feature = "server")]
55pub mod challenges;
56
57#[cfg(test)]
58mod tests;
59
60use uuid::Uuid;
61use sqids::Sqids;
62pub use result::*;
63pub use cards::*;
64pub use game_state::*;
65
66fn sqids_instance() -> Sqids {
67 Sqids::builder()
68 .min_length(6)
69 .build()
70 .expect("valid sqids config")
71}
72
73pub fn uuid_to_short_id(uuid: Uuid) -> String {
74 let bytes = uuid.as_bytes();
75 let high = u64::from_be_bytes(bytes[0..8].try_into().unwrap());
76 let low = u64::from_be_bytes(bytes[8..16].try_into().unwrap());
77 sqids_instance().encode(&[high, low]).expect("sqids encode")
78}
79
80pub fn short_id_to_uuid(short_id: &str) -> Option<Uuid> {
81 let nums = sqids_instance().decode(short_id);
82 if nums.len() != 2 {
83 return None;
84 }
85 let mut bytes = [0u8; 16];
86 bytes[0..8].copy_from_slice(&nums[0].to_be_bytes());
87 bytes[8..16].copy_from_slice(&nums[1].to_be_bytes());
88 Some(Uuid::from_bytes(bytes))
89}
90
91pub fn encode_player_url(game_id: Uuid, player_id: Uuid) -> String {
92 let gb = game_id.as_bytes();
93 let pb = player_id.as_bytes();
94 sqids_instance().encode(&[
95 u64::from_be_bytes(gb[0..8].try_into().unwrap()),
96 u64::from_be_bytes(gb[8..16].try_into().unwrap()),
97 u64::from_be_bytes(pb[0..8].try_into().unwrap()),
98 u64::from_be_bytes(pb[8..16].try_into().unwrap()),
99 ]).expect("sqids encode")
100}
101
102pub fn decode_player_url(s: &str) -> Option<(Uuid, Uuid)> {
103 let nums = sqids_instance().decode(s);
104 if nums.len() != 4 {
105 return None;
106 }
107 let mut gb = [0u8; 16];
108 gb[0..8].copy_from_slice(&nums[0].to_be_bytes());
109 gb[8..16].copy_from_slice(&nums[1].to_be_bytes());
110 let mut pb = [0u8; 16];
111 pb[0..8].copy_from_slice(&nums[2].to_be_bytes());
112 pb[8..16].copy_from_slice(&nums[3].to_be_bytes());
113 Some((Uuid::from_bytes(gb), Uuid::from_bytes(pb)))
114}
115
116pub enum GameTransition {
118 Bet(i32),
119 Card(Card),
120 Start,
121}
122
123#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
125pub struct TimerConfig {
126 pub initial_time_secs: u64,
127 pub increment_secs: u64,
128}
129
130#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
132pub struct PlayerClocks {
133 pub remaining_ms: [u64; 4],
134}
135
136#[derive(Debug, serde::Serialize, serde::Deserialize)]
137struct Player{
138 id: Uuid,
139 hand: Vec<Card>,
140 #[serde(default)]
141 name: Option<String>,
142}
143
144impl Player {
145 pub fn new(id: Uuid) -> Player {
146 Player {
147 id: id,
148 hand: vec![],
149 name: None,
150 }
151 }
152}
153
154#[derive(Debug, serde::Serialize, serde::Deserialize)]
156pub struct Game {
157 id: Uuid,
158 state: State,
159 scoring: scoring::Scoring,
160 current_player_index: usize,
161 deck: Vec<cards::Card>,
162 hands_played: Vec<[cards::Card; 4]>,
163 leading_suit: Suit,
164 player_a: Player,
165 player_b: Player,
166 player_c: Player,
167 player_d: Player,
168 #[serde(default)]
169 timer_config: Option<TimerConfig>,
170 #[serde(default)]
171 player_clocks: Option<PlayerClocks>,
172 #[serde(default)]
173 turn_started_at_epoch_ms: Option<u64>,
174}
175
176impl Game {
177 pub fn new(id: Uuid, player_ids: [Uuid; 4], max_points: i32, timer_config: Option<TimerConfig>) -> Game {
178 let player_clocks = timer_config.map(|tc| PlayerClocks {
179 remaining_ms: [tc.initial_time_secs * 1000; 4],
180 });
181 Game {
182 id,
183 state: State::NotStarted,
184 scoring: scoring::Scoring::new(max_points),
185 hands_played: vec![new_pot()],
186 deck: cards::new_deck(),
187 current_player_index: 0,
188 leading_suit: Suit::Blank,
189 player_a: Player::new(player_ids[0]),
190 player_b: Player::new(player_ids[1]),
191 player_c: Player::new(player_ids[2]),
192 player_d: Player::new(player_ids[3]),
193 timer_config,
194 player_clocks,
195 turn_started_at_epoch_ms: None,
196 }
197 }
198
199 pub fn get_id(&self) -> &Uuid {
200 &self.id
201 }
202
203 pub fn get_state(&self) -> &State {
205 &self.state
206 }
207
208 pub fn get_team_a_score(&self) -> Result<&i32, GetError> {
209 match (&self.state, self.current_player_index) {
210 (State::NotStarted, _) => {Err(GetError::GameNotStarted)},
211 _ => {Ok(&self.scoring.team_a.cumulative_points)}
212 }
213 }
214
215 pub fn get_team_b_score(&self) -> Result<&i32, GetError> {
216 match (&self.state, self.current_player_index) {
217 (State::NotStarted, _) => {Err(GetError::GameNotStarted)},
218 _ => {Ok(&self.scoring.team_b.cumulative_points)}
219 }
220 }
221
222 pub fn get_team_a_bags(&self) -> Result<&i32, GetError> {
223 match self.state {
224 State::NotStarted => {Err(GetError::GameNotStarted)},
225 _ => {Ok(&self.scoring.team_a.bags)}
226 }
227 }
228
229 pub fn get_team_b_bags(&self) -> Result<&i32, GetError> {
230 match self.state {
231 State::NotStarted => {Err(GetError::GameNotStarted)},
232 _ => {Ok(&self.scoring.team_b.bags)}
233 }
234 }
235
236 pub fn get_current_player_id(&self) -> Result<&Uuid, GetError>{
238 match (&self.state, self.current_player_index) {
239 (State::NotStarted, _) => {Err(GetError::GameNotStarted)},
240 (State::Completed, _) => {Err(GetError::GameCompleted)},
241 (State::Betting(_), 0) | (State::Trick(_), 0) => Ok(&self.player_a.id),
242 (State::Betting(_), 1) | (State::Trick(_), 1) => Ok(&self.player_b.id),
243 (State::Betting(_), 2) | (State::Trick(_), 2) => Ok(&self.player_c.id),
244 (State::Betting(_), 3) | (State::Trick(_), 3) => Ok(&self.player_d.id),
245 _ => {Err(GetError::Unknown)}
246 }
247 }
248
249 pub fn get_hand_by_player_id(&self, player_id: Uuid) -> Result<&Vec<Card>, GetError> {
251 if player_id == self.player_a.id {
252 return Ok(&self.player_a.hand);
253 }
254 if player_id == self.player_b.id {
255 return Ok(&self.player_b.hand);
256 }
257 if player_id == self.player_c.id {
258 return Ok(&self.player_c.hand);
259 }
260 if player_id == self.player_d.id {
261 return Ok(&self.player_d.hand);
262 }
263
264 return Err(GetError::InvalidUuid);
265 }
266
267 pub fn get_current_hand(&self) -> Result<&Vec<Card>, GetError> {
268 match (&self.state, self.current_player_index) {
269 (State::NotStarted, _) => {Err(GetError::GameNotStarted)},
270 (State::Completed, _) => {Err(GetError::GameCompleted)},
271 (State::Betting(_), 0) | (State::Trick(_), 0) => Ok(&self.player_a.hand),
272 (State::Betting(_), 1) | (State::Trick(_), 1) => Ok(&self.player_b.hand),
273 (State::Betting(_), 2) | (State::Trick(_), 2) => Ok(&self.player_c.hand),
274 (State::Betting(_), 3) | (State::Trick(_), 3) => Ok(&self.player_d.hand),
275 _ => {Err(GetError::Unknown)}
276 }
277 }
278
279 pub fn get_leading_suit(&self) -> Result<&Suit, GetError> {
280 match &self.state {
281 State::NotStarted => {Err(GetError::GameNotStarted)},
282 State::Completed => {Err(GetError::GameCompleted)},
283 State::Trick(_) => Ok(&self.leading_suit),
284 _ => {Err(GetError::Unknown)}
285 }
286 }
287
288 pub fn get_current_trick_cards(&self) -> Result<&[cards::Card; 4], GetError> {
290 match self.state {
291 State::NotStarted => {Err(GetError::GameNotStarted)},
292 State::Completed | State::Aborted => {Err(GetError::GameCompleted)},
293 State::Betting(_) => {Err(GetError::GameCompleted)},
294 State::Trick(_) => {Ok(self.hands_played.last().unwrap())},
295 }
296 }
297
298 #[deprecated(since="1.0.0", note="Please use `get_current_hand` or `get_hand_by_player_id`")]
299 pub fn get_hand(&self, player: usize) -> Result<&Vec<Card>, GetError> {
300 match player {
301 0 => Ok(&self.player_a.hand),
302 1 => Ok(&self.player_b.hand),
303 2 => Ok(&self.player_c.hand),
304 3 => Ok(&self.player_d.hand),
305 _ => Ok(&self.player_d.hand),
306 }
307 }
308
309 pub fn get_winner_ids(&self) -> Result<(&Uuid, &Uuid), GetError> {
310 match self.state {
311 State::Completed => {
312 if self.scoring.team_a.cumulative_points > self.scoring.team_b.cumulative_points {
313 return Ok((&self.player_a.id, &self.player_c.id));
314 } else if self.scoring.team_b.cumulative_points > self.scoring.team_a.cumulative_points {
315 return Ok((&self.player_b.id, &self.player_d.id));
316 } else {
317 return Err(GetError::GameNotCompleted);
319 }
320 },
321 _ => {
322 Err(GetError::GameNotCompleted)
323 }
324 }
325 }
326
327 pub fn play(&mut self, entry: GameTransition) -> Result<TransitionSuccess, TransitionError> {
333 match entry {
334 GameTransition::Bet(bet) => {
335 match self.state {
336 State::NotStarted => {
337 return Err(TransitionError::NotStarted);
338 },
339 State::Trick(_rotation_status) => {
340 return Err(TransitionError::BetInTrickStage);
341 },
342 State::Completed | State::Aborted => {
343 return Err(TransitionError::CompletedGame);
344 },
345 State::Betting(rotation_status) => {
346 self.scoring.add_bet(self.current_player_index,bet);
347 if rotation_status == 3 {
348 self.scoring.bet();
349 self.state = State::Trick((rotation_status + 1) % 4);
350 self.current_player_index = 0;
351 return Ok(TransitionSuccess::BetComplete);
352 } else {
353 self.current_player_index = (self.current_player_index + 1) % 4;
354 self.state = State::Betting((rotation_status + 1) % 4);
355 }
356
357 return Ok(TransitionSuccess::Bet);
358 },
359 };
360 },
361 GameTransition::Card(card) => {
362 match self.state {
363 State::NotStarted => {
364 return Err(TransitionError::NotStarted);
365 },
366 State::Completed | State::Aborted => {
367 return Err(TransitionError::CompletedGame);
368 },
369 State::Betting(_rotation_status) => {
370 return Err(TransitionError::CardInBettingStage)
371 },
372 State::Trick(rotation_status) => {
373 {
374 let player_hand = &mut match self.current_player_index {
375 0 => &mut self.player_a,
376 1 => &mut self.player_b,
377 2 => &mut self.player_c,
378 3 => &mut self.player_d,
379 _ => &mut self.player_d,
380 }.hand;
381
382 if !player_hand.contains(&card) {
383 return Err(TransitionError::CardNotInHand);
384 }
385 let leading_suit = self.leading_suit;
386 if rotation_status == 0 {
387 self.leading_suit = card.suit;
388 }
389 if self.leading_suit != card.suit && player_hand.iter().any(|ref x| x.suit == leading_suit) {
390 return Err(TransitionError::CardIncorrectSuit);
391 }
392
393 let card_index = player_hand.iter().position(|x| x == &card).unwrap();
394 self.deck.push(player_hand.remove(card_index));
395 }
396
397 self.hands_played.last_mut().unwrap()[self.current_player_index] = card;
398
399 if rotation_status == 3 {
400 let winner = self.scoring.trick(self.current_player_index, self.hands_played.last().unwrap());
401 if self.scoring.is_over {
402 self.state = State::Completed;
403 return Ok(TransitionSuccess::GameOver);
404 }
405 if self.scoring.in_betting_stage {
406 self.current_player_index = 0;
407 self.state = State::Betting((rotation_status + 1) % 4);
408 self.deal_cards();
409 } else {
410 self.current_player_index = winner;
411 self.state = State::Trick((rotation_status + 1) % 4);
412 self.hands_played.push(new_pot());
413 }
414 return Ok(TransitionSuccess::Trick);
415 } else {
416 self.current_player_index = (self.current_player_index + 1) % 4;
417 self.state = State::Trick((rotation_status + 1) % 4);
418 return Ok(TransitionSuccess::PlayCard);
419 }
420 }
421 };
422 },
423 GameTransition::Start => {
424 if self.state != State::NotStarted {
425 return Err(TransitionError::AlreadyStarted);
426 }
427 self.deal_cards();
428 self.state = State::Betting(0);
429 return Ok(TransitionSuccess::Start);
430 }
431 }
432 }
433
434 pub fn set_player_name(&mut self, player_id: Uuid, name: Option<String>) -> Result<(), GetError> {
435 if player_id == self.player_a.id {
436 self.player_a.name = name;
437 } else if player_id == self.player_b.id {
438 self.player_b.name = name;
439 } else if player_id == self.player_c.id {
440 self.player_c.name = name;
441 } else if player_id == self.player_d.id {
442 self.player_d.name = name;
443 } else {
444 return Err(GetError::InvalidUuid);
445 }
446 Ok(())
447 }
448
449 pub fn get_player_names(&self) -> [(Uuid, Option<&str>); 4] {
450 [
451 (self.player_a.id, self.player_a.name.as_deref()),
452 (self.player_b.id, self.player_b.name.as_deref()),
453 (self.player_c.id, self.player_c.name.as_deref()),
454 (self.player_d.id, self.player_d.name.as_deref()),
455 ]
456 }
457
458 pub fn get_timer_config(&self) -> Option<&TimerConfig> {
459 self.timer_config.as_ref()
460 }
461
462 pub fn get_player_clocks(&self) -> Option<&PlayerClocks> {
463 self.player_clocks.as_ref()
464 }
465
466 pub fn get_player_clocks_mut(&mut self) -> Option<&mut PlayerClocks> {
467 self.player_clocks.as_mut()
468 }
469
470 pub fn get_current_player_index_num(&self) -> usize {
471 self.current_player_index
472 }
473
474 pub fn is_first_round_betting(&self) -> bool {
476 self.scoring.round == 0 && matches!(self.state, State::Betting(_))
477 }
478
479 pub fn get_turn_started_at_epoch_ms(&self) -> Option<u64> {
480 self.turn_started_at_epoch_ms
481 }
482
483 pub fn set_turn_started_at_epoch_ms(&mut self, epoch_ms: Option<u64>) {
484 self.turn_started_at_epoch_ms = epoch_ms;
485 }
486
487 pub fn set_state(&mut self, state: State) {
489 self.state = state;
490 }
491
492 pub fn get_legal_cards(&self) -> Result<Vec<Card>, GetError> {
495 match &self.state {
496 State::Trick(rotation_status) => {
497 let hand = self.get_current_hand()?;
498 if *rotation_status == 0 {
499 Ok(hand.clone())
501 } else {
502 let has_leading_suit = hand.iter().any(|c| c.suit == self.leading_suit);
504 if has_leading_suit {
505 Ok(hand.iter().filter(|c| c.suit == self.leading_suit).cloned().collect())
506 } else {
507 Ok(hand.clone())
508 }
509 }
510 }
511 _ => Err(GetError::Unknown),
512 }
513 }
514
515 fn deal_cards(&mut self) {
516 cards::shuffle(&mut self.deck);
517 let mut hands = cards::deal_four_players(&mut self.deck);
518
519 self.player_a.hand = hands.pop().unwrap();
520 self.player_b.hand = hands.pop().unwrap();
521 self.player_c.hand = hands.pop().unwrap();
522 self.player_d.hand = hands.pop().unwrap();
523
524 self.player_a.hand.sort();
525 self.player_b.hand.sort();
526 self.player_c.hand.sort();
527 self.player_d.hand.sort();
528 }
529}