M9: navigeringslägen, linjenät och terminal
All checks were successful
release / build-release (push) Successful in 1m34s

internal/nav med resources/data/network.json (76 stationer, 11 linjer,
Yamanote som ringlinje). Tåg bedöms från lineId + terminus mot banans
station/to: rätt, fel håll, för långt bort (fler än 12 stationer) eller
ingen trafik; correct räknas ut för nätkopplade banor, 1-4 rätta tåg.
Tågresan visar linjens riktiga stationer. Nya förlustorsaker "Fel håll"
och "För långt bort". Listläge (nästa tåg nere till vänster) och
skyltläge (+50 % poäng) växlas med N på bankartan och sparas; -mode.
Terminalen: biljettautomaten visar linjetavlan på nätbanor. Perronger
P p Q q R r. Bana 006 Ikebukuro med fyra perronger. Banorna 001-005
kopplade till nätet. Skript: waitover och pick calm; poängraden
radbryts; skärmdumpar uppdaterade.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 13:07:36 +02:00
parent de61568e44
commit f762f9554e
53 changed files with 3041 additions and 122 deletions

128
internal/sim/nav_test.go Normal file
View File

@@ -0,0 +1,128 @@
package sim
import (
"strings"
"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/nav"
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
)
// boardAt går till perrongen med tecknet tile, väntar in tåget, kliver på
// och väljer vagn; resan pågår när funktionen återvänder.
func boardAt(t *testing.T, r *Run, tile level.Tile) *level.Train {
t.Helper()
r.NPCs = nil // folket ska inte stå i vägen för testboten
r.Party.nextEventAt = 1e9
l := r.Level
path := l.Path(r.Player.Tile, l.Find(tile)[0], r.PathCanEnter)
if path == nil {
t.Fatalf("ingen väg till perrong %c", tile)
}
walk(t, r, path)
tr := waitForTrain(t, r)
r.Step(frame, None, true)
if r.MiniKind == MiniCarPick {
pickCar(t, r)
}
if r.MiniKind != MiniRide {
t.Fatalf("ingen tågresa (kind=%v)", r.MiniKind)
}
return tr
}
// TestNetworkRideUsesRealStops: på nätkopplade banor är tågresans stationer
// linjens riktiga stationer i färdriktningen, med resmålet på rätt plats.
func TestNetworkRideUsesRealStops(t *testing.T) {
l := loadLevel(t, "005-ueno")
if l.Net == nil {
t.Fatal("005 ska vara kopplad till nätet")
}
r := NewRun(l, RunOptions{Seed: rng.FromInt(5)})
tr := boardAt(t, r, level.Platform)
g := r.Mini.(*minigame.Ride)
if tr.Verdict != nav.Direct || g.Dest != 1 || g.Stops[0] != "御徒町" || g.Stops[1] != "秋葉原" || len(g.Stops) != l.RideStops {
t.Fatalf("Yamanote från Ueno: dest=%d stops=%v", g.Dest, g.Stops)
}
ride(t, r)
if r.Outcome != Won {
t.Fatalf("outcome=%v (%s)", r.Outcome, r.LostReason)
}
}
// TestWrongDirectionLoses: Keihin-Tohoku mot Omiya från Ueno går åt fel håll
// – resan visar stationerna norrut, resmålet kommer aldrig och banan förloras
// med orsaken "fel håll".
func TestWrongDirectionLoses(t *testing.T) {
l := loadLevel(t, "005-ueno")
r := NewRun(l, RunOptions{Seed: rng.FromInt(5)})
tr := boardAt(t, r, level.WrongPlatform)
g := r.Mini.(*minigame.Ride)
if tr.Verdict != nav.WrongDirection || g.Dest != -1 || g.Stops[0] != "鶯谷" {
t.Fatalf("Keihin-Tohoku mot Omiya: verdict=%v dest=%d stops=%v", tr.Verdict, g.Dest, g.Stops)
}
ride(t, r)
if r.Outcome != Lost || r.LostReason != LostWrongDirection {
t.Fatalf("outcome=%v reason=%q", r.Outcome, r.LostReason)
}
}
// TestTooFarLoses: Marunouchi från Ikebukuro når Shinjuku först via city –
// för långt bort.
func TestTooFarLoses(t *testing.T) {
l := loadLevel(t, "006-ikebukuro")
r := NewRun(l, RunOptions{Seed: rng.FromInt(6)})
if n := r.NextTrains(); len(n) != 2 {
t.Fatalf("006 ska ha två rätta tåg, har %d", len(n))
}
tr := boardAt(t, r, level.Platform3)
if tr.Verdict != nav.TooFar || tr.LineID != "marunouchi" {
t.Fatalf("q-perrongen: %v %s", tr.Verdict, tr.LineID)
}
ride(t, r)
if r.Outcome != Lost || r.LostReason != LostTooFar {
t.Fatalf("outcome=%v reason=%q", r.Outcome, r.LostReason)
}
}
// TestSignsModeBonus: skyltläget ger 50 % bonus på grundpoängen, listläget inte.
func TestSignsModeBonus(t *testing.T) {
l := loadLevel(t, "001-flygplatsen")
list := NewRun(l, RunOptions{Seed: rng.FromInt(1)})
signs := NewRun(l, RunOptions{Seed: rng.FromInt(1), Mode: nav.Signs})
if list.Mode != nav.List || signs.Mode != nav.Signs {
t.Fatal("läget ska följa RunOptions")
}
for _, r := range []*Run{list, signs} {
r.Time = 30
r.computeScore()
}
base := list.Score.Time + list.Score.Money + list.Score.Meters + list.Score.Explore + list.Score.Car
if list.Score.Bonus != 0 || signs.Score.Bonus != base*ScoreSignsBonusPct/100 || signs.Score.Total != list.Score.Total+signs.Score.Bonus {
t.Fatalf("list=%+v signs=%+v", list.Score, signs.Score)
}
if n := list.NextTrains(); len(n) != 1 || !n[0].Correct {
t.Fatalf("NextTrains=%v", n)
}
}
// TestTerminalLines: terminalen listar linjerna vid stationen med grannstationer.
func TestTerminalLines(t *testing.T) {
r := NewRun(loadLevel(t, "005-ueno"), RunOptions{Seed: rng.FromInt(5)})
lines := r.TerminalLines()
if len(lines) < 3 {
t.Fatalf("Ueno har flera linjer, fick %v", lines)
}
joined := strings.Join(lines, "\n")
for _, want := range []string{"山手線: ", "御徒町", "鶯谷", "← 上野 →", "京浜東北線"} {
if !strings.Contains(joined, want) {
t.Errorf("terminalen saknar %q:\n%s", want, joined)
}
}
plain := NewRun(loadLevel(t, "002b-kabukicho"), RunOptions{Seed: rng.FromInt(1)})
if plain.TerminalLines() != nil || plain.Level.Net != nil {
t.Fatal("okopplad bana har ingen terminal")
}
}