diff --git a/Cargo.lock b/Cargo.lock
index 8992cbe..de6765c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -189,7 +189,7 @@ dependencies = [
[[package]]
name = "backbone-lib"
-version = "0.2.12"
+version = "0.2.11"
dependencies = [
"bytes",
"ewebsock",
@@ -2649,7 +2649,7 @@ dependencies = [
[[package]]
name = "protocol"
-version = "0.2.12"
+version = "0.2.11"
dependencies = [
"serde",
]
@@ -2883,7 +2883,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "relay-server"
-version = "0.2.12"
+version = "0.2.11"
dependencies = [
"argon2",
"axum",
@@ -3893,7 +3893,7 @@ dependencies = [
[[package]]
name = "trictrac-store"
-version = "0.2.12"
+version = "0.2.11"
dependencies = [
"anyhow",
"base64 0.21.7",
@@ -3906,7 +3906,7 @@ dependencies = [
[[package]]
name = "trictrac-web"
-version = "0.2.12"
+version = "0.2.11"
dependencies = [
"backbone-lib",
"futures",
diff --git a/README.md b/README.md
index f9485c7..ca4c0de 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
This is a game of [Trictrac](https://en.wikipedia.org/wiki/Trictrac) rust implementation.
+The project is still on its early stages.
+
## Usage
Install [devenv](https://devenv.sh/getting-started/), start a devenv shell `devenv shell`, and run the following commands.
@@ -15,18 +17,118 @@ just run-relay # listens on :8080
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.
+Open two browser windows at `http://127.0.0.1:9091`. In one, create a room; in the other, join with the same room name.
+
+Playing with the cli against the 'random' bot: `cargo run --bin=client_cli -- --bot random`
+
+## Roadmap
+
+- [x] rules
+- [x] command line interface
+- [x] basic bot (random play)
+- [ ] web client (in progress)
+- [ ] network game (in progress)
+- [ ] AI bot
## 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.
+- the bots algorithms and the training of their models are implemented in the _bot/_ and _spiel_bot_ folders.
-## Inspirations
+### _store_ package
-The multiplayer game architecture, implemented in packages _clients/backbone-lib_, _clients/web/game_, _server/protocol_, _server/relay-server_ is a Leptos-optimized adaptation of the macroquad-based [Carbonfreezer/multiplayer](https://github.com/Carbonfreezer/multiplayer) project. It is a multiplayer game system in Rust targeting browser-based board games compiled as WASM. The original project used Macroquad with a polling-based transport layer; this version replaces that with an async session API built for [Leptos](https://leptos.dev/).
+The game state is defined by the `GameState` struct in _store/src/game.rs_. The `to_string_id()` method allows this state to be encoded compactly in a string (without the played moves history). For a more readable textual representation, the `fmt::Display` trait is implemented.
-The web client UX/UI is inspired by https://playtiao.com.
+### _clients/cli_ package
+
+`clients/cli/src/game_runner.rs` contains the logic to make two bots play against each other.
+
+### _bot_ package
+
+- `bot/src/strategy/default.rs` contains the code for a basic bot strategy: it determines the list of valid moves (using the `get_possible_moves_sequences` method of `store::MoveRules`) and simply executes the first move in the list.
+- `bot/src/strategy/dqnburn.rs` is another bot strategy that uses a reinforcement learning trained model with the DQN algorithm via the burn library ().
+- `bot/scripts/trains.sh` allows you to train agents using different algorithms (DQN, PPO, SAC).
+
+### multiplayer game
+
+Packages "clients/backbone-lib", "clients/web/game", "server/protocol", "server/relay-server" are a Leptos-optimized adaptation of the macroquad-based [Carbonfreezer/multiplayer](https://github.com/Carbonfreezer/multiplayer) project. It is a multiplayer game system in Rust targeting browser-based board games compiled as WASM. The original project used Macroquad with a polling-based transport layer; this version replaces that with an async session API built for [Leptos](https://leptos.dev/).
+
+The system consists of:
+
+- A **relay server** (Axum/Tokio) that routes messages between players and manages rooms, without knowing anything about game rules.
+- A **backbone library** that handles WebSocket connection, handshake, and message routing, exposing an async API to the game frontend.
+- Game-specific **backend logic** implementing the `BackEndArchitecture` trait, which runs only on the hosting client.
+- A **Leptos frontend** that connects to a session and reacts to state updates.
+
+There is no dedicated game server. One of the players acts as the host: their browser runs the game backend locally. The relay server only forwards messages — it never touches game state.
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ Host Client │
+│ ┌─────────────┐ ┌──────────────────┐ ┌────────────┐ │
+│ │ Leptos UI │◄──►│ GameSession │◄──►│ Backend │ │
+│ └─────────────┘ └────────┬─────────┘ └────────────┘ │
+└───────────────────────────── │ ────────────────────────────┘
+ │ WebSocket
+ ┌──────▼──────┐
+ │ Relay Server│
+ └──────┬──────┘
+ │ WebSocket
+┌───────────────────────────────│────────────────────────────┐
+│ ┌─────────────┐ ┌─────────▼────────┐ │
+│ │ Leptos UI │◄──►│ GameSession │ (no backend) │
+│ └─────────────┘ └──────────────────┘ │
+│ Remote Client │
+└─────────────────────────────────────────────────────────────┘
+```
+
+#### Data flow
+
+- **Actions** (e.g. "place stone at B3") flow from the UI to the host backend via `GameSession::send_action()`.
+- **State updates** flow back as `ViewStateUpdate::Full` (full snapshot, on join or reset) or `ViewStateUpdate::Incremental` (delta, for animations).
+- **Timers** are managed by the host's background task (wall-clock, no polling required from the game).
+
+#### backbone-lib session API
+
+The key design choice: `backbone-lib` owns a background async task per session. The Leptos app never drives a loop — it just awaits on events.
+
+#### Workspace
+
+**server/protocol**
+
+Shared message-type constants and the `JoinRequest` struct used during the WebSocket handshake.
+
+**server/relay-server**
+
+Listens on port 8080. Loads `GameConfig.json` on startup to know which games exist and their player limits:
+
+```json
+[{ "name": "trictrac", "max_players": 10 }]
+```
+
+Games can be added at runtime via the `/reload` endpoint. `/enlist` lists active rooms. A watchdog cleans up inactive rooms every 20 minutes.
+
+For production, put it behind a reverse proxy with SSL (the browser requires `wss://` on HTTPS pages). Example Caddy config:
+
+```
+your-domain.com {
+ handle_path /api/* {
+ reverse_proxy localhost:8080
+ }
+ file_server
+}
+```
+
+**clients/backbone-lib**
+
+Modules:
+
+| Module | Purpose |
+| ---------- | ---------------------------------------------------------------------------------------------------------- |
+| `session` | `GameSession`, `connect()`, `SessionEvent`, `RoomConfig` |
+| `host` | Background async task for the hosting client (drives `BackEndArchitecture`, manages timers) |
+| `client` | Background async task for non-hosting clients |
+| `protocol` | Wire encoding/decoding helpers (postcard + message-type bytes) |
+| `platform` | `spawn_task` / `sleep_ms` abstractions (WASM: `spawn_local` + gloo-timers; native: thread + thread::sleep) |
+| `traits` | `BackEndArchitecture`, `BackendCommand`, `ViewStateUpdate`, `SerializationCap` |
diff --git a/clients/web/assets/style.css b/clients/web/assets/style.css
index dcc1b7b..09b21e9 100644
--- a/clients/web/assets/style.css
+++ b/clients/web/assets/style.css
@@ -2045,7 +2045,6 @@ a:hover { text-decoration: underline; }
text-decoration: none;
opacity: 0.8;
transition: opacity 0.15s;
- cursor: pointer;
}
.game-sidebar-link:hover { opacity: 1; text-decoration: underline; text-underline-offset: 2px; }
@@ -2079,13 +2078,13 @@ a:hover { text-decoration: underline; }
}
/* Push the version wrapper to the bottom of the sidebar flex column */
-.sidebar-footer {
+.game-sidebar > div:has(.site-nav-version) {
margin-top: auto;
+ padding: 0.75rem 1rem;
border-top: 1px solid rgba(200,164,72,0.12);
}
.site-nav-version {
- margin: 2em 0 1em;
display: block;
text-align: center;
font-family: var(--font-ui);
diff --git a/clients/web/locales/fr.json b/clients/web/locales/fr.json
index d429838..569d66b 100644
--- a/clients/web/locales/fr.json
+++ b/clients/web/locales/fr.json
@@ -130,12 +130,14 @@
"copy_link": "Copier le lien",
"link_copied": "Copié !",
"scan_qr": "ou scannez le QR code",
+ "join_code_label": "Rejoindre avec un code",
+ "join_code_placeholder": "Code de la salle",
"share_btn": "Partager",
"nickname_modal_title": "Choisissez votre pseudo",
"nickname_modal_hint": "Vous jouerez sous le nom de :",
"nickname_modal_play": "Jouer",
"nickname_modal_or": "ou",
- "nickname_modal_sign_in": "connectez-vous",
+ "nickname_modal_sign_in": "Se connecter",
"nickname_modal_register": "Créer un compte",
"new_game": "Nouvelle partie",
"language": "Langue"
diff --git a/clients/web/src/app.rs b/clients/web/src/app.rs
index 9288be3..5c38d33 100644
--- a/clients/web/src/app.rs
+++ b/clients/web/src/app.rs
@@ -29,26 +29,7 @@ use trictrac_store::CheckerMove;
use std::collections::VecDeque;
-/// Newtype wrappers so context lookup can distinguish signals of the same inner type.
-#[derive(Clone, Copy)]
-pub(crate) struct AnonNickname(pub RwSignal
diff --git a/clients/web/src/portal/verify_email.rs b/clients/web/src/portal/verify_email.rs
index 03736e2..0ce0cae 100644
--- a/clients/web/src/portal/verify_email.rs
+++ b/clients/web/src/portal/verify_email.rs
@@ -2,7 +2,6 @@ use leptos::prelude::*;
use leptos_router::hooks::use_query_map;
use crate::api;
-use crate::app::AuthEmailVerified;
use crate::i18n::*;
#[derive(Clone, PartialEq)]
@@ -17,8 +16,8 @@ pub fn VerifyEmailPage() -> impl IntoView {
let i18n = use_i18n();
let auth_username =
use_context::>>().expect("auth_username context not found");
- let auth_email_verified = use_context::()
- .expect("auth_email_verified context not found").0;
+ let auth_email_verified =
+ use_context::>().expect("auth_email_verified context not found");
let query = use_query_map();
let token = query.with(|m| m.get("token").map(|s| s.to_string()).unwrap_or_default());
diff --git a/devenv.lock b/devenv.lock
index e6e8ef6..991fcf7 100644
--- a/devenv.lock
+++ b/devenv.lock
@@ -3,11 +3,10 @@
"devenv": {
"locked": {
"dir": "src/modules",
- "lastModified": 1779486363,
- "narHash": "sha256-6rNmvwTngbmz/j3DGsEYkyakrHHY8N5wgRD9k35HyuM=",
+ "lastModified": 1776863933,
"owner": "cachix",
"repo": "devenv",
- "rev": "90692720b2ad7a7811204155900bf6bea3a3b420",
+ "rev": "863b4204725efaeeb73811e376f928232b720646",
"type": "github"
},
"original": {
@@ -19,11 +18,10 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1779102034,
- "narHash": "sha256-vZJZjLo513IeI8hjzHFc6TDezUd4uCE2Eq4SNO3DNNg=",
+ "lastModified": 1776734388,
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "687f05a9184cad4eaf905c48b63649e3a86f5433",
+ "rev": "10e7ad5bbcb421fe07e3a4ad53a634b0cd57ffac",
"type": "github"
},
"original": {
@@ -36,7 +34,6 @@
"nixpkgs-cmake3": {
"locked": {
"lastModified": 1758213207,
- "narHash": "sha256-rqoqF0LEi+6ZT59tr+hTQlxVwrzQsET01U4uUdmqRtM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f4b140d5b253f5e2a1ff4e5506edbf8267724bde",
diff --git a/justfile b/justfile
index bc78103..1ac30e4 100644
--- a/justfile
+++ b/justfile
@@ -55,10 +55,6 @@ build-relay:
cp target/release/relay-server deploy
cp -u server/relay-server/GameConfig.json deploy/
-# generate web stats report from the current nginx logs
-stats:
- ssh -t raspberry sudo goaccess /var/log/nginx/trictrac_access.log --log-format=COMBINED -o html > var/stats/report.html
-
# start a trictrac container with nixos-container
# `boot.enableContainers = true` must be set on local nixos system
local:
diff --git a/server/relay-server/src/http.rs b/server/relay-server/src/http.rs
index c8701fc..9c6071f 100644
--- a/server/relay-server/src/http.rs
+++ b/server/relay-server/src/http.rs
@@ -245,7 +245,6 @@ async fn register(
async fn login(
mut auth_session: AuthSession,
- State(state): State>,
Json(body): Json,
) -> Result {
let creds = Credentials {
@@ -261,18 +260,6 @@ async fn login(
auth_session.login(&user).await.map_err(|_| AppError::Internal)?;
- if !user.email_verified {
- let _ = db::delete_email_tokens(&state.db, user.id, "verify").await;
- let token = generate_token();
- let expires_at = now_unix() + VERIFY_TOKEN_EXPIRY;
- if db::create_email_token(&state.db, user.id, &token, "verify", expires_at)
- .await
- .is_ok()
- {
- state.mailer.send_verification(&user.email, &token).await;
- }
- }
-
Ok(Json(MeResponse {
id: user.id,
username: user.username,