diff options
author | Kjetil Orbekk <kj@orbekk.com> | 2023-01-01 11:52:28 -0500 |
---|---|---|
committer | Kjetil Orbekk <kj@orbekk.com> | 2023-01-01 11:52:28 -0500 |
commit | bb2ed3a2926384df063e476d10613fa310cd7ffa (patch) | |
tree | cc9c6ea4979eef3850d78cd0b1390dfbccb5921b | |
parent | 1e3014a777805d3dcb691ee6ebe59c62f58f8222 (diff) |
Add Table to be used with db schema
-rw-r--r-- | protocol/src/bridge_engine.rs | 11 | ||||
-rw-r--r-- | server/migrations/20221008120534_init.down.sql | 4 | ||||
-rw-r--r-- | server/migrations/20221008120534_init.up.sql | 22 | ||||
-rw-r--r-- | server/src/lib.rs | 21 | ||||
-rw-r--r-- | server/src/main.rs | 48 | ||||
-rw-r--r-- | server/src/table.rs | 115 | ||||
-rw-r--r-- | server/tests/table_test.rs | 26 | ||||
-rw-r--r-- | sqlx-data.json | 40 |
8 files changed, 228 insertions, 59 deletions
diff --git a/protocol/src/bridge_engine.rs b/protocol/src/bridge_engine.rs index 44e5c91..d04bdf0 100644 --- a/protocol/src/bridge_engine.rs +++ b/protocol/src/bridge_engine.rs @@ -1009,6 +1009,17 @@ impl Default for TableState { } } +impl TryFrom<TableState> for GameState { + type Error = anyhow::Error; + + fn try_from(value: TableState) -> Result<Self, Self::Error> { + match value { + TableState::Game(game) => Ok(game), + _ => Err(anyhow::anyhow!("no game")), + } + } +} + impl From<MoveResult<GameState, PlayResult>> for TableState { fn from(val: MoveResult<GameState, PlayResult>) -> Self { match val { diff --git a/server/migrations/20221008120534_init.down.sql b/server/migrations/20221008120534_init.down.sql index 3bff171..c57e653 100644 --- a/server/migrations/20221008120534_init.down.sql +++ b/server/migrations/20221008120534_init.down.sql @@ -2,8 +2,10 @@ begin; drop table if exists sessions; drop table if exists table_players; +drop table if exists table_moves; +drop table if exists table_boards; drop table if exists object_journal; -drop table if exists active_tables; +drop table if exists bridge_table; drop table if exists players; drop type if exists player_position; drop type if exists suit; diff --git a/server/migrations/20221008120534_init.up.sql b/server/migrations/20221008120534_init.up.sql index 05b7697..b8e8470 100644 --- a/server/migrations/20221008120534_init.up.sql +++ b/server/migrations/20221008120534_init.up.sql @@ -12,7 +12,7 @@ create table sessions ( last_refresh timestamp with time zone not null default now() ); -create table active_tables ( +create table bridge_table ( id uuid primary key not null ); @@ -29,9 +29,25 @@ create table object_journal ( create unique index journal_entry on object_journal (id, seq); create table table_players ( - active_tables_id uuid not null references active_tables (id), + table_id uuid not null references bridge_table (id), player_id varchar(64) not null references players (id), position player_position, - primary key(active_tables_id, player_id, position) + primary key(table_id, player_id, position) ); create unique index player_table on table_players (player_id); + +create table table_boards ( + table_id uuid not null references bridge_table (id), + board_number integer not null, + deal jsonb not null, + primary key(table_id, board_number) +); + +create table table_moves ( + table_id uuid not null, + board_number integer not null, + move_number integer not null, + move jsonb not null, + foreign key (table_id, board_number) references table_boards (table_id, board_number), + primary key(table_id, board_number, move_number) +); diff --git a/server/src/lib.rs b/server/src/lib.rs new file mode 100644 index 0000000..6ca9e49 --- /dev/null +++ b/server/src/lib.rs @@ -0,0 +1,21 @@ +pub mod auth; +pub mod error; +#[cfg(debug_assertions)] +pub mod fake_auth; +pub mod play; +pub mod server; +pub mod table; + +#[cfg(test)] +mod tests { + use env_logger::Env; + + pub fn test_setup() { + dotenv::dotenv().ok(); + let _ = env_logger::Builder::from_env( + Env::default().default_filter_or("info"), + ) + .is_test(true) + .try_init(); + } +} diff --git a/server/src/main.rs b/server/src/main.rs index 10b1361..3e3985f 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -2,7 +2,7 @@ use serde_json::json; use std::{collections::HashMap, env, str::FromStr, sync::Arc}; use uuid::Uuid; -use auth::AuthenticatedSession; +use server::auth::AuthenticatedSession; use axum::{ extract::{Path, Query, State}, response::{Html, Redirect}, @@ -14,27 +14,18 @@ use protocol::{ card::Card, }; use protocol::{Table, UserInfo}; -use server::ServerState; +use server::server::ServerState; use tower_cookies::{Cookie, CookieManagerLayer, Cookies}; use tower_http::trace::TraceLayer; use tracing::{info, log::warn}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; -mod auth; -mod error; -#[cfg(debug_assertions)] -mod fake_auth; -mod play; -mod server; -use crate::{ - auth::{OauthAuthenticator, SessionId}, +use server::{ + auth::{OauthAuthenticator, SessionId, Authenticator}, play::advance_play, server::ServerContext, -}; -use crate::{ error::BridgeError, play::{DbJournal, Journal}, }; -use auth::Authenticator; use sqlx::{postgres::PgPoolOptions, PgPool}; async fn create_default_authenticator( @@ -51,7 +42,7 @@ async fn create_authenticator( if std::env::var("AUTHENTICATOR").unwrap_or("".to_string()) == FAKE_AUTHENTICATOR { - Box::new(fake_auth::FakeAuthenticator::new()) + Box::new(server::fake_auth::FakeAuthenticator::new()) } else { create_default_authenticator(db_pool).await } @@ -119,7 +110,7 @@ async fn main() { .route("/api/table/:id/play", post(post_play)) .route("/api/table/:id/admin/deal", post(table_new_deal)) .route("/api/login", get(login)) - .route(auth::LOGIN_CALLBACK, get(login_callback)) + .route(server::auth::LOGIN_CALLBACK, get(login_callback)) .layer(CookieManagerLayer::new()) .layer(TraceLayer::new_for_http()) .with_state(state); @@ -155,7 +146,7 @@ async fn get_table_view( info!("Getting table state for {id:}"); let player_position = Player::South; let jnl = DbJournal::new(state.db.clone(), id); - let mut table = play::Table::new_or_replay(jnl).await?; + let mut table = server::play::Table::new_or_replay(jnl).await?; info!("Advancing play"); while table.game_in_progress() && table.game()?.current_player() != player_position @@ -179,7 +170,7 @@ async fn table_new_deal( ) -> Result<Json<()>, BridgeError> { info!("Getting table state for {id:}"); let jnl = DbJournal::new(state.db.clone(), id); - let mut table = play::Table::replay(jnl).await?; + let mut table = server::play::Table::replay(jnl).await?; table.new_deal().await?; Ok(Json(())) } @@ -192,7 +183,7 @@ async fn post_bid( ) -> Result<Json<()>, BridgeError> { info!("Getting table state for {id:}"); let jnl = DbJournal::new(state.db.clone(), id); - let mut table = play::Table::replay(jnl).await?; + let mut table = server::play::Table::replay(jnl).await?; table.bid(bid).await?; Ok(Json(())) } @@ -205,7 +196,7 @@ async fn post_play( ) -> Result<Json<()>, BridgeError> { info!("Getting table state for {id:}"); let jnl = DbJournal::new(state.db.clone(), id); - let mut table = play::Table::replay(jnl).await?; + let mut table = server::play::Table::replay(jnl).await?; table.play(card).await?; Ok(Json(())) } @@ -232,7 +223,7 @@ async fn create_table( let txn = state.db.begin().await?; let table_id = sqlx::query!( r#" - insert into active_tables (id) + insert into bridge_table (id) values ($1) returning id "#, @@ -244,7 +235,7 @@ async fn create_table( sqlx::query!( r#" - insert into table_players (active_tables_id, + insert into table_players (table_id, player_id, position) values ($1, $2, 'south') @@ -282,7 +273,7 @@ async fn user_table( r#" select tables.id from table_players players - natural join active_tables tables + natural join bridge_table tables where player_id = $1 "#, session.player_id @@ -318,16 +309,3 @@ async fn login(cookies: Cookies, State(state): ServerState) -> Redirect { Redirect::temporary(auth_url.as_str()) } -#[cfg(test)] -mod tests { - use env_logger::Env; - - pub fn test_setup() { - dotenv::dotenv().ok(); - let _ = env_logger::Builder::from_env( - Env::default().default_filter_or("info"), - ) - .is_test(true) - .try_init(); - } -} diff --git a/server/src/table.rs b/server/src/table.rs new file mode 100644 index 0000000..068fb24 --- /dev/null +++ b/server/src/table.rs @@ -0,0 +1,115 @@ +use async_trait::async_trait; +use protocol::{ + bot::{BiddingBot, PlayingBot}, + bridge_engine::{ + Bid, BiddingStatePlayerView, GameState, PlayStatePlayerView, TableState, + }, + card::Card, + simple_bots::{AlwaysPassBiddingBot, RandomPlayingBot}, +}; +use rand::random; + +use crate::error::BridgeError; + +#[async_trait] +pub trait Table { + fn state(&self) -> &TableState; + async fn bid( + self: Box<Self>, + bid: Bid, + ) -> Result<Box<dyn Table>, BridgeError>; + async fn play( + self: Box<Self>, + card: Card, + ) -> Result<Box<dyn Table>, BridgeError>; + async fn new_deal(self: Box<Self>) -> Result<Box<dyn Table>, BridgeError>; +} + +pub struct InMemoryTable { + pub state: TableState, +} + +impl InMemoryTable { + pub fn new() -> Self { + Self { + state: TableState::Unknown, + } + } +} + +#[async_trait] +impl Table for InMemoryTable { + fn state(&self) -> &TableState { + &self.state + } + + async fn bid( + self: Box<Self>, + bid: Bid, + ) -> Result<Box<dyn Table>, BridgeError> { + let game = match self.state { + TableState::Game(game) => game, + _ => { + return Err(BridgeError::InvalidRequest("no game".to_string())) + } + }; + let game = game.bid(bid)?; + Ok(Box::new(Self { state: game.into() })) + } + + async fn play( + self: Box<Self>, + card: Card, + ) -> Result<Box<dyn Table>, BridgeError> { + let game = match self.state { + TableState::Game(game) => game, + _ => { + return Err(BridgeError::InvalidRequest("no game".to_string())) + } + }; + let game = game.play(card)?; + Ok(Box::new(Self { state: game.into() })) + } + + async fn new_deal(self: Box<Self>) -> Result<Box<dyn Table>, BridgeError> { + Ok(Box::new(Self { + state: GameState::new(random(), random()).into(), + })) + } +} + +pub async fn advance_play( + table: Box<dyn Table>, +) -> Result<Box<dyn Table>, BridgeError> { + let game = match table.state() { + TableState::Game(game) => game, + _ => return Err(BridgeError::InvalidRequest("no game".to_string())), + }; + let table = match game { + GameState::Bidding(ref bidding) => { + let player_view = BiddingStatePlayerView::from_bidding_state( + &bidding, + game.current_player(), + ); + let bot = AlwaysPassBiddingBot {}; + let bid = bot.bid(&player_view).await; + table.bid(bid).await + } + GameState::Play(game) => { + let player_view = PlayStatePlayerView::from_play_state( + &game, + game.current_player(), + ); + let bot = RandomPlayingBot {}; + let card = bot.play(&player_view).await; + table.play(card).await + } + }; + table +} + +// pub struct DbTable +// { +// db: PgPool, +// pub state: TableState, +// } diff --git a/server/tests/table_test.rs b/server/tests/table_test.rs new file mode 100644 index 0000000..4d80f32 --- /dev/null +++ b/server/tests/table_test.rs @@ -0,0 +1,26 @@ +use protocol::{card::{Rank, Suit}, bridge_engine::TableState}; +use server::table::{Table, InMemoryTable}; + +mod common; + +async fn table_basic_test(table: Box<dyn Table>) -> Result<(), anyhow::Error> { + assert!(matches!(table.state(), TableState::Unknown)); + let mut table = table.new_deal().await?; + assert!(matches!(table.state(), TableState::Game(_))); + while matches!(table.state(), TableState::Game(_)) { + table = server::table::advance_play(table).await?; + } + assert!(matches!(table.state(), TableState::Result(_))); + table = table.new_deal().await?; + assert!(matches!(table.state(), TableState::Game(_))); + + Ok(()) +} + +#[tokio::test] +#[ignore] +async fn in_memory_table() -> Result<(), anyhow::Error> { + common::test_setup(); + table_basic_test(Box::new(InMemoryTable::new())).await?; + Ok(()) +} diff --git a/sqlx-data.json b/sqlx-data.json index a0a5c19..60c58a4 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -1,6 +1,6 @@ { "db": "PostgreSQL", - "05a3ad4f0b6c1c34d120e2fbf37927259ffbcac0b6b4c6e9dc5864cef9ce6640": { + "0c7dd3e0a78d04fce27f2162f8087a4c53227d8610802f2b322cd535f0315adc": { "describe": { "columns": [], "nullable": [], @@ -11,7 +11,7 @@ ] } }, - "query": "\n insert into table_players (active_tables_id,\n player_id,\n position)\n values ($1, $2, 'south')\n " + "query": "\n insert into table_players (table_id,\n player_id,\n position)\n values ($1, $2, 'south')\n " }, "26fc6af83759bf876a88aebcdf36b5282bdd354f09a7ecdc2e21f9418874ae41": { "describe": { @@ -129,7 +129,7 @@ }, "query": "\n select * from sessions\n where id = $1\n " }, - "9c334e7646337f746e885253c8942750ed49ddb7fb0860c75afa6f8430dbc560": { + "567087d6306c95f02e79d2c841d090f3ea759f764e10737a332eb35b6e1b4dff": { "describe": { "columns": [ { @@ -143,25 +143,13 @@ ], "parameters": { "Left": [ - "Uuid" - ] - } - }, - "query": "\n insert into active_tables (id)\n values ($1)\n returning id\n " - }, - "b87f3b68e682f1db7552db6b18e9fb2ba208a47b526cc764f740b9c687d75992": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Varchar" + "Text" ] } }, - "query": "\n insert into players (id)\n values ($1)\n on conflict do nothing\n " + "query": "\n select tables.id\n from table_players players\n natural join bridge_table tables\n where player_id = $1\n " }, - "bab6adb0a18c6dcb8cd61b19c84347a2495c09fa12229299d6a64c1a01a38395": { + "5cf70f99c659838c9a30c10aa53c9ef13c26f02a2c421809b9c1d84bfb17f0f6": { "describe": { "columns": [ { @@ -175,11 +163,23 @@ ], "parameters": { "Left": [ - "Text" + "Uuid" + ] + } + }, + "query": "\n insert into bridge_table (id)\n values ($1)\n returning id\n " + }, + "b87f3b68e682f1db7552db6b18e9fb2ba208a47b526cc764f740b9c687d75992": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar" ] } }, - "query": "\n select tables.id\n from table_players players\n natural join active_tables tables\n where player_id = $1\n " + "query": "\n insert into players (id)\n values ($1)\n on conflict do nothing\n " }, "c343b2efe469200c56d080c82caead4a6ca48a344dde344561de81ebf6343747": { "describe": { |