All checks were successful
release / build-release (push) Successful in 1m41s
- sim.Wallet: IC-kort, kontanter, pappersbiljett, telefon med batteri, spenderat; följer med mellan banor (Game.Wallet) - internal/minigame: gemensamt Game-gränssnitt (Update/Done/Success), all slump från del-seeds: TopUp (hitta チャージ, belopp, mata sedlar), Ticket (taxetavla med resmål på japanska), Crowd (tidsfönster, två försök), Ride (stationerna passerar, kliv av vid resmålet) - sim.Run: spärren kräver biljett på biljettbanor annars användbart kort; telefonen ger ×1,1 fart men 15 % seedad omskanning (frysning 1,5 s), dött batteri gör kortet oanvändbart; automaten startar påfyllning eller biljettköp; tåget kontrollerar biljetten; fulla tåg kräver trängsel; resan avgör vinst/förlust (fel station, åkte förbi, fel tåg); StepMini låter klockan och sällskapet ticka medan rörelsen pausar - level: ticketRequired, rideStops, Train.Crowded; 002 är biljettbana med fullt tåg och fyra stopp; ordboken har stationer (kind) + sju nya namn - game: vyer för minispelen, plånbok i HUD, P i menyn = kort i telefonen, förlustorsaker och spenderat på resultatet, flaggor -phone/-card, skriptkommandon hold/waitstop; inforuta/minikarta döljs under minispel - plan: M7b (Björn) – tågvind, ankomstindikator nära rälsen, in-/utkörningsanimation för tåg och bussar – i spelbeskrivning, roadmap, testplan - docs/README/wiki med tre nya bilder (automat, biljettavla, tågresa) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
667 lines
21 KiB
Go
667 lines
21 KiB
Go
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/minigame"
|
||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/res"
|
||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
|
||
)
|
||
|
||
// ride spelar tågresan: kliver av när tåget står vid resmålet. På fel tåg
|
||
// finns inget resmål, så resan går till slutstationen.
|
||
func ride(t *testing.T, r *Run) {
|
||
t.Helper()
|
||
if r.MiniKind != MiniRide {
|
||
t.Fatalf("ingen tågresa pågår (kind=%v)", r.MiniKind)
|
||
}
|
||
for i := 0; i < 60*120 && r.Mini != nil; i++ {
|
||
g := r.Mini.(*minigame.Ride)
|
||
r.StepMini(frame, minigame.Input{Confirm: g.Index == g.Dest})
|
||
}
|
||
if r.Mini != nil {
|
||
t.Fatal("resan tog aldrig slut")
|
||
}
|
||
}
|
||
|
||
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. Tangenten
|
||
// släpps så fort steget mot nästa ruta påbörjats, så att boten stannar exakt
|
||
// där även när farten inte är bildruteexakt (telefon- eller sällskapsfaktor).
|
||
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
|
||
}
|
||
if r.Player.Target == next || r.Player.Tile == next {
|
||
r.Step(frame, None, false)
|
||
} else {
|
||
r.Step(frame, d, false)
|
||
}
|
||
}
|
||
if r.Player.Tile != next || r.Player.Moving() {
|
||
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, RunOptions{Seed: rng.FromInt(1)})
|
||
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 != Ongoing || r.MiniKind != MiniRide {
|
||
t.Fatalf("att kliva på ska starta tågresan: outcome=%v kind=%v", r.Outcome, r.MiniKind)
|
||
}
|
||
ride(t, r)
|
||
if r.Outcome != Won || r.Boarded != tr {
|
||
t.Fatalf("outcome=%v boarded=%v", r.Outcome, r.Boarded)
|
||
}
|
||
if r.GatePasses != 1 || r.Wallet.Card != StartCardYen-l.FareYen {
|
||
t.Fatalf("spärrpassager=%d saldo=%d (vill ha 1 och %d)", r.GatePasses, r.Wallet.Card, StartCardYen-l.FareYen)
|
||
}
|
||
if r.Steps != len(path)-1 {
|
||
t.Fatalf("steg=%d, vägen är %d rutor", r.Steps, len(path)-1)
|
||
}
|
||
if r.Stars != 3 || r.PointsEarned != 3*lang.PointsPerStar || r.Know.Points != r.PointsEarned {
|
||
t.Fatalf("snabb vinst ska ge 3 stjärnor: stars=%d points=%d", r.Stars, r.PointsEarned)
|
||
}
|
||
// 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, RunOptions{Seed: rng.FromInt(1)})
|
||
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)
|
||
ride(t, r)
|
||
if r.Outcome != Lost || r.Boarded != tr || tr.Correct || r.LostReason != LostWrongTrain {
|
||
t.Fatalf("outcome=%v boarded=%+v reason=%q", r.Outcome, r.Boarded, r.LostReason)
|
||
}
|
||
}
|
||
|
||
func TestGateNeedsCardBalance(t *testing.T) {
|
||
l := loadLevel(t, "001-flygplatsen")
|
||
r := NewRun(l, RunOptions{Seed: rng.FromInt(1)})
|
||
gate := l.Find(level.Gate)[0]
|
||
r.Wallet.Card = 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.Wallet.Card = 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.Wallet.Card = 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, RunOptions{Seed: rng.FromInt(1)})
|
||
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, RunOptions{Seed: rng.FromInt(1)})
|
||
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, RunOptions{Dict: loadDict(t), Know: know, Seed: rng.FromInt(1)})
|
||
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, RunOptions{Dict: loadDict(t), Seed: rng.FromInt(1)})
|
||
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, RunOptions{Dict: loadDict(t), Seed: rng.FromInt(1)})
|
||
if r.AskHelp() {
|
||
t.Fatal("ingen skylt på startrutan")
|
||
}
|
||
}
|
||
|
||
func TestStarsFromParTime(t *testing.T) {
|
||
l := loadLevel(t, "001-flygplatsen")
|
||
r := NewRun(l, RunOptions{Seed: rng.FromInt(1)})
|
||
par := r.ParSec()
|
||
n := len(l.Path(l.SpawnPoint(), l.CorrectPlatform(), l.CanEnter)) - 1
|
||
if want := float64(n)*ParSecPerTile + ParSlackSec; par != want {
|
||
t.Fatalf("par=%.1f, vill ha %.1f (%d rutor)", par, want, n)
|
||
}
|
||
if r.StarsFor(par) != 3 || r.StarsFor(par*1.5) != 2 || r.StarsFor(par*1.51) != 1 {
|
||
t.Fatal("stjärntrösklarna")
|
||
}
|
||
l.ParSec = 40
|
||
if NewRun(l, RunOptions{}).ParSec() != 40 {
|
||
t.Fatal("parSec i bandatan ska vinna")
|
||
}
|
||
}
|
||
|
||
// TestGoodResultTeachesDestination: tre stjärnor lär ut resmålets ord (新宿),
|
||
// en långsam vinst gör det inte men ger ändå en poäng.
|
||
func TestGoodResultTeachesDestination(t *testing.T) {
|
||
l := loadLevel(t, "001-flygplatsen")
|
||
for _, tc := range []struct {
|
||
name string
|
||
slow bool
|
||
stars int
|
||
knowsDst bool
|
||
}{{"snabb", false, 3, true}, {"långsam", true, 1, false}} {
|
||
know := lang.NewKnowledge()
|
||
r := NewRun(l, RunOptions{Dict: loadDict(t), Know: know, Seed: rng.FromInt(2)})
|
||
walk(t, r, l.Path(r.Player.Tile, l.CorrectPlatform(), r.CanEnter))
|
||
waitForTrain(t, r)
|
||
if tc.slow {
|
||
r.Time = 10 * r.ParSec()
|
||
waitForTrain(t, r)
|
||
}
|
||
r.Step(frame, None, true)
|
||
ride(t, r)
|
||
if r.Outcome != Won || r.Stars != tc.stars {
|
||
t.Fatalf("%s: outcome=%v stars=%d", tc.name, r.Outcome, r.Stars)
|
||
}
|
||
if know.Knows("新宿") != tc.knowsDst || (tc.knowsDst && (len(r.LearnedDest) != 1 || r.LearnedDest[0] != "新宿")) {
|
||
t.Fatalf("%s: knows 新宿=%v learnedDest=%v", tc.name, know.Knows("新宿"), r.LearnedDest)
|
||
}
|
||
if know.Points != tc.stars*lang.PointsPerStar {
|
||
t.Fatalf("%s: poäng=%d", tc.name, know.Points)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestReadingGivesSeededProgress: att stanna på en skylt ger okända ord en
|
||
// chans till framsteg; samma seed ger exakt samma utfall, och efter många
|
||
// besök har något ord lossnat.
|
||
func TestReadingGivesSeededProgress(t *testing.T) {
|
||
l := loadLevel(t, "001-flygplatsen")
|
||
sign := l.Find(level.Sign)[1]
|
||
left := level.Point{X: sign.X - 1, Y: sign.Y}
|
||
play := func(seed uint64) *lang.Knowledge {
|
||
know := lang.NewKnowledge()
|
||
r := NewRun(l, RunOptions{Dict: loadDict(t), Know: know, Seed: rng.FromInt(seed)})
|
||
walk(t, r, l.Path(r.Player.Tile, sign, r.CanEnter))
|
||
for i := 0; i < 15; i++ { // gå av och på skylten = nya besök
|
||
walk(t, r, []level.Point{sign, left})
|
||
walk(t, r, []level.Point{left, sign})
|
||
}
|
||
if r.Visits[sign] != 16 {
|
||
t.Fatalf("besök=%d", r.Visits[sign])
|
||
}
|
||
return know
|
||
}
|
||
a, b := play(5), play(5)
|
||
if a.Count() != b.Count() || len(a.Progress) != len(b.Progress) {
|
||
t.Fatalf("samma seed ska ge samma inlärning: %v/%v vs %v/%v", a.Words, a.Progress, b.Words, b.Progress)
|
||
}
|
||
for w, n := range a.Progress {
|
||
if b.Progress[w] != n {
|
||
t.Fatalf("framsteg skiljer sig för %s", w)
|
||
}
|
||
}
|
||
if a.Count() == 0 {
|
||
t.Fatalf("efter 16 läsningar borde minst ett ord ha lossnat: progress=%v", a.Progress)
|
||
}
|
||
for w := range a.Words {
|
||
if a.ProgressOf(w) != 0 {
|
||
t.Fatalf("känt ord %s ska inte ha framsteg kvar", w)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestToiletAndKioskInRun(t *testing.T) {
|
||
l := loadLevel(t, "001-flygplatsen")
|
||
r := NewRun(l, RunOptions{Seed: rng.FromInt(1)})
|
||
if r.Party.Size() != l.Party || r.Party.Size() < 2 {
|
||
t.Fatalf("sällskap=%d, banan säger %d", r.Party.Size(), l.Party)
|
||
}
|
||
anna := r.Party.Members[1]
|
||
r.Party.nextEventAt = 1e9 // inga slumphändelser som stör testet
|
||
|
||
// Toalett: tar ToiletSecPerMember per person, avbryts om man går.
|
||
toilet := l.Find(level.Toilet)[0]
|
||
walk(t, r, l.Path(r.Player.Tile, toilet, r.CanEnter))
|
||
anna.Meters.Toilet = 10
|
||
anna.Urge = NeedToilet
|
||
r.Step(frame, None, true)
|
||
if !r.Facility.Active || r.Facility.Count != 1 || r.Facility.Total != ToiletSecPerMember {
|
||
t.Fatalf("toalettbesök: %+v", r.Facility)
|
||
}
|
||
r.Step(frame, Right, false)
|
||
if r.Facility.Active {
|
||
t.Fatal("att gå avbryter besöket")
|
||
}
|
||
for r.Player.Moving() { // gå klart rutan innan vi vänder tillbaka
|
||
r.Step(frame, None, false)
|
||
}
|
||
walk(t, r, l.Path(r.Player.Tile, toilet, r.CanEnter))
|
||
r.Step(frame, None, true)
|
||
if !r.Facility.Active {
|
||
t.Fatal("besöket ska starta igen")
|
||
}
|
||
for i := 0; i < int(ToiletSecPerMember/frame)+2 && r.Facility.Active; i++ {
|
||
r.Step(frame, None, false)
|
||
}
|
||
if r.Facility.Active || anna.Meters.Toilet != 100 || anna.Urge != NoUrge || r.Served != 1 {
|
||
t.Fatalf("efter besöket: active=%v toa=%.0f urge=%v served=%d", r.Facility.Active, anna.Meters.Toilet, anna.Urge, r.Served)
|
||
}
|
||
|
||
// Kiosk: direkt, kostar KioskPriceYen per person från kortet.
|
||
kiosk := l.Find(level.Kiosk)[0]
|
||
walk(t, r, l.Path(r.Player.Tile, kiosk, r.CanEnter))
|
||
anna.Meters.Drink = 5
|
||
anna.Urge = Thirsty
|
||
before := r.Wallet.Card
|
||
r.Step(frame, None, true)
|
||
if anna.Meters.Drink != 100 || anna.Urge != NoUrge || r.Wallet.Card != before-KioskPriceYen || r.Served != 2 {
|
||
t.Fatalf("kiosken: dryck=%.0f urge=%v saldo=%d->%d served=%d", anna.Meters.Drink, anna.Urge, before, r.Wallet.Card, r.Served)
|
||
}
|
||
}
|
||
|
||
func TestPartySlowsLeader(t *testing.T) {
|
||
l := loadLevel(t, "001-flygplatsen")
|
||
r := NewRun(l, RunOptions{Seed: rng.FromInt(1)})
|
||
r.Party.Members[1].Urge = Distracted
|
||
r.Party.Members[1].UrgeAt = 1000 // går inte över under testet
|
||
r.Step(frame, Right, false)
|
||
if r.Player.Speed != DefaultSpeed*DistractedSpeedFactor {
|
||
t.Fatalf("fart=%.2f", r.Player.Speed)
|
||
}
|
||
}
|
||
|
||
func TestRideWrongOrMissedStopLoses(t *testing.T) {
|
||
l := loadLevel(t, "001-flygplatsen")
|
||
for _, tc := range []struct {
|
||
name string
|
||
getOff bool
|
||
reason string
|
||
}{{"fel station", true, LostWrongStop}, {"åker förbi", false, LostMissedStop}} {
|
||
r := NewRun(l, RunOptions{Dict: loadDict(t), Seed: rng.FromInt(1)})
|
||
walk(t, r, l.Path(r.Player.Tile, l.CorrectPlatform(), r.CanEnter))
|
||
waitForTrain(t, r)
|
||
r.Step(frame, None, true)
|
||
g := r.Mini.(*minigame.Ride)
|
||
if g.Dest < 1 {
|
||
t.Fatalf("resmålet ska ligga på linjen men inte först: %d", g.Dest)
|
||
}
|
||
if tc.getOff {
|
||
r.StepMini(frame, minigame.Input{Confirm: true}) // första stationen = fel
|
||
} else {
|
||
for i := 0; i < 60*120 && r.Mini != nil; i++ {
|
||
r.StepMini(frame, minigame.Input{})
|
||
}
|
||
}
|
||
if r.Outcome != Lost || r.LostReason != tc.reason {
|
||
t.Fatalf("%s: outcome=%v reason=%q", tc.name, r.Outcome, r.LostReason)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestTopUpFlowAtMachine(t *testing.T) {
|
||
l := loadLevel(t, "001-flygplatsen")
|
||
r := NewRun(l, RunOptions{Dict: loadDict(t), Seed: rng.FromInt(1)})
|
||
r.Wallet.Card = 0
|
||
gate := l.Find(level.Gate)[0]
|
||
if r.CanEnter(gate) {
|
||
t.Fatal("tomt kort stoppas")
|
||
}
|
||
machine := l.Find(level.TicketMachine)[0]
|
||
walk(t, r, l.Path(r.Player.Tile, machine, r.CanEnter))
|
||
r.Step(frame, None, true)
|
||
g, ok := r.Mini.(*minigame.TopUp)
|
||
if !ok || r.MiniKind != MiniTopUp {
|
||
t.Fatalf("automaten ska starta påfyllning: %T", r.Mini)
|
||
}
|
||
cash := r.Wallet.Cash
|
||
for g.Buttons[g.Cursor] != "チャージ" {
|
||
r.StepMini(frame, minigame.Input{Move: 1})
|
||
}
|
||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||
r.StepMini(frame, minigame.Input{Confirm: true}) // första beloppet (1000)
|
||
for i := 0; i < 600 && r.Mini != nil; i++ {
|
||
r.StepMini(frame, minigame.Input{Hold: true})
|
||
}
|
||
if r.Mini != nil || r.Wallet.Card != 1000 || r.Wallet.Cash != cash-1000 || r.TopUps != 1 {
|
||
t.Fatalf("efter påfyllning: card=%d cash=%d topups=%d", r.Wallet.Card, r.Wallet.Cash, r.TopUps)
|
||
}
|
||
if !r.CanEnter(gate) {
|
||
t.Fatal("med påfyllt kort ska spärren släppa igenom")
|
||
}
|
||
// Minispelet pausar rörelsen men inte klockan.
|
||
r.Step(frame, None, true)
|
||
t0 := r.Time
|
||
r.Step(frame, Right, false)
|
||
if r.Mini == nil || r.Time != t0 || r.Player.Moving() {
|
||
t.Fatal("under minispelet ska Step vara en no-op")
|
||
}
|
||
r.StepMini(frame, minigame.Input{Cancel: true})
|
||
if r.Mini != nil || r.Time <= t0 {
|
||
t.Fatal("Esc avbryter och klockan går")
|
||
}
|
||
}
|
||
|
||
// TestTicketLevelFlow spelar bana 002: köp rätt biljett, passera spärren,
|
||
// tränga sig på det fulla tåget och kliva av i Shibuya.
|
||
func TestTicketLevelFlow(t *testing.T) {
|
||
l := loadLevel(t, "002-shinjuku")
|
||
if !l.TicketRequired || !l.Trains[0].Crowded {
|
||
t.Fatal("002 ska kräva biljett och ha ett fullt tåg")
|
||
}
|
||
r := NewRun(l, RunOptions{Dict: loadDict(t), Seed: rng.FromInt(11)})
|
||
r.Party.nextEventAt = 1e9
|
||
gate := l.Find(level.Gate)[0]
|
||
if r.CanEnter(gate) {
|
||
t.Fatal("utan biljett stoppar spärren även med kort")
|
||
}
|
||
machine := l.Find(level.TicketMachine)[0]
|
||
walk(t, r, l.Path(r.Player.Tile, machine, r.CanEnter))
|
||
|
||
// Fel biljett först: tåget vägrar.
|
||
r.Step(frame, None, true)
|
||
tk := r.Mini.(*minigame.Ticket)
|
||
for tk.Options[tk.Cursor].Dest == "渋谷" {
|
||
r.StepMini(frame, minigame.Input{Move: 1})
|
||
}
|
||
wrong := tk.Options[tk.Cursor]
|
||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||
if r.Wallet.Ticket != wrong.Dest || r.Wallet.Cash != StartCashYen-wrong.Fare {
|
||
t.Fatalf("biljett=%q cash=%d", r.Wallet.Ticket, r.Wallet.Cash)
|
||
}
|
||
goal := l.CorrectPlatform()
|
||
walk(t, r, l.Path(r.Player.Tile, goal, r.CanEnter))
|
||
waitForTrain(t, r)
|
||
r.Step(frame, None, true)
|
||
if r.Mini != nil || r.FlashKey != FlashWrongTicket || r.Outcome != Ongoing {
|
||
t.Fatal("fel biljett ska stoppas vid tåget")
|
||
}
|
||
|
||
// Rätt biljett.
|
||
walk(t, r, l.Path(r.Player.Tile, machine, r.CanEnter))
|
||
r.Step(frame, None, true)
|
||
tk = r.Mini.(*minigame.Ticket)
|
||
for tk.Options[tk.Cursor].Dest != "渋谷" {
|
||
r.StepMini(frame, minigame.Input{Move: 1})
|
||
}
|
||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||
if r.Wallet.Ticket != "渋谷" {
|
||
t.Fatalf("biljett=%q", r.Wallet.Ticket)
|
||
}
|
||
walk(t, r, l.Path(r.Player.Tile, goal, r.CanEnter))
|
||
waitForTrain(t, r)
|
||
r.Step(frame, None, true)
|
||
cr, ok := r.Mini.(*minigame.Crowd)
|
||
if !ok {
|
||
t.Fatalf("fullt tåg ska ge trängselminispel, fick %T", r.Mini)
|
||
}
|
||
for i := 0; i < 600 && !cr.InZone(); i++ {
|
||
r.StepMini(frame, minigame.Input{})
|
||
}
|
||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||
if r.MiniKind != MiniRide || r.Wallet.Ticket != "" {
|
||
t.Fatalf("efter trängseln: kind=%v biljett=%q", r.MiniKind, r.Wallet.Ticket)
|
||
}
|
||
ride(t, r)
|
||
if r.Outcome != Won {
|
||
t.Fatalf("outcome=%v reason=%q", r.Outcome, r.LostReason)
|
||
}
|
||
}
|
||
|
||
func TestCrowdFailureMissesTrain(t *testing.T) {
|
||
l := loadLevel(t, "002-shinjuku")
|
||
r := NewRun(l, RunOptions{Dict: loadDict(t), Seed: rng.FromInt(11)})
|
||
r.Party.nextEventAt = 1e9
|
||
r.Wallet.Ticket = "渋谷"
|
||
goal := l.CorrectPlatform()
|
||
walk(t, r, l.Path(r.Player.Tile, goal, r.CanEnter))
|
||
waitForTrain(t, r)
|
||
r.Step(frame, None, true)
|
||
cr := r.Mini.(*minigame.Crowd)
|
||
for n := 0; n < CrowdTries; n++ {
|
||
for cr.InZone() {
|
||
r.StepMini(frame, minigame.Input{})
|
||
}
|
||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||
}
|
||
if r.Mini != nil || r.MissedTrains != 1 || r.Outcome != Ongoing || r.FlashKey != FlashMissedTrain {
|
||
t.Fatalf("missat tåg: mini=%v missed=%d outcome=%v", r.Mini, r.MissedTrains, r.Outcome)
|
||
}
|
||
if r.Wallet.Ticket != "渋谷" {
|
||
t.Fatal("biljetten ska vara kvar när man inte kom med")
|
||
}
|
||
}
|
||
|
||
func TestPhoneSpeedRescanAndBattery(t *testing.T) {
|
||
l := loadLevel(t, "001-flygplatsen")
|
||
rescans := map[int]int{}
|
||
for seed := uint64(1); seed <= 12; seed++ {
|
||
r := NewRun(l, RunOptions{Seed: rng.FromInt(seed)})
|
||
r.Party.nextEventAt = 1e9
|
||
r.Wallet.Phone = true
|
||
r.Step(frame, Right, false)
|
||
if math.Abs(r.Player.Speed-DefaultSpeed*PhoneSpeedFactor) > 1e-9 {
|
||
t.Fatalf("telefonen ska ge fartbuff: %.2f", r.Player.Speed)
|
||
}
|
||
for r.Player.Moving() { // gå klart rutan innan vi väljer väg
|
||
r.Step(frame, None, false)
|
||
}
|
||
gate := l.Find(level.Gate)[0]
|
||
path := l.Path(r.Player.Tile, gate, r.CanEnter)
|
||
for i, p := range path { // stanna på den första spärrutan – där sker skanningen
|
||
if l.At(p.X, p.Y) == level.Gate {
|
||
path = path[:i+1]
|
||
break
|
||
}
|
||
}
|
||
walk(t, r, path)
|
||
rescans[r.Rescans]++
|
||
if r.Rescans == 1 {
|
||
if r.Time >= r.RescanUntil || r.FlashKey != FlashRescan {
|
||
t.Fatal("omskanning ska frysa spelaren en stund")
|
||
}
|
||
before := r.Player.Tile
|
||
r.Step(frame, Right, false)
|
||
if r.Player.Moving() || r.Player.Tile != before {
|
||
t.Fatal("under omskanningen står man still")
|
||
}
|
||
}
|
||
}
|
||
if rescans[1] == 0 || rescans[0] == 0 {
|
||
t.Fatalf("på 12 seeds borde några men inte alla ge omskanning: %v", rescans)
|
||
}
|
||
// Determinism: samma seed -> samma utfall.
|
||
run := func() int {
|
||
r := NewRun(l, RunOptions{Seed: rng.FromInt(5)})
|
||
r.Party.nextEventAt = 1e9
|
||
r.Wallet.Phone = true
|
||
walk(t, r, l.Path(r.Player.Tile, l.Find(level.Gate)[0], r.CanEnter))
|
||
return r.Rescans
|
||
}
|
||
if run() != run() {
|
||
t.Fatal("omskanningen ska vara seedad")
|
||
}
|
||
// Dött batteri: kortet i telefonen går inte -> spärren stoppar.
|
||
r := NewRun(l, RunOptions{Seed: rng.FromInt(1)})
|
||
r.Wallet.Phone = true
|
||
r.Wallet.Battery = 0.001
|
||
r.Step(frame, None, false)
|
||
if r.Wallet.Battery != 0 || r.CanEnter(l.Find(level.Gate)[0]) {
|
||
t.Fatal("dött batteri ska stoppa spärren")
|
||
}
|
||
if r.Player.Speed != DefaultSpeed {
|
||
t.Fatal("ingen fartbuff med dött batteri")
|
||
}
|
||
}
|