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

335
internal/nav/nav.go Normal file
View File

@@ -0,0 +1,335 @@
// Package nav är linjenätet: stationer, linjer i stationsordning och
// bedömningen av om ett tåg tar en dit man ska – rakt fram, åt fel håll,
// för långt bort eller inte alls. Ren Go utan Ebitengine.
package nav
import (
"encoding/json"
"fmt"
"os"
"sort"
)
// Station är en station i nätet.
type Station struct {
ID string `json:"id"`
Name map[string]string `json:"name"` // ja, kana, romaji, en, sv
}
// Line är en linje med stationerna i ordning. En ringlinje (loop) fortsätter
// från sista stationen till första.
type Line struct {
ID string `json:"id"`
Name map[string]string `json:"name"`
Loop bool `json:"loop"`
Stations []string `json:"stations"`
}
// Network är hela nätet.
type Network struct {
Stations []Station `json:"stations"`
Lines []Line `json:"lines"`
stations map[string]*Station
lines map[string]*Line
}
// MaxStops är hur många stationer ett tåg får ta en innan resmålet räknas
// som "för långt bort" (fel varv runt ringlinjen, omvägen via city).
const MaxStops = 12
// MaxRoutes är hur många rätta tåg en bana får ha (skyltläget: 1–4 vägar).
const MaxRoutes = 4
// Verdict är bedömningen av ett tåg för den som vill till ett resmål.
type Verdict int
// Bedömningar. Unjudged är nollvärdet: banan är inte kopplad till nätet.
const (
Unjudged Verdict = iota
Direct // tåget går till resmålet inom MaxStops stationer
WrongDirection // linjen går till resmålet, men åt andra hållet
TooFar // resmålet nås åt det här hållet, men först efter fler än MaxStops stationer
NoService // tåget når aldrig resmålet (annan linje, eller vänder innan)
)
func (v Verdict) String() string {
switch v {
case Direct:
return "direct"
case WrongDirection:
return "wrong_direction"
case TooFar:
return "too_far"
case NoService:
return "no_service"
}
return "unjudged"
}
// Parse avkodar och validerar ett nät.
func Parse(b []byte) (*Network, error) {
var n Network
if err := json.Unmarshal(b, &n); err != nil {
return nil, fmt.Errorf("nav: ogiltig JSON: %w", err)
}
if err := n.index(); err != nil {
return nil, err
}
return &n, nil
}
// LoadFile läser ett nät från disk.
func LoadFile(path string) (*Network, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("nav: %w", err)
}
return Parse(b)
}
func (n *Network) index() error {
n.stations = map[string]*Station{}
for i := range n.Stations {
s := &n.Stations[i]
if s.ID == "" || s.Name["ja"] == "" {
return fmt.Errorf("nav: station %d saknar id eller japanskt namn", i)
}
if n.stations[s.ID] != nil {
return fmt.Errorf("nav: stationen %q finns två gånger", s.ID)
}
n.stations[s.ID] = s
}
n.lines = map[string]*Line{}
for i := range n.Lines {
l := &n.Lines[i]
if l.ID == "" || l.Name["ja"] == "" {
return fmt.Errorf("nav: linje %d saknar id eller japanskt namn", i)
}
if n.lines[l.ID] != nil {
return fmt.Errorf("nav: linjen %q finns två gånger", l.ID)
}
if len(l.Stations) < 2 {
return fmt.Errorf("nav: linjen %q har färre än två stationer", l.ID)
}
seen := map[string]bool{}
for _, id := range l.Stations {
if n.stations[id] == nil {
return fmt.Errorf("nav: linjen %q har okänd station %q", l.ID, id)
}
if seen[id] {
return fmt.Errorf("nav: linjen %q har stationen %q två gånger", l.ID, id)
}
seen[id] = true
}
n.lines[l.ID] = l
}
return nil
}
// Station slår upp en station (nil om okänd).
func (n *Network) Station(id string) *Station { return n.stations[id] }
// Line slår upp en linje (nil om okänd).
func (n *Network) Line(id string) *Line { return n.lines[id] }
// Name ger ett namn på ett språk med fallback: lang → romaji → en → ja.
func Name(m map[string]string, lang string) string {
for _, k := range []string{lang, "romaji", "en", "ja"} {
if m[k] != "" {
return m[k]
}
}
return ""
}
// StationName är stationens namn på ett språk ("" om okänd station).
func (n *Network) StationName(id, lang string) string {
if s := n.stations[id]; s != nil {
return Name(s.Name, lang)
}
return ""
}
func (l *Line) index(id string) int {
for i, s := range l.Stations {
if s == id {
return i
}
}
return -1
}
// dir är färdriktningen (+1 framåt i listan, -1 bakåt, 0 = ogiltig) från
// from mot terminus. På en ringlinje väljs kortaste vägen till terminus.
func (l *Line) dir(from, terminus string) int {
i, j := l.index(from), l.index(terminus)
if i < 0 || j < 0 || i == j {
return 0
}
if !l.Loop {
if j > i {
return 1
}
return -1
}
L := len(l.Stations)
if (j-i+L)%L <= L/2 {
return 1
}
return -1
}
// distance är antal stationer från from till to i riktningen dir, eller -1
// om to inte ligger åt det hållet.
func (l *Line) distance(from, to string, dir int) int {
i, j := l.index(from), l.index(to)
if i < 0 || j < 0 || dir == 0 {
return -1
}
L := len(l.Stations)
if l.Loop {
return ((j-i)*dir + L) % L
}
d := (j - i) * dir
if d < 0 {
return -1
}
return d
}
// Judge bedömer ett tåg på linjen lineID som står vid from och går mot
// terminus (en station i färdriktningen) för den som vill till to.
func (n *Network) Judge(lineID, from, terminus, to string) Verdict {
l := n.lines[lineID]
if l == nil || n.stations[to] == nil || from == to {
return Unjudged
}
dir := l.dir(from, terminus)
if dir == 0 {
return Unjudged
}
if l.index(to) < 0 {
return NoService
}
d := l.distance(from, to, dir)
switch {
case d < 0:
return WrongDirection
case !l.Loop && d > l.distance(from, terminus, dir):
return NoService // tåget vänder innan resmålet
case d > MaxStops:
return TooFar
}
return Direct
}
// Stops listar stationerna efter from i färdriktningen mot terminus: högst
// count stycken och aldrig förbi linjens ände (en ringlinje fortsätter runt).
func (n *Network) Stops(lineID, from, terminus string, count int) []string {
l := n.lines[lineID]
if l == nil {
return nil
}
return n.StopsDir(lineID, from, l.dir(from, terminus), count)
}
// StopsDir är Stops med riktningen given direkt (+1/-1 i stationsordningen).
func (n *Network) StopsDir(lineID, from string, dir, count int) []string {
l := n.lines[lineID]
if l == nil {
return nil
}
i := l.index(from)
if dir == 0 || i < 0 {
return nil
}
L := len(l.Stations)
var out []string
for k := 1; k <= count; k++ {
j := i + k*dir
if l.Loop {
j = ((j % L) + L) % L
} else if j < 0 || j >= L {
break
}
out = append(out, l.Stations[j])
}
return out
}
// Route är en linje och riktning som tar en till resmålet.
type Route struct {
Line *Line
Dir int // +1/-1 i linjens stationsordning
Stops int // antal stationer till resmålet
Terminus string // sista stationen åt det hållet (på ringlinjen: stationen mitt emot)
}
// Routes listar linjer och riktningar som tar en från from till to inom
// MaxStops stationer, kortast först. Skyltläget tillåter 1–4 sådana.
func (n *Network) Routes(from, to string) []Route {
var out []Route
for i := range n.Lines {
l := &n.Lines[i]
if l.index(from) < 0 || l.index(to) < 0 || from == to {
continue
}
for _, dir := range []int{1, -1} {
d := l.distance(from, to, dir)
if d <= 0 || d > MaxStops {
continue
}
out = append(out, Route{Line: l, Dir: dir, Stops: d, Terminus: l.terminus(from, dir)})
}
}
sort.SliceStable(out, func(a, b int) bool { return out[a].Stops < out[b].Stops })
return out
}
func (l *Line) terminus(from string, dir int) string {
L := len(l.Stations)
if l.Loop {
return l.Stations[((l.index(from)+dir*(L/2))%L+L)%L]
}
if dir > 0 {
return l.Stations[L-1]
}
return l.Stations[0]
}
// LinesAt listar linjerna som stannar vid stationen.
func (n *Network) LinesAt(station string) []*Line {
var out []*Line
for i := range n.Lines {
if n.Lines[i].index(station) >= 0 {
out = append(out, &n.Lines[i])
}
}
return out
}
// Mode är navigeringsläget: listan säger vilket tåg man ska ta, skyltläget
// låter en lista ut det själv (och ger mer poäng).
type Mode int
// Navigeringslägen.
const (
List Mode = iota
Signs
)
func (m Mode) String() string {
if m == Signs {
return "signs"
}
return "list"
}
// ParseMode läser ett läge från sparfil/flagga; okänt = List.
func ParseMode(s string) Mode {
if s == "signs" {
return Signs
}
return List
}