Skip to main content

spades/
lib.rs

1//! This crate provides an implementation of the four person card game, [spades](https://www.pagat.com/auctionwhist/spades.html). 
2//! ## Example usage
3//! ```
4//! use std::{io};
5//! use spades::{Game, GameTransition, State};
6//! use rand::seq::SliceRandom;
7//! use rand::thread_rng;
8//! 
9//! let mut g = Game::new(uuid::Uuid::new_v4(),
10//!    [uuid::Uuid::new_v4(),
11//!     uuid::Uuid::new_v4(),
12//!     uuid::Uuid::new_v4(),
13//!     uuid::Uuid::new_v4()],
14//!     500, None);
15//! 
16//! 
17//! g.play(GameTransition::Start);
18//! 
19//! while *g.get_state() != State::Completed {
20//!     let mut stdin = io::stdin();
21//!     let input = &mut String::new();
22//!     let mut rng = thread_rng();
23//!     if let State::Trick(_playerindex) = *g.get_state() {
24//!         assert!(g.get_current_hand().is_ok());
25//!         let hand = g.get_current_hand().ok().unwrap().clone();
26//! 
27//!         let random_card = hand.as_slice().choose(&mut rng).unwrap();
28//!         
29//!         g.play(GameTransition::Card(random_card.clone()));
30//!     } else {
31//!         g.play(GameTransition::Bet(3));
32//!     }
33//! }
34//! assert_eq!(*g.get_state(), State::Completed);
35//! ```
36
37mod 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
116/// The primary way to interface with a spades game. Used as an argument to [Game::play](struct.Game.html#method.play).
117pub enum GameTransition {
118    Bet(i32),
119    Card(Card),
120    Start,
121}
122
123/// Fischer increment timer configuration (X+Y: X minutes initial, Y seconds increment per move).
124#[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/// Remaining clock time for each player in milliseconds.
131#[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/// Primary game state. Internally manages player rotation, scoring, and cards.
155#[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    /// See [`State`](enum.State.html)
204    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    /// Returns `GetError` when the current game is not in the Betting or Trick stages.
237    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    /// Returns a `GetError::InvalidUuid` if the game does not contain a player with the given `Uuid`.
250    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    /// Returns an array with (only if in the trick stage).
289    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                    // Tie should not happen (is_over prevents it), but guard against it
318                    return Err(GetError::GameNotCompleted);
319                }
320            },
321            _ => {
322                Err(GetError::GameNotCompleted)
323            }
324        }
325    }
326
327    /// The primary function used to progress the game state. The first `GameTransition` argument must always be 
328    /// [`GameTransition::Start`](enum.GameTransition.html#variant.Start). The stages and player rotations are managed
329    /// internally. The order of `GameTransition` arguments should be:
330    /// 
331    /// Start -> Bet * 4 -> Card * 13 -> Bet * 4 -> Card * 13 -> Bet * 4 -> ...
332    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    /// Returns true if the game is in the first round's betting phase (round 0, Betting state).
475    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    /// Set the game state directly (used by GameManager for abort).
488    pub fn set_state(&mut self, state: State) {
489        self.state = state;
490    }
491
492    /// Returns the list of legal cards the current player can play.
493    /// Only valid in the Trick state.
494    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                    // First card in trick: any card is legal
500                    Ok(hand.clone())
501                } else {
502                    // Must follow leading suit if possible
503                    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}