Files
scrabble-game/backend/internal/game/gcg_test.go
T
Ilia Denisov 222eaf730f
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 46s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m13s
feat(game): void unreplayable games as a draw instead of erroring
A committed move that becomes illegal under a tightened rule (the single-word
connectivity fix) makes engine replay fail, which left such games unopenable —
an empty screen and an 'illegal play' error. Now the first open closes the game
gracefully as a draw (engine.EndAborted -> end_reason 'aborted', no winner),
preserves the journal, and surfaces an impersonal organizer note at the end of
the move history and in the GCG export.

- engine: EndAborted + Abort() (draw, no rack adjustment; winner -1).
- service: replay aborts on ErrIllegalPlay; liveGame persists the void once
  (lazy, on open); GameState re-reads for the settled view.
- store: VoidGame finishes the game and stamps a draw without a journal row.
- migration 00002: allow end_reason 'aborted'.
- ui: organizer note under the history grid; i18n en/ru.
- docs: ARCHITECTURE 6/9.1, FUNCTIONAL(+ru), PRERELEASE MW3.
2026-06-14 16:57:27 +02:00

95 lines
2.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package game
import (
"strings"
"testing"
"github.com/google/uuid"
"scrabble/backend/internal/engine"
)
func TestWriteGCG(t *testing.T) {
g := Game{
ID: uuid.MustParse("00000000-0000-7000-8000-000000000001"),
Variant: engine.VariantEnglish,
DictVersion: "v1",
Players: 2,
}
moves := []HistoryMove{
{
Seq: 0, Seat: 0, Action: "play", Score: 10, RunningTotal: 10,
Dir: "H", MainRow: 7, MainCol: 7,
Tiles: []engine.TileRecord{{Row: 7, Col: 7, Letter: "c"}, {Row: 7, Col: 8, Letter: "a"}, {Row: 7, Col: 9, Letter: "t"}},
Words: []string{"cat"}, Rack: []string{"c", "a", "t", "s", "e", "r", "?"},
},
{
Seq: 1, Seat: 1, Action: "play", Score: 2, RunningTotal: 2,
Dir: "V", MainRow: 7, MainCol: 8,
Tiles: []engine.TileRecord{{Row: 8, Col: 8, Letter: "s", Blank: true}},
Words: []string{"as"}, Rack: []string{"a", "s", "?", "e"},
},
{Seq: 2, Seat: 0, Action: "pass", RunningTotal: 10, Rack: []string{"x", "y", "z"}},
{Seq: 3, Seat: 1, Action: "exchange", RunningTotal: 2, Exchanged: []string{"q", "u"}, Rack: []string{"q", "u", "i"}},
{Seq: 4, Seat: 0, Action: "resign", RunningTotal: 10, Rack: []string{"a", "b"}},
{Seq: 5, Seat: 1, Action: "timeout", RunningTotal: 2, Rack: []string{"c"}},
}
out := writeGCG(g, []string{"Alice", "Bob"}, moves)
wantLines := []string{
"#character-encoding UTF-8",
"#player1 p1 Alice",
"#player2 p2 Bob",
"#lexicon scrabble_en/v1",
"#title game 00000000-0000-7000-8000-000000000001",
">p1: CATSER? 8H CAT +10 10",
">p2: AS?E I8 .s +2 2",
">p1: XYZ - +0 10",
">p2: QUI -QU +0 2",
"#note p1 resigned (rack AB)",
"#note p2 timed out (rack C)",
}
lines := strings.Split(strings.TrimRight(out, "\n"), "\n")
got := make(map[string]bool, len(lines))
for _, l := range lines {
got[l] = true
}
for _, want := range wantLines {
if !got[want] {
t.Errorf("GCG missing line %q\n--- full output ---\n%s", want, out)
}
}
}
func TestWriteGCGAbortedNote(t *testing.T) {
g := Game{
ID: uuid.MustParse("00000000-0000-7000-8000-000000000002"),
Variant: engine.VariantErudit,
DictVersion: "v1",
Players: 2,
Status: StatusFinished,
EndReason: "aborted",
}
out := writeGCG(g, []string{"Alice", "Bob"}, nil)
if !strings.Contains(out, "#note [organizer]") {
t.Errorf("aborted game GCG missing the organizer note:\n%s", out)
}
}
func TestGCGTilesUppercasesCyrillic(t *testing.T) {
if got := gcgTiles([]string{"к", "о", "т", "?"}); got != "КОТ?" {
t.Errorf("gcgTiles = %q, want КОТ?", got)
}
}
func TestGCGPos(t *testing.T) {
across := gcgPos(HistoryMove{Dir: "H", MainRow: 7, MainCol: 6})
if across != "8G" {
t.Errorf("across pos = %q, want 8G", across)
}
down := gcgPos(HistoryMove{Dir: "V", MainRow: 6, MainCol: 7})
if down != "H7" {
t.Errorf("down pos = %q, want H7", down)
}
}