Files
metro-puzzle-jp/internal/nav/nav.go
claude f762f9554e
All checks were successful
release / build-release (push) Successful in 1m34s
M9: navigeringslägen, linjenät och terminal
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>
2026-10-24 13:07:36 +02:00

336 lines
8.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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
}