fix: integrate multiplayer (wip)

This commit is contained in:
Henri Bourcereau 2026-04-23 17:37:10 +02:00
parent 03b614c62e
commit 3f3f4598f6
7 changed files with 65 additions and 12 deletions

View file

@ -20,11 +20,13 @@ gloo-storage = "0.3"
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen-futures = "0.4"
gloo-net = { version = "0.5", features = ["http"] }
gloo-timers = { version = "0.3", features = ["futures"] }
# getrandom 0.3 requires an explicit WASM backend; "wasm_js" uses window.crypto.getRandomValues.
# Must be a direct dependency (not just transitive) for the feature to take effect.
getrandom = { version = "0.3", features = ["wasm_js"] }
web-sys = { version = "0.3", features = [
"RequestCredentials",
"AudioContext",
"AudioParam",
"AudioNode",

View file

@ -1,2 +1,2 @@
[serve]
port = 9092
port = 9091

View file

@ -23,6 +23,13 @@ const RELAY_URL: &str = "ws://127.0.0.1:8080/ws";
const GAME_ID: &str = "trictrac";
const STORAGE_KEY: &str = "trictrac_session";
// In debug builds trunk serves on 9091, relay is on 8080.
// In release the game is served by the relay itself — use relative paths.
#[cfg(debug_assertions)]
const HTTP_BASE: &str = "http://localhost:8080";
#[cfg(not(debug_assertions))]
const HTTP_BASE: &str = "";
/// The state the UI needs to render the game screen.
#[derive(Clone, PartialEq)]
pub struct GameUiState {
@ -93,6 +100,11 @@ struct StoredSession {
view_state: Option<ViewState>,
}
#[derive(Deserialize)]
struct MeResponse {
username: String,
}
fn save_session(session: &StoredSession) {
LocalStorage::set(STORAGE_KEY, session).ok();
}
@ -105,6 +117,31 @@ fn clear_session() {
LocalStorage::delete(STORAGE_KEY);
}
/// Fire-and-forget: tell the relay server who won. Only called by the host.
async fn submit_game_result(room_code: String, game_state: ViewState) {
let [score_pl1, score_pl2] = game_state.scores;
let result_str = format!("{:?} - {:?}", score_pl1.holes, score_pl2.holes);
let outcomes = if score_pl1.holes < score_pl2.holes {
[("0", "loss"), ("1", "win")]
} else if score_pl2.holes < score_pl1.holes {
[("0", "win"), ("1", "loss")]
} else {
[("0", "draw"), ("1", "draw")]
};
let body = serde_json::json!({
"room_code": room_code,
"game_id": GAME_ID,
"result": result_str,
"outcomes": std::collections::HashMap::from(outcomes),
});
let _ = gloo_net::http::Request::post(&format!("{HTTP_BASE}/games/result"))
.credentials(web_sys::RequestCredentials::Include)
.json(&body)
.unwrap()
.send()
.await;
}
#[component]
pub fn App() -> impl IntoView {
let stored = load_session();
@ -423,7 +460,11 @@ async fn run_local_bot_game(
/// Returns the checker moves to animate when the board changed between two ViewStates.
/// Returns `None` when the board is unchanged or no real moves were recorded.
/// `own_move`: when true, m1 was already shown via staged-moves UI, so only animate m2.
fn compute_last_moves(prev: &ViewState, next: &ViewState, own_move: bool) -> Option<(CheckerMove, CheckerMove)> {
fn compute_last_moves(
prev: &ViewState,
next: &ViewState,
own_move: bool,
) -> Option<(CheckerMove, CheckerMove)> {
if prev.board == next.board {
return None;
}
@ -436,7 +477,9 @@ fn compute_last_moves(prev: &ViewState, next: &ViewState, own_move: bool) -> Opt
}
if own_move {
// m1 was already shown via the staged-moves overlay; only animate m2.
if m2 == CheckerMove::default() { return None; }
if m2 == CheckerMove::default() {
return None;
}
return Some((m2, CheckerMove::default()));
}
Some((m1, m2))