All checks were successful
release / build-release (push) Successful in 1m42s
- level.Schedule.Phase: away/approaching/arriving/dwelling/closing/leaving ur tidtabellen med progress; Boardable (öppna dörrar) och Present; validering av minsta uppehåll/borta-tid; TrackTile för rälsen - sim.Run: ombordstigning bara med öppna dörrar, TrainVisible via Present, PlatformPhase och NearTrack (närmaste perrong inom två rutor) - game/trains.go: rälsrutor längs kantraden (roterade för vertikala spår), tre vagnar som bromsar in och accelererar ut längs rälsen, strålkastare under inrullningen, blinkande lampor vid kanten för den som står nära, dammpartiklar längs perrongen; kameran får en rutas marginal så att kanträlsen syns - game/audio.go: ebiten/v2/audio med avgångsmelodi (approaching) och dörrsignal (closing), volym efter avstånd, -mute; tools/gensound syntetiserar placeholder-WAV:ar - HUD: fasberoende perrongtext och "tåg på väg in" nära rälsen; skriptkommandot waitphase; skärmdump av ankomsten - plan (Björn 2026-10-24): räls/väg-grafik och melodierna i M7b; folk/NPC:er, vänster-/högertrafik, bredare gångar, reklam- och ljudröra som nytt steg M9b – i spelbeskrivning, roadmap, testplan - docs/README/wiki uppdaterade Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
957 lines
32 KiB
Go
957 lines
32 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"
|
||
)
|
||
|
||
// fillSeats placerar sällskapet på första lediga platser (sittplatssteget).
|
||
func fillSeats(r *Run, tk *minigame.Ticket) {
|
||
for r.Mini != nil && tk.Stage == minigame.TicketPickSeat {
|
||
for tk.SeatTaken(tk.Seat) {
|
||
r.StepMini(frame, minigame.Input{MoveX: 1})
|
||
if tk.Seat.Col == 0 {
|
||
r.StepMini(frame, minigame.Input{Move: 1})
|
||
}
|
||
}
|
||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||
}
|
||
}
|
||
|
||
// 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{MoveX: 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})
|
||
fillSeats(r, tk) // 002 har sittplatsval
|
||
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})
|
||
fillSeats(r, tk)
|
||
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")
|
||
}
|
||
}
|
||
|
||
func TestTimeLimitAndMissedTrainsLose(t *testing.T) {
|
||
l := loadLevel(t, "002-shinjuku")
|
||
r := NewRun(l, RunOptions{Dict: loadDict(t), Seed: rng.FromInt(1)})
|
||
if r.TimeLeft() != float64(l.TimeLimitSec) {
|
||
t.Fatalf("TimeLeft=%.0f", r.TimeLeft())
|
||
}
|
||
r.Time = float64(l.TimeLimitSec) - 0.5
|
||
for i := 0; i < 60 && r.Outcome == Ongoing; i++ {
|
||
r.Step(frame, None, false)
|
||
}
|
||
if r.Outcome != Lost || r.LostReason != LostTimeUp || r.Boarded != nil {
|
||
t.Fatalf("tidsgräns: outcome=%v reason=%q", r.Outcome, r.LostReason)
|
||
}
|
||
r2 := NewRun(l, RunOptions{Dict: loadDict(t), Seed: rng.FromInt(1)})
|
||
r2.MissedTrains = l.MaxMissedTrains + 1
|
||
r2.Step(frame, None, false)
|
||
if r2.Outcome != Lost || r2.LostReason != LostMissedTrains {
|
||
t.Fatalf("missade tåg: outcome=%v reason=%q", r2.Outcome, r2.LostReason)
|
||
}
|
||
// Bana 001 har inga gränser.
|
||
r3 := NewRun(loadLevel(t, "001-flygplatsen"), RunOptions{Seed: rng.FromInt(1)})
|
||
r3.Time, r3.MissedTrains = 99999, 99
|
||
r3.Step(frame, None, false)
|
||
if r3.Outcome != Ongoing || r3.TimeLeft() != 0 {
|
||
t.Fatal("utan gränser förlorar man inte på tid eller missade tåg")
|
||
}
|
||
}
|
||
|
||
func TestScoreBreakdown(t *testing.T) {
|
||
l := loadLevel(t, "001-flygplatsen")
|
||
r := NewRun(l, RunOptions{Dict: loadDict(t), Seed: rng.FromInt(2)})
|
||
r.Party.nextEventAt = 1e9
|
||
walk(t, r, l.Path(r.Player.Tile, l.CorrectPlatform(), r.CanEnter))
|
||
waitForTrain(t, r)
|
||
r.Step(frame, None, true)
|
||
ride(t, r)
|
||
sc := r.Score
|
||
if r.Outcome != Won || sc.Total <= 0 || sc.Total != sc.Time+sc.Money+sc.Meters+sc.Explore-sc.Penalty {
|
||
t.Fatalf("score=%+v", sc)
|
||
}
|
||
if sc.Time != ScoreTimeMax || sc.Money != ScoreMoneyMax-l.FareYen/10 || sc.Penalty != 0 {
|
||
t.Fatalf("snabb, billig vinst: %+v", sc)
|
||
}
|
||
if sc.Meters <= 0 || sc.Meters > ScoreMetersMax || sc.Explore <= 0 || sc.Explore > ScoreExploreMax {
|
||
t.Fatalf("mätare/utforskat: %+v", sc)
|
||
}
|
||
// Avdrag räknas.
|
||
r2 := NewRun(l, RunOptions{Dict: loadDict(t), Seed: rng.FromInt(2)})
|
||
r2.MissedTrains, r2.Rescans, r2.Grumpy = 1, 2, 1
|
||
r2.Time = 10 * r2.ParSec()
|
||
r2.computeScore()
|
||
if r2.Score.Penalty != PenaltyMissed+2*PenaltyRescan+PenaltyGrumpy || r2.Score.Time != ScoreTimeMax/10 {
|
||
t.Fatalf("avdrag: %+v", r2.Score)
|
||
}
|
||
}
|
||
|
||
func TestDebuffsAffectRunAndAreEarned(t *testing.T) {
|
||
l := loadLevel(t, "001-flygplatsen")
|
||
r := NewRun(l, RunOptions{Seed: rng.FromInt(1), Debuffs: []Debuff{{Blisters, 1}, {FoodPoisoning, 1}, {Hangry, 1}}})
|
||
r.Party.nextEventAt = 1e9
|
||
r.Step(frame, Right, false)
|
||
if math.Abs(r.Player.Speed-DefaultSpeed*0.8) > 1e-9 || r.Party.Rates.Toilet != 2 || r.Party.Rates.Thirst != 1.2 {
|
||
t.Fatalf("speed=%.2f rates=%+v", r.Player.Speed, r.Party.Rates)
|
||
}
|
||
r.Step(frame, None, false)
|
||
for r.Player.Moving() {
|
||
r.Step(frame, None, false)
|
||
}
|
||
walk(t, r, l.Path(r.Player.Tile, l.Find(level.Sign)[1], r.CanEnter))
|
||
r.AskHelp()
|
||
if want := lang.HelpSeconds(0) / 0.7; math.Abs(r.Help.Total-want) > 1e-9 {
|
||
t.Fatalf("hjälptid med dåligt fokus %.2f, vill ha %.2f", r.Help.Total, want)
|
||
}
|
||
// Intjänade debuffar: många steg, lång tid, tom matmätare.
|
||
r.Steps, r.Time = SoreFeetSteps, TiredSec
|
||
r.Party.Members[1].Meters.Food = 0
|
||
r.finish(nil, false, LostTimeUp)
|
||
kinds := map[DebuffKind]bool{}
|
||
for _, d := range r.NewDebuffs {
|
||
kinds[d.Kind] = true
|
||
}
|
||
if !kinds[SoreFeet] || !kinds[Tired] || !kinds[Hangry] || kinds[FoodPoisoning] {
|
||
t.Fatalf("intjänade=%v", r.NewDebuffs)
|
||
}
|
||
}
|
||
|
||
func TestIndividualCardsAtGateAndMachine(t *testing.T) {
|
||
l := loadLevel(t, "003-shibuya")
|
||
if !l.IndividualCards || !l.TicketRequired || !l.SeatSelection || l.Party != 4 {
|
||
t.Fatal("003 ska ha egna kort, biljetter, sittplatser och fyra i sällskapet")
|
||
}
|
||
// Egna kort på en kortbana (utan biljettkrav): alla måste ha saldo.
|
||
l1 := loadLevel(t, "001-flygplatsen")
|
||
l1.IndividualCards = true
|
||
r := NewRun(l1, RunOptions{Dict: loadDict(t), Seed: rng.FromInt(1)})
|
||
r.Party.nextEventAt = 1e9
|
||
gate := l1.Find(level.Gate)[0]
|
||
anna := r.Party.Members[1]
|
||
if anna.Card != MemberCardYen || !r.CanEnter(gate) {
|
||
t.Fatalf("Annas kort=%d canEnter=%v", anna.Card, r.CanEnter(gate))
|
||
}
|
||
anna.Card = 0
|
||
if r.CanEnter(gate) || len(r.MembersWithoutFare()) != 1 {
|
||
t.Fatal("utan saldo på Annas kort stoppar spärren alla")
|
||
}
|
||
// Fyll på Annas kort i automaten (personval först).
|
||
walk(t, r, l1.Path(r.Player.Tile, l1.Find(level.TicketMachine)[0], r.CanEnter))
|
||
r.Step(frame, None, true)
|
||
g := r.Mini.(*minigame.TopUp)
|
||
if g.Stage != minigame.TopUpPickMember || len(g.Members) != 2 {
|
||
t.Fatalf("stage=%v members=%v", g.Stage, g.Members)
|
||
}
|
||
r.StepMini(frame, minigame.Input{Move: 1})
|
||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||
for g.Buttons[g.Cursor] != "チャージ" {
|
||
r.StepMini(frame, minigame.Input{MoveX: 1})
|
||
}
|
||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||
cash := r.Wallet.Cash
|
||
for i := 0; i < 600 && r.Mini != nil; i++ {
|
||
r.StepMini(frame, minigame.Input{Hold: true})
|
||
}
|
||
if anna.Card != 1000 || r.Wallet.Cash != cash-1000 || r.Wallet.Card != StartCardYen {
|
||
t.Fatalf("Annas kort=%d cash=%d eget=%d", anna.Card, r.Wallet.Cash, r.Wallet.Card)
|
||
}
|
||
// Spärren drar från allas kort.
|
||
before := r.Wallet.Spent
|
||
path := l1.Path(r.Player.Tile, gate, r.CanEnter)
|
||
for i, p := range path {
|
||
if l1.At(p.X, p.Y) == level.Gate {
|
||
path = path[:i+1]
|
||
break
|
||
}
|
||
}
|
||
walk(t, r, path)
|
||
if anna.Card != 1000-l1.FareYen || r.Wallet.Card != StartCardYen-l1.FareYen || r.Wallet.Spent != before+2*l1.FareYen {
|
||
t.Fatalf("efter spärren: anna=%d egen=%d spent=%d", anna.Card, r.Wallet.Card, r.Wallet.Spent)
|
||
}
|
||
}
|
||
|
||
// TestLevel003Flow: biljettbana med egna kort och sittplatser – alla
|
||
// behöver biljett, sällskapet placeras, och boten vinner.
|
||
func TestLevel003Flow(t *testing.T) {
|
||
l := loadLevel(t, "003-shibuya")
|
||
r := NewRun(l, RunOptions{Dict: loadDict(t), Seed: rng.FromInt(21)})
|
||
r.Party.nextEventAt = 1e9
|
||
machine := l.Find(level.TicketMachine)[0]
|
||
walk(t, r, l.Path(r.Player.Tile, machine, r.CanEnter))
|
||
r.Step(frame, None, true)
|
||
tk := r.Mini.(*minigame.Ticket)
|
||
if len(tk.Members) != 4 || tk.Cash != StartCashYen/4 {
|
||
t.Fatalf("members=%v cash=%d", tk.Members, tk.Cash)
|
||
}
|
||
for tk.Options[tk.Cursor].Dest != "池袋" {
|
||
r.StepMini(frame, minigame.Input{Move: 1})
|
||
}
|
||
fare := tk.Options[tk.Cursor].Fare
|
||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||
if tk.Stage != minigame.TicketPickSeat {
|
||
t.Fatal("sittplatsval ska följa")
|
||
}
|
||
// Placera alla så att önskemålen uppfylls när det går: prova platser tills nöjd.
|
||
for cur := 0; r.Mini != nil; cur++ {
|
||
best := minigame.Seat{Row: -1}
|
||
for row := 0; row < minigame.SeatRows && best.Row < 0; row++ {
|
||
for col := 0; col < minigame.SeatCols; col++ {
|
||
cand := minigame.Seat{Row: row, Col: col}
|
||
if tk.SeatTaken(cand) {
|
||
continue
|
||
}
|
||
trial := append([]minigame.Seat(nil), tk.Assigned...)
|
||
trial[cur] = cand
|
||
ok := true
|
||
for _, p := range tk.Prefs {
|
||
if p.Member == cur && !p.Satisfied(trial) {
|
||
ok = false
|
||
}
|
||
}
|
||
if ok {
|
||
best = cand
|
||
break
|
||
}
|
||
}
|
||
}
|
||
if best.Row < 0 {
|
||
t.Fatal("ingen ledig plats")
|
||
}
|
||
for tk.Seat != best {
|
||
in := minigame.Input{}
|
||
if tk.Seat.Row != best.Row {
|
||
in.Move = 1
|
||
} else {
|
||
in.MoveX = 1
|
||
}
|
||
r.StepMini(frame, in)
|
||
}
|
||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||
}
|
||
if r.Wallet.TicketCount != 4 || r.Wallet.Ticket != "池袋" || r.Wallet.Cash != StartCashYen-4*fare {
|
||
t.Fatalf("biljetter=%d dest=%q cash=%d", r.Wallet.TicketCount, r.Wallet.Ticket, r.Wallet.Cash)
|
||
}
|
||
// Den giriga placeringen kan missa "bredvid X" som placeras senare; det
|
||
// är tillåtet men ska då synas som missnöje.
|
||
if r.Grumpy > 0 && r.FlashKey != FlashGrumpy {
|
||
t.Fatal("missnöje ska flashas")
|
||
}
|
||
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 i := 0; i < 600 && !cr.InZone(); i++ {
|
||
r.StepMini(frame, minigame.Input{})
|
||
}
|
||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||
ride(t, r)
|
||
if r.Outcome != Won || r.Wallet.TicketCount != 0 {
|
||
t.Fatalf("outcome=%v reason=%q tickets=%d", r.Outcome, r.LostReason, r.Wallet.TicketCount)
|
||
}
|
||
}
|
||
|
||
func TestNearTrackAndPhases(t *testing.T) {
|
||
l := loadLevel(t, "001-flygplatsen")
|
||
r := NewRun(l, RunOptions{Seed: rng.FromInt(1)})
|
||
r.Party.nextEventAt = 1e9
|
||
if _, _, _, _, _, ok := r.NearTrack(2); ok {
|
||
t.Fatal("från starten är ingen perrong nära")
|
||
}
|
||
walk(t, r, l.Path(r.Player.Tile, l.CorrectPlatform(), r.CanEnter))
|
||
tr, p, phase, prog, dist, ok := r.NearTrack(2)
|
||
if !ok || dist != 0 || p != l.CorrectPlatform() || !tr.Correct || prog < 0 || prog > 1 {
|
||
t.Fatalf("ok=%v dist=%d p=%v", ok, dist, p)
|
||
}
|
||
if want, _ := tr.Schedule.Phase(r.Time); want != phase {
|
||
t.Fatal("fasen ska komma från tidtabellen")
|
||
}
|
||
// Ombordstigning bara med öppna dörrar: under inrullningen vägras man.
|
||
for {
|
||
ph, _ := r.PlatformPhase(level.Platform)
|
||
if ph == level.Arriving {
|
||
break
|
||
}
|
||
r.Step(frame, None, false)
|
||
}
|
||
if _, at, _ := r.PlatformHere(); at || !r.TrainVisible(level.Platform) {
|
||
t.Fatal("under inrullningen syns tåget men dörrarna är stängda")
|
||
}
|
||
r.Step(frame, None, true)
|
||
if r.Mini != nil {
|
||
t.Fatal("man kan inte kliva på ett tåg som rullar in")
|
||
}
|
||
for {
|
||
ph, _ := r.PlatformPhase(level.Platform)
|
||
if ph == level.Dwelling {
|
||
break
|
||
}
|
||
r.Step(frame, None, false)
|
||
}
|
||
if _, at, _ := r.PlatformHere(); !at {
|
||
t.Fatal("med öppna dörrar kan man kliva på")
|
||
}
|
||
// Står man två rutor bort ser man tåget komma; tre rutor bort inte.
|
||
r2 := NewRun(l, RunOptions{Seed: rng.FromInt(1)})
|
||
goal := l.CorrectPlatform()
|
||
r2.Player.Tile, r2.Player.Target = level.Point{X: goal.X, Y: goal.Y - 2}, level.Point{X: goal.X, Y: goal.Y - 2}
|
||
if _, _, _, _, d, ok := r2.NearTrack(2); !ok || d != 2 {
|
||
t.Fatalf("två rutor bort: ok=%v d=%d", ok, d)
|
||
}
|
||
r2.Player.Tile, r2.Player.Target = level.Point{X: goal.X - 3, Y: goal.Y}, level.Point{X: goal.X - 3, Y: goal.Y}
|
||
if _, _, _, _, _, ok := r2.NearTrack(2); ok {
|
||
t.Fatal("tre rutor bort är utom räckhåll")
|
||
}
|
||
}
|