From 2ab671a5b375f260abe7e3a62c7220b40b9c19c4 Mon Sep 17 00:00:00 2001 From: Jonas Molander Date: Wed, 29 May 2024 21:30:38 +0200 Subject: [PATCH] feat: deck of cards & table draw (#2) ### Why? To play the game we need a deck of cards. Ranked by values and suit. To start off the game we also need players to draw a random card for the dealer button. ### What's changed? - Added cards - Added deck of cards - Tables can be created out of four players. Where the player who draws the highest card starts at the north position (with their team m8 across the table at south). The player who draw the highest card in the opposite team starts at the east position. --- Cargo.lock | 68 ++++++++++++++++++ Cargo.toml | 1 + src/card.rs | 178 ++++++++++++++++++++++++++++++++++++++++++++++ src/deck.rs | 67 +++++++++++++++++ src/game.rs | 1 + src/game/lobby.rs | 4 +- src/game/table.rs | 112 +++++++++++++++++++++++++++++ src/lib.rs | 2 + src/user.rs | 2 +- 9 files changed, 432 insertions(+), 3 deletions(-) create mode 100644 src/card.rs create mode 100644 src/deck.rs create mode 100644 src/game/table.rs diff --git a/Cargo.lock b/Cargo.lock index c196b38..1341373 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,74 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "swedish_whist" version = "0.1.0" +dependencies = [ + "rand", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" diff --git a/Cargo.toml b/Cargo.toml index ca3aef4..9832200 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,4 @@ version = "0.1.0" edition = "2021" [dependencies] +rand = "0.8.5" diff --git a/src/card.rs b/src/card.rs new file mode 100644 index 0000000..132c954 --- /dev/null +++ b/src/card.rs @@ -0,0 +1,178 @@ +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)] +pub enum Suit { + Clubs, + Diamonds, + Hearts, + Spades, +} + +impl Suit { + pub fn all() -> Vec { + vec![ + Suit::Clubs, + Suit::Diamonds, + Suit::Hearts, + Suit::Spades, + ] + } +} + +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)] +pub enum Rank { + Two, + Three, + Four, + Five, + Six, + Seven, + Eight, + Nine, + Ten, + Jack, + Queen, + King, + Ace, +} + +impl Rank { + pub fn all() -> Vec { + vec![ + Rank::Two, + Rank::Three, + Rank::Four, + Rank::Five, + Rank::Six, + Rank::Seven, + Rank::Eight, + Rank::Nine, + Rank::Ten, + Rank::Jack, + Rank::Queen, + Rank::King, + Rank::Ace, + ] + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Card { + suit: Suit, + rank: Rank, +} + +impl Card { + pub fn new(suit: Suit, rank: Rank) -> Card { + Card { suit, rank } + } + + pub fn suit(&self) -> &Suit { + &self.suit + } + + pub fn rank(&self) -> &Rank { + &self.rank + } + + pub fn compare_bridge_value(&self, other: &Card) -> std::cmp::Ordering { + // Compare the ranks + let rank_comparison = self.rank().cmp(other.rank()); + // If the ranks are equal, compare the suits + if rank_comparison == std::cmp::Ordering::Equal { + self.suit().cmp(other.suit()) + } else { + rank_comparison + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_ace_of_spades() { + let card = Card { + suit: Suit::Spades, + rank: Rank::Ace, + }; + assert_eq!(*card.suit(), Suit::Spades); + assert_eq!(*card.rank(), Rank::Ace); + } + + #[test] + fn suit() { + let card = Card::new(Suit::Hearts, Rank::Two); + assert_eq!(*card.suit(), Suit::Hearts); + } + + #[test] + fn rank() { + let card = Card::new(Suit::Hearts, Rank::Two); + assert_eq!(*card.rank(), Rank::Two); + } + + #[test] + fn ace_higher_than_king() { + let ace = Card::new(Suit::Spades, Rank::Ace); + let king = Card::new(Suit::Spades, Rank::King); + + assert!(ace.rank() > king.rank()); + } + + #[test] + fn deuce_lower_than_three() { + let deuce = Card::new(Suit::Spades, Rank::Two); + let three = Card::new(Suit::Spades, Rank::Three); + + assert!(deuce.rank() < three.rank()); + } + + #[test] + fn ace_of_spades_equal_to_ace_of_spades() { + let ace1 = Card::new(Suit::Spades, Rank::Ace); + let ace2 = Card::new(Suit::Spades, Rank::Ace); + + assert_eq!(ace1, ace2); + } + + #[test] + fn same_suits_equals() { + let card1 = Card::new(Suit::Hearts, Rank::Two); + let card2 = Card::new(Suit::Hearts, Rank::Three); + + assert_eq!(card1.suit(), card2.suit()); + } + + #[test] + fn three_of_clubs_less_than_three_of_diamonds() { + let three_of_clubs = Card::new(Suit::Clubs, Rank::Three); + let three_of_diamonds = Card::new(Suit::Diamonds, Rank::Three); + + assert_eq!( + three_of_clubs.compare_bridge_value(&three_of_diamonds), + std::cmp::Ordering::Less + ); + } + + #[test] + fn compare_bridge_value() { + let aces = vec![ + Card::new(Suit::Diamonds, Rank::Ace), + Card::new(Suit::Hearts, Rank::Ace), + Card::new(Suit::Spades, Rank::Ace), + Card::new(Suit::Clubs, Rank::Ace), + ]; + let mut sorted_aces = aces.clone(); + sorted_aces.sort_by(|a, b| a.compare_bridge_value(b)); + + assert_eq!( + sorted_aces, + vec![ + Card::new(Suit::Clubs, Rank::Ace), + Card::new(Suit::Diamonds, Rank::Ace), + Card::new(Suit::Hearts, Rank::Ace), + Card::new(Suit::Spades, Rank::Ace), + ], + ); + } +} diff --git a/src/deck.rs b/src/deck.rs new file mode 100644 index 0000000..db269fd --- /dev/null +++ b/src/deck.rs @@ -0,0 +1,67 @@ +use crate::card::{ + Card, + Suit, + Rank, +}; + +#[derive(Debug)] +pub struct Deck { + pub cards: Vec, +} + +impl Deck { + pub fn new() -> Deck { + let mut cards = Vec::new(); + + for suit in Suit::all() { + for rank in Rank::all() { + cards.push(Card::new(suit, rank)); + } + } + + Deck { cards } + } + + pub fn len(&self) -> usize { + self.cards.len() + } + + pub fn shuffle(&mut self) { + use rand::seq::SliceRandom; + use rand::thread_rng; + + self.cards.shuffle(&mut thread_rng()); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_deck_has_52_cards() { + let deck = Deck::new(); + assert_eq!(deck.len(), 52); + } + + #[test] + fn shuffled_deck_differs_from_new_deck() { + let mut deck = Deck::new(); + let original = deck.cards.clone(); + + deck.shuffle(); + + assert_ne!(deck.cards, original); + } + + #[test] + fn shuffled_decks_are_different() { + let mut deck1 = Deck::new(); + let mut deck2 = Deck::new(); + + deck1.shuffle(); + deck2.shuffle(); + + assert_ne!(deck1.cards, deck2.cards); + } +} diff --git a/src/game.rs b/src/game.rs index 278212e..bda3288 100644 --- a/src/game.rs +++ b/src/game.rs @@ -2,6 +2,7 @@ use crate::user::User; use crate::game::lobby::Lobby; mod lobby; +mod table; #[derive(Debug, PartialEq, Eq, Clone, Copy)] struct Settings { diff --git a/src/game/lobby.rs b/src/game/lobby.rs index cc4d422..0c10c30 100644 --- a/src/game/lobby.rs +++ b/src/game/lobby.rs @@ -2,14 +2,14 @@ use crate::errors::GameError; use crate::game::{BidRound, Player, Settings, Team}; use crate::user::User; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub(crate) struct Lobby<'a> { pub(crate) settings: Settings, pub(crate) players: Vec>, } impl<'a> Lobby<'a> { - fn add_user(&mut self, user: &'a User) { + pub(crate) fn add_user(&mut self, user: &'a User) { // Create player let player = Player::build( &user, diff --git a/src/game/table.rs b/src/game/table.rs new file mode 100644 index 0000000..daea6be --- /dev/null +++ b/src/game/table.rs @@ -0,0 +1,112 @@ +use crate::card::Card; +use crate::game::lobby::Lobby; +use crate::game::Player; +use crate::deck::Deck; + +#[derive(Debug, PartialEq, Eq, Clone)] +struct Table<'a> { + north: Player<'a>, + east: Player<'a>, + south: Player<'a>, + west: Player<'a>, +} + +impl<'a> Table<'a> { + pub fn new(lobby: Lobby<'a>) -> Table { + // Each player draws a card + let high_card_draws = Self::high_card_for_dealer_button(lobby); + let highest_card_team = high_card_draws[0].0.team(); + + // Highest card becomes the dealer at the north position + // Her team mate becomes the south position + let dealer_team: Vec = high_card_draws.iter() + .filter(|(p, _)| p.team() == highest_card_team) + .map(|(p, _)| p.clone()) + .collect(); + + // The player in the other team with the highest draw sits at the east position + // Her team mate sits at the west position + let starting_team: Vec = high_card_draws.iter() + .filter(|(p, _)| p.team() != highest_card_team) + .map(|(p, _)| p.clone()) + .collect(); + + Table { + north: dealer_team[0], + east: starting_team[0], + south: dealer_team[1], + west: starting_team[1], + } + } + + fn high_card_for_dealer_button(lobby: Lobby<'a>) -> Vec<(Player, Card)> { + let mut deck = Deck::new(); + deck.shuffle(); + + let mut player_cards: Vec<(Player, Card)> = lobby.players.iter().map(|p| { + let card = deck.cards.pop().expect("Deck should have enough cards"); + ((*p).clone(), card) + }).collect(); + + player_cards.sort_by(|a, b| { + a.1.compare_bridge_value(&b.1) + }); + + player_cards + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::game::lobby::Lobby; + use crate::game::{Game, Settings}; + use crate::user::User; + + fn setup_users() -> Vec { + vec![ + User::new("A"), + User::new("B"), + User::new("C"), + User::new("D"), + ] + } + + fn setup_lobby() -> Lobby<'static> { + let settings = Settings { to_win: 13 }; + Game::new(settings) + } + + #[test] + fn high_card_for_dealer_button() { + let users = setup_users(); + let mut lobby = setup_lobby(); + users.iter().for_each(|u| lobby.add_user(u)); + + let high_card_draws = Table::high_card_for_dealer_button(lobby); + + let cards = high_card_draws.iter() + .map(|(_, c)| c.clone()) + .collect::>(); + + let mut cards_sorted_by_bridge_rank = cards.clone(); + cards_sorted_by_bridge_rank.sort_by(|a, b| a.compare_bridge_value(b)); + + assert_eq!( + cards, + cards_sorted_by_bridge_rank, + ); + } + + #[test] + fn new_table() { + let users = setup_users(); + let mut lobby = setup_lobby(); + users.iter().for_each(|u| lobby.add_user(u)); + + let table = Table::new(lobby); + + assert_eq!(table.north.team(), table.south.team()); + assert_eq!(table.east.team(), table.west.team()); + } +} diff --git a/src/lib.rs b/src/lib.rs index 709e245..3015656 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ pub mod user; mod game; mod errors; +mod card; +mod deck; diff --git a/src/user.rs b/src/user.rs index 61644df..d1b41af 100644 --- a/src/user.rs +++ b/src/user.rs @@ -1,4 +1,4 @@ -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct User { name: String, }