Files
metro-puzzle-jp/internal/sim/gates_test.go
claude 46bd330dfc
All checks were successful
release / build-release (push) Successful in 1m38s
M10d: toaletter innanför och utanför spärrarna
Björns tillägg 2026-10-24. Generatorn har nu ett betalt område per perrong
(paidDepth) och toilets inside/outside/both: 005 Ueno och 008b Nippori har
toalett bara innanför, 007b Ueno på båda sidor. Perrongceller görs till
återvändsgränder (bara passagen mot starten behålls, labyrinten repareras)
och det betalda området krymper om det skulle bli genomfart; regeln att
varje perrong nås utan att passera ett annat betalt område testas i
level och levelcheck. level.PathOutside används av testbotar och shots.
Alla genererade banor utom 003 omgjorda, skärmdumpar omtagna.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-24 23:09:27 +02:00

152 lines
6.1 KiB
Go

package sim
import (
"testing"
"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/rng"
)
// pathTo är testbotens väg till goal: som PathCanEnter, men andra perronger
// används inte som genvägar (på spärrar utan återvändo skulle man fastna).
func pathTo(r *Run, goal level.Point) []level.Point {
return r.Level.PathOutside(r.Player.Tile, goal, func(p level.Point) bool {
return r.PathCanEnter(p) && (!r.Level.At(p).IsPlatform() || p == goal)
})
}
// stepUntilStill tar ett steg i riktningen och låter rörelsen bli klar.
func stepUntilStill(r *Run, d Dir) {
r.Step(frame, d, false)
for i := 0; i < 120 && r.Player.Moving(); i++ {
r.Step(frame, None, false)
}
}
// TestKioskAsksWhoPays: med egna kort och fler i sällskapet frågar kiosken
// vem som betalar; den valda följarens kort dras, inte spelarens.
func TestKioskAsksWhoPays(t *testing.T) {
l := loadLevel(t, "007-akihabara")
if !l.IndividualCards || l.Party < 3 {
t.Fatal("007 ska ha egna kort och sällskap")
}
r := NewRun(l, RunOptions{Seed: rng.FromInt(7)})
r.NPCs = nil
r.Party.nextEventAt = 1e9
ville := r.Party.Members[2]
ville.Meters.Drink, ville.Urge = 5, Thirsty
walk(t, r, pathTo(r, l.Find(level.Kiosk)[0]))
card, villeCard := r.Wallet.Card, ville.Card
r.Step(frame, None, true)
g, ok := r.Mini.(*minigame.Payer)
if !ok || r.MiniKind != MiniPayer || len(g.Options) != r.Party.Size()+1 || g.Price != KioskPriceYen {
t.Fatalf("kiosken ska fråga vem som betalar: kind=%v", r.MiniKind)
}
if !g.Options[0].Cash || g.Options[1].Member != 0 || g.Options[3].Name != ville.Name {
t.Fatalf("källorna: %+v", g.Options)
}
for g.Cursor != 3 {
r.StepMini(frame, minigame.Input{MoveX: 1})
}
r.StepMini(frame, minigame.Input{Confirm: true})
if r.Mini != nil || ville.Card != villeCard-KioskPriceYen || r.Wallet.Card != card || ville.Meters.Drink != 100 || r.Served != 1 {
t.Fatalf("Villes kort ska betala: ville=%d (var %d) kort=%d served=%d", ville.Card, villeCard, r.Wallet.Card, r.Served)
}
// Ingen som behöver något: ingen fråga. Avbrutet köp: inget dras.
r.Step(frame, None, true)
if r.Mini != nil {
t.Fatal("utan behov ska kiosken inte fråga")
}
anna := r.Party.Members[1]
anna.Meters.Food, anna.Urge, anna.Card = 5, Hungry, 0
r.Step(frame, None, true)
g = r.Mini.(*minigame.Payer)
for g.Cursor != 2 { // Anna, utan pengar
r.StepMini(frame, minigame.Input{MoveX: 1})
}
r.StepMini(frame, minigame.Input{Confirm: true})
if r.Mini == nil || !g.Refused {
t.Fatal("den som inte har råd nekas")
}
r.StepMini(frame, minigame.Input{Cancel: true})
if r.Mini != nil || anna.Meters.Food > 10 || r.Served != 1 {
t.Fatal("avbrutet köp ska inte dra något")
}
// Kontant betalar.
cash := r.Wallet.Cash
r.Step(frame, None, true)
r.StepMini(frame, minigame.Input{Confirm: true}) // markören står på kontant (Anna har inte råd, kontant står först)
if r.Wallet.Cash != cash-KioskPriceYen || anna.Meters.Food != 100 {
t.Fatalf("kontant: cash=%d (var %d) food=%.0f", r.Wallet.Cash, cash, anna.Meters.Food)
}
}
// TestOneWayGatesNeedStaff: i Ueno släpper spärren inte ut en som checkat
// in; Enter vid spärren kallar på personal, sedan får man gå ut, och går man
// in igen dras avgiften igen.
func TestOneWayGatesNeedStaff(t *testing.T) {
l := loadLevel(t, "005-ueno")
if !l.OneWayGates {
t.Fatal("005 ska ha spärrar utan återvändo")
}
r := NewRun(l, RunOptions{Seed: rng.FromInt(5)})
r.NPCs = nil
r.Party.nextEventAt = 1e9
plat := l.CorrectPlatform()
start := r.Wallet.Card
walk(t, r, pathTo(r, plat))
if !r.CheckedIn() || !r.NeedsStaff() || r.GatePasses != 1 || r.Wallet.Card != start-l.FareYen {
t.Fatalf("på perrongen ska man vara incheckad och ha betalat en gång: checkedIn=%v passes=%d card=%d", r.CheckedIn(), r.GatePasses, r.Wallet.Card)
}
card := r.Wallet.Card
// Tillbaka mot utgången: vägen till starten, kapad vid första spärren.
back := l.PathOutside(plat, l.SpawnPoint(), r.PathCanEnter)
gi := 0
for gi < len(back) && l.At(back[gi]) != level.Gate {
gi++
}
if gi >= len(back)-1 {
t.Fatal("ingen spärr på vägen ut")
}
walk(t, r, back[:gi+1]) // inifrån och ut på spärren: kostar inget
if !r.inGate() || r.GatePasses != 1 || r.Wallet.Card != card {
t.Fatalf("spärren igen: inGate=%v passes=%d card=%d", r.inGate(), r.GatePasses, r.Wallet.Card)
}
gate, outside := r.Player.Tile, back[gi+1]
out := dirTo(gate, outside)
stepUntilStill(r, out) // ut? nej
if r.Player.Tile != gate || r.NoExitAt < 0 {
t.Fatalf("spärren ska inte släppa ut: tile=%v noExitAt=%.1f", r.Player.Tile, r.NoExitAt)
}
r.Step(frame, None, true) // kalla på personal
if !r.Staff.Active {
t.Fatal("Enter vid spärren ska kalla på personal")
}
for i := 0; i < int(StaffSec*60)+5 && r.Staff.Active; i++ {
r.Step(frame, None, false)
}
if r.Staff.Active || !r.StaffUnlocked || r.StaffCalls != 1 || r.FlashKey != FlashStaff {
t.Fatalf("personalen ska ha låst upp: active=%v unlocked=%v calls=%d", r.Staff.Active, r.StaffUnlocked, r.StaffCalls)
}
stepUntilStill(r, out)
if r.Player.Tile != outside || r.CheckedIn() || r.StaffUnlocked {
t.Fatalf("ute igen: tile=%v checkedIn=%v unlocked=%v", r.Player.Tile, r.CheckedIn(), r.StaffUnlocked)
}
stepUntilStill(r, dirTo(outside, gate)) // på spärren utifrån: inget dras än
if !r.inGate() || r.CheckedIn() || r.GatePasses != 1 {
t.Fatalf("spärren utifrån: inGate=%v checkedIn=%v passes=%d", r.inGate(), r.CheckedIn(), r.GatePasses)
}
walk(t, r, pathTo(r, plat)) // in igen: kostar igen
if r.Player.Tile != plat || !r.CheckedIn() || r.GatePasses != 2 || r.Wallet.Card != card-l.FareYen {
t.Fatalf("ny inpassering: tile=%v checkedIn=%v passes=%d card=%d (var %d)", r.Player.Tile, r.CheckedIn(), r.GatePasses, r.Wallet.Card, card)
}
// Utan flaggan är det fritt fram.
l1 := loadLevel(t, "001-flygplatsen")
r1 := NewRun(l1, RunOptions{Seed: rng.FromInt(1)})
walk(t, r1, l1.Path(r1.Player.Tile, l1.CorrectPlatform(), r1.PathCanEnter))
if r1.CheckedIn() || r1.NeedsStaff() {
t.Fatal("utan oneWayGates ska ingen vara incheckad")
}
}