summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKjetil Orbekk <kj@orbekk.com>2022-11-25 17:10:17 -0500
committerKjetil Orbekk <kj@orbekk.com>2022-11-27 17:23:11 -0500
commit685ac902e3faf4ed5a76b8c859b01f7d2e2d9ea0 (patch)
tree063eac21566c0dd83f849d7ea9cccb51a618d4de
parenteeeea174157202cb812fab04844292cbd96bfac0 (diff)
Add state machine for GameState and corresponding player view of the state
-rw-r--r--protocol/src/bridge_engine.rs188
-rw-r--r--server/src/main.rs6
-rw-r--r--server/src/play.rs7
-rw-r--r--webapp/src/components/game.rs6
-rw-r--r--webapp/src/components/table.rs9
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<Self, anyhow::Error> {
+ 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<Card>,
+pub enum GameStatePlayerView {
+ Bidding {
+ dealer: Player,
+ player_position: Player,
+ hand: Vec<Card>,
+ bidding: Bidding,
+ },
+ PassedOut {
+ dealer: Player,
+ player_position: Player,
+ deal: Deal,
+ bidding: Bidding,
+ },
+ Lead {
+ dealer: Player,
+ player_position: Player,
+ contract: Contract,
+ hand: Vec<Card>,
+ },
+ Play {
+ dealer: Player,
+ player_position: Player,
+ contract: Contract,
+ trick: Trick,
+ dummy: Vec<Card>,
+ hand: Vec<Card>,
+ },
}
-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<Card> {
+ 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"),
}
}
@@ -747,16 +857,44 @@ 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<Uuid>,
-) -> Result<Json<protocol::bridge_engine::TableView>, BridgeError> {
+) -> Result<Json<protocol::bridge_engine::GameStatePlayerView>, 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<J: Journal> Table<J> {
}
async fn init(journal: &mut J) -> Result<GameState, BridgeError> {
- 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<TestJournal> = 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<Option<TableView>> = use_state(|| None);
+ let table_state: UseStateHandle<Option<GameStatePlayerView>> = 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! {
<>
<div class="hand south">
- <Hand cards={ props.table.hand.clone() } on_card_clicked={ on_card_clicked.clone() } />
+ <Hand cards={ props.table.hand().clone() } on_card_clicked={ on_card_clicked.clone() } />
</div>
<h2>{ "Table view" }</h2>
<pre>{ format!("{:#?}", props.table) }</pre>
@@ -76,5 +77,5 @@ pub fn table(props: &TableProps) -> Html {
#[derive(PartialEq, Properties, Clone)]
pub struct TableProps {
- pub table: TableView,
+ pub table: GameStatePlayerView,
}