diff options
author | Kjetil Orbekk <kj@orbekk.com> | 2022-12-21 08:10:38 -0500 |
---|---|---|
committer | Kjetil Orbekk <kj@orbekk.com> | 2022-12-21 08:10:38 -0500 |
commit | 10ecb9e30568bf20287b053a620252d7a80dbd6b (patch) | |
tree | c6a82f7cb4d24071234a1429dbb91815709052be | |
parent | 27f74d8c366be675e7ab64ca746496a66b3cf024 (diff) |
Add struct for the player view of a hand in play
-rw-r--r-- | protocol/src/bot.rs | 9 | ||||
-rw-r--r-- | protocol/src/bridge_engine.rs | 289 | ||||
-rw-r--r-- | server/src/play.rs | 116 |
3 files changed, 323 insertions, 91 deletions
diff --git a/protocol/src/bot.rs b/protocol/src/bot.rs index 3bae7db..cc676df 100644 --- a/protocol/src/bot.rs +++ b/protocol/src/bot.rs @@ -1,8 +1,15 @@ use async_trait::async_trait; -use crate::bridge_engine::{BiddingStatePlayerView, Bid}; +use crate::{bridge_engine::{BiddingStatePlayerView, Bid}, card::Card}; #[async_trait] pub trait BiddingBot { async fn bid(&self, bidding: &BiddingStatePlayerView) -> Bid; } + +#[async_trait] +pub trait PlayingBot { + async fn play(&self) -> Card { + todo!() + } +} diff --git a/protocol/src/bridge_engine.rs b/protocol/src/bridge_engine.rs index 925676e..50f619b 100644 --- a/protocol/src/bridge_engine.rs +++ b/protocol/src/bridge_engine.rs @@ -1,7 +1,10 @@ -use crate::card::{Card, RankOrder, Suit, sort_cards, make_deck}; +use crate::card::{make_deck, sort_cards, Card, RankOrder, Suit}; use anyhow::{anyhow, bail}; use log::{error, info}; -use rand::{prelude::{Distribution, SliceRandom}, distributions::Standard}; +use rand::{ + distributions::Standard, + prelude::{Distribution, SliceRandom}, random, +}; use regex::Regex; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; @@ -10,10 +13,20 @@ use std::str::FromStr; use strum::{EnumCount, IntoEnumIterator}; use strum_macros::{EnumCount as EnumCountMacro, EnumIter, FromRepr}; -pub const SUIT_DISPLAY_ORDER: [Suit; 4] = [Suit::Diamond, Suit::Club, Suit::Heart, Suit::Spade]; +pub const SUIT_DISPLAY_ORDER: [Suit; 4] = + [Suit::Diamond, Suit::Club, Suit::Heart, Suit::Spade]; #[derive( - PartialEq, Eq, Clone, Copy, Debug, FromRepr, EnumCountMacro, Serialize, Deserialize, EnumIter, + PartialEq, + Eq, + Clone, + Copy, + Debug, + FromRepr, + EnumCountMacro, + Serialize, + Deserialize, + EnumIter, )] #[repr(u8)] pub enum Player { @@ -166,7 +179,10 @@ impl DealInPlay { &self.deal } - pub fn play(mut self: Self, card: Card) -> Result<DealInPlayResult, anyhow::Error> { + pub fn play( + mut self: Self, + card: Card, + ) -> Result<DealInPlayResult, anyhow::Error> { let player = self.in_progress.next_player(); let player_cards = player.get_cards_mut(&mut self.deal); @@ -182,24 +198,38 @@ impl DealInPlay { player_cards.remove(i); Ok(match self.in_progress.play(card) { - TurnInPlayResult::InProgress(turn) => DealInPlayResult::InProgress(Self { - in_progress: turn, - ..self - }), - TurnInPlayResult::Trick(trick) => DealInPlayResult::InProgress(Self { - in_progress: TurnInPlay::new(trick.winner()), - tricks_played: { - let mut tricks = self.tricks_played; - tricks.push(trick); - tricks - }, - deal: self.deal, - }), + TurnInPlayResult::InProgress(turn) => { + DealInPlayResult::InProgress(Self { + in_progress: turn, + ..self + }) + } + TurnInPlayResult::Trick(trick) => { + DealInPlayResult::InProgress(Self { + in_progress: TurnInPlay::new(trick.winner()), + tricks_played: { + let mut tricks = self.tricks_played; + tricks.push(trick); + tricks + }, + deal: self.deal, + }) + } }) } } -#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, EnumIter, Serialize, Deserialize)] +#[derive( + PartialEq, + Eq, + PartialOrd, + Ord, + Clone, + Copy, + EnumIter, + Serialize, + Deserialize, +)] pub enum ContractLevel { One = 1, Two, @@ -211,13 +241,19 @@ pub enum ContractLevel { } impl fmt::Display for ContractLevel { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + fn fmt( + &self, + f: &mut std::fmt::Formatter<'_>, + ) -> std::result::Result<(), std::fmt::Error> { write!(f, "{}", *self as u8) } } impl fmt::Debug for ContractLevel { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + fn fmt( + &self, + f: &mut std::fmt::Formatter<'_>, + ) -> std::result::Result<(), std::fmt::Error> { write!(f, "{}", self) } } @@ -225,7 +261,9 @@ impl fmt::Debug for ContractLevel { impl FromStr for ContractLevel { type Err = anyhow::Error; - fn from_str(s: &str) -> std::result::Result<Self, <Self as std::str::FromStr>::Err> { + fn from_str( + s: &str, + ) -> std::result::Result<Self, <Self as std::str::FromStr>::Err> { match s { "1" => Ok(ContractLevel::One), "2" => Ok(ContractLevel::Two), @@ -257,7 +295,10 @@ impl Bid { } impl fmt::Display for Bid { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + fn fmt( + &self, + f: &mut std::fmt::Formatter<'_>, + ) -> std::result::Result<(), std::fmt::Error> { match self { Bid::Pass => write!(f, "Pass"), Bid::Double => write!(f, "Double"), @@ -268,7 +309,10 @@ impl fmt::Display for Bid { } impl fmt::Debug for Bid { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + fn fmt( + &self, + f: &mut std::fmt::Formatter<'_>, + ) -> std::result::Result<(), std::fmt::Error> { match self { Bid::Pass => write!(f, "Pass"), Bid::Double => write!(f, "Double"), @@ -280,7 +324,9 @@ impl fmt::Debug for Bid { impl FromStr for Bid { type Err = anyhow::Error; - fn from_str(s: &str) -> std::result::Result<Self, <Self as std::str::FromStr>::Err> { + fn from_str( + s: &str, + ) -> std::result::Result<Self, <Self as std::str::FromStr>::Err> { match s.trim().to_ascii_lowercase().as_str() { "pass" => Ok(Bid::Pass), "double" => Ok(Bid::Double), @@ -337,7 +383,10 @@ impl Ord for Raise { } impl fmt::Display for Raise { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + fn fmt( + &self, + f: &mut std::fmt::Formatter<'_>, + ) -> std::result::Result<(), std::fmt::Error> { write!( f, "{}{}", @@ -349,14 +398,19 @@ impl fmt::Display for Raise { } impl fmt::Debug for Raise { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + fn fmt( + &self, + f: &mut std::fmt::Formatter<'_>, + ) -> std::result::Result<(), std::fmt::Error> { write!(f, "{}", self) } } impl FromStr for Raise { type Err = anyhow::Error; - fn from_str(s: &str) -> std::result::Result<Self, <Self as std::str::FromStr>::Err> { + fn from_str( + s: &str, + ) -> std::result::Result<Self, <Self as std::str::FromStr>::Err> { lazy_static::lazy_static! { static ref RE: Regex = Regex::new(r#"\s*(.[0-9]*)\s*(.*)"#).unwrap(); }; @@ -379,7 +433,10 @@ pub enum ContractModifier { } impl fmt::Display for ContractModifier { - fn fmt(&self, f: &mut fmt::Formatter) -> std::result::Result<(), std::fmt::Error> { + fn fmt( + &self, + f: &mut fmt::Formatter, + ) -> std::result::Result<(), std::fmt::Error> { match self { ContractModifier::None => Ok(()), ContractModifier::Doubled => write!(f, "x"), @@ -396,7 +453,10 @@ pub struct Contract { } impl fmt::Display for Contract { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::result::Result<(), fmt::Error> { + fn fmt( + &self, + f: &mut fmt::Formatter<'_>, + ) -> std::result::Result<(), fmt::Error> { write!( f, "{}{}{}", @@ -440,7 +500,9 @@ impl Bidding { } fn passed_out(&self) -> bool { - if self.bids.len() < 4 { return false; } + if self.bids.len() < 4 { + return false; + } let mut passes = 0; for b in self.bids.iter().rev().take(3) { if b == &Bid::Pass { @@ -514,7 +576,10 @@ pub struct BiddingStatePlayerView { } impl BiddingStatePlayerView { - pub fn from_bidding_state(bidding_state: &BiddingState, player_position: Player) -> Self { + pub fn from_bidding_state( + bidding_state: &BiddingState, + player_position: Player, + ) -> Self { let BiddingState { dealer, deal, @@ -531,13 +596,29 @@ impl BiddingStatePlayerView { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct PlayState { - pub dealer: Player, 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), + deal.clone()); + Self { + deal, + contract, + bidding, + playing_deal, + } + } + + pub fn dealer(&self) -> Player { + self.bidding.dealer + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum GameState { Bidding(BiddingState), @@ -567,18 +648,24 @@ impl GameState { } pub fn dealer(&self) -> Player { - match *self { - Self::Bidding(BiddingState { dealer, .. }) => dealer, - Self::PassedOut { dealer, .. } => dealer, - Self::Play(PlayState { dealer, .. }) => dealer, + match self { + Self::Bidding(BiddingState { dealer, .. }) => *dealer, + Self::PassedOut { dealer, .. } => *dealer, + Self::Play(play_state) => play_state.dealer(), } } pub fn current_player(&self) -> Option<Player> { match self { - GameState::Bidding(bidding) => Some(bidding.bidding.current_bidder()), + GameState::Bidding(bidding) => { + Some(bidding.bidding.current_bidder()) + } GameState::Play(_) => todo!(), - GameState::PassedOut { dealer, deal, bidding } => None, + GameState::PassedOut { + dealer, + deal, + bidding, + } => None, } } @@ -608,23 +695,22 @@ impl GameState { _ => bail!("not currently bidding: {self:?}"), }; Ok(match bidding.bid(bid)? { - BiddingResult::InProgress(bidding) => GameState::Bidding(BiddingState { - dealer, - deal, - bidding, - }), + BiddingResult::InProgress(bidding) => { + GameState::Bidding(BiddingState { + dealer, + deal, + bidding, + }) + } BiddingResult::Contract(None, bidding) => GameState::PassedOut { dealer, deal, bidding, }, - BiddingResult::Contract(Some(contract), bidding) => GameState::Play(PlayState { - deal: deal.clone(), - dealer, - playing_deal: DealInPlay::new(contract.declarer, deal), - contract, - bidding, - }), + BiddingResult::Contract(Some(contract), bidding) => { + GameState::Play( + PlayState::new(deal, contract, bidding)) + } }) } } @@ -665,6 +751,37 @@ impl Distribution<Deal> for Standard { } #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] +pub struct PlayStatePlayerView { + player_position: Player, + bidding: Bidding, + contract: Contract, + // If None, the lead has not been played. + dummy: Option<Vec<Card>>, + declarer_tricks: u8, + hand: Vec<Card>, + previous_trick: Trick, + current_trick: TurnInPlay, +} + +impl PlayStatePlayerView { + pub fn from_play_state( + play_state: &PlayState, + player_position: Player, + ) -> Self { + Self { + player_position, + bidding: play_state.bidding.clone(), + contract: play_state.contract, + dummy: None, + declarer_tricks: 0, + hand: vec!(), + previous_trick: Trick { leader: random(), cards_played: vec!() }, + current_trick: TurnInPlay::new(random()), + } + } +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] pub enum GameStatePlayerView { Bidding(BiddingStatePlayerView), PassedOut { @@ -690,10 +807,16 @@ pub enum GameStatePlayerView { } impl GameStatePlayerView { - pub fn from_game_state(game_state: &GameState, player_position: Player) -> Self { + 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), + BiddingStatePlayerView::from_bidding_state( + bidding_state, + player_position, + ), ), GameState::PassedOut { .. } => todo!(), GameState::Play { .. } => todo!(), @@ -702,7 +825,10 @@ impl GameStatePlayerView { pub fn hand(&self) -> &Vec<Card> { match self { - GameStatePlayerView::Bidding(BiddingStatePlayerView { hand, .. }) => hand, + GameStatePlayerView::Bidding(BiddingStatePlayerView { + hand, + .. + }) => hand, GameStatePlayerView::PassedOut { deal, player_position, @@ -741,7 +867,8 @@ mod tests { 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()); + 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); @@ -773,7 +900,10 @@ mod tests { let mut checked_raises = 0; for bid in Raise::all_raises() { - assert_eq!(bid, Raise::from_str(format!("{}", bid).as_str()).unwrap()); + assert_eq!( + bid, + Raise::from_str(format!("{}", bid).as_str()).unwrap() + ); assert_eq!( Bid::Raise(bid), Bid::from_str(format!("{}", bid).as_str()).unwrap() @@ -836,10 +966,11 @@ mod tests { #[test] fn next_player() { - let next_players = vec![Player::North, Player::East, Player::South, Player::West] - .iter() - .map(Player::next) - .collect::<Vec<_>>(); + let next_players = + vec![Player::North, Player::East, Player::South, Player::West] + .iter() + .map(Player::next) + .collect::<Vec<_>>(); assert_eq!( next_players, vec![Player::East, Player::South, Player::West, Player::North] @@ -975,7 +1106,11 @@ mod tests { info!("Testing view for {p:?}"); let view = GameStatePlayerView::from_game_state(&game_state, p); match view { - GameStatePlayerView::Bidding(BiddingStatePlayerView { dealer, hand, .. }) => { + GameStatePlayerView::Bidding(BiddingStatePlayerView { + dealer, + hand, + .. + }) => { assert_eq!(dealer, Player::East); assert_eq!(&hand, p.get_cards(&mini_deal())); } @@ -984,6 +1119,38 @@ mod tests { } } + fn some_play_state() -> PlayState { + crate::tests::test_setup(); + let deal = random(); + let raise1c = Raise { + level: ContractLevel::One, + suit: Some(Suit::Club), + }; + let contract = Contract { + declarer: random(), + highest_bid: raise1c, + modifier: ContractModifier::Doubled, + }; + let bidding = Bidding { + dealer: random(), + 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_deal = + PlayStatePlayerView::from_play_state(&play_state, random()); + } + fn as_playing_hand(result: DealInPlayResult) -> DealInPlay { match result { DealInPlayResult::InProgress(r) => r, diff --git a/server/src/play.rs b/server/src/play.rs index 79d241c..86a4e06 100644 --- a/server/src/play.rs +++ b/server/src/play.rs @@ -1,5 +1,9 @@ use async_trait::async_trait; -use protocol::{bridge_engine::{GameState, Player, Bid, Deal, BiddingStatePlayerView}, simple_bots::AlwaysPassBiddingBot, bot::BiddingBot}; +use protocol::{ + bot::BiddingBot, + bridge_engine::{Bid, BiddingStatePlayerView, Deal, GameState, Player}, + simple_bots::AlwaysPassBiddingBot, +}; use rand::random; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -15,16 +19,23 @@ pub trait Journal { fn next(&self) -> i64; // Append payload to the journal at sequence number `seq`. - async fn append(&mut self, seq: i64, payload: serde_json::Value) -> Result<(), BridgeError>; + async fn append( + &mut self, + seq: i64, + payload: serde_json::Value, + ) -> Result<(), BridgeError>; // Fetch all journal entries with sequence number greater or equal to `seq`. - async fn replay(&mut self, seq: i64) -> Result<Vec<serde_json::Value>, BridgeError>; + async fn replay( + &mut self, + seq: i64, + ) -> Result<Vec<serde_json::Value>, BridgeError>; } pub struct DbJournal { db: PgPool, id: Uuid, - seq: i64 + seq: i64, } impl DbJournal { @@ -35,7 +46,11 @@ impl DbJournal { #[async_trait] impl Journal for DbJournal { - async fn append(&mut self, seq: i64, payload: serde_json::Value) -> Result<(), BridgeError> { + async fn append( + &mut self, + seq: i64, + payload: serde_json::Value, + ) -> Result<(), BridgeError> { let result = query!( r#" insert into object_journal (id, seq, payload) @@ -46,18 +61,24 @@ impl Journal for DbJournal { payload, ) .execute(&self.db) - .await; + .await; if let Err(sqlx::Error::Database(e)) = result { if e.constraint() == Some("journal_entry") { - return Err(BridgeError::JournalConflict(format!("{}", self.id), seq)); + return Err(BridgeError::JournalConflict( + format!("{}", self.id), + seq, + )); } } self.seq += 1; Ok(()) } - async fn replay(&mut self, seq: i64) -> Result<Vec<serde_json::Value>, BridgeError> { - let rows =query!( + async fn replay( + &mut self, + seq: i64, + ) -> Result<Vec<serde_json::Value>, BridgeError> { + let rows = query!( r#" select seq, payload from object_journal where id = $1 and seq >= $2 @@ -67,8 +88,8 @@ impl Journal for DbJournal { seq ) .fetch_all(&self.db) - .await?; - let mut payloads = vec!(); + .await?; + let mut payloads = vec![]; for v in rows { payloads.push(v.payload); self.seq = v.seq; @@ -106,13 +127,19 @@ where } impl<J: Journal> Table<J> { - pub fn game(&self) -> &GameState { &self.game } + pub fn game(&self) -> &GameState { + &self.game + } } impl<J: Journal> Table<J> { pub async fn new(mut journal: J) -> Result<Self, BridgeError> { let game = Self::init(&mut journal).await?; - Ok(Table { journal, game, settings: Default::default() }) + Ok(Table { + journal, + game, + settings: Default::default(), + }) } async fn init(journal: &mut J) -> Result<GameState, BridgeError> { @@ -123,7 +150,9 @@ impl<J: Journal> Table<J> { pub async fn bid(&mut self, bid: Bid) -> Result<(), BridgeError> { let game = self.game.clone().bid(bid)?; - self.journal.append(self.journal.next(), json!(game)).await?; + self.journal + .append(self.journal.next(), json!(game)) + .await?; self.game = game; Ok(()) } @@ -131,10 +160,16 @@ impl<J: Journal> Table<J> { pub async fn replay(mut journal: J) -> Result<Self, BridgeError> { let games = journal.replay(0).await?; if games.is_empty() { - return Err(BridgeError::NotFound("table journal missing".to_string())); + return Err(BridgeError::NotFound( + "table journal missing".to_string(), + )); } let game = serde_json::from_value(games[games.len() - 1].clone())?; - Ok(Table { journal, game, settings: Default::default() } ) + Ok(Table { + journal, + game, + settings: Default::default(), + }) } pub async fn new_or_replay(mut journal: J) -> Result<Self, BridgeError> { @@ -142,29 +177,43 @@ impl<J: Journal> Table<J> { if let Err(BridgeError::JournalConflict(..)) = game { return Self::replay(journal).await; } - Ok(Self { journal, game: game?, settings: Default::default() } ) + Ok(Self { + journal, + game: game?, + settings: Default::default(), + }) } } - -pub async fn advance_play<J: Journal>(table: &mut Table<J>) -> Result<(), BridgeError> { +pub async fn advance_play<J: Journal>( + table: &mut Table<J>, +) -> Result<(), BridgeError> { let current_player = match table.game().current_player() { Some(player) => player, None => { info!("Could not make play. Game: {:#?}", table.game()); - return Err(BridgeError::InvalidRequest(format!("No play to make for game"))); - }, + return Err(BridgeError::InvalidRequest(format!( + "No play to make for game" + ))); + } }; match table.game() { GameState::Bidding(bidding) => { - let player_view = BiddingStatePlayerView::from_bidding_state(bidding, current_player); + let player_view = BiddingStatePlayerView::from_bidding_state( + bidding, + current_player, + ); let bot = AlwaysPassBiddingBot {}; let bid = bot.bid(&player_view).await; table.bid(bid).await?; Ok(()) - }, + } GameState::Play(_) => todo!(), - GameState::PassedOut { dealer, deal, bidding } => todo!(), + GameState::PassedOut { + dealer, + deal, + bidding, + } => Err(BridgeError::InvalidRequest(format!("The game is over"))), } } @@ -185,13 +234,19 @@ mod test { payload: serde_json::Value, ) -> Result<(), BridgeError> { if seq != self.log.len() as i64 { - return Err(BridgeError::UpdateConflict(self.log.len() as i64, seq)); + return Err(BridgeError::UpdateConflict( + self.log.len() as i64, + seq, + )); } self.log.push(Some(payload)); Ok(()) } - async fn replay(&mut self, seq: i64) -> Result<Vec<serde_json::Value>, BridgeError> { + async fn replay( + &mut self, + seq: i64, + ) -> Result<Vec<serde_json::Value>, BridgeError> { Ok(self.log[seq as usize..] .into_iter() .filter_map(|e| e.clone()) @@ -218,7 +273,8 @@ mod test { #[tokio::test] async fn test_new_table() { - let t1: Table<TestJournal> = Table::new(Default::default()).await.unwrap(); + let t1: Table<TestJournal> = + Table::new(Default::default()).await.unwrap(); match t1.game { GameState::Bidding { .. } => (), _ => panic!("should be Bidding"), @@ -227,7 +283,8 @@ mod test { #[tokio::test] async fn test_replay_table() { - let t1: Table<TestJournal> = Table::new(Default::default()).await.unwrap(); + let t1: Table<TestJournal> = + Table::new(Default::default()).await.unwrap(); let game = t1.game; let journal = t1.journal; @@ -237,7 +294,8 @@ mod test { #[tokio::test] async fn test_advance_play() { - let mut t1: Table<TestJournal> = Table::new(Default::default()).await.unwrap(); + let mut t1: Table<TestJournal> = + Table::new(Default::default()).await.unwrap(); let player = t1.game().current_player(); advance_play(&mut t1).await.unwrap(); assert_ne!(player, t1.game().current_player()); |