Skip to main content

go_fish/
lib.rs

1//! # go-fish
2//!
3//! `go-fish` is a library providing core functionality for the classic [Go Fish card game](https://en.wikipedia.org/wiki/Go_Fish).
4
5use enum_iterator::{all, Sequence};
6use rand::prelude::SliceRandom;
7use serde::{Deserialize, Serialize};
8use std::fmt::Display;
9use tracing::debug;
10use tracing::warn;
11
12/// A playing card
13#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
14pub struct Card {
15    pub suit: Suit,
16    pub rank: Rank,
17}
18
19/// The suit of a [Card]
20#[derive(Debug, PartialEq, Sequence, Clone, Copy, Serialize, Deserialize)]
21pub enum Suit {
22    Clubs,
23    Diamonds,
24    Hearts,
25    Spades,
26}
27
28/// The rank (or value) of a [Card]
29#[derive(
30    Debug,
31    PartialEq,
32    Eq,
33    Sequence,
34    Clone,
35    Copy,
36    PartialOrd,
37    Ord,
38    Serialize,
39    Deserialize
40)]
41pub enum Rank {
42    Two,
43    Three,
44    Four,
45    Five,
46    Six,
47    Seven,
48    Eight,
49    Nine,
50    Ten,
51    Jack,
52    Queen,
53    King,
54    Ace,
55}
56
57impl Display for Rank {
58    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
59        let s = match self {
60            Rank::Two => "Two",
61            Rank::Three => "Three",
62            Rank::Four => "Four",
63            Rank::Five => "Five",
64            Rank::Six => "Six",
65            Rank::Seven => "Seven",
66            Rank::Eight => "Eight",
67            Rank::Nine => "Nine",
68            Rank::Ten => "Ten",
69            Rank::Jack => "Jack",
70            Rank::Queen => "Queen",
71            Rank::King => "King",
72            Rank::Ace => "Ace",
73        };
74        write!(f, "{}", s)
75    }
76}
77
78/// A deck of [Cards](Card)
79#[derive(Debug, Serialize, Deserialize)]
80pub struct Deck {
81    pub cards: Vec<Card>,
82}
83
84/// A collection of three or fewer [Cards](Card) of the same [Rank]
85#[derive(Debug, Serialize, Deserialize, Clone)]
86pub struct IncompleteBook {
87    pub rank: Rank,
88    pub cards: Vec<Card>,
89}
90
91/// A collection of four [Cards](Card) of the same [Rank]
92#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
93pub struct CompleteBook {
94    pub rank: Rank,
95    pub cards: [Card; 4],
96}
97
98/// A players hand
99#[derive(Debug, Serialize, Deserialize, Clone)]
100pub struct Hand {
101    pub books: Vec<IncompleteBook>,
102}
103
104/// A player actively trying to win the game
105#[derive(Debug, Serialize, Deserialize, Clone)]
106pub struct Player {
107    pub id: PlayerId,
108    pub hand: Hand,
109    pub completed_books: Vec<CompleteBook>,
110}
111
112/// A player who no longer has any viable moves. They can still win the game, if they have
113/// more [Completed Books](CompleteBook) than any other player at the end of the game
114#[derive(Debug, Serialize, Deserialize, Clone)]
115pub struct InactivePlayer {
116    pub id: PlayerId,
117    pub completed_books: Vec<CompleteBook>,
118}
119
120#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq, Hash)]
121pub struct PlayerId(pub u8);
122
123/// A request for another players cards
124/// > Player 2, got any three's?
125#[derive(Debug, Serialize, Deserialize)]
126pub struct Hook {
127    pub target: PlayerId,
128    pub rank: Rank,
129}
130
131#[derive(Debug)]
132pub enum TurnError {
133    /// The [Hooks](Hook) target is not a player in the game
134    TargetNotFound(PlayerId),
135    GameIsFinished
136}
137
138/// Handling for a game of Go Fish
139#[derive(Debug, Serialize, Deserialize)]
140pub struct Game {
141    pub deck: Deck,
142    pub players: Vec<Player>,
143    pub inactive_players: Vec<InactivePlayer>,
144    pub is_finished: bool,
145    player_turn: usize,
146}
147
148impl Deck {
149    /// Creates a new, ordered deck of [Cards](Card)
150    pub fn new() -> Deck {
151        let cards = Self::ordered_cards();
152        Deck { cards }
153    }
154
155    /// Shuffles the remaining cards
156    pub fn shuffle(&mut self) {
157        let mut rng = rand::rng();
158        self.cards.shuffle(&mut rng);
159    }
160
161    pub fn is_empty(&self) -> bool {
162        self.cards.is_empty()
163    }
164
165    /// Draws a card from the deck“
166    /// ```
167    /// use go_fish::Deck;
168    /// let mut deck = Deck::new();
169    /// let card = deck.draw();
170    /// assert!(card.is_some())
171    /// ```
172    pub fn draw(&mut self) -> Option<Card> {
173        self.cards.pop()
174    }
175
176    fn ordered_cards() -> Vec<Card> {
177        let cards_from_suit = |suit| {
178            all::<Rank>()
179                .map(|rank| Card { suit, rank })
180                .collect::<Vec<_>>()
181        };
182        all::<Suit>().flat_map(cards_from_suit).collect::<Vec<_>>()
183    }
184}
185
186impl Default for Deck {
187    fn default() -> Self {
188        Self::new()
189    }
190}
191
192impl IncompleteBook {
193    /// Try to combine one [IncompleteBook] with another. This can result in a [CompleteBook].
194    pub(crate) fn combine(self, other: IncompleteBook) -> CombineBookResult {
195        if self.rank != other.rank {
196            return CombineBookResult::NotCombined(self, other);
197        }
198
199        let rank = self.rank;
200        let combined_cards = self
201            .cards
202            .into_iter()
203            .chain(other.cards)
204            .collect::<Vec<_>>();
205
206        if combined_cards.len() == 4 {
207            let cards = [
208                combined_cards[0],
209                combined_cards[1],
210                combined_cards[2],
211                combined_cards[3],
212            ];
213            let complete_book = CompleteBook { rank, cards };
214            return CombineBookResult::Completed(complete_book);
215        }
216
217        let combined_book = IncompleteBook {
218            rank,
219            cards: combined_cards,
220        };
221        CombineBookResult::Combined(combined_book)
222    }
223}
224
225impl From<Card> for IncompleteBook {
226    fn from(card: Card) -> Self {
227        let rank = card.rank;
228        let cards = vec![card];
229        IncompleteBook { rank, cards }
230    }
231}
232
233#[derive(Debug, Serialize, Deserialize, Clone)]
234pub struct GameResult {
235    pub winners: Vec<InactivePlayer>,
236    pub losers: Vec<InactivePlayer>,
237}
238
239#[derive(Debug, Serialize, Deserialize, Clone)]
240pub enum HookResult {
241    Catch(IncompleteBook),
242    GoFish,
243}
244
245impl Hand {
246    /// Create a new, empty Hand.
247    pub fn empty() -> Hand {
248        let books = vec![];
249        Hand { books }
250    }
251
252    /// Add an [IncompleteBook] to the Hand. This may produce an [CompleteBook].
253    pub fn add_book(&mut self, book: IncompleteBook) -> Option<CompleteBook> {
254        let position = self.books.iter().position(|b| b.rank == book.rank);
255        let position = match position {
256            Some(position) => position,
257            None => {
258                self.books.push(book);
259                return None;
260            }
261        };
262
263        let existing_book = self.books.swap_remove(position);
264        let combined_result = existing_book.combine(book);
265
266        match combined_result {
267            CombineBookResult::NotCombined(a, b) => {
268                panic!(
269                    "Books {:?} and {:?} failed to combine, but we expect them to have the same rank",
270                    a, b
271                )
272            }
273            CombineBookResult::Combined(combined_book) => {
274                self.books.push(combined_book);
275                None
276            }
277            CombineBookResult::Completed(completed_book) => Some(completed_book),
278        }
279    }
280
281    pub(crate) fn receive_hook(&mut self, rank: Rank) -> HookResult {
282        let position = self.books.iter().position(|b| b.rank == rank);
283        let position = match position {
284            None => return HookResult::GoFish,
285            Some(pos) => pos,
286        };
287
288        let catch = self.books.swap_remove(position);
289        HookResult::Catch(catch)
290    }
291}
292
293impl Player {
294    pub(crate) fn add_book(&mut self, book: IncompleteBook) {
295        let completed_book = self.hand.add_book(book);
296
297        if let Some(completed_book) = completed_book {
298            self.completed_books.push(completed_book);
299        }
300    }
301
302    pub(crate) fn receive_hook(&mut self, rank: Rank) -> HookResult {
303        self.hand.receive_hook(rank)
304    }
305}
306
307impl Game {
308    /// Create a new Game of Go Fish, with the given [Deck] and number of players
309    pub fn new(deck: Deck, player_count: u8) -> Game {
310        debug!(?deck, player_count, "Creating new Game");
311        let hand_size = match player_count {
312            2 | 3 => 7,
313            _ => 5,
314        };
315
316        let mut players: Vec<Player> = vec![];
317        let mut deck = deck;
318        for n in 0..player_count {
319            let player = Self::deal_player(PlayerId(n), hand_size, &mut deck);
320            players.push(player);
321        }
322
323        Game {
324            deck,
325            players,
326            is_finished: false,
327            inactive_players: vec![],
328            player_turn: 0,
329        }
330    }
331
332    /// Take a turn in the game
333    pub fn take_turn(&mut self, hook: Hook) -> Result<HookResult, TurnError> {
334        debug!(game.players = ?self.players,
335            game.inactive_players = ?self.inactive_players,
336            game.is_deck_empty = self.deck.is_empty(),
337            game.player_turn = self.player_turn,
338            game.is_finished = self.is_finished,
339            ?hook,
340            "Taking turn");
341        if self.is_finished {
342            warn!("Taking turn when game is finished");
343            return Err(TurnError::GameIsFinished);
344        }
345
346        let player_order = self.players.iter().map(|p| p.id).collect::<Vec<PlayerId>>();
347
348        let (mut fisher, target) =
349            Self::find_hook_players(&mut self.players, self.player_turn, hook.target);
350
351        let mut target = match target {
352            Some(target) => target,
353            None => {
354                self.players.push(fisher);
355                Self::reorder_players(&mut self.players, &player_order);
356                debug!(hook.target = hook.target.0, "Target player was not found");
357                return Err(TurnError::TargetNotFound(hook.target));
358            }
359        };
360
361        let result = target.receive_hook(hook.rank);
362        debug!(?target, ?hook, ?result, "Target received hook");
363
364        match result.clone() {
365            HookResult::Catch(catch) => {
366                fisher.add_book(catch);
367                let fisher = match fisher.hand.books.is_empty() {
368                    true => Self::handle_active_player_has_empty_hand(fisher, &mut self.deck),
369                    false => PlayerEmptyHandOutcome::Active(fisher),
370                };
371
372                debug!(?fisher, "Handled fisher hand state");
373                match fisher {
374                    PlayerEmptyHandOutcome::Active(fisher) => {
375                        self.players.push(fisher);
376                        self.players.push(target);
377                        Self::reorder_players(&mut self.players, &player_order);
378                    }
379                    PlayerEmptyHandOutcome::Inactive(fisher) => {
380                        self.players.push(target);
381                        Self::reorder_players(&mut self.players, &player_order);
382                        self.inactive_players.push(fisher);
383                        self.player_turn = match self.player_turn {
384                            0 => self.players.len() - 1,
385                            n => n - 1,
386                        };
387
388                        self.advance_player_turn()
389                    }
390                }
391            }
392            HookResult::GoFish => {
393                let draw = self.deck.draw();
394                if let Some(card) = draw {
395                    fisher.add_book(card.into())
396                }
397
398                self.players.push(fisher);
399                self.players.push(target);
400                Self::reorder_players(&mut self.players, &player_order);
401                self.advance_player_turn()
402            }
403        };
404
405        if self.players.is_empty() {
406            self.is_finished = true;
407        }
408
409        debug!(game.players = ?self.players,
410            game.inactive_players = ?self.inactive_players,
411            game.is_deck_empty = self.deck.is_empty(),
412            game.player_turn = self.player_turn,
413            game.is_finished = self.is_finished,
414            "Finished taking turn");
415
416        Ok(result)
417    }
418
419    /// Get the current player
420    pub fn get_current_player(&self) -> Option<&Player> {
421        if self.is_finished {
422            return None;
423        }
424        if self.players.is_empty() {
425            warn!("Current game has no current player, is not finished");
426            return None;
427        }
428
429        let player = self.players.get(self.player_turn);
430        if player.is_none() {
431            warn!(player_turn = self.player_turn, players = ?self.players, "player_turn index is out of bounds");
432        }
433
434        player
435    }
436
437    pub fn get_game_result(&self) -> Option<GameResult> {
438        if !self.is_finished {
439            return None;
440        }
441
442        let max_books = self.inactive_players.iter().map(|p| p.completed_books.len()).max().unwrap();
443        let mut winners = vec![];
444        let mut losers = vec![];
445        for player in self.inactive_players.clone().into_iter() {
446            if player.completed_books.len() == max_books {
447                winners.push(player);
448            } else {
449                losers.push(player);
450            }
451        };
452        Some(GameResult { winners, losers })
453    }
454
455    fn deal_player(id: PlayerId, hand_size: usize, deck: &mut Deck) -> Player {
456        let mut hand = Hand::empty();
457        let mut completed_books = vec![];
458
459        for _ in 0..hand_size {
460            let draw = deck.draw();
461            let book = IncompleteBook::from(draw.unwrap());
462            let completed_book = hand.add_book(book);
463            if let Some(c) = completed_book {
464                completed_books.push(c);
465            }
466        }
467
468        Player {
469            id,
470            hand,
471            completed_books,
472        }
473    }
474
475    fn advance_player_turn(&mut self) {
476        if self.players.is_empty() {
477            return;
478        }
479
480        if self.players.len() == 1 {
481            let player = self.players.remove(0);
482            if !player.hand.books.is_empty() {
483                panic!("Shouldn't get here I don't think")
484            }
485            let player = InactivePlayer { id: player.id, completed_books: player.completed_books };
486            self.inactive_players.push(player);
487            return;
488        }
489
490        let mut new_turn = (self.player_turn + 1) % self.players.len();
491        let player_order = self.players.iter().map(|p| p.id).collect::<Vec<PlayerId>>();
492
493        for _ in 1..self.players.len() {
494            let current_player = self.players.swap_remove(new_turn);
495            let result = match current_player.hand.books.is_empty() {
496                true => Self::handle_active_player_has_empty_hand(current_player, &mut self.deck),
497                false => PlayerEmptyHandOutcome::Active(current_player),
498            };
499            let found_new_player = match result {
500                PlayerEmptyHandOutcome::Active(player) => {
501                    self.players.push(player);
502                    Self::reorder_players(&mut self.players, &player_order);
503                    true
504                }
505                PlayerEmptyHandOutcome::Inactive(player) => {
506                    self.inactive_players.push(player);
507                    Self::reorder_players(&mut self.players, &player_order);
508                    false
509                }
510            };
511
512            if found_new_player {
513                break;
514            }
515
516            new_turn = (new_turn + 1) % self.players.len();
517        }
518
519        if self.players.len() == 1 {
520            let player = self.players.remove(0);
521            if !player.hand.books.is_empty() {
522                panic!("Shouldn't get here I don't think")
523            }
524            let player = InactivePlayer { id: player.id, completed_books: player.completed_books };
525            self.inactive_players.push(player);
526            return;
527        }
528
529        self.player_turn = new_turn;
530    }
531
532    fn handle_active_player_has_empty_hand(
533        mut player: Player,
534        deck: &mut Deck,
535    ) -> PlayerEmptyHandOutcome {
536        let draw = deck.draw();
537        match draw {
538            Some(card) => {
539                player.add_book(IncompleteBook::from(card));
540                PlayerEmptyHandOutcome::Active(player)
541            }
542            None => PlayerEmptyHandOutcome::Inactive(InactivePlayer {
543                id: player.id,
544                completed_books: player.completed_books,
545            }),
546        }
547    }
548
549    fn find_hook_players(
550        players: &mut Vec<Player>,
551        current_player_index: usize,
552        target_id: PlayerId,
553    ) -> (Player, Option<Player>) {
554        let fisher = players.swap_remove(current_player_index);
555
556        let target_index = players.iter().position(|p| p.id == target_id);
557        let target = match target_index {
558            Some(index) => players.swap_remove(index),
559            None => return (fisher, None),
560        };
561
562        (fisher, Some(target))
563    }
564
565    fn reorder_players(players: &mut [Player], order: &[PlayerId]) {
566        players.sort_by_key(|p| order.iter().position(|pos| &p.id == pos).unwrap());
567    }
568}
569
570#[derive(Debug)]
571pub(crate) enum CombineBookResult {
572    Combined(IncompleteBook),
573    NotCombined(IncompleteBook, IncompleteBook),
574    Completed(CompleteBook),
575}
576
577#[derive(Debug)]
578enum PlayerEmptyHandOutcome {
579    Active(Player),
580    Inactive(InactivePlayer),
581}
582
583#[cfg(test)]
584mod game_tests;