8793bd34f2
The statistics screen gains real depth, plus a hint-count bug fix found along the way. - Best move per variant: the screen shows the actual best-move word (drawn as game tiles; a wildcard shows its letter but no value), broken down by game variant, empty variants omitted. New account_best_move table, written at game finish. - Moves & hint share: two new lifetime tiles — the player's play count and the share of plays that used a hint — from summed account_stats counters (moves, hints_used). Honest-AI games are excluded, like the rest of the stats. - Hint-count fix: the in-game hint badge no longer goes stale across games. The global wallet now rides the wire apart from the per-game allowance (wallet_balance on StateView/HintResult/StatsView), so the client reads the live wallet rather than a per-game snapshot; game_players.hints_used now counts every hint (allowance + wallet), its true per-game total. - Account merge: sums the new moves/hints_used counters and merges the per-variant best moves (higher score kept), which it previously dropped. - Admin: the user card shows Moves and Hints used. - UI polish: tab/label wording, game-over text, and e2e selectors hardened against label changes. All wire additions are trailing (backward-compatible). Docs (ARCHITECTURE, FUNCTIONAL +ru, UISN_DESIGN) updated in step.
91 lines
3.0 KiB
Go
91 lines
3.0 KiB
Go
package game
|
|
|
|
import (
|
|
"github.com/google/uuid"
|
|
|
|
"scrabble/backend/internal/engine"
|
|
"scrabble/backend/internal/notify"
|
|
)
|
|
|
|
// The mappers below project the game domain into the wire-agnostic notify.* input
|
|
// structs the enriched live events carry. They keep the wire schema out of the
|
|
// game package: notify owns the FlatBuffers encoding, this file only resolves the
|
|
// values (seat display names, last-activity sort key) into its input shapes.
|
|
|
|
// gameSummary projects a game.Game into the notify.GameSummary embedded in enriched
|
|
// events. names is the seat-indexed display-name slice from seatNames; LastActivityUnix
|
|
// mirrors the gateway view (the current turn's start while active, the finish time once
|
|
// finished).
|
|
func gameSummary(g Game, names []string) notify.GameSummary {
|
|
seats := make([]notify.SeatStanding, 0, len(g.Seats))
|
|
for _, s := range g.Seats {
|
|
name := ""
|
|
if s.Seat >= 0 && s.Seat < len(names) {
|
|
name = names[s.Seat]
|
|
}
|
|
// An open game's still-empty opponent seat carries no account: send an empty id
|
|
// (not the nil-UUID string) so the client renders it as "searching for opponent".
|
|
accountID := ""
|
|
if s.AccountID != uuid.Nil {
|
|
accountID = s.AccountID.String()
|
|
}
|
|
seats = append(seats, notify.SeatStanding{
|
|
Seat: s.Seat,
|
|
AccountID: accountID,
|
|
DisplayName: name,
|
|
Score: s.Score,
|
|
HintsUsed: s.HintsUsed,
|
|
IsWinner: s.IsWinner,
|
|
})
|
|
}
|
|
last := g.TurnStartedAt
|
|
if g.FinishedAt != nil {
|
|
last = *g.FinishedAt
|
|
}
|
|
return notify.GameSummary{
|
|
ID: g.ID.String(),
|
|
Variant: g.Variant.String(),
|
|
DictVersion: g.DictVersion,
|
|
Status: g.Status,
|
|
Players: g.Players,
|
|
ToMove: g.ToMove,
|
|
TurnTimeoutSecs: int(g.TurnTimeout.Seconds()),
|
|
MultipleWordsPerTurn: g.MultipleWordsPerTurn,
|
|
VsAI: g.VsAI,
|
|
MoveCount: g.MoveCount,
|
|
EndReason: g.EndReason,
|
|
Seats: seats,
|
|
LastActivityUnix: last.Unix(),
|
|
}
|
|
}
|
|
|
|
// playerState projects a StateView into the notify.PlayerState carried by the
|
|
// match_found / game_started events. The rack is re-encoded to wire alphabet indices;
|
|
// the variant alphabet display table is embedded when includeAlphabet is set (an
|
|
// initial view whose recipient may not have cached the variant yet).
|
|
func playerState(v StateView, names []string, includeAlphabet bool) (notify.PlayerState, error) {
|
|
rack, err := engine.EncodeRack(v.Game.Variant, v.Rack)
|
|
if err != nil {
|
|
return notify.PlayerState{}, err
|
|
}
|
|
ps := notify.PlayerState{
|
|
Game: gameSummary(v.Game, names),
|
|
Seat: v.Seat,
|
|
Rack: rack,
|
|
BagLen: v.BagLen,
|
|
HintsRemaining: v.HintsRemaining,
|
|
WalletBalance: v.WalletBalance,
|
|
}
|
|
if includeAlphabet {
|
|
tab, err := engine.AlphabetTable(v.Game.Variant)
|
|
if err != nil {
|
|
return notify.PlayerState{}, err
|
|
}
|
|
ps.Alphabet = make([]notify.AlphabetLetter, len(tab))
|
|
for i, e := range tab {
|
|
ps.Alphabet[i] = notify.AlphabetLetter{Index: int(e.Index), Letter: e.Letter, Value: e.Value}
|
|
}
|
|
}
|
|
return ps, nil
|
|
}
|