M9: navigeringslägen, linjenät och terminal
All checks were successful
release / build-release (push) Successful in 1m34s
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:
@@ -14,6 +14,8 @@ import (
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/nav"
|
||||
)
|
||||
|
||||
// Tile är ett tecken i rutnätet.
|
||||
@@ -24,8 +26,12 @@ const (
|
||||
Wall Tile = '#' // vägg, ej passerbar
|
||||
Floor Tile = '.' // golv
|
||||
Spawn Tile = 'S' // startpunkt (exakt en)
|
||||
Platform Tile = 'P' // perrong med ett tåg (målet om tåget är rätt)
|
||||
WrongPlatform Tile = 'p' // perrong med fel tåg
|
||||
Platform Tile = 'P' // perrong med ett tåg (vilket tåg som är rätt avgör tåget, inte tecknet)
|
||||
WrongPlatform Tile = 'p' // ytterligare perrong (namnet är historiskt: i de första banorna stod fel tåg här)
|
||||
Platform2 Tile = 'Q' // fler perronger för banor med flera giltiga vägar och fällor
|
||||
Platform3 Tile = 'q'
|
||||
Platform4 Tile = 'R'
|
||||
Platform5 Tile = 'r'
|
||||
Gate Tile = 'G' // spärr (kräver kort/biljett)
|
||||
TicketMachine Tile = 'T' // biljett-/påfyllningsautomat
|
||||
Sign Tile = 'i' // skylt
|
||||
@@ -37,12 +43,26 @@ const (
|
||||
LaneRight Tile = '>' // fil för gång neråt/åt höger
|
||||
)
|
||||
|
||||
// PlatformTiles är alla perrongtecken, i den ordning tåg brukar listas.
|
||||
var PlatformTiles = []Tile{Platform, WrongPlatform, Platform2, Platform3, Platform4, Platform5}
|
||||
|
||||
var knownTiles = map[Tile]bool{
|
||||
Wall: true, Floor: true, Spawn: true, Platform: true, WrongPlatform: true,
|
||||
Platform2: true, Platform3: true, Platform4: true, Platform5: true,
|
||||
Gate: true, TicketMachine: true, Sign: true, Toilet: true, Kiosk: true,
|
||||
StairsUp: true, StairsDown: true, LaneLeft: true, LaneRight: true,
|
||||
}
|
||||
|
||||
// IsPlatform rapporterar om rutan är en perrong (något av PlatformTiles).
|
||||
func (t Tile) IsPlatform() bool {
|
||||
for _, p := range PlatformTiles {
|
||||
if t == p {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsLane rapporterar om rutan är en markerad fil.
|
||||
func (t Tile) IsLane() bool { return t == LaneLeft || t == LaneRight }
|
||||
|
||||
@@ -171,13 +191,17 @@ type Train struct {
|
||||
Track string `json:"track"` // spårnummer som visas på skyltar/HUD, t.ex. "2"
|
||||
Line Text `json:"line"` // linjens namn
|
||||
Destination Text `json:"destination"` // slutstation
|
||||
Correct bool `json:"correct"` // tar spelaren till rätt resmål
|
||||
Correct bool `json:"correct"` // tar spelaren till rätt resmål (räknas ut från nätet när banan har station/to)
|
||||
LineID string `json:"lineId"` // linjen i linjenätet (resources/data/network.json)
|
||||
Terminus string `json:"terminus"` // en station i färdriktningen (nätets id) – avgör riktningen
|
||||
Crowded bool `json:"crowded"` // fullt tåg: minispel för att få plats (gäller alla vagnar)
|
||||
Cars int `json:"cars"` // antal vagnar (0 = DefaultCars)
|
||||
WomenOnlyCar int `json:"womenOnlyCar"` // 1-baserat vagnnummer för kvinnovagn, 0 = ingen
|
||||
WomenOnlyFrom int `json:"womenOnlyFrom"` // kvinnovagn gäller från den här bantiden (s); 0 och To=0 = alltid
|
||||
WomenOnlyTo int `json:"womenOnlyTo"` // …till den här bantiden (s)
|
||||
Schedule Schedule `json:"schedule"`
|
||||
|
||||
Verdict nav.Verdict `json:"-"` // nätets bedömning (Unjudged när banan inte är kopplad)
|
||||
}
|
||||
|
||||
// DefaultCars och gränser för vagnar.
|
||||
@@ -232,6 +256,8 @@ type Level struct {
|
||||
Name Text `json:"name"`
|
||||
Difficulty int `json:"difficulty"` // 1 (lätt) … 5 (svår)
|
||||
Destination Text `json:"destination"` // dit spelaren ska
|
||||
Station string `json:"station"` // stationen i linjenätet (nätets id); tomt = banan är inte kopplad till nätet
|
||||
To string `json:"to"` // resmålets station i nätet
|
||||
TimeLimitSec int `json:"timeLimitSec"` // 0 = ingen tidsgräns
|
||||
ParSec int `json:"parSec"` // "bra tid" för tre stjärnor (0 = räknas ut från kortaste vägen)
|
||||
Party int `json:"party"` // antal i sällskapet inkl. spelaren (0 = 1)
|
||||
@@ -245,6 +271,8 @@ type Level struct {
|
||||
Floors []Storey `json:"floors,omitempty"` // flervåningsbana; alla våningar lika stora
|
||||
Trains []Train `json:"trains"`
|
||||
Signs []SignText `json:"signs"`
|
||||
|
||||
Net *nav.Network `json:"-"` // linjenätet banan är kopplad till (nil om inte)
|
||||
}
|
||||
|
||||
// Point är en rutkoordinat: x, y och våning z.
|
||||
@@ -393,11 +421,12 @@ func (l *Level) Validate() error {
|
||||
if !knownTiles[t] {
|
||||
return fmt.Errorf("level %s: okänt tecken %q på (%d,%d,%d)", l.ID, row[x], x, y, z)
|
||||
}
|
||||
if t.IsPlatform() {
|
||||
platforms++
|
||||
}
|
||||
switch t {
|
||||
case Spawn:
|
||||
spawns++
|
||||
case Platform, WrongPlatform:
|
||||
platforms++
|
||||
case StairsUp:
|
||||
if l.FloorAt(z+1, x, y) != StairsDown {
|
||||
return fmt.Errorf("level %s: trappa upp på (%d,%d,%d) saknar trappa ner ovanför", l.ID, x, y, z)
|
||||
@@ -414,7 +443,7 @@ func (l *Level) Validate() error {
|
||||
return fmt.Errorf("level %s: förväntade exakt en start (S), hittade %d", l.ID, spawns)
|
||||
}
|
||||
if platforms == 0 {
|
||||
return fmt.Errorf("level %s: saknar perronger (P/p)", l.ID)
|
||||
return fmt.Errorf("level %s: saknar perronger (P/p/Q/q/R/r)", l.ID)
|
||||
}
|
||||
if len(l.Trains) == 0 {
|
||||
return fmt.Errorf("level %s: saknar tåg", l.ID)
|
||||
@@ -422,7 +451,7 @@ func (l *Level) Validate() error {
|
||||
correct := 0
|
||||
for i := range l.Trains {
|
||||
tr := &l.Trains[i]
|
||||
if len(tr.Platform) != 1 || (Tile(tr.Platform[0]) != Platform && Tile(tr.Platform[0]) != WrongPlatform) {
|
||||
if len(tr.Platform) != 1 || !Tile(tr.Platform[0]).IsPlatform() {
|
||||
return fmt.Errorf("level %s: tåg %d har ogiltig perrong %q", l.ID, i, tr.Platform)
|
||||
}
|
||||
if len(l.Find(Tile(tr.Platform[0]))) == 0 {
|
||||
@@ -454,7 +483,7 @@ func (l *Level) Validate() error {
|
||||
return fmt.Errorf("level %s: tåg %d: uppehållet måste vara ≥ %.1f s och tiden borta ≥ %.1f s för tågfaserna", l.ID, i, MinDwellSec, LeaveSec+ApproachSec+1)
|
||||
}
|
||||
}
|
||||
if correct == 0 {
|
||||
if correct == 0 && !l.Networked() {
|
||||
return fmt.Errorf("level %s: inget tåg är markerat som rätt", l.ID)
|
||||
}
|
||||
if l.FareYen == 0 {
|
||||
@@ -623,7 +652,7 @@ func (l *Level) UngatedPlatforms() []Point {
|
||||
func (l *Level) Unreachable() []Point {
|
||||
reach := l.Reachable()
|
||||
var out []Point
|
||||
for _, t := range []Tile{Platform, WrongPlatform, TicketMachine, Sign, Gate, Toilet, Kiosk} {
|
||||
for _, t := range append(append([]Tile(nil), PlatformTiles...), TicketMachine, Sign, Gate, Toilet, Kiosk) {
|
||||
for _, p := range l.Find(t) {
|
||||
if !reach[p] {
|
||||
out = append(out, p)
|
||||
@@ -677,28 +706,106 @@ func (l *Level) TrainAt(t Tile) *Train {
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadFile läser och validerar en bana från disk.
|
||||
// LoadFile läser och validerar en bana från disk. Är banan kopplad till
|
||||
// linjenätet läses nätet från NetworkPath bredvid banmappen.
|
||||
func LoadFile(path string) (*Level, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("level: %w", err)
|
||||
}
|
||||
return Parse(b)
|
||||
l, err := Parse(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !l.Networked() {
|
||||
return l, nil
|
||||
}
|
||||
net, err := nav.LoadFile(NetworkPath(filepath.Dir(path)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("level %s: %w", l.ID, err)
|
||||
}
|
||||
if err := l.Annotate(net); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// NetworkPath är linjenätets fil för en banmapp: ../network.json.
|
||||
func NetworkPath(levelsDir string) string { return filepath.Join(levelsDir, "..", "network.json") }
|
||||
|
||||
// Networked rapporterar om banan pekar på linjenätet (station/to satta).
|
||||
func (l *Level) Networked() bool { return l.Station != "" || l.To != "" }
|
||||
|
||||
// Annotate kopplar banan till linjenätet: varje tåg bedöms av nätet utifrån
|
||||
// lineId och terminus, och correct räknas ut därifrån i stället för att
|
||||
// skrivas i banfilen. Skyltläget tillåter 1–nav.MaxRoutes rätta tåg.
|
||||
func (l *Level) Annotate(net *nav.Network) error {
|
||||
if !l.Networked() {
|
||||
return nil
|
||||
}
|
||||
if net == nil {
|
||||
return fmt.Errorf("level %s: pekar på linjenätet men inget nät finns", l.ID)
|
||||
}
|
||||
if net.Station(l.Station) == nil || net.Station(l.To) == nil {
|
||||
return fmt.Errorf("level %s: station %q eller resmål %q finns inte i nätet", l.ID, l.Station, l.To)
|
||||
}
|
||||
if want := net.StationName(l.To, "ja"); l.Destination.Get("ja") != want {
|
||||
return fmt.Errorf("level %s: destination %q matchar inte nätets %q", l.ID, l.Destination.Get("ja"), want)
|
||||
}
|
||||
l.Net = net
|
||||
correct := 0
|
||||
for i := range l.Trains {
|
||||
tr := &l.Trains[i]
|
||||
ln := net.Line(tr.LineID)
|
||||
if ln == nil {
|
||||
return fmt.Errorf("level %s: tåg %d har okänd linje %q", l.ID, i, tr.LineID)
|
||||
}
|
||||
if tr.Line.Get("ja") != ln.Name["ja"] {
|
||||
return fmt.Errorf("level %s: tåg %d heter %q men linjen %s heter %q", l.ID, i, tr.Line.Get("ja"), tr.LineID, ln.Name["ja"])
|
||||
}
|
||||
v := net.Judge(tr.LineID, l.Station, tr.Terminus, l.To)
|
||||
if v == nav.Unjudged {
|
||||
return fmt.Errorf("level %s: tåg %d (%s mot %q) går inte att bedöma i nätet", l.ID, i, tr.LineID, tr.Terminus)
|
||||
}
|
||||
tr.Verdict = v
|
||||
tr.Correct = v == nav.Direct
|
||||
if tr.Correct {
|
||||
correct++
|
||||
}
|
||||
}
|
||||
if correct < 1 || correct > nav.MaxRoutes {
|
||||
return fmt.Errorf("level %s: %d rätta tåg, vill ha 1–%d", l.ID, correct, nav.MaxRoutes)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsSide rapporterar om banan är en sidobana.
|
||||
func (l *Level) IsSide() bool { return l.Kind == "side" }
|
||||
|
||||
// LoadAll läser alla banor i en mapp, sorterade på id.
|
||||
// LoadAll läser alla banor i en mapp, sorterade på id, och kopplar dem som
|
||||
// pekar på linjenätet till ../network.json (läses en gång).
|
||||
func LoadAll(dir string) ([]*Level, error) {
|
||||
ids, err := List(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var net *nav.Network
|
||||
if _, statErr := os.Stat(NetworkPath(dir)); statErr == nil {
|
||||
if net, err = nav.LoadFile(NetworkPath(dir)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var out []*Level
|
||||
for _, id := range ids {
|
||||
l, err := LoadFile(filepath.Join(dir, id+".json"))
|
||||
b, err := os.ReadFile(filepath.Join(dir, id+".json"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("level: %w", err)
|
||||
}
|
||||
l, err := Parse(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := l.Annotate(net); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, l)
|
||||
|
||||
Reference in New Issue
Block a user