c305363ccd
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 45s
CI / gate (pull_request) Successful in 1s
CI / deploy (pull_request) Successful in 1m4s
Quick auto-match no longer waits on a separate screen: Enqueue opens a real game seating the caller with an empty opponent seat (new game status 'open') and the player enters it at once. A second human searching the same variant+rule joins that open game; otherwise a background reaper seats a robot after a 90s + random 0-90s wait, pushing a new in-app opponent_joined event that fills the opponent card and re-enables resign and chat in place. Matchmaking state is now the open games in the database (the in-memory pool, lobby.poll and lobby.cancel are gone), serialised by a per-bucket advisory lock. While a game is open the starter may move on their turn, but resign, chat and nudge are refused; the lobby and opponent card show "searching for opponent". Schema edited in the baseline (no prod data): 'open' status, nullable game_players.account_id for the empty seat, and a games.open_deadline_at stamp; jet code regenerated.
89 lines
3.0 KiB
Go
89 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,
|
|
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
|
|
}
|