fix(web client): sidebar shows login link for anonymous with a nickname
This commit is contained in:
parent
2f40a0a507
commit
91981e6872
6 changed files with 32 additions and 126 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
|
@ -189,7 +189,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backbone-lib"
|
name = "backbone-lib"
|
||||||
version = "0.2.11"
|
version = "0.2.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"ewebsock",
|
"ewebsock",
|
||||||
|
|
@ -2649,7 +2649,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "protocol"
|
name = "protocol"
|
||||||
version = "0.2.11"
|
version = "0.2.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
@ -2883,7 +2883,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "relay-server"
|
name = "relay-server"
|
||||||
version = "0.2.11"
|
version = "0.2.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"axum",
|
"axum",
|
||||||
|
|
@ -3893,7 +3893,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "trictrac-store"
|
name = "trictrac-store"
|
||||||
version = "0.2.11"
|
version = "0.2.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
|
|
@ -3906,7 +3906,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "trictrac-web"
|
name = "trictrac-web"
|
||||||
version = "0.2.11"
|
version = "0.2.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backbone-lib",
|
"backbone-lib",
|
||||||
"futures",
|
"futures",
|
||||||
|
|
|
||||||
116
README.md
116
README.md
|
|
@ -2,8 +2,6 @@
|
||||||
|
|
||||||
This is a game of [Trictrac](https://en.wikipedia.org/wiki/Trictrac) rust implementation.
|
This is a game of [Trictrac](https://en.wikipedia.org/wiki/Trictrac) rust implementation.
|
||||||
|
|
||||||
The project is still on its early stages.
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Install [devenv](https://devenv.sh/getting-started/), start a devenv shell `devenv shell`, and run the following commands.
|
Install [devenv](https://devenv.sh/getting-started/), start a devenv shell `devenv shell`, and run the following commands.
|
||||||
|
|
@ -17,118 +15,18 @@ just run-relay # listens on :8080
|
||||||
just dev
|
just dev
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
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
|
## Code structure
|
||||||
|
|
||||||
- game rules and game state are implemented in the _store/_ folder.
|
- 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 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.
|
- 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.
|
||||||
|
|
||||||
### _store_ package
|
## Inspirations
|
||||||
|
|
||||||
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 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/).
|
||||||
|
|
||||||
### _clients/cli_ package
|
The web client UX/UI is inspired by https://playtiao.com.
|
||||||
|
|
||||||
`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 (<https://burn.dev/>).
|
|
||||||
- `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` |
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,12 @@ use trictrac_store::CheckerMove;
|
||||||
|
|
||||||
use std::collections::VecDeque;
|
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<Option<String>>);
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(crate) struct AuthEmailVerified(pub RwSignal<bool>);
|
||||||
|
|
||||||
fn relay_url() -> String {
|
fn relay_url() -> String {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
{
|
{
|
||||||
|
|
@ -170,14 +176,14 @@ pub fn App() -> impl IntoView {
|
||||||
let auth_username: RwSignal<Option<String>> = RwSignal::new(None);
|
let auth_username: RwSignal<Option<String>> = RwSignal::new(None);
|
||||||
let auth_email_verified: RwSignal<bool> = RwSignal::new(false);
|
let auth_email_verified: RwSignal<bool> = RwSignal::new(false);
|
||||||
provide_context(auth_username);
|
provide_context(auth_username);
|
||||||
provide_context(auth_email_verified);
|
provide_context(AuthEmailVerified(auth_email_verified));
|
||||||
// Set to true once get_me resolves (success or failure) so lobby can
|
// Set to true once get_me resolves (success or failure) so lobby can
|
||||||
// decide immediately whether to show the nickname modal.
|
// decide immediately whether to show the nickname modal.
|
||||||
let auth_loaded: RwSignal<bool> = RwSignal::new(false);
|
let auth_loaded: RwSignal<bool> = RwSignal::new(false);
|
||||||
provide_context(auth_loaded);
|
provide_context(auth_loaded);
|
||||||
// Nickname chosen by an anonymous player; used instead of "Anonymous".
|
// Nickname chosen by an anonymous player; used instead of "Anonymous".
|
||||||
let anon_nickname: RwSignal<Option<String>> = RwSignal::new(None);
|
let anon_nickname: RwSignal<Option<String>> = RwSignal::new(None);
|
||||||
provide_context(anon_nickname);
|
provide_context(AnonNickname(anon_nickname));
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
if let Ok(me) = api::get_me().await {
|
if let Ok(me) = api::get_me().await {
|
||||||
auth_username.set(Some(me.username));
|
auth_username.set(Some(me.username));
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use leptos::prelude::*;
|
||||||
use leptos_router::hooks::use_navigate;
|
use leptos_router::hooks::use_navigate;
|
||||||
|
|
||||||
use crate::api;
|
use crate::api;
|
||||||
|
use crate::app::AuthEmailVerified;
|
||||||
use crate::i18n::*;
|
use crate::i18n::*;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
|
|
@ -9,8 +10,8 @@ pub fn AccountPage() -> impl IntoView {
|
||||||
let i18n = use_i18n();
|
let i18n = use_i18n();
|
||||||
let auth_username =
|
let auth_username =
|
||||||
use_context::<RwSignal<Option<String>>>().expect("auth_username context not found");
|
use_context::<RwSignal<Option<String>>>().expect("auth_username context not found");
|
||||||
let auth_email_verified =
|
let auth_email_verified = use_context::<AuthEmailVerified>()
|
||||||
use_context::<RwSignal<bool>>().expect("auth_email_verified context not found");
|
.expect("auth_email_verified context not found").0;
|
||||||
let navigate = use_navigate();
|
let navigate = use_navigate();
|
||||||
|
|
||||||
// Only redirect to profile when the email is actually verified.
|
// Only redirect to profile when the email is actually verified.
|
||||||
|
|
@ -107,8 +108,8 @@ fn LoginForm() -> impl IntoView {
|
||||||
let i18n = use_i18n();
|
let i18n = use_i18n();
|
||||||
let auth_username =
|
let auth_username =
|
||||||
use_context::<RwSignal<Option<String>>>().expect("auth_username context not found");
|
use_context::<RwSignal<Option<String>>>().expect("auth_username context not found");
|
||||||
let auth_email_verified =
|
let auth_email_verified = use_context::<AuthEmailVerified>()
|
||||||
use_context::<RwSignal<bool>>().expect("auth_email_verified context not found");
|
.expect("auth_email_verified context not found").0;
|
||||||
let navigate = use_navigate();
|
let navigate = use_navigate();
|
||||||
|
|
||||||
let login = RwSignal::new(String::new());
|
let login = RwSignal::new(String::new());
|
||||||
|
|
@ -177,8 +178,8 @@ fn RegisterForm() -> impl IntoView {
|
||||||
let i18n = use_i18n();
|
let i18n = use_i18n();
|
||||||
let auth_username =
|
let auth_username =
|
||||||
use_context::<RwSignal<Option<String>>>().expect("auth_username context not found");
|
use_context::<RwSignal<Option<String>>>().expect("auth_username context not found");
|
||||||
let auth_email_verified =
|
let auth_email_verified = use_context::<AuthEmailVerified>()
|
||||||
use_context::<RwSignal<bool>>().expect("auth_email_verified context not found");
|
.expect("auth_email_verified context not found").0;
|
||||||
|
|
||||||
let username = RwSignal::new(String::new());
|
let username = RwSignal::new(String::new());
|
||||||
let email = RwSignal::new(String::new());
|
let email = RwSignal::new(String::new());
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use leptos::prelude::*;
|
||||||
use leptos_router::components::A;
|
use leptos_router::components::A;
|
||||||
use leptos_router::hooks::use_query_map;
|
use leptos_router::hooks::use_query_map;
|
||||||
|
|
||||||
use crate::app::{NetCommand, Screen};
|
use crate::app::{AnonNickname, NetCommand, Screen};
|
||||||
use crate::i18n::*;
|
use crate::i18n::*;
|
||||||
|
|
||||||
// ── Room/nickname generation ──────────────────────────────────────────────────
|
// ── Room/nickname generation ──────────────────────────────────────────────────
|
||||||
|
|
@ -103,7 +103,7 @@ pub fn LobbyPage() -> impl IntoView {
|
||||||
let cmd_tx = use_context::<UnboundedSender<NetCommand>>().expect("NetCommand sender");
|
let cmd_tx = use_context::<UnboundedSender<NetCommand>>().expect("NetCommand sender");
|
||||||
let auth_username = use_context::<RwSignal<Option<String>>>().expect("auth_username context");
|
let auth_username = use_context::<RwSignal<Option<String>>>().expect("auth_username context");
|
||||||
let auth_loaded = use_context::<RwSignal<bool>>().expect("auth_loaded context");
|
let auth_loaded = use_context::<RwSignal<bool>>().expect("auth_loaded context");
|
||||||
let anon_nickname = use_context::<RwSignal<Option<String>>>().expect("anon_nickname context");
|
let anon_nickname = use_context::<AnonNickname>().expect("anon_nickname context").0;
|
||||||
let query = use_query_map();
|
let query = use_query_map();
|
||||||
|
|
||||||
let view_state: RwSignal<LobbyView> = RwSignal::new(LobbyView::Idle);
|
let view_state: RwSignal<LobbyView> = RwSignal::new(LobbyView::Idle);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use leptos::prelude::*;
|
||||||
use leptos_router::hooks::use_query_map;
|
use leptos_router::hooks::use_query_map;
|
||||||
|
|
||||||
use crate::api;
|
use crate::api;
|
||||||
|
use crate::app::AuthEmailVerified;
|
||||||
use crate::i18n::*;
|
use crate::i18n::*;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
|
|
@ -16,8 +17,8 @@ pub fn VerifyEmailPage() -> impl IntoView {
|
||||||
let i18n = use_i18n();
|
let i18n = use_i18n();
|
||||||
let auth_username =
|
let auth_username =
|
||||||
use_context::<RwSignal<Option<String>>>().expect("auth_username context not found");
|
use_context::<RwSignal<Option<String>>>().expect("auth_username context not found");
|
||||||
let auth_email_verified =
|
let auth_email_verified = use_context::<AuthEmailVerified>()
|
||||||
use_context::<RwSignal<bool>>().expect("auth_email_verified context not found");
|
.expect("auth_email_verified context not found").0;
|
||||||
|
|
||||||
let query = use_query_map();
|
let query = use_query_map();
|
||||||
let token = query.with(|m| m.get("token").map(|s| s.to_string()).unwrap_or_default());
|
let token = query.with(|m| m.get("token").map(|s| s.to_string()).unwrap_or_default());
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue