M8: progression, items, bankarta och sparfil
All checks were successful
release / build-release (push) Successful in 1m46s

- internal/progress: katalog (skills.json 12 noder, items.json 25 items)
  med validering; XP/nivåer (150·n(n+1)/2) och skillpoäng; skillträd med
  krav och kostnad; items per plats med buffar, genvägar (JR Pass, kartor,
  powerbank) och färgton; banresultat och upplåsning (huvudbanor i ordning,
  sidospår efter föräldern); 33 %-regeln som test
- internal/save: versionerad JSON med kunskap, plånbok, debuffar,
  progression och pågående bana + seed; atomisk skrivning; migrering;
  ~/.config/metro-puzzle-jp/save.json resp. %AppData%
- sim: RunOptions får Extra/FogRadius/Landmarks/FreeGates/RevealAll/
  InfiniteBattery; luck påverkar trängselzonen; Fog.RevealAll och extra
  landmärken
- game: bankarta i spelbrädesstil ersätter menyn (⟳ återupptar med sparad
  seed), skillträd (T), garderob (I), butik i kiosken (B), XP/nivå/items/
  upplåsningar på resultatet, spelaren tonas av skins; autospar vid banstart,
  efter banan och vid köp; flaggor -save/-nosave
- level: kind/order/parent/rewards/shop; ny sidobana 002b Kabukicho som
  belönar skinnet Kimono; 001 belönar Salaryman-skinnet; butikssortiment
  på 002/003
- tools/shots: demo-sparfil (build/, aldrig den riktiga), bilder av karta,
  skills, garderob och butik; alla körningar med -nosave
- docs/README/wiki uppdaterade

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 11:29:30 +02:00
parent d466333f01
commit 915c684dc2
46 changed files with 3058 additions and 316 deletions

View File

@@ -0,0 +1,78 @@
package save
import (
"os"
"path/filepath"
"testing"
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/sim"
)
func TestRoundTrip(t *testing.T) {
path := filepath.Join(t.TempDir(), "save.json")
st, exists, err := Load(path)
if err != nil || exists || st.Version != Version || st.Progress == nil || st.Know.Count() != 0 {
t.Fatalf("saknad fil ska ge nytt spel: exists=%v err=%v", exists, err)
}
st.Know.Learn("改札")
st.Know.Points = 3
st.Wallet.Card = 1234
st.Wallet.Phone = true
st.Profile.Add(sim.Tired, 2)
st.Progress.AddXP(500)
st.Progress.Skills["kana"] = true
st.Progress.Items["geta"] = true
st.Progress.Equipped["feet"] = "geta"
st.Progress.RecordLevel("001-flygplatsen", true, 3, 941)
st.Current = &RunRef{LevelID: "002-shinjuku", Seed: [2]uint64{7, 99}}
if err := Store(path, st); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(path + ".tmp"); err == nil {
t.Fatal("tempfilen ska vara borta")
}
got, exists, err := Load(path)
if err != nil || !exists {
t.Fatalf("exists=%v err=%v", exists, err)
}
if !got.Know.Knows("改札") || got.Know.Points != 3 || got.Wallet.Card != 1234 || !got.Wallet.Phone {
t.Fatal("kunskap/plånbok")
}
if !got.Profile.Has(sim.Tired) || got.Progress.XP != 500 || got.Progress.Level() != 3 || !got.Progress.Has("kana") || got.Progress.Equipped["feet"] != "geta" {
t.Fatalf("profil/progression: %+v", got.Progress)
}
if !got.Progress.Cleared("001-flygplatsen") || got.Progress.Levels["001-flygplatsen"].Score != 941 {
t.Fatal("banresultat")
}
if got.Current == nil || got.Current.LevelID != "002-shinjuku" || got.Current.Seed != [2]uint64{7, 99} {
t.Fatalf("pågående omgång: %+v", got.Current)
}
if got.SavedAt.IsZero() {
t.Fatal("SavedAt")
}
}
func TestCorruptAndFutureVersions(t *testing.T) {
path := filepath.Join(t.TempDir(), "save.json")
os.WriteFile(path, []byte("{nope"), 0o644)
if _, _, err := Load(path); err == nil {
t.Fatal("trasig fil ska ge fel")
}
os.WriteFile(path, []byte(`{"version": 99}`), 0o644)
if _, _, err := Load(path); err == nil {
t.Fatal("nyare version ska ge fel")
}
// Version 0 utan fält migreras till ett komplett tillstånd.
os.WriteFile(path, []byte(`{"knowledge": {"words": {"改札": true}}}`), 0o644)
st, exists, err := Load(path)
if err != nil || !exists || st.Version != Version || !st.Know.Knows("改札") || st.Wallet == nil || st.Progress == nil || st.Profile == nil {
t.Fatalf("migrering: %+v err=%v", st, err)
}
}
func TestDefaultPath(t *testing.T) {
p, err := DefaultPath()
if err != nil || filepath.Base(p) != "save.json" || filepath.Base(filepath.Dir(p)) != AppDir {
t.Fatalf("path=%q err=%v", p, err)
}
}