fix: integrate multiplayer (wip)
This commit is contained in:
parent
03b614c62e
commit
3f3f4598f6
7 changed files with 65 additions and 12 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -15,3 +15,5 @@ profile.json
|
||||||
bot/models
|
bot/models
|
||||||
client_web/dist
|
client_web/dist
|
||||||
var
|
var
|
||||||
|
|
||||||
|
deploy
|
||||||
|
|
|
||||||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1456,6 +1456,7 @@ dependencies = [
|
||||||
"backbone-lib",
|
"backbone-lib",
|
||||||
"futures",
|
"futures",
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
|
"gloo-net 0.5.0",
|
||||||
"gloo-storage",
|
"gloo-storage",
|
||||||
"gloo-timers",
|
"gloo-timers",
|
||||||
"leptos",
|
"leptos",
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,13 @@ gloo-storage = "0.3"
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
wasm-bindgen-futures = "0.4"
|
wasm-bindgen-futures = "0.4"
|
||||||
|
gloo-net = { version = "0.5", features = ["http"] }
|
||||||
gloo-timers = { version = "0.3", features = ["futures"] }
|
gloo-timers = { version = "0.3", features = ["futures"] }
|
||||||
# getrandom 0.3 requires an explicit WASM backend; "wasm_js" uses window.crypto.getRandomValues.
|
# 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.
|
# Must be a direct dependency (not just transitive) for the feature to take effect.
|
||||||
getrandom = { version = "0.3", features = ["wasm_js"] }
|
getrandom = { version = "0.3", features = ["wasm_js"] }
|
||||||
web-sys = { version = "0.3", features = [
|
web-sys = { version = "0.3", features = [
|
||||||
|
"RequestCredentials",
|
||||||
"AudioContext",
|
"AudioContext",
|
||||||
"AudioParam",
|
"AudioParam",
|
||||||
"AudioNode",
|
"AudioNode",
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
[serve]
|
[serve]
|
||||||
port = 9092
|
port = 9091
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,13 @@ const RELAY_URL: &str = "ws://127.0.0.1:8080/ws";
|
||||||
const GAME_ID: &str = "trictrac";
|
const GAME_ID: &str = "trictrac";
|
||||||
const STORAGE_KEY: &str = "trictrac_session";
|
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.
|
/// The state the UI needs to render the game screen.
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct GameUiState {
|
pub struct GameUiState {
|
||||||
|
|
@ -93,6 +100,11 @@ struct StoredSession {
|
||||||
view_state: Option<ViewState>,
|
view_state: Option<ViewState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct MeResponse {
|
||||||
|
username: String,
|
||||||
|
}
|
||||||
|
|
||||||
fn save_session(session: &StoredSession) {
|
fn save_session(session: &StoredSession) {
|
||||||
LocalStorage::set(STORAGE_KEY, session).ok();
|
LocalStorage::set(STORAGE_KEY, session).ok();
|
||||||
}
|
}
|
||||||
|
|
@ -105,6 +117,31 @@ fn clear_session() {
|
||||||
LocalStorage::delete(STORAGE_KEY);
|
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]
|
#[component]
|
||||||
pub fn App() -> impl IntoView {
|
pub fn App() -> impl IntoView {
|
||||||
let stored = load_session();
|
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 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.
|
/// 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.
|
/// `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 {
|
if prev.board == next.board {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
@ -436,7 +477,9 @@ fn compute_last_moves(prev: &ViewState, next: &ViewState, own_move: bool) -> Opt
|
||||||
}
|
}
|
||||||
if own_move {
|
if own_move {
|
||||||
// m1 was already shown via the staged-moves overlay; only animate m2.
|
// 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()));
|
return Some((m2, CheckerMove::default()));
|
||||||
}
|
}
|
||||||
Some((m1, m2))
|
Some((m1, m2))
|
||||||
|
|
|
||||||
9
justfile
9
justfile
|
|
@ -16,10 +16,10 @@ dev-game:
|
||||||
[working-directory: 'clients/web-game']
|
[working-directory: 'clients/web-game']
|
||||||
build-game:
|
build-game:
|
||||||
trunk build --release
|
trunk build --release
|
||||||
cp dist/index.html deploy/trictrac.html
|
cp dist/index.html ../../deploy/trictrac.html
|
||||||
cp dist/*.wasm deploy/
|
cp dist/*.wasm ../../deploy/
|
||||||
cp dist/*.js deploy/
|
cp dist/*.js ../../deploy/
|
||||||
cp dist/*.css deploy/
|
cp dist/*.css ../../deploy/
|
||||||
|
|
||||||
[working-directory: 'deploy']
|
[working-directory: 'deploy']
|
||||||
run-relay:
|
run-relay:
|
||||||
|
|
@ -41,6 +41,7 @@ build-relay:
|
||||||
CARGO_PROFILE_RELEASE_OPT_LEVEL=3 cargo build -p relay-server --release
|
CARGO_PROFILE_RELEASE_OPT_LEVEL=3 cargo build -p relay-server --release
|
||||||
mkdir -p deploy
|
mkdir -p deploy
|
||||||
cp target/release/relay-server deploy
|
cp target/release/relay-server deploy
|
||||||
|
cp -u service/relay-server/GameConfig.json deploy/
|
||||||
|
|
||||||
runclibots:
|
runclibots:
|
||||||
cargo run --bin=client_cli -- --bot random,dqnburn:./bot/models/burnrl_dqn_40.mpk
|
cargo run --bin=client_cli -- --bot random,dqnburn:./bot/models/burnrl_dqn_40.mpk
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ use crate::message_relay::{handle_client_logic, handle_server_logic};
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use axum::extract::ws::{Message, WebSocket};
|
use axum::extract::ws::{Message, WebSocket};
|
||||||
use axum::extract::{State, WebSocketUpgrade};
|
use axum::extract::{State, WebSocketUpgrade};
|
||||||
|
use axum::http::{HeaderName, Method};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
use axum_login::{AuthManagerLayerBuilder, AuthSession};
|
use axum_login::{AuthManagerLayerBuilder, AuthSession};
|
||||||
|
|
@ -25,11 +26,10 @@ use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use time::Duration as TimeDuration;
|
use time::Duration as TimeDuration;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use axum::http::{HeaderName, Method};
|
|
||||||
use tower_http::cors::{AllowOrigin, CorsLayer};
|
use tower_http::cors::{AllowOrigin, CorsLayer};
|
||||||
use tower_http::services::{ServeDir, ServeFile};
|
use tower_http::services::{ServeDir, ServeFile};
|
||||||
use tower_sessions::{Expiry, SessionManagerLayer};
|
|
||||||
use tower_sessions::MemoryStore;
|
use tower_sessions::MemoryStore;
|
||||||
|
use tower_sessions::{Expiry, SessionManagerLayer};
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
|
@ -81,7 +81,7 @@ async fn main() {
|
||||||
|
|
||||||
let cors = CorsLayer::new()
|
let cors = CorsLayer::new()
|
||||||
.allow_origin(AllowOrigin::list([
|
.allow_origin(AllowOrigin::list([
|
||||||
"http://localhost:9091".parse().unwrap(), // tic-tac-toe dev server
|
"http://localhost:9091".parse().unwrap(), // game dev server
|
||||||
"http://localhost:9092".parse().unwrap(), // portal dev server
|
"http://localhost:9092".parse().unwrap(), // portal dev server
|
||||||
]))
|
]))
|
||||||
.allow_methods([Method::GET, Method::POST, Method::OPTIONS])
|
.allow_methods([Method::GET, Method::POST, Method::OPTIONS])
|
||||||
|
|
@ -96,7 +96,10 @@ async fn main() {
|
||||||
.route("/enlist", get(enlist_handler))
|
.route("/enlist", get(enlist_handler))
|
||||||
.route("/ws", get(websocket_handler))
|
.route("/ws", get(websocket_handler))
|
||||||
.merge(http::router())
|
.merge(http::router())
|
||||||
.nest_service("/portal", ServeDir::new("portal").not_found_service(ServeFile::new("portal/index.html")))
|
.nest_service(
|
||||||
|
"/portal",
|
||||||
|
ServeDir::new("portal").not_found_service(ServeFile::new("portal/index.html")),
|
||||||
|
)
|
||||||
.with_state(app_state)
|
.with_state(app_state)
|
||||||
.layer(auth_layer)
|
.layer(auth_layer)
|
||||||
.layer(cors)
|
.layer(cors)
|
||||||
|
|
@ -180,7 +183,8 @@ async fn websocket(stream: WebSocket, state: Arc<AppState>, user_id: Option<i64>
|
||||||
// By splitting, we can send and receive at the same time.
|
// By splitting, we can send and receive at the same time.
|
||||||
let (mut sender, mut receiver) = stream.split();
|
let (mut sender, mut receiver) = stream.split();
|
||||||
|
|
||||||
let handshake_result = init_and_connect(&mut sender, &mut receiver, state.clone(), user_id).await;
|
let handshake_result =
|
||||||
|
init_and_connect(&mut sender, &mut receiver, state.clone(), user_id).await;
|
||||||
if handshake_result.is_none() {
|
if handshake_result.is_none() {
|
||||||
// We quit here, as the handshake did not work out.
|
// We quit here, as the handshake did not work out.
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue