feat: record bot games results

This commit is contained in:
Henri Bourcereau 2026-06-24 21:47:57 +02:00
parent 592dfe52af
commit 203825e28f
8 changed files with 98 additions and 14 deletions

View file

@ -342,7 +342,6 @@ 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);

View file

@ -188,6 +188,22 @@ pub async fn get_user_games(username: &str, page: i64) -> Result<GamesResponse,
}
}
pub async fn submit_bot_game_result(result: String, outcome: String) -> 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<GameDetail, String> {
let resp = gloo_net::http::Request::get(&url(&format!("/games/{id}")))
.credentials(web_sys::RequestCredentials::Include)

View file

@ -293,12 +293,19 @@ 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())
.await
run_local_bot_game(
screen,
&mut cmd_rx,
pending,
player_name.clone(),
auth_username,
)
.await
}
};
if !restart {

View file

@ -1,8 +1,10 @@
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;
@ -18,6 +20,7 @@ pub async fn run_local_bot_game(
cmd_rx: &mut mpsc::UnboundedReceiver<NetCommand>,
pending: RwSignal<VecDeque<GameUiState>>,
player_name: String,
auth_username: RwSignal<Option<String>>,
) -> bool {
let mut backend = TrictracBackend::new(0);
backend.player_arrival(0);
@ -49,7 +52,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).await
run_local_bot_game_loop(screen, cmd_rx, pending, player_name, backend, vs, auth_username).await
}
/// Runs a bot game from a pre-built backend and initial ViewState (used for snapshot replay).
@ -60,6 +63,7 @@ pub async fn run_local_bot_game_with_backend(
pending: RwSignal<VecDeque<GameUiState>>,
player_name: String,
backend: TrictracBackend,
auth_username: RwSignal<Option<String>>,
) -> bool {
let mut vs = backend.get_view_state().clone();
patch_bot_names(&mut vs, &player_name);
@ -76,7 +80,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).await
run_local_bot_game_loop(screen, cmd_rx, pending, player_name, backend, vs, auth_username).await
}
async fn run_local_bot_game_loop(
@ -86,8 +90,10 @@ async fn run_local_bot_game_loop(
player_name: String,
mut backend: TrictracBackend,
mut vs: ViewState,
auth_username: RwSignal<Option<String>>,
) -> bool {
use futures::StreamExt;
let mut result_submitted = false;
loop {
match cmd_rx.next().await {
Some(NetCommand::Action(action)) => {
@ -152,6 +158,19 @@ 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;
});
}
}
}
}

View file

@ -216,6 +216,7 @@ fn GamesTable(games: Vec<GameSummary>, page: RwSignal<i64>) -> 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",
@ -230,7 +231,7 @@ fn GamesTable(games: Vec<GameSummary>, page: RwSignal<i64>) -> impl IntoView {
};
view! {
<tr>
<td>{ g.room_code.clone() }</td>
<td>{ room_display }</td>
<td>{ started }</td>
<td>{ ended }</td>
<td class=outcome_class>{ outcome_text }</td>