41a642ef97
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 37s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
Enrich the in-app live stream into a delta channel so the UI renders a move from the event without a follow-up game.state, and make the matchmaking poll a stream-down fallback. - pkg/fbs: trailing fields on opponent_moved (move+game+bag_len), your_turn (move_count), match_found (state), game_over (game), notify (account/invitation/state), MoveResult (rack+bag_len); regenerate Go + TS. - backend: notify owns the FB encoding (encode.go + payload.go input structs); game/lobby/social map their domain types in. emitMove builds the move delta; game.Service.InitialState feeds match_found/game_started the recipient's initial StateView; friends/invitations notify carry their account/invitation. The move-commit response (submit_play/pass/exchange/resign) returns the actor's refilled rack + bag size. - gateway: MoveResult transcode carries rack+bag_len. - ui: pure lib/gamedelta.ts reducer advances the per-game cache keyed on move_count (idempotent + gap-safe); app.svelte seeds the cache on match_found/game_started; Game.svelte applies the delta (commit/pass/exchange/resign drop their load()); NewGame polls only while app.streamAlive is false. - docs: ARCHITECTURE §10, FUNCTIONAL(+ru), backend/gateway/ui READMEs; PRERELEASE R4 marked done + Refinements.
80 lines
2.6 KiB
Go
80 lines
2.6 KiB
Go
package game
|
|
|
|
import (
|
|
"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 (R4). 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]
|
|
}
|
|
seats = append(seats, notify.SeatStanding{
|
|
Seat: s.Seat,
|
|
AccountID: s.AccountID.String(),
|
|
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()),
|
|
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,
|
|
}
|
|
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
|
|
}
|