From 42f6ef6d44e893b47e5e2a49496b5dd2122df232 Mon Sep 17 00:00:00 2001 From: Kjetil Orbekk Date: Thu, 22 Dec 2022 08:31:35 -0500 Subject: Add a simple bot that plays random (legal) cards - Leads random cards - Follows suit if possible --- protocol/src/bot.rs | 8 +-- protocol/src/bridge_engine.rs | 97 ++++++++++++++++++++++++--------- protocol/src/card.rs | 5 ++ protocol/src/simple_bots.rs | 123 ++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 201 insertions(+), 32 deletions(-) diff --git a/protocol/src/bot.rs b/protocol/src/bot.rs index cc676df..250731e 100644 --- a/protocol/src/bot.rs +++ b/protocol/src/bot.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; -use crate::{bridge_engine::{BiddingStatePlayerView, Bid}, card::Card}; +use crate::{bridge_engine::{BiddingStatePlayerView, Bid, PlayStatePlayerView}, card::Card}; #[async_trait] pub trait BiddingBot { @@ -9,7 +9,7 @@ pub trait BiddingBot { #[async_trait] pub trait PlayingBot { - async fn play(&self) -> Card { - todo!() - } + // TODO: May need a PlayStateBotView here to expose past cards played, + // to avoid the need for stateful bots. + async fn play(&self, play_state: &PlayStatePlayerView) -> Card; } diff --git a/protocol/src/bridge_engine.rs b/protocol/src/bridge_engine.rs index 50f619b..35eb90f 100644 --- a/protocol/src/bridge_engine.rs +++ b/protocol/src/bridge_engine.rs @@ -3,7 +3,8 @@ use anyhow::{anyhow, bail}; use log::{error, info}; use rand::{ distributions::Standard, - prelude::{Distribution, SliceRandom}, random, + prelude::{Distribution, SliceRandom}, + random, }; use regex::Regex; use serde::{Deserialize, Serialize}; @@ -140,7 +141,7 @@ impl TurnInPlay { TurnInPlayResult::InProgress(self) } - pub fn next_player(&self) -> Player { + pub fn current_player(&self) -> Player { self.trick.leader.many_next(self.trick.cards_played.len()) } } @@ -179,11 +180,15 @@ impl DealInPlay { &self.deal } + pub fn current_player(&self) -> Player { + self.in_progress.current_player() + } + pub fn play( mut self: Self, card: Card, ) -> Result { - let player = self.in_progress.next_player(); + let player = self.current_player(); let player_cards = player.get_cards_mut(&mut self.deal); info!( @@ -604,8 +609,8 @@ pub struct PlayState { impl PlayState { pub fn new(deal: Deal, contract: Contract, bidding: Bidding) -> Self { - let playing_deal = DealInPlay::new(contract.declarer.many_next(3), - deal.clone()); + let playing_deal = + DealInPlay::new(contract.declarer.many_next(3), deal.clone()); Self { deal, contract, @@ -617,6 +622,36 @@ impl PlayState { pub fn dealer(&self) -> Player { self.bidding.dealer } + + pub fn current_player(&self) -> Player { + self.playing_deal.current_player() + } + + pub fn play( + mut self, + card: Card, + ) -> Result { + Ok(match self.playing_deal.play(card)? { + DealInPlayResult::InProgress(playing_deal) => { + PlayStateResult::InProgress(Self { + playing_deal, + ..self + }) + } + DealInPlayResult::PlayFinished(_) => { + PlayStateResult::PlayFinished(PlayResult) + } + }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct PlayResult; + +#[derive(Debug)] +pub enum PlayStateResult { + InProgress(PlayState), + PlayFinished(PlayResult), } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -708,8 +743,7 @@ impl GameState { bidding, }, BiddingResult::Contract(Some(contract), bidding) => { - GameState::Play( - PlayState::new(deal, contract, bidding)) + GameState::Play(PlayState::new(deal, contract, bidding)) } }) } @@ -752,15 +786,15 @@ impl Distribution for Standard { #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] pub struct PlayStatePlayerView { - player_position: Player, - bidding: Bidding, - contract: Contract, + pub player_position: Player, + pub bidding: Bidding, + pub contract: Contract, // If None, the lead has not been played. - dummy: Option>, - declarer_tricks: u8, - hand: Vec, - previous_trick: Trick, - current_trick: TurnInPlay, + pub dummy: Option>, + pub declarer_tricks: u8, + pub hand: Vec, + pub previous_trick: Trick, + pub current_trick: TurnInPlay, } impl PlayStatePlayerView { @@ -774,11 +808,22 @@ impl PlayStatePlayerView { contract: play_state.contract, dummy: None, declarer_tricks: 0, - hand: vec!(), - previous_trick: Trick { leader: random(), cards_played: vec!() }, - current_trick: TurnInPlay::new(random()), + hand: player_position.get_cards(&play_state.deal).clone(), + previous_trick: Trick { + leader: random(), + cards_played: vec![], + }, + 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)] @@ -1001,13 +1046,13 @@ mod tests { #[test] fn play_turn() { let turn = TurnInPlay::new(Player::South); - assert_eq!(turn.next_player(), Player::South); + assert_eq!(turn.current_player(), Player::South); let turn = as_turn(turn.play("♣4".parse().unwrap())); - assert_eq!(turn.next_player(), Player::West); + assert_eq!(turn.current_player(), Player::West); let turn = as_turn(turn.play("♥A".parse().unwrap())); - assert_eq!(turn.next_player(), Player::North); + assert_eq!(turn.current_player(), Player::North); let turn = as_turn(turn.play("♣4".parse().unwrap())); - assert_eq!(turn.next_player(), Player::East); + assert_eq!(turn.current_player(), Player::East); let trick = as_trick(turn.play("♣A".parse().unwrap())); assert_eq!( trick, @@ -1147,8 +1192,12 @@ mod tests { 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()); + 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: DealInPlayResult) -> DealInPlay { diff --git a/protocol/src/card.rs b/protocol/src/card.rs index 3d6081d..b6b57e7 100644 --- a/protocol/src/card.rs +++ b/protocol/src/card.rs @@ -124,6 +124,11 @@ impl std::str::FromStr for Rank { #[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] pub struct Card(pub Suit, pub Rank); +impl Card { + pub fn suit(&self) -> Suit { self.0 } + pub fn rank(&self) -> Rank { self.1 } +} + impl fmt::Display for Card { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { let Card(suit, rank) = self; diff --git a/protocol/src/simple_bots.rs b/protocol/src/simple_bots.rs index b7986b6..7cc2ce8 100644 --- a/protocol/src/simple_bots.rs +++ b/protocol/src/simple_bots.rs @@ -1,6 +1,14 @@ use async_trait::async_trait; +use rand::{ + prelude::{IteratorRandom, SliceRandom}, + thread_rng, +}; -use crate::{bot::BiddingBot, bridge_engine::{BiddingStatePlayerView, Bid}}; +use crate::{ + bot::{BiddingBot, PlayingBot}, + bridge_engine::{Bid, BiddingStatePlayerView, PlayStatePlayerView}, + card::Card, +}; pub struct AlwaysPassBiddingBot {} @@ -11,12 +19,113 @@ impl BiddingBot for AlwaysPassBiddingBot { } } +pub struct RandomPlayingBot {} + +#[async_trait] +impl PlayingBot for RandomPlayingBot { + async fn play(&self, state: &PlayStatePlayerView) -> Card { + let mut rng = thread_rng(); + if let Some(suit) = state.current_trick.suit() { + if let Some(card) = state + .hand + .iter() + .filter(|c| c.suit() == suit) + .choose(&mut rng) + { + return *card; + } + } + + *state + .hand + .choose(&mut rng) + .expect("must have at least one card") + } +} + #[cfg(test)] mod tests { + use std::str::FromStr; + use super::*; + use crate::{ + bridge_engine::{ + Bidding, BiddingState, BiddingStatePlayerView, Contract, + ContractLevel, ContractModifier, Deal, PlayState, PlayStateResult, + Player, Raise, + }, + card::Suit, + }; use log::info; use rand::random; - use crate::bridge_engine::{BiddingState, Bidding, BiddingStatePlayerView}; + + #[tokio::test] + async fn random_playing_bot() { + crate::tests::test_setup(); + let play_state = example_play_state(); + info!("Play state: {play_state:#?}"); + + let south_state = + PlayStatePlayerView::from_play_state(&play_state, Player::South); + let card1 = (RandomPlayingBot {}).play(&south_state).await; + info!("South state: {south_state:#?}"); + + let play_state = match play_state.play(card1).unwrap() { + PlayStateResult::InProgress(p) => p, + PlayStateResult::PlayFinished(_) => { + panic!("game should not be over") + } + }; + + let west_state = + PlayStatePlayerView::from_play_state(&play_state, Player::West); + let card2 = (RandomPlayingBot {}).play(&west_state).await; + info!("West state: {west_state:#?}"); + + assert_eq!(card1.suit(), card2.suit()); + + let play_state = match play_state.play(card2).unwrap() { + PlayStateResult::InProgress(p) => p, + PlayStateResult::PlayFinished(_) => { + panic!("game should not be over") + } + }; + } + + 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 { + 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 example_play_state() -> PlayState { + let deal = example_deal(); + let raise1c = Raise { + level: ContractLevel::One, + suit: Some(Suit::Club), + }; + let contract = Contract { + declarer: Player::West, + 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) + } #[tokio::test] async fn always_passing_bot_passes() { @@ -28,8 +137,14 @@ mod tests { deal: random(), bidding: Bidding::new(dealer), }; - let player_view = BiddingStatePlayerView::from_bidding_state(&bidding_state, player_position); + let player_view = BiddingStatePlayerView::from_bidding_state( + &bidding_state, + player_position, + ); info!("Bidding state: {bidding_state:#?}"); - assert_eq!(Bid::Pass, (AlwaysPassBiddingBot {}).bid(&player_view).await); + assert_eq!( + Bid::Pass, + (AlwaysPassBiddingBot {}).bid(&player_view).await + ); } } -- cgit v1.2.3