package sim import ( "math" "path/filepath" "testing" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/lang" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/res" ) func loadDict(t *testing.T) *lang.Dictionary { t.Helper() b, err := res.ReadFile("text", "jp-dictionary.json") if err != nil { t.Fatal(err) } d, err := lang.ParseDictionary(b) if err != nil { t.Fatal(err) } return d } const frame = 1.0 / 60 func loadLevel(t *testing.T, id string) *level.Level { t.Helper() dir, err := res.Path("data", "levels") if err != nil { t.Fatal(err) } l, err := level.LoadFile(filepath.Join(dir, id+".json")) if err != nil { t.Fatal(err) } return l } func dirTo(from, to level.Point) Dir { switch { case to.X > from.X: return Right case to.X < from.X: return Left case to.Y > from.Y: return Down default: return Up } } // walk låter en bot följa en väg ruta för ruta, en bildruta i taget. func walk(t *testing.T, r *Run, path []level.Point) { t.Helper() for _, next := range path[1:] { d := dirTo(r.Player.Tile, next) for i := 0; i < 600; i++ { if r.Player.Tile == next && !r.Player.Moving() { break } r.Step(frame, d, false) } if r.Player.Tile != next { t.Fatalf("boten fastnade på %v på väg mot %v", r.Player.Tile, next) } } } // waitForTrain står still tills tåget vid perrongen är inne. func waitForTrain(t *testing.T, r *Run) *level.Train { t.Helper() for i := 0; i < 60*120; i++ { tr, at, _ := r.PlatformHere() if tr == nil { t.Fatalf("står inte på en perrong: %v", r.Player.Tile) } if at { return tr } r.Step(frame, None, false) } t.Fatal("tåget kom aldrig") return nil } // TestBotWinsLevel001 är simuleringstestet för M2: en bot går kortaste vägen // från start till rätt perrong i bana 001, passerar spärren, väntar in tåget // och kliver på – helt utan fönster. func TestBotWinsLevel001(t *testing.T) { l := loadLevel(t, "001-flygplatsen") r := NewRun(l, 6, nil, nil) goal := l.Find(level.Platform)[0] path := l.Path(r.Player.Tile, goal, r.CanEnter) if path == nil { t.Fatal("ingen väg till perrongen") } walk(t, r, path) if r.Outcome != Ongoing { t.Fatal("banan ska inte vara avgjord innan man kliver på") } r.Step(frame, None, true) // Enter medan tåget kanske inte är inne får inte avgöra fel tr := waitForTrain(t, r) if !tr.Correct { t.Fatal("rätt perrong ska ha rätt tåg") } r.Step(frame, None, true) if r.Outcome != Won || r.Boarded != tr { t.Fatalf("outcome=%v boarded=%v", r.Outcome, r.Boarded) } if r.GatePasses != 1 || r.Card.Balance != StartBalanceYen-l.FareYen { t.Fatalf("spärrpassager=%d saldo=%d (vill ha 1 och %d)", r.GatePasses, r.Card.Balance, StartBalanceYen-l.FareYen) } if r.Steps != len(path)-1 { t.Fatalf("steg=%d, vägen är %d rutor", r.Steps, len(path)-1) } // Efter utfall händer inget mer. before := r.Time r.Step(frame, Right, true) if r.Time != before { t.Fatal("Step ska vara en no-op när banan är avgjord") } } func TestBotLosesOnWrongTrain(t *testing.T) { l := loadLevel(t, "001-flygplatsen") r := NewRun(l, 6, nil, nil) goal := l.Find(level.WrongPlatform)[0] walk(t, r, l.Path(r.Player.Tile, goal, r.CanEnter)) tr := waitForTrain(t, r) r.Step(frame, None, true) if r.Outcome != Lost || r.Boarded != tr || tr.Correct { t.Fatalf("outcome=%v boarded=%+v", r.Outcome, r.Boarded) } } func TestGateNeedsCardBalance(t *testing.T) { l := loadLevel(t, "001-flygplatsen") r := NewRun(l, 6, nil, nil) gate := l.Find(level.Gate)[0] r.Card.Balance = 0 if r.CanEnter(gate) { t.Fatal("spärren ska stoppa ett tomt kort") } if r.DeniedAt < 0 { t.Fatal("nekandet ska registreras för HUD:en") } if l.Path(r.Player.Tile, l.Find(level.Platform)[0], r.CanEnter) != nil { t.Fatal("utan saldo ska perrongen vara onåbar") } r.Card.Balance = l.FareYen if !r.CanEnter(gate) { t.Fatal("med exakt avgiften ska spärren släppa igenom") } // Mellan två spärrutor är det gratis: ställ spelaren i spärren. r.Player.Tile, r.Player.Target = gate, gate r.Card.Balance = 0 other := level.Point{X: gate.X + 1, Y: gate.Y} if l.At(other.X, other.Y) == level.Gate && !r.CanEnter(other) { t.Fatal("att gå mellan två spärrutor ska inte kosta") } } func TestBoardOnlyWhenTrainPresent(t *testing.T) { l := loadLevel(t, "001-flygplatsen") r := NewRun(l, 6, nil, nil) goal := l.Find(level.Platform)[0] walk(t, r, l.Path(r.Player.Tile, goal, r.CanEnter)) // Spola fram till en tidpunkt då tåget är borta. for { _, at, _ := r.PlatformHere() if !at { break } r.Step(frame, None, false) } r.Step(frame, None, true) if r.Outcome != Ongoing { t.Fatal("man ska inte kunna kliva på ett tåg som inte är inne") } } func TestSignHereOnlyWhenStill(t *testing.T) { l := loadLevel(t, "001-flygplatsen") r := NewRun(l, 6, nil, nil) sign := l.Find(level.Sign)[0] walk(t, r, l.Path(r.Player.Tile, sign, r.CanEnter)) if sg := r.SignHere(); sg == nil || sg.Text.Get("sv") == "" { t.Fatal("stilla på skyltrutan ska ge skylten") } r.Step(frame, Left, false) if r.Player.Moving() && r.SignHere() != nil { t.Fatal("i rörelse visas ingen skylt") } } // TestAskHelpRevealsSignAndTeachesWord: att fråga om hjälp tar // lang.HelpSeconds, avslöjar skylten och lär spelaren ett ord från den. func TestAskHelpRevealsSignAndTeachesWord(t *testing.T) { l := loadLevel(t, "001-flygplatsen") know := lang.NewKnowledge() r := NewRun(l, 6, loadDict(t), know) sign := l.Find(level.Sign)[1] walk(t, r, l.Path(r.Player.Tile, sign, r.CanEnter)) if !r.AskHelp() { t.Fatal("ska kunna fråga om hjälp på en skylt") } if r.AskHelp() { t.Fatal("inte två hjälp samtidigt") } want := lang.HelpSeconds(0) if math.Abs(r.Help.Total-want) > 1e-9 { t.Fatalf("hjälptid %.1f, vill ha %.1f", r.Help.Total, want) } frames := int(want/frame) - 2 for i := 0; i < frames; i++ { r.Step(frame, None, false) } if !r.Help.Active || r.SignRevealed(sign) || r.HelpProgress() < 0.9 { t.Fatalf("strax före klart: active=%v revealed=%v progress=%.2f", r.Help.Active, r.SignRevealed(sign), r.HelpProgress()) } for i := 0; i < 4; i++ { r.Step(frame, None, false) } if r.Help.Active || !r.SignRevealed(sign) { t.Fatal("hjälpen ska vara klar och skylten avslöjad") } if know.Count() != 1 || r.Learned == "" || !know.Knows(r.Learned) || know.Helps != 1 { t.Fatalf("lärde sig fel: count=%d learned=%q helps=%d", know.Count(), r.Learned, know.Helps) } if first := lang.FirstUnknownWord(r.Dict, lang.NewKnowledge(), l.SignAt(sign).Text["ja"]); first != r.Learned { t.Fatalf("ska lära sig första okända ordet %q, lärde sig %q", first, r.Learned) } if r.AskHelp() { t.Fatal("en avslöjad skylt behöver ingen hjälp") } } func TestHelpIsCancelledByWalkingAway(t *testing.T) { l := loadLevel(t, "001-flygplatsen") r := NewRun(l, 6, loadDict(t), nil) sign := l.Find(level.Sign)[1] walk(t, r, l.Path(r.Player.Tile, sign, r.CanEnter)) r.AskHelp() r.Step(frame, Left, false) if r.Help.Active { t.Fatal("att gå iväg ska avbryta hjälpen") } if r.Know.Count() != 0 || r.SignRevealed(sign) { t.Fatal("inget lärt, inget avslöjat") } } func TestNoHelpOffSign(t *testing.T) { l := loadLevel(t, "001-flygplatsen") r := NewRun(l, 6, loadDict(t), nil) if r.AskHelp() { t.Fatal("ingen skylt på startrutan") } }