Skip to main content

go_fish/
lib.rs

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