diff --git a/Cargo.lock b/Cargo.lock index 1da8b38..b9533db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,7 +189,7 @@ dependencies = [ [[package]] name = "backbone-lib" -version = "0.2.18" +version = "0.2.16" dependencies = [ "bytes", "ewebsock", @@ -2658,7 +2658,7 @@ dependencies = [ [[package]] name = "protocol" -version = "0.2.18" +version = "0.2.16" dependencies = [ "serde", ] @@ -2911,7 +2911,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "relay-server" -version = "0.2.18" +version = "0.2.16" dependencies = [ "argon2", "axum", @@ -3921,7 +3921,7 @@ dependencies = [ [[package]] name = "trictrac-store" -version = "0.2.18" +version = "0.2.16" dependencies = [ "anyhow", "base64 0.21.7", @@ -3934,7 +3934,7 @@ dependencies = [ [[package]] name = "trictrac-web" -version = "0.2.18" +version = "0.2.16" dependencies = [ "backbone-lib", "futures", diff --git a/Cargo.toml b/Cargo.toml index 7744c11..f8efdb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace.package] -version = "0.2.19" +version = "0.2.18" [workspace] resolver = "2" diff --git a/README.md b/README.md index 46930e0..2094edb 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,14 @@ just dev Open a browser window at `http://127.0.0.1:9091`. You can play against a very basic bot, or invite an other player to connect at the same address. +## Code structure + +- game rules and game state are implemented in the _store/_ folder. +- a server for the network game is implemented in _server/relay-server_, which uses _server/protocol_ +- the web client is in _clients/web_, it connects to the server using the _clients/backbone-lib_ library +- the command-line application is implemented in _clients/cli/_; it allows you to play against a bot, or to have two bots play against each other +- the bots algorithms and the training of their models are implemented in the _bot/_ and _spiel_bot_ folders. This is a work in progress, they are not performant at all. + ## Inspirations The multiplayer game architecture, implemented in packages _clients/backbone-lib_, _clients/web/game_, _server/protocol_ and _server/relay-server_ is a [Leptos](https://leptos.dev/)-optimized adaptation of the macroquad-based [Carbonfreezer/multiplayer](https://github.com/Carbonfreezer/multiplayer) project. diff --git a/clients/web/assets/style.css b/clients/web/assets/style.css index b111501..665aac9 100644 --- a/clients/web/assets/style.css +++ b/clients/web/assets/style.css @@ -342,6 +342,7 @@ a:hover { text-decoration: underline; } .portal-danger-zone { border: 1px solid rgba(122, 30, 42, 0.4); + background: rgba(122, 30, 42, 0.04); } .portal-danger-zone h2 { color: var(--ui-red-accent); diff --git a/clients/web/src/api.rs b/clients/web/src/api.rs index 9d20050..2452b67 100644 --- a/clients/web/src/api.rs +++ b/clients/web/src/api.rs @@ -188,22 +188,6 @@ pub async fn get_user_games(username: &str, page: i64) -> Result Result<(), String> { - let body = serde_json::json!({ "result": result, "outcome": outcome }); - let resp = gloo_net::http::Request::post(&url("/games/bot-result")) - .credentials(web_sys::RequestCredentials::Include) - .json(&body) - .map_err(|e| e.to_string())? - .send() - .await - .map_err(|e| e.to_string())?; - if resp.status() == 200 { - Ok(()) - } else { - Err(format!("status {}", resp.status())) - } -} - pub async fn get_game_detail(id: i64) -> Result { let resp = gloo_net::http::Request::get(&url(&format!("/games/{id}"))) .credentials(web_sys::RequestCredentials::Include) diff --git a/clients/web/src/app.rs b/clients/web/src/app.rs index b5ebe4e..d490e4c 100644 --- a/clients/web/src/app.rs +++ b/clients/web/src/app.rs @@ -293,19 +293,12 @@ pub fn App() -> impl IntoView { pending, player_name.clone(), backend, - auth_username, ) .await } None => { - run_local_bot_game( - screen, - &mut cmd_rx, - pending, - player_name.clone(), - auth_username, - ) - .await + run_local_bot_game(screen, &mut cmd_rx, pending, player_name.clone()) + .await } }; if !restart { diff --git a/clients/web/src/game/session.rs b/clients/web/src/game/session.rs index 33f5c42..aa6e86c 100644 --- a/clients/web/src/game/session.rs +++ b/clients/web/src/game/session.rs @@ -1,10 +1,8 @@ use futures::channel::mpsc; use leptos::prelude::*; -use leptos::task::spawn_local; use backbone_lib::traits::{BackEndArchitecture, BackendCommand}; -use crate::api; use crate::app::{GameUiState, NetCommand, PauseReason, Screen}; use crate::game::trictrac::backend::TrictracBackend; use crate::game::trictrac::bot_local::bot_decide; @@ -20,7 +18,6 @@ pub async fn run_local_bot_game( cmd_rx: &mut mpsc::UnboundedReceiver, pending: RwSignal>, player_name: String, - auth_username: RwSignal>, ) -> bool { let mut backend = TrictracBackend::new(0); backend.player_arrival(0); @@ -52,7 +49,7 @@ pub async fn run_local_bot_game( suppress_dice_anim: false, })); - run_local_bot_game_loop(screen, cmd_rx, pending, player_name, backend, vs, auth_username).await + run_local_bot_game_loop(screen, cmd_rx, pending, player_name, backend, vs).await } /// Runs a bot game from a pre-built backend and initial ViewState (used for snapshot replay). @@ -63,7 +60,6 @@ pub async fn run_local_bot_game_with_backend( pending: RwSignal>, player_name: String, backend: TrictracBackend, - auth_username: RwSignal>, ) -> bool { let mut vs = backend.get_view_state().clone(); patch_bot_names(&mut vs, &player_name); @@ -80,7 +76,7 @@ pub async fn run_local_bot_game_with_backend( suppress_dice_anim: false, })); - run_local_bot_game_loop(screen, cmd_rx, pending, player_name, backend, vs, auth_username).await + run_local_bot_game_loop(screen, cmd_rx, pending, player_name, backend, vs).await } async fn run_local_bot_game_loop( @@ -90,10 +86,8 @@ async fn run_local_bot_game_loop( player_name: String, mut backend: TrictracBackend, mut vs: ViewState, - auth_username: RwSignal>, ) -> bool { use futures::StreamExt; - let mut result_submitted = false; loop { match cmd_rx.next().await { Some(NetCommand::Action(action)) => { @@ -158,19 +152,6 @@ async fn run_local_bot_game_loop( } } } - - if vs.stage == SerStage::Ended && !result_submitted { - result_submitted = true; - if let Some(_) = auth_username.get_untracked() { - let scores = vs.scores.clone(); - spawn_local(async move { - let (h0, h1) = (scores[0].holes, scores[1].holes); - let outcome = if h0 > h1 { "win" } else if h0 < h1 { "loss" } else { "draw" }; - let result_str = format!("{} - {}", h0, h1); - let _ = api::submit_bot_game_result(result_str, outcome.to_string()).await; - }); - } - } } } diff --git a/clients/web/src/portal/profile.rs b/clients/web/src/portal/profile.rs index fbdfec1..ac11bd6 100644 --- a/clients/web/src/portal/profile.rs +++ b/clients/web/src/portal/profile.rs @@ -216,7 +216,6 @@ fn GamesTable(games: Vec, page: RwSignal) -> impl IntoView { {rows.into_iter().map(|g| { let started = api::format_ts(g.started_at, locale_tag, &api::DateFormatOptions::date_only()); let ended = g.ended_at.map(|ts| api::format_ts(ts, locale_tag, &api::DateFormatOptions::date_only())).unwrap_or_else(|| "—".into()); - let room_display = if g.room_code == "bot" { "vs Bot".to_string() } else { g.room_code.clone() }; let outcome_class = match g.outcome.as_deref() { Some("win") => "outcome-win", Some("loss") => "outcome-loss", @@ -231,7 +230,7 @@ fn GamesTable(games: Vec, page: RwSignal) -> impl IntoView { }; view! { - { room_display } + { g.room_code.clone() } { started } { ended } { outcome_text } diff --git a/flake.nix b/flake.nix index 14d0b2d..cce29d4 100644 --- a/flake.nix +++ b/flake.nix @@ -39,7 +39,7 @@ frontendCargoDeps = rustPlatform.fetchCargoVendor { src = ./.; name = "trictrac-frontend-vendor"; - hash = "sha256-CSz5FJdBdVYf5pxqhtKprchlYt8jsZY2dFzhDh+nL4U="; + hash = "sha256-HJ9iUzsaMKapKb9DH0aoX7keuShq5fnHk1TSvNlw2CQ="; }; # Must match the wasm-bindgen version in Cargo.lock wasm-bindgen-version = "0.2.118"; @@ -102,7 +102,7 @@ trictrac = with final; rustPlatform.buildRustPackage { pname = "trictrac"; - version = "0.2.19"; # trictrac-version + version = "0.2.16"; # trictrac-version src = ./.; nativeBuildInputs = [ pkg-config ]; diff --git a/module.nix b/module.nix index 2f38d5b..28bec85 100644 --- a/module.nix +++ b/module.nix @@ -202,7 +202,6 @@ in DATABASE_URL = "postgresql://${cfg.user}@127.0.0.1/${cfg.user}"; APP_URL = "${cfg.protocol}://${cfg.hostname}"; PAGES_DIR = cfg.pages_dir; - RELAY_PORT = toString cfg.apiPort; SMTP_HOST = cfg.smtp.host; SMTP_PORT = toString (if cfg.smtp.port != null then cfg.smtp.port else if cfg.smtp.tls then 465 else 1025); diff --git a/server/relay-server/src/db.rs b/server/relay-server/src/db.rs index 31d8d26..0b9c878 100644 --- a/server/relay-server/src/db.rs +++ b/server/relay-server/src/db.rs @@ -321,34 +321,6 @@ pub async fn insert_participant( Ok(()) } -/// Records a completed bot game for a logged-in user in a single transaction. -pub async fn insert_bot_game( - pool: &Pool, - user_id: i64, - result: &str, - outcome: &str, -) -> Result<(), DbError> { - let mut client = pool.get().await?; - let tx = client.transaction().await?; - let now = now_unix(); - let row = tx - .query_one( - "INSERT INTO game_records (game_id, room_code, started_at, ended_at, result) \ - VALUES ('trictrac', 'bot', $1, $1, $2) RETURNING id", - &[&now, &result], - ) - .await?; - let record_id: i64 = row.get(0); - tx.execute( - "INSERT INTO game_participants (game_record_id, user_id, player_id, outcome) \ - VALUES ($1, $2, 0, $3)", - &[&record_id, &user_id, &outcome], - ) - .await?; - tx.commit().await?; - Ok(()) -} - /// Returns win/loss/draw counts for a user. All values are 0 when the user has no games. pub async fn get_user_stats(pool: &Pool, user_id: i64) -> Result { let client = pool.get().await?; diff --git a/server/relay-server/src/http.rs b/server/relay-server/src/http.rs index 77c03fe..0104c76 100644 --- a/server/relay-server/src/http.rs +++ b/server/relay-server/src/http.rs @@ -13,7 +13,6 @@ //! GET /users/:username/games?page=0&per_page=20 //! GET /games/:id //! POST /games/result -//! POST /games/bot-result use axum::{ Json, Router, @@ -53,7 +52,6 @@ pub fn router() -> Router> { .route("/users/{username}", get(user_profile)) .route("/users/{username}/games", get(user_games)) .route("/games/result", post(game_result)) - .route("/games/bot-result", post(bot_game_result)) .route("/games/{id}", get(game_detail)) .route("/pages/{slug}", get(get_page)) } @@ -550,26 +548,6 @@ async fn game_result( Ok(Json(GameResultResponse { game_record_id })) } -// ── Bot game result ─────────────────────────────────────────────────────────── - -#[derive(Deserialize)] -struct BotGameResultBody { - result: String, - outcome: String, -} - -/// Called by the WASM client when a logged-in user finishes a bot game. -async fn bot_game_result( - auth_session: AuthSession, - State(state): State>, - Json(body): Json, -) -> Result { - let user = auth_session.user.ok_or(AppError::Unauthorized)?; - db::insert_bot_game(&state.db, user.id, &body.result, &body.outcome).await?; - tracing::info!(user_id = user.id, outcome = body.outcome, "Bot game recorded"); - Ok(StatusCode::OK) -} - // ── Static content pages ────────────────────────────────────────────────────── #[derive(Deserialize)] diff --git a/server/relay-server/src/main.rs b/server/relay-server/src/main.rs index c60e0a9..367ef98 100644 --- a/server/relay-server/src/main.rs +++ b/server/relay-server/src/main.rs @@ -35,7 +35,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; #[tokio::main] /// Activates error tracing, spawns a watch dog task to eliminate eventual dead rooms, then it sets up the roting system to serve the -/// web sockets and listen for the pages enlist and reload. +/// web sockets and listen for the pages enlist and reload. The server listens on port 8080. async fn main() { tracing_subscriber::registry() .with( @@ -104,11 +104,7 @@ async fn main() { .layer(auth_layer) .layer(cors); - let port: u16 = std::env::var("RELAY_PORT") - .ok() - .and_then(|p| p.parse().ok()) - .unwrap_or(8080); - let listener = tokio::net::TcpListener::bind(("127.0.0.1", port)) + let listener = tokio::net::TcpListener::bind("127.0.0.1:8080") .await .unwrap();