use crate::{ card::{Card, Suit}, move_result::MoveResult, core::{Player, Deal, Vulnerability}, actions::Bid, contract::{LevelAndSuit, Contract, ContractModifier} }; use anyhow::{anyhow, bail}; use log::info; use serde::{Deserialize, Serialize}; use std::borrow::Cow; pub const SUIT_DISPLAY_ORDER: [Suit; 4] = [Suit::Diamond, Suit::Club, Suit::Heart, Suit::Spade]; #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] pub struct Trick { pub leader: Player, pub cards_played: Vec, } impl Trick { pub fn suit(&self) -> Option { self.cards_played.first().map(|&Card(suit, _)| suit) } // TODO: This should be moved somewhere we can guarantee that cards are non-empty. pub fn winner(&self, trump_suit: Option) -> Player { let suit = self.suit(); let value = |c: &Card| { if Some(c.suit()) == trump_suit { 14 + c.rank() as i8 } else if Some(c.suit()) == suit { c.rank() as i8 } else { 0 } }; let (i, _) = self .cards_played .iter() .enumerate() .max_by(|&(_, c1), &(_, c2)| value(c1).cmp(&value(c2))) .unwrap(); self.leader.many_next(i) } } #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] pub struct TurnInPlay { pub trick: Trick, } impl TurnInPlay { pub fn new(p: Player) -> TurnInPlay { TurnInPlay { trick: Trick { leader: p, cards_played: Vec::with_capacity(4), }, } } pub fn suit(&self) -> Option { self.trick.cards_played.first().map(|&Card(suit, _)| suit) } pub fn leader(&self) -> Player { self.trick.leader } pub fn cards_played(&self) -> &[Card] { &self.trick.cards_played[..] } pub fn play( mut self: TurnInPlay, card: Card, ) -> MoveResult { self.trick.cards_played.push(card); if self.trick.cards_played.len() >= 4 { return MoveResult::Next(self.trick); } MoveResult::Current(self) } pub fn current_player(&self) -> Player { self.trick.leader.many_next(self.trick.cards_played.len()) } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct DealInPlay { deal: Deal, trump_suit: Option, tricks_played: Vec, in_progress: TurnInPlay, } impl DealInPlay { pub fn new( leader: Player, trump_suit: Option, deal: Deal, ) -> DealInPlay { DealInPlay { deal, trump_suit, tricks_played: Vec::with_capacity(13), in_progress: TurnInPlay::new(leader), } } pub fn tricks(&self) -> &Vec { &self.tricks_played } pub fn trick_in_play(&self) -> &TurnInPlay { &self.in_progress } pub fn deal(&self) -> &Deal { &self.deal } pub fn current_player(&self) -> Player { self.in_progress.current_player() } pub fn is_dummy_visible(&self) -> bool { !self.tricks_played.is_empty() || !self.in_progress.trick.cards_played.is_empty() } pub fn play( mut self, card: Card, ) -> Result>, anyhow::Error> { let player = self.current_player(); let player_cards = player.get_cards_mut(&mut self.deal); if let Some(suit) = self.in_progress.suit() { if card.suit() != suit && player_cards.iter().any(|c| c.suit() == suit) { return Err(anyhow!("Must follow {suit} suit")); } } info!( "Next player is {:?}, playing card {} from {:?}", player, card, player_cards ); let i = player_cards .iter() .position(|&c| c == card) .ok_or_else(|| anyhow!("{:?} does not have {}", player, card))?; player_cards.remove(i); Ok(match self.in_progress.play(card) { MoveResult::Current(turn) => MoveResult::Current(Self { in_progress: turn, ..self }), MoveResult::Next(trick) => { let trick_winner = trick.winner(self.trump_suit); let mut tricks = self.tricks_played; tricks.push(trick); if player_cards.is_empty() { MoveResult::Next(tricks) } else { MoveResult::Current(Self { trump_suit: self.trump_suit, in_progress: TurnInPlay::new(trick_winner), tricks_played: tricks, deal: self.deal, }) } } }) } } #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub struct Bidding { pub dealer: Player, pub bids: Vec, } impl Bidding { pub fn new(dealer: Player) -> Bidding { Bidding { dealer, bids: vec![], } } fn declarer(&self) -> Player { // Bids are: [..., winning bid, pass, pass, pass]. self.dealer.many_next(self.bids.len() - 4) } pub fn current_bidder(&self) -> Player { self.dealer.many_next(self.bids.len()) } pub fn highest_bid(&self) -> Option { for bid in self.bids.iter().rev() { if let Some(raise) = bid.as_raise() { return Some(raise); } } None } fn passed_out(&self) -> bool { if self.bids.len() < 4 { return false; } let mut passes = 0; for b in self.bids.iter().rev().take(3) { if b == &Bid::Pass { passes += 1 } } passes == 3 } fn contract(&self) -> Option { self.highest_bid().map(|highest_bid| Contract { declarer: self.declarer(), highest_bid, modifier: ContractModifier::None, }) } pub fn bid(mut self, bid: Bid) -> Result { // TODO: Need logic for double and redouble here. if bid.as_raise().is_some() && bid.as_raise() <= self.highest_bid() { bail!( "bid too low: {:?} <= {:?}", bid.as_raise(), self.highest_bid() ); } self.bids.push(bid); if self.passed_out() { Ok(BiddingResult::Contract(self.contract(), self)) } else { Ok(BiddingResult::InProgress(self)) } } } #[derive(Debug, Clone)] pub enum BiddingResult { InProgress(Bidding), Contract(Option, Bidding), } impl BiddingResult { pub fn new(dealer: Player) -> Self { BiddingResult::InProgress(Bidding::new(dealer)) } pub fn bidding(&self) -> &Bidding { match self { BiddingResult::InProgress(bidding) => bidding, BiddingResult::Contract(_, bidding) => bidding, } } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct BiddingState { pub deal: Deal, pub bidding: Bidding, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct BiddingStatePlayerView { pub player_position: Player, pub hand: Vec, pub bidding: Bidding, } impl BiddingStatePlayerView { pub fn from_bidding_state( bidding_state: &BiddingState, player_position: Player, ) -> Self { let BiddingState { deal, bidding, } = bidding_state; Self { player_position, hand: player_position.get_cards(deal).clone(), bidding: bidding.clone(), } } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct PlayState { pub deal: Deal, pub contract: Contract, pub bidding: Bidding, pub playing_deal: DealInPlay, } impl PlayState { pub fn new(deal: Deal, contract: Contract, bidding: Bidding) -> Self { let playing_deal = DealInPlay::new( contract.declarer.many_next(3), contract.highest_bid.suit, deal.clone(), ); Self { deal, contract, bidding, playing_deal, } } pub fn dealer(&self) -> Player { self.deal.dealer } pub fn current_player(&self) -> Player { self.playing_deal.current_player() } pub fn play( self, card: Card, ) -> Result, anyhow::Error> { Ok(match self.playing_deal.play(card)? { MoveResult::Current(playing_deal) => MoveResult::Current(Self { playing_deal, ..self }), MoveResult::Next(tricks) => { MoveResult::Next(PlayResult::Played(PlayedResult { vulnerability: self.deal.vulnerability, bidding: self.bidding, contract: self.contract, tricks, })) } }) } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct PassedOutResult { pub deal: Deal, pub bidding: Bidding, } impl PassedOutResult { pub fn deal(&self) -> Cow { Cow::Borrowed(&self.deal) } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct PlayedResult { pub vulnerability: Vulnerability, pub bidding: Bidding, pub contract: Contract, pub tricks: Vec, } impl PlayedResult { pub fn deal(&self) -> Cow { let mut deal = Deal::empty(self.bidding.dealer, self.vulnerability); let mut leader = self.contract.leader(); let trump_suit = self.contract.highest_bid.suit; for trick in &self.tricks { let mut player = leader; for card in &trick.cards_played { player.get_cards_mut(&mut deal).push(*card); player = player.next(); } leader = trick.winner(trump_suit); } Cow::Owned(deal) } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum PlayResult { PassedOut(PassedOutResult), Played(PlayedResult), } impl PlayResult { pub fn deal(&self) -> Cow { match self { PlayResult::PassedOut(r) => r.deal(), PlayResult::Played(r) => r.deal(), } } pub fn bidding(&self) -> &Bidding { match self { PlayResult::PassedOut(r) => &r.bidding, PlayResult::Played(r) => &r.bidding, } } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum GameState { Bidding(BiddingState), Play(PlayState), } impl From for GameState { fn from(val: PlayState) -> Self { GameState::Play(val) } } impl GameState { pub fn new(deal: Deal) -> Self { let dealer = deal.dealer; Self::Bidding(BiddingState { deal, bidding: Bidding::new(dealer), }) } pub fn deal(&self) -> &Deal { match self { Self::Bidding(BiddingState { deal, .. }) => deal, Self::Play(PlayState { playing_deal, .. }) => playing_deal.deal(), } } pub fn dealer(&self) -> Player { match self { Self::Bidding(BiddingState { deal, .. }) => deal.dealer, Self::Play(play_state) => play_state.dealer(), } } pub fn current_player(&self) -> Player { match self { GameState::Bidding(bidding) => bidding.bidding.current_bidder(), GameState::Play(play) => play.current_player(), } } pub fn is_bidding(&self) -> bool { matches!(self, GameState::Bidding { .. }) } pub fn is_playing(&self) -> bool { matches!(self, GameState::Play { .. }) } pub fn bidding(&self) -> Result<&BiddingState, anyhow::Error> { match self { GameState::Bidding(bidding_state) => Ok(bidding_state), _ => Err(anyhow::anyhow!("not currently bidding")), } } pub fn play_state(&self) -> Result<&PlayState, anyhow::Error> { match self { GameState::Play(play_state) => Ok(play_state), _ => Err(anyhow::anyhow!("not currently playing")), } } pub fn bid( self, bid: Bid, ) -> Result, anyhow::Error> { let BiddingState { deal, bidding, } = self.bidding()?.clone(); Ok(match bidding.bid(bid)? { BiddingResult::InProgress(bidding) => { MoveResult::Current(GameState::Bidding(BiddingState { deal, bidding, })) } BiddingResult::Contract(None, bidding) => { MoveResult::Next(PlayResult::PassedOut(PassedOutResult { deal, bidding })) } BiddingResult::Contract(Some(contract), bidding) => { MoveResult::Current(GameState::Play(PlayState::new( deal, contract, bidding, ))) } }) } pub fn play( self, card: Card, ) -> Result, anyhow::Error> { Ok(match self.play_state()?.clone().play(card)? { MoveResult::Current(play_state) => { MoveResult::Current(play_state.into()) } MoveResult::Next(result) => MoveResult::Next(result), }) } } #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] pub struct PlayStatePlayerView { pub player_position: Player, pub bidding: Bidding, pub contract: Contract, // If None, the lead has not been played. pub dummy: Option>, pub declarer_tricks: u8, pub hand: Vec, pub previous_trick: Option, pub current_trick: TurnInPlay, } impl PlayStatePlayerView { pub fn from_play_state( play_state: &PlayState, player_position: Player, ) -> Self { let dummy = if play_state.playing_deal.is_dummy_visible() { Some( play_state .contract .dummy() .get_cards(&play_state.playing_deal.deal) .clone(), ) } else { None }; Self { player_position, bidding: play_state.bidding.clone(), contract: play_state.contract, dummy, declarer_tricks: 0, hand: player_position .get_cards(&play_state.playing_deal.deal) .clone(), previous_trick: play_state .playing_deal .tricks_played .last() .cloned(), current_trick: play_state.playing_deal.in_progress.clone(), } } pub fn dealer(&self) -> Player { self.bidding.dealer } pub fn current_player(&self) -> Player { self.current_trick.current_player() } } #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] pub enum GameStatePlayerView { Bidding(BiddingStatePlayerView), Playing(PlayStatePlayerView), } impl GameStatePlayerView { pub fn from_game_state( game_state: &GameState, player_position: Player, ) -> Self { match game_state { GameState::Bidding(bidding_state) => GameStatePlayerView::Bidding( BiddingStatePlayerView::from_bidding_state( bidding_state, player_position, ), ), GameState::Play(play_state) => GameStatePlayerView::Playing( PlayStatePlayerView::from_play_state( play_state, player_position, ), ), } } pub fn hand(&self) -> &Vec { match self { GameStatePlayerView::Bidding(BiddingStatePlayerView { hand, .. }) => hand, GameStatePlayerView::Playing(PlayStatePlayerView { hand, .. }) => hand, } } } #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub enum TableState { Unknown, Game(GameState), Result(PlayResult), } impl Default for TableState { fn default() -> Self { TableState::Unknown } } impl TryFrom for GameState { type Error = anyhow::Error; fn try_from(value: TableState) -> Result { match value { TableState::Game(game) => Ok(game), _ => Err(anyhow::anyhow!("no game")), } } } impl From> for TableState { fn from(val: MoveResult) -> Self { match val { MoveResult::Current(game) => TableState::Game(game), MoveResult::Next(result) => TableState::Result(result), } } } impl From for TableState { fn from(val: GameState) -> Self { TableState::Game(val) } } #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub enum TableStatePlayerView { Unknown, Game(GameStatePlayerView), Result(PlayResult), } impl TableStatePlayerView { pub fn from_table_state( table: &TableState, player_position: Player, ) -> Self { match table { TableState::Unknown => TableStatePlayerView::Unknown, TableState::Game(g) => TableStatePlayerView::Game( GameStatePlayerView::from_game_state(g, player_position), ), TableState::Result(r) => TableStatePlayerView::Result(r.clone()), } } } #[cfg(test)] mod tests { use std::str::FromStr; use crate::{contract::ContractLevel, card::RankOrder}; use super::*; use log::info; use rand::random; use strum::IntoEnumIterator; fn as_bidding(r: BiddingResult) -> Bidding { match r { BiddingResult::InProgress(bidding) => bidding, _ => panic!("expected BiddingResult::InProgress(): {:?}", r), } } fn as_contract(r: BiddingResult) -> Option { match r { BiddingResult::Contract(contract, _) => contract, _ => panic!("expected BiddingResult::Contract(): {:?}", r), } } #[test] fn bidding() { crate::tests::test_setup(); let bidding = Bidding::new(Player::South); assert_eq!(bidding.current_bidder(), Player::South); let bidding = as_bidding(bidding.bid(Bid::Pass).unwrap()); assert_eq!(bidding.current_bidder(), Player::West); let bidding = as_bidding(bidding.bid(Bid::Raise("1♦".parse().unwrap())).unwrap()); assert_eq!(bidding.current_bidder(), Player::North); let bidding = as_bidding(bidding.bid(Bid::Pass).unwrap()); assert_eq!(bidding.current_bidder(), Player::East); let bidding = as_bidding(bidding.bid(Bid::Pass).unwrap()); assert_eq!(bidding.current_bidder(), Player::South); let contract = as_contract(bidding.bid(Bid::Pass).unwrap()); assert_eq!( Some(Contract { declarer: Player::West, highest_bid: "1♦".parse().unwrap(), modifier: ContractModifier::None }), contract ); } #[test] fn bid_conversion() { crate::tests::test_setup(); let bid1d = LevelAndSuit { level: ContractLevel::One, suit: Some(Suit::Diamond), }; assert_eq!("1♢", format!("{}", bid1d)); assert_eq!("1♢", format!("{:?}", bid1d)); assert_eq!(bid1d, LevelAndSuit::from_str("1D").unwrap()); assert_eq!(Bid::Pass, Bid::from_str("pass").unwrap()); let mut checked_raises = 0; for bid in LevelAndSuit::all_raises() { assert_eq!( bid, LevelAndSuit::from_str(format!("{}", bid).as_str()).unwrap() ); assert_eq!( Bid::Raise(bid), Bid::from_str(format!("{}", bid).as_str()).unwrap() ); checked_raises += 1; } assert_eq!(checked_raises, 35); } #[test] fn fmt_contract() { assert_eq!( format!( "{}", Contract { declarer: Player::West, highest_bid: "1♥".parse().unwrap(), modifier: ContractModifier::None } ), "1♡W" ); assert_eq!( format!( "{}", Contract { declarer: Player::East, highest_bid: "1♥".parse().unwrap(), modifier: ContractModifier::Doubled } ), "1♡Ex" ); } #[test] fn bid_ord() { let bid = |s| LevelAndSuit::from_str(s).unwrap(); assert!(bid("2♦") < bid("3♦")); assert!(bid("3♦") < bid("3♥")); assert!(bid("1♠") < bid("2♣")); assert!(bid("1♠") < bid("1NT")); for bid in LevelAndSuit::all_raises() { assert_eq!(bid, bid); } } #[test] fn contract_level_conversion() { crate::tests::test_setup(); assert_eq!("2", format!("{}", ContractLevel::Two)); assert_eq!("3", format!("{:?}", ContractLevel::Three)); let result = ContractLevel::from_str("8"); info!("{:?}", result); assert!(result.unwrap_err().to_string().contains("invalid")); assert_eq!(ContractLevel::Seven, "7".parse().unwrap()); } #[test] fn next_player() { let next_players = vec![Player::North, Player::East, Player::South, Player::West] .iter() .map(Player::next) .collect::>(); assert_eq!( next_players, vec![Player::East, Player::South, Player::West, Player::North] ); } #[test] fn many_next_player() { assert_eq!(Player::South, Player::South.many_next(4 * 1234567890)); } #[test] fn play_turn() { let turn = TurnInPlay::new(Player::South); assert_eq!(turn.current_player(), Player::South); let turn = turn.play("♣4".parse().unwrap()).current().unwrap(); assert_eq!(turn.current_player(), Player::West); let turn = turn.play("♥A".parse().unwrap()).current().unwrap(); assert_eq!(turn.current_player(), Player::North); let turn = turn.play("♣4".parse().unwrap()).current().unwrap(); assert_eq!(turn.current_player(), Player::East); let trick = turn.play("♣A".parse().unwrap()).next().unwrap(); assert_eq!( trick, Trick { leader: Player::South, cards_played: ["♣4", "♥A", "♣4", "♣A"] .into_iter() .map(|c| c.parse().unwrap()) .collect() } ); } #[test] fn lead_suit() { let turn = TurnInPlay::new(Player::South); assert_eq!(turn.suit(), None); let turn = turn.play("♣4".parse().unwrap()).current().unwrap(); assert_eq!(turn.suit(), Some("♣".parse().unwrap())); } fn mkcard(s: &str) -> Card { Card::from_str(s).unwrap() } fn mkcards(s: &str) -> Vec { s.split(' ').map(mkcard).collect() } fn example_deal() -> Deal { Deal { dealer: Player::North, vulnerability: Vulnerability::None, west: mkcards("♠Q ♠9 ♠5 ♠2 ♥K ♥J ♥4 ♣5 ♣4 ♣2 ♦10 ♦5 ♦3"), north: mkcards("♠A ♠8 ♠7 ♠6 ♥A ♥9 ♥5 ♥3 ♣9 ♣3 ♦Q ♦J ♦9"), east: mkcards("♠K ♠3 ♥Q ♥10 ♥8 ♥7 ♣K ♣Q ♣J ♣10 ♣6 ♦A ♦4"), south: mkcards("♠J ♠10 ♠4 ♥6 ♥2 ♣A ♣8 ♣7 ♦K ♦8 ♦7 ♦6 ♦2"), } } fn mini_deal() -> Deal { Deal { dealer: Player::North, vulnerability: Vulnerability::None, west: mkcards("♢A ♡Q"), north: mkcards("♢Q ♡9"), east: mkcards("♢7 ♡K"), south: mkcards("♥10 ♠9"), } } #[test] fn example_deal_is_sorted() { crate::tests::test_setup(); let mut sorted = example_deal(); sorted.sort(&SUIT_DISPLAY_ORDER, RankOrder::Descending); let pp = |hand: &[Card]| { hand.iter() .map(|c| format!("{}", c)) .collect::>() .join(" ") }; info!("{}", pp(&sorted.west)); info!("{}", pp(&sorted.north)); info!("{}", pp(&sorted.east)); info!("{}", pp(&sorted.south)); assert_eq!(example_deal(), sorted); } #[test] fn game_state() { crate::tests::test_setup(); let game_state = GameState::new(mini_deal()); assert_eq!(game_state.deal(), &mini_deal()); assert_eq!(game_state.dealer(), Player::North); assert!(game_state.is_bidding()); info!("Start bidding with game state {game_state:#?}"); let raise = |s| Bid::Raise(LevelAndSuit::from_str(s).unwrap()); let game_state = game_state.bid(raise("1H")).unwrap().current().unwrap(); let game_state = game_state .bid(Bid::Pass) .unwrap() .current() .unwrap() .bid(Bid::Pass) .unwrap() .current() .unwrap() .bid(Bid::Pass) .unwrap() .current() .unwrap(); info!("Start playing with game state {game_state:#?}"); assert!(!game_state.is_bidding()); assert!(game_state.is_playing()); } #[test] fn table_view() { crate::tests::test_setup(); let game_state = GameState::new(mini_deal()); info!("Game state: {game_state:?}"); for p in Player::iter() { info!("Testing view for {p:?}"); let view = GameStatePlayerView::from_game_state(&game_state, p); match view { GameStatePlayerView::Bidding(BiddingStatePlayerView { bidding, hand, .. }) => { assert_eq!(bidding.dealer, Player::North); assert_eq!(&hand, p.get_cards(&mini_deal())); } _ => panic!("expected bidding: {view:#?}"), } } } fn some_play_state() -> PlayState { crate::tests::test_setup(); let deal: Deal = random(); let raise1c = LevelAndSuit { level: ContractLevel::One, suit: Some(Suit::Club), }; let contract = Contract { declarer: random(), highest_bid: raise1c, modifier: ContractModifier::Doubled, }; let bidding = Bidding { dealer: deal.dealer, bids: vec![Bid::Raise(raise1c), Bid::Pass, Bid::Pass, Bid::Pass], }; PlayState::new(deal, contract, bidding) } #[test] fn play_state() { some_play_state(); } #[test] fn play_state_player_view() { crate::tests::test_setup(); let play_state = some_play_state(); let player = random(); let player_state = PlayStatePlayerView::from_play_state(&play_state, player); assert_eq!(play_state.dealer(), player_state.dealer()); assert_eq!(player_state.player_position, player); assert_eq!(player_state.current_player(), play_state.current_player()); } fn as_playing_hand( result: MoveResult>, ) -> DealInPlay { match result { MoveResult::Current(r) => r, MoveResult::Next(_) => { panic!("expected PlayingDealResult::InProgress(): {:?}", result) } } } #[test] fn pass_out_bid() { let mut bidding = Bidding::new(random()); for _i in 0..3 { bidding = as_bidding(bidding.bid(Bid::Pass).unwrap()); assert!(!bidding.passed_out()); } bidding = match bidding.bid(Bid::Pass).unwrap() { BiddingResult::Contract(None, bidding) => bidding, _ => panic!("should be passed out"), }; assert!(bidding.passed_out()); } #[test] fn play_hand() { let deal = DealInPlay::new(Player::West, None, mini_deal()); assert_eq!(deal.tricks_played, vec!()); { let err = deal.clone().play(mkcard("♥9")).unwrap_err().to_string(); assert_eq!(err, "West does not have ♡9"); } let deal = as_playing_hand(deal.play(mkcard("♢A")).unwrap()); assert_eq!(deal.in_progress.trick.cards_played, vec!(mkcard("♢A"))); { let err = deal.clone().play(mkcard("♡9")).unwrap_err().to_string(); assert_eq!(err, "Must follow ♢ suit"); } let deal = as_playing_hand(deal.play(mkcard("♢Q")).unwrap()); let deal = as_playing_hand(deal.play(mkcard("♢7")).unwrap()); let deal = as_playing_hand(deal.play(mkcard("♡10")).unwrap()); assert_eq!(deal.in_progress.trick.cards_played, []); assert_eq!( deal.tricks_played, vec!(Trick { leader: Player::West, cards_played: mkcards("♢A ♢Q ♢7 ♡10"), }) ); let trick = &deal.tricks_played[0]; assert_eq!(trick.winner(None), Player::West); assert_eq!(trick.winner(Some(Suit::Heart)), Player::South); } }