Commit Graph
18 Commits
Author SHA1 Message Date
woggioni 46f10f145a Show the OIDC profile picture with an identicon fallback
CI / Build and push docker image (push) Successful in 2m9s
GET /api/me now carries the profile picture URL when the provider
supplies an absolute http(s) one; the SPA shows it in the lobby navbar
and draws a deterministic identicon from the sub otherwise, falling back
to the identicon when the image fails to load.

The identicon is a 5x5 mirrored grid seeded by an inline FNV-1a hash so
it stays stable across Rust versions and is unit-testable on the host.
The dev mock IdP hands alice and bob a picture claim.
2026-10-15 00:14:17 +08:00
woggioni 68200bbe62 Add scopa: classic two-player and scopa a quattro
New tavolo-scopa package with a pure rules engine and the platform
adapters for two-player scopa and four-player fixed-partnership scopa a
quattro: three cards each and four on the table, capture by rank or sum,
sweep for scope and score carte/denara/settebello/primiera.

The optional asso pigliatutto creation option turns either variant into
scopa d'assi (an ace sweeps the whole table and scores no scopa), reusing
scopone's rule semantics.

Wire both engines into the app and the Docker install, add the scopa
table view to the SPA (duo and four-seat layouts, deck count,
captured/scope stats, hand-summary and final screens, player names in the
two-player summary), and cover the rules, the platform contract, the
REST/WebSocket flows, match persistence and Elo.
2026-10-09 14:45:20 +08:00
woggioni 0674e06565 Redirect to login on 401 and return afterwards
The SPA now sends the user to a new /login page whenever an API call
answers 401, remembering the page they were on. The OIDC callback always
lands back on the configured post-login redirect, so the saved page is
restored client-side once a session exists.

- auth: save/restore the return path in sessionStorage; defer the
  navigation so the failing request can finish first
- api: centralize 401 handling (me() still treats 401 as logged out)
- login page + shared login card, extracted from the lobby
- guard async signal writes so an in-flight request cannot touch a page
  unmounted by the redirect
2026-10-09 05:53:21 +08:00
woggioni 6427afd12b Reset the game options form when the game changes
The creation options were rendered as a keyed list, so a field shared by
two games (target_score) kept the previous game's widget when the game
changed: after switching scopone -> briscola the input still showed 11
with scopone's min/max instead of the briscola default 61 (1/100 instead
of 61/120), even though the submitted payload was already correct.

Recreate the option inputs on every game switch instead, so each one is
initialized from the new game's defaults and schema constraints.
2026-10-06 18:08:58 +08:00
woggioni 18877b8109 Offer a Continue link for ongoing games in the lobby
A player who accidentally closes the game page can now get back to it
from the lobby.

Backend:
- GameStore.list_ongoing() returns live unfinished sessions (newest
  first), skipping expired keys and finished matches;
- GET /api/me/games lists the games the caller is seated in, with the
  game name/description, players, open seats and your seat (no join
  code);
- store, platform route and app integration tests.

Frontend:
- MyGame/MyGamesPage model plus api::my_games();
- the lobby shows a "Your games" card with phase, seats, players and a
  Continue button linking to /game/<id>; hidden when empty or logged
  out.
2026-10-06 17:41:44 +08:00
woggioni b821274f0b Use player names in two-player briscola
Classic briscola has no partnerships: its "teams" are just the two
players, but the UI labelled them Team A/Team B.

- add is_individual_game() and GameView::player_name_for_team() to the
  model;
- the briscola table HUD reads "Alice 0 — 0 Bob (target 61)" and the
  seat panels drop the Team badge for two-player games;
- the deal summary and game-over screen name the sides and the winner;
- the waiting room and join preview hide the Team badge.

briscola a quattro and scopone keep their team labels, and the match
history table is unchanged.
2026-10-06 17:37:55 +08:00
woggioni 97d4b3f217 Scope the leaderboard to a selectable game
Elo ratings are stored per game type, so the leaderboard now always
requests /api/leaderboard?game_type=<id> and shows only that game's
players, Elo, wins, matches and points instead of mixing all games'
aggregates with the default game's rating.

- api::leaderboard takes the game type;
- a "Game" dropdown next to the heading lists every registered game and
  reloads the board on change (stale in-flight responses are dropped);
- move fallback_game_types() from the lobby to model.rs so both the
  lobby and leaderboard selectors work when /api/game-types fails.
2026-10-06 16:55:51 +08:00
woggioni 7bb6b74fdc Add briscola: classic two-player and briscola a quattro
CI / Build and push docker image (push) Successful in 1m45s
New tavolo-briscola distribution implementing briscola on the platform:

- pure rules (state.py, engine.py): 3-card deals, a face-up trump that
  is the last card drawn, trump-first trick resolution (A > 3 > K > H >
  J > 7 > 6 > 5 > 4 > 2), 120 points per deal, 60-60 patta re-deals, a
  configurable target score (default 61), deal-end acknowledgements and
  turn auto-play;
- BriscolaEngine (briscola, 2 players) and BriscolaQuattroEngine
  (briscola_a_quattro, 4 players in fixed partnerships) adapters;
- registered in tavolo-app, packaged in the Docker image, documented.

Platform fixes surfaced by a two-player lobby:

- the websocket state and snapshot payloads now carry seats_open (they
  were only in the REST lobby payload), so lobbies with player counts
  other than four render correctly; seats_open is clamped at zero;
- the web client logs unparseable server messages instead of silently
  dropping them.

Web client: briscola table for both variants (stock, face-up trump,
trick, last-trick banner), deal summary modal and game-over overlay.

Tests: tavolo-briscola rules/adapter suites, app integration tests
(lobby, websocket play, turn timeout, deal-end acks, persistence), a
platform seats_open test and frontend model parsing tests.
2026-10-06 14:08:25 +08:00
woggioni 915a294dc8 Support multiple OIDC providers
kaya-oidc 0.0.6 can run several providers in one app (OIDCProviders),
each with its own login route and namespaced session state. Adopt it:
providers are configured in indexed, contiguous OIDC_<n>_* blocks
(0-based) with OIDC_<n>_PROVIDER naming the provider in its login URL
and session keys. The old unindexed OIDC_ISSUER/OIDC_CLIENT_ID/... names
are no longer read; startup fails with a rename hint when they are set.
With no OIDC variables at all, the single dev mock provider named
"default" is used, as before.

The platform now authenticates through OIDCProviders: auth.current_user
returns the session's user (sessions stay single-identity, so at most one
provider is logged in) and require_auth/route handlers use it, while the
websocket path keeps scanning for any provider's user. New routes expose
the configuration to the SPA and keep old links working: GET
/api/auth/providers lists the login URLs, GET /auth/login redirects to
the first provider and GET /auth/logout to whichever provider the
session is logged in with. The lobby renders one login button per
provider from that endpoint.

Update the env examples, compose and k8s manifests to the indexed scheme,
document the multiple-provider setup and the migration, and cover it with
config, auth and route tests.
2026-10-01 12:38:29 +00:00
woggioni a3566f2083 Preview a match by join code before joining
CI / Build and push docker image (push) Successful in 1m22s
GET /api/games/preview returns the lobby behind a code plus the game's
name and engine-echoed options. The lobby debounces the lookup as a
complete code is typed and shows the game, seated players, open seats
and option badges rendered from the game's schema, disabling Join when
the lobby is full or the match has already started.
2026-09-24 09:03:31 +08:00
woggioni 12fa3f5aae Rebuild the web UI on Bootstrap 5 with the dark felt theme
CI / Build and push docker image (push) Failing after 39s
The lobby was hand-rolled CSS and looked plain. Vendor Bootstrap 5.3
(MIT) under web/vendor so builds stay offline, load it in dark mode and
bridge the felt/gold palette onto Bootstrap's CSS variables in a new
theme.css; style.css now only owns the game table, cards and scoring
summary.

Rebuild the lobby, waiting room and shared chrome with Bootstrap
components: a shared navbar, creation form as cards with schema-driven
controls, roster as a list group with seat/team badges, a seats progress
bar and a copy-to-clipboard button on the join code. History,
leaderboard, connection/error states and the 404 page use cards, tables
and buttons too; the play table itself is unchanged.

The copy feedback timer is cancelled when the waiting room is rebuilt by
another player joining, fixing a disposed-signal panic that froze the
page mid-match.
2026-09-21 23:40:44 +08:00
woggioni 83ecf9bed3 Make the web client game-generic against the platform API
Send creation options as the nested options object the lobby expects,
drive the creation form from each game's options_schema, and render
history/leaderboard from the generic result payload (nullable teams,
float points). The lobby uses the payload's seats_open, and the
scopone-specific table, hand-end and game-over views are gated on its
game_type with an unsupported-game fallback for future games. The
websocket protocol is unchanged.
2026-09-21 13:04:08 +08:00
woggioni 93d041b004 Add chess-style Elo ratings for players
Each player's rating starts at 1500 and updates transactionally with
every finished match: a team's rating is the mean of its two members
and the standard K=32 formula decides the zero-sum delta applied to
both members of a team. Ratings are per game type in a new
player_rating table; match_player records each match's elo_delta.

- GET /api/leaderboard exposes elo and sorts by it
- GET /api/me/matches includes per-player elo deltas
- new GET /api/me/ratings returns the caller's rating per game type
- frontend: Elo column on the leaderboard, per-match delta in the
  history page, current rating in the lobby
- python -m tavolo.backfill_elo recomputes all ratings from the
  recorded match history (one-off backfill for existing matches)
2026-09-19 10:09:24 +08:00
woggioni c1e7f70a3b Add configurable napola rule with instant win on a full denari sweep 2026-09-18 19:14:54 +08:00
woggioni bf04a9b38d Auto-dismiss error toasts after 10 seconds 2026-09-18 19:14:49 +08:00
woggioni ab4130a4ca Add preliminary support for multiple card games
CI / Build and push docker image (push) Successful in 3m5s
A game-type registry (server/src/tavolo/games.py) is now the single
source of truth for the games the platform can host; only scopone
scientifico is registered so far. GET /api/game-types exposes it for the
lobby's new game dropdown, and POST /api/games accepts a validated
game_type (default scopone_scientifico) which is carried on the live
GameState and onto each finished Match row (new indexed column,
migration 1_20260916235833_update), so statistics can be scoped per
game: /api/me/matches and /api/leaderboard take an optional game_type
filter and every serialized match includes its game_type.

Game states serialized before this change still load with the default
game type.
2026-09-17 08:26:46 +08:00
woggioni 8414831849 web: fix OIDC navigation intercepted by the SPA router; tidy table layout
- Login/Logout anchors now carry rel="external": sycamore-router
  intercepts same-origin anchor clicks otherwise, so the OIDC login
  round-trip never left the SPA (found with a real browser test)
- Opponents' hidden hands render as a tidy 5x2 grid of card backs, seats
  have a fixed height and shrink-wrap so the table no longer jumps as
  cards are played
- Dockerfile: cache the Rust target dir so frontend rebuilds are fast
- ignore .playwright-mcp/ tooling output
2026-09-16 13:20:06 +08:00
woggioni 96a95d74b6 Add Sycamore/WASM frontend and restructure into server/ + web/
Repo is now a monorepo:

- server/: the kaya backend, unchanged in behaviour, plus:
  - GET /api/me for SPA session detection
  - last_move recorded on every play and broadcast in the game state, so
    clients can show who played which card the moment they play it
  - legal_moves per hand card for the player on turn (rules stay
    server-side)
  - static catch-all route serving the compiled SPA with index.html
    fallback; Tortoise context now bound only for /api/* requests
  - configurable OIDC post-login/logout redirects for dev against trunk
- web/: Sycamore 0.9 + WASM frontend (trunk): login via the OIDC flow,
  lobby (create match / join by code), live game page over websocket with
  card images (CC0 woodcut napoletane deck), capture picker, move banner,
  game-over overlay, match history and leaderboard pages
- server/Dockerfile gains a rust+trunk stage building web/dist; the single
  app image serves the SPA; compose builds from the repo root with
  overridable ports/OIDC env

Verified end-to-end against the compose stack: four OIDC logins, game
creation, three joins by code, websocket play with broadcasts to all
players and out-of-turn rejection. 51 backend tests, mypy and cargo tests
all green.
2026-09-16 13:20:05 +08:00