M10c: vem betalar i kiosken och spärrar utan återvändo
All checks were successful
release / build-release (push) Successful in 2m7s

Björns tillägg 2026-10-24. Med egna kort och fler i sällskapet frågar
kiosken vem som betalar (minigame.Payer: kontant, spelarens kort eller en
följares kort; den som inte har råd nekas, Esc avbryter). Avgiften dras
nu när man passerar spärren in i det betalda området (level.InsideGates),
inte på spärrrutan. Banflaggan oneWayGates: incheckad släpps inte ut igen
förrän man åkt; Enter vid spärren kallar på personal (10 s) som låser upp
korten; ny inpassering kostar igen. På i 005, 007/007b och 008/008b.
Skriptkommandon urge och push. Docs, wiki och skärmdumpar uppdaterade.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 22:12:13 +02:00
parent 4ae6a1d24c
commit f04d6a88f9
26 changed files with 674 additions and 64 deletions

141
internal/sim/gates_test.go Normal file
View File

@@ -0,0 +1,141 @@
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.Path(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
stepUntilStill(r, Left) // tillbaka 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 := r.Player.Tile
stepUntilStill(r, Left) // 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, Left)
if r.Player.Tile == gate || r.CheckedIn() || r.StaffUnlocked {
t.Fatalf("ute igen: tile=%v checkedIn=%v unlocked=%v", r.Player.Tile, r.CheckedIn(), r.StaffUnlocked)
}
stepUntilStill(r, Right) // 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)
}
stepUntilStill(r, Right) // 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")
}
}