summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKjetil Orbekk <kj@orbekk.com>2023-01-01 11:52:28 -0500
committerKjetil Orbekk <kj@orbekk.com>2023-01-01 11:52:28 -0500
commitbb2ed3a2926384df063e476d10613fa310cd7ffa (patch)
treecc9c6ea4979eef3850d78cd0b1390dfbccb5921b
parent1e3014a777805d3dcb691ee6ebe59c62f58f8222 (diff)
Add Table to be used with db schema
-rw-r--r--protocol/src/bridge_engine.rs11
-rw-r--r--server/migrations/20221008120534_init.down.sql4
-rw-r--r--server/migrations/20221008120534_init.up.sql22
-rw-r--r--server/src/lib.rs21
-rw-r--r--server/src/main.rs48
-rw-r--r--server/src/table.rs115
-rw-r--r--server/tests/table_test.rs26
-rw-r--r--sqlx-data.json40
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": {