clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

save state management

Dami committed 3c05f0f
commit
3c05f0f6ac8122f4ee8df4196ba00c48d1aa66ca
parent
c28818af137a70d5fe351950e953a02358edda4f
author
Dami <dami.etoile@gmail.com>
3 files changed, +175 -9
Mapp/game_session.go8+-
Mapp/update.go12+-
Mapp/update_test.go164++++++++++++++++++++++++++++++
M app/game_session.go +4 -4
M app/update.go +7 -5
M app/update_test.go +164 -0
@@ -1,11 +1,18 @@
11 package app22 33 import (4+ "path/filepath"45 "testing"6+ "time"57 68 "charm.land/bubbles/v2/key"9+ "charm.land/bubbles/v2/textinput"710 tea "charm.land/bubbletea/v2"11+ "github.com/FelineStateMachine/puzzletea/daily"812 "github.com/FelineStateMachine/puzzletea/game"13+ "github.com/FelineStateMachine/puzzletea/namegen"14+ "github.com/FelineStateMachine/puzzletea/resolve"15+ "github.com/FelineStateMachine/puzzletea/store"916 )1017 1118 type escapeTrackingGame struct {
@@ -74,3 +81,160 @@ func TestGameViewEscapePassesThroughToGame(t *testing.T) {
7481 t.Fatal("expected game to receive escape key")7582 }7683 }84+85+func TestImportAndActivateRecordSuccessFlag(t *testing.T) {86+ unknown := model{state: playMenuView}87+ if _, ok := unknown.importAndActivateRecord(store.GameRecord{GameType: "NoSuchGameType"}); ok {88+ t.Fatal("expected unknown game type import to fail")89+ }90+91+ const gameType = "TestImportAndActivateRecordGame"92+ prev, hadPrev := game.Registry[gameType]93+ game.Register(gameType, func(_ []byte) (game.Gamer, error) {94+ return &escapeTrackingGame{}, nil95+ })96+ t.Cleanup(func() {97+ if hadPrev {98+ game.Registry[gameType] = prev99+ return100+ }101+ delete(game.Registry, gameType)102+ })103+104+ rec := store.GameRecord{105+ ID: 42,106+ Name: "resume-me",107+ GameType: gameType,108+ SaveState: "{}",109+ Status: store.StatusCompleted,110+ }111+112+ next, ok := (model{113+ state: playMenuView,114+ width: 80,115+ height: 24,116+ showFullHelp: true,117+ }).importAndActivateRecord(rec)118+ if !ok {119+ t.Fatal("expected known game type import to succeed")120+ }121+ if next.state != gameView {122+ t.Fatalf("state = %d, want %d (gameView)", next.state, gameView)123+ }124+ if next.activeGameID != rec.ID {125+ t.Fatalf("activeGameID = %d, want %d", next.activeGameID, rec.ID)126+ }127+ if !next.completionSaved {128+ t.Fatal("expected completionSaved to be true for completed record")129+ }130+ if next.game == nil {131+ t.Fatal("expected game to be activated")132+ }133+}134+135+func TestHandleSeedConfirmDoesNotResumeStatusWhenImportFails(t *testing.T) {136+ s := openAppTestStore(t)137+ seed := "import-failure-seed"138+ nameRNG := resolve.RNGFromString("name:" + seed)139+ name := seed + " - " + namegen.GenerateSeeded(nameRNG)140+ rec := &store.GameRecord{141+ Name: name,142+ GameType: "NoSuchGameType",143+ Mode: "Any",144+ InitialState: "{}",145+ SaveState: "{}",146+ Status: store.StatusAbandoned,147+ }148+ if err := s.CreateGame(rec); err != nil {149+ t.Fatal(err)150+ }151+152+ ti := textinput.New()153+ ti.SetValue(seed)154+ m := model{155+ state: seedInputView,156+ store: s,157+ seedInput: ti,158+ }159+160+ next, _ := m.handleSeedConfirm()161+ got := next.(model)162+ if got.state != seedInputView {163+ t.Fatalf("state = %d, want %d (seedInputView)", got.state, seedInputView)164+ }165+166+ saved, err := s.GetDailyGame(name)167+ if err != nil {168+ t.Fatal(err)169+ }170+ if saved == nil {171+ t.Fatal("expected seeded record to exist")172+ }173+ if saved.Status != store.StatusAbandoned {174+ t.Fatalf("status = %q, want %q", saved.Status, store.StatusAbandoned)175+ }176+}177+178+func TestHandleDailyPuzzleDoesNotResumeStatusWhenImportFails(t *testing.T) {179+ s := openAppTestStore(t)180+ now := time.Now()181+ dates := []time.Time{182+ now.AddDate(0, 0, -1),183+ now,184+ now.AddDate(0, 0, 1),185+ }186+187+ seen := map[string]bool{}188+ for _, d := range dates {189+ name := daily.Name(d)190+ if seen[name] {191+ continue192+ }193+ seen[name] = true194+ rec := &store.GameRecord{195+ Name: name,196+ GameType: "NoSuchGameType",197+ Mode: "Any",198+ InitialState: "{}",199+ SaveState: "{}",200+ Status: store.StatusAbandoned,201+ }202+ if err := s.CreateGame(rec); err != nil {203+ t.Fatal(err)204+ }205+ }206+207+ m := model{208+ state: playMenuView,209+ store: s,210+ }211+212+ next, _ := m.handleDailyPuzzle()213+ got := next.(model)214+ if got.state != playMenuView {215+ t.Fatalf("state = %d, want %d (playMenuView)", got.state, playMenuView)216+ }217+218+ for name := range seen {219+ saved, err := s.GetDailyGame(name)220+ if err != nil {221+ t.Fatal(err)222+ }223+ if saved == nil {224+ t.Fatalf("expected daily record %q to exist", name)225+ }226+ if saved.Status != store.StatusAbandoned {227+ t.Fatalf("record %q status = %q, want %q", name, saved.Status, store.StatusAbandoned)228+ }229+ }230+}231+232+func openAppTestStore(t *testing.T) *store.Store {233+ t.Helper()234+ s, err := store.Open(filepath.Join(t.TempDir(), "test.db"))235+ if err != nil {236+ t.Fatal(err)237+ }238+ t.Cleanup(func() { _ = s.Close() })239+ return s240+}