Compare commits
No commits in common. "03a9cb112843febd284c5c93ec4bf79ad330e5ea" and "84e0d127c093e40780a2fad04ae76df6eebe5a42" have entirely different histories.
03a9cb1128
...
84e0d127c0
13 changed files with 24 additions and 113 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[workspace.package]
|
||||
version = "0.2.19"
|
||||
version = "0.2.18"
|
||||
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -188,22 +188,6 @@ 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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<NetCommand>,
|
||||
pending: RwSignal<VecDeque<GameUiState>>,
|
||||
player_name: String,
|
||||
auth_username: RwSignal<Option<String>>,
|
||||
) -> 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<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);
|
||||
|
|
@ -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<Option<String>>,
|
||||
) -> 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -216,7 +216,6 @@ 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",
|
||||
|
|
@ -231,7 +230,7 @@ fn GamesTable(games: Vec<GameSummary>, page: RwSignal<i64>) -> impl IntoView {
|
|||
};
|
||||
view! {
|
||||
<tr>
|
||||
<td>{ room_display }</td>
|
||||
<td>{ g.room_code.clone() }</td>
|
||||
<td>{ started }</td>
|
||||
<td>{ ended }</td>
|
||||
<td class=outcome_class>{ outcome_text }</td>
|
||||
|
|
|
|||
|
|
@ -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 ];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<UserStats, DbError> {
|
||||
let client = pool.get().await?;
|
||||
|
|
|
|||
|
|
@ -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<Arc<AppState>> {
|
|||
.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<AuthBackend>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(body): Json<BotGameResultBody>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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)]
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue