From 685ac902e3faf4ed5a76b8c859b01f7d2e2d9ea0 Mon Sep 17 00:00:00 2001 From: Kjetil Orbekk Date: Fri, 25 Nov 2022 17:10:17 -0500 Subject: Add state machine for GameState and corresponding player view of the state --- protocol/src/bridge_engine.rs | 188 +++++++++++++++++++++++++++++++++++------ server/src/main.rs | 6 +- server/src/play.rs | 7 +- webapp/src/components/game.rs | 6 +- webapp/src/components/table.rs | 9 +- 5 files changed, 175 insertions(+), 41 deletions(-) diff --git a/protocol/src/bridge_engine.rs b/protocol/src/bridge_engine.rs index a11eda4..5c7ce4d 100644 --- a/protocol/src/bridge_engine.rs +++ b/protocol/src/bridge_engine.rs @@ -1,8 +1,8 @@ -use crate::card::{Card, Deal, Suit, RankOrder}; -use serde::{Deserialize, Serialize}; +use crate::card::{Card, Deal, RankOrder, Suit}; use anyhow::{anyhow, bail}; use log::{error, info}; use regex::Regex; +use serde::{Deserialize, Serialize}; use std::cmp::Ordering; use std::fmt; use std::str::FromStr; @@ -11,7 +11,9 @@ use strum_macros::{EnumCount as EnumCountMacro, EnumIter, FromRepr}; 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)] +#[derive( + PartialEq, Eq, Clone, Copy, Debug, FromRepr, EnumCountMacro, Serialize, Deserialize, EnumIter, +)] #[repr(u8)] pub enum Player { West = 0, @@ -378,9 +380,9 @@ impl fmt::Display for ContractModifier { #[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] pub struct Contract { - declarer: Player, - highest_bid: Raise, - modifier: ContractModifier, + pub declarer: Player, + pub highest_bid: Raise, + pub modifier: ContractModifier, } impl fmt::Display for Contract { @@ -486,6 +488,7 @@ pub enum GameState { Bidding { dealer: Player, deal: Deal, + bidding: Bidding, }, PassedOut { dealer: Player, @@ -501,6 +504,14 @@ pub enum GameState { } impl GameState { + pub fn new(deal: Deal, dealer: Player) -> Self { + Self::Bidding { + dealer, + deal, + bidding: Bidding::new(dealer), + } + } + pub fn deal(&self) -> &Deal { match self { Self::Bidding { deal, .. } => deal, @@ -516,6 +527,51 @@ impl GameState { Self::Play { dealer, .. } => dealer, } } + + pub fn is_bidding(&self) -> bool { + if let GameState::Bidding { .. } = self { + true + } else { + false + } + } + + pub fn is_playing(&self) -> bool { + if let GameState::Play { .. } = self { + true + } else { + false + } + } + + pub fn bid(self, bid: Bid) -> Result { + let (dealer, deal, bidding) = match self { + GameState::Bidding { + dealer, + deal, + bidding, + } => (dealer, deal, bidding), + _ => bail!("not currently bidding: {self:?}"), + }; + Ok(match bidding.bid(bid)? { + BiddingResult::InProgress(bidding) => GameState::Bidding { + dealer, + deal, + bidding, + }, + BiddingResult::Contract(None, bidding) => GameState::PassedOut { + dealer, + deal, + bidding, + }, + BiddingResult::Contract(Some(contract), bidding) => GameState::Play { + dealer, + playing_deal: DealInPlay::new(contract.declarer, deal), + contract, + bidding, + }, + }) + } } pub fn deal() -> Deal { @@ -526,18 +582,72 @@ pub fn deal() -> Deal { } #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] -pub struct TableView { - pub dealer: Player, - pub player_position: Player, - pub hand: Vec, +pub enum GameStatePlayerView { + Bidding { + dealer: Player, + player_position: Player, + hand: Vec, + bidding: Bidding, + }, + PassedOut { + dealer: Player, + player_position: Player, + deal: Deal, + bidding: Bidding, + }, + Lead { + dealer: Player, + player_position: Player, + contract: Contract, + hand: Vec, + }, + Play { + dealer: Player, + player_position: Player, + contract: Contract, + trick: Trick, + dummy: Vec, + hand: Vec, + }, } -impl TableView { +impl GameStatePlayerView { pub fn from_game_state(game_state: &GameState, player_position: Player) -> Self { - TableView { - dealer: game_state.dealer(), - player_position, - hand: player_position.get_cards(game_state.deal()).clone(), + match game_state { + GameState::Bidding { + dealer, + deal, + bidding, + } => GameStatePlayerView::Bidding { + dealer: *dealer, + player_position, + bidding: bidding.clone(), + hand: player_position.get_cards(deal).clone(), + }, + GameState::PassedOut { + dealer, + deal, + bidding, + } => todo!(), + GameState::Play { + dealer, + playing_deal, + contract, + bidding, + } => todo!(), + } + } + + pub fn hand(&self) -> &Vec { + match self { + GameStatePlayerView::Bidding { hand, .. } => hand, + GameStatePlayerView::PassedOut { + deal, + player_position, + .. + } => player_position.get_cards(deal), + GameStatePlayerView::Lead { hand, .. } => hand, + GameStatePlayerView::Play { hand, .. } => hand, } } } @@ -728,12 +838,12 @@ mod tests { s.split(" ").map(mkcard).collect() } - fn _example_deal() -> Deal { + fn example_deal() -> Deal { Deal { - west: mkcards("♠5 ♢10 ♡K ♣4 ♡J ♣5 ♢5 ♠9 ♢3 ♠2 ♣2 ♡4 ♠Q"), - north: mkcards("♢Q ♡9 ♠7 ♠8 ♠A ♡A ♡5 ♠6 ♢9 ♣3 ♡3 ♣9 ♢J"), - east: mkcards("♣10 ♡7 ♢A ♣6 ♡8 ♣Q ♠K ♡10 ♣K ♠3 ♡Q ♣J ♢4"), - south: mkcards("♢K ♡6 ♣8 ♢6 ♢7 ♢8 ♣A ♡2 ♣7 ♠10 ♠4 ♠J ♢2"), + west: mkcards("♠5 ♦10 ♥K ♣4 ♥J ♣5 ♦5 ♠9 ♦3 ♠2 ♣2 ♥4 ♠Q"), + north: mkcards("♦Q ♥9 ♠7 ♠8 ♠A ♥A ♥5 ♠6 ♦9 ♣3 ♥3 ♣9 ♦J"), + east: mkcards("♣10 ♥7 A ♣6 ♥8 ♣Q ♠K ♥10 ♣K ♠3 ♥Q ♣J ♦4"), + south: mkcards("♦K ♥6 ♣8 ♦6 ♦7 ♦8 ♣A ♥2 ♣7 ♠10 ♠4 ♠J ♦2"), } } @@ -746,17 +856,45 @@ mod tests { } } + #[test] + fn game_state() { + crate::tests::test_setup(); + let game_state = GameState::new(mini_deal(), Player::North); + assert_eq!(game_state.deal(), &mini_deal()); + assert_eq!(game_state.dealer(), Player::North); + assert_eq!(game_state.is_bidding(), true); + + info!("Start bidding with game state {game_state:#?}"); + let raise = |s| Bid::Raise(Raise::from_str(s).unwrap()); + let game_state = game_state.bid(raise("1H")).unwrap(); + assert_eq!(game_state.is_bidding(), true); + let game_state = game_state + .bid(Bid::Pass) + .unwrap() + .bid(Bid::Pass) + .unwrap() + .bid(Bid::Pass) + .unwrap(); + info!("Start playing with game state {game_state:#?}"); + assert_eq!(game_state.is_bidding(), false); + assert_eq!(game_state.is_playing(), true); + } + #[test] fn table_view() { crate::tests::test_setup(); - let game_state = GameState::Bidding { dealer: Player::East, deal: mini_deal() }; + let game_state = GameState::new(mini_deal(), Player::East); info!("Game state: {game_state:?}"); for p in Player::iter() { info!("Testing view for {p:?}"); - let view = TableView::from_game_state(&game_state, p); - assert_eq!(view.player_position, p); - assert_eq!(view.dealer, Player::East); - assert_eq!(&view.hand, p.get_cards_mut(&mut mini_deal())); + let view = GameStatePlayerView::from_game_state(&game_state, p); + match view { + GameStatePlayerView::Bidding { dealer, hand, .. } => { + assert_eq!(dealer, Player::East); + assert_eq!(&hand, p.get_cards(&mini_deal())); + } + _ => panic!("expected bidding: {view:#?}"), + } } } diff --git a/server/src/main.rs b/server/src/main.rs index b8ee403..cf42b3f 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -10,7 +10,7 @@ use axum::{ Json, Router, }; use protocol::{Table, UserInfo}; -use protocol::bridge_engine::{TableView, Player}; +use protocol::bridge_engine::{GameStatePlayerView, Player}; use server::ContextExtension; use tower_cookies::{Cookie, CookieManagerLayer, Cookies}; use tower_http::trace::TraceLayer; @@ -138,12 +138,12 @@ async fn get_table_view( _session: AuthenticatedSession, extension: ContextExtension, Path(id): Path, -) -> Result, BridgeError> { +) -> Result, BridgeError> { info!("Getting table state for {id:}"); let jnl = DbJournal::new(extension.db.clone(), id); let table = play::Table::new_or_replay(jnl).await?; let response = - Json(TableView::from_game_state(table.game(), Player::South)); + Json(GameStatePlayerView::from_game_state(table.game(), Player::South)); info!("Response: {response:#?}"); Ok(response) } diff --git a/server/src/play.rs b/server/src/play.rs index 256392d..9ae3d54 100644 --- a/server/src/play.rs +++ b/server/src/play.rs @@ -91,10 +91,7 @@ impl Table { } async fn init(journal: &mut J) -> Result { - let game = GameState::Bidding { - dealer: Player::East, - deal: bridge_engine::deal(), - }; + let game = GameState::new(bridge_engine::deal(), Player::East); journal.append(0, json!(game)).await?; Ok(game) } @@ -169,7 +166,7 @@ mod test { async fn test_new_table() { let t1: Table = Table::new(Default::default()).await.unwrap(); match t1.game { - GameState::Bidding { dealer, deal } => (), + GameState::Bidding { .. } => (), _ => panic!("should be Bidding"), }; } diff --git a/webapp/src/components/game.rs b/webapp/src/components/game.rs index 9511eb2..c5e2602 100644 --- a/webapp/src/components/game.rs +++ b/webapp/src/components/game.rs @@ -4,9 +4,7 @@ use log::{error, info}; use yew::prelude::*; fn init_state() -> GameState { - let dealer = Player::East; - let deal = deal(); - GameState::Bidding { dealer, deal } + GameState::new(deal(), Player::East) } #[function_component(Game)] @@ -46,7 +44,7 @@ pub fn game() -> Html { }; let center = match &*state { - GameState::Bidding { dealer, deal } => { + GameState::Bidding { dealer, deal, bidding } => { let on_contract = { let state = state.clone(); let dealer = dealer.clone(); diff --git a/webapp/src/components/table.rs b/webapp/src/components/table.rs index 92302b2..c4f693e 100644 --- a/webapp/src/components/table.rs +++ b/webapp/src/components/table.rs @@ -1,6 +1,6 @@ use gloo_net::http::Request; use log::info; -use protocol::bridge_engine::TableView; +use protocol::bridge_engine::GameStatePlayerView; use yew::prelude::*; use crate::use_app_context; use crate::components::Hand; @@ -9,7 +9,7 @@ use crate::components::Hand; pub fn online_table(props: &OnlineTableProps) -> Html { let ctx = use_app_context(); - let table_state: UseStateHandle> = use_state(|| None); + let table_state: UseStateHandle> = use_state(|| None); { // TODO update this from server state let table_state = table_state.clone(); @@ -21,6 +21,7 @@ pub fn online_table(props: &OnlineTableProps) -> Html { let response = Request::get(&format!("/api/table/{}", props.table.id)) .send() .await?; + // info!("Got response: {:#?}", response.body()); let table = response.json().await?; table_state.set(Some(table)); Ok(()) @@ -66,7 +67,7 @@ pub fn table(props: &TableProps) -> Html { html! { <>
- +

{ "Table view" }

{ format!("{:#?}", props.table) }
@@ -76,5 +77,5 @@ pub fn table(props: &TableProps) -> Html { #[derive(PartialEq, Properties, Clone)] pub struct TableProps { - pub table: TableView, + pub table: GameStatePlayerView, } -- cgit v1.2.3