summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKjetil Orbekk <kj@orbekk.com>2022-12-22 08:31:35 -0500
committerKjetil Orbekk <kj@orbekk.com>2022-12-22 08:31:35 -0500
commit42f6ef6d44e893b47e5e2a49496b5dd2122df232 (patch)
treedff2f84bbc41295b27deb2397071cb16bb742058
parent10ecb9e30568bf20287b053a620252d7a80dbd6b (diff)
Add a simple bot that plays random (legal) cards
- Leads random cards - Follows suit if possible
-rw-r--r--protocol/src/bot.rs8
-rw-r--r--protocol/src/bridge_engine.rs97
-rw-r--r--protocol/src/card.rs5
-rw-r--r--protocol/src/simple_bots.rs123
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<DealInPlayResult, anyhow::Error> {
- 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<PlayStateResult, anyhow::Error> {
+ 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<Deal> 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<Vec<Card>>,
- declarer_tricks: u8,
- hand: Vec<Card>,
- previous_trick: Trick,
- current_trick: TurnInPlay,
+ pub dummy: Option<Vec<Card>>,
+ pub declarer_tricks: u8,
+ pub hand: Vec<Card>,
+ 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<Card> {
+ 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
+ );
}
}