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>
336 lines
8.3 KiB
Go
336 lines
8.3 KiB
Go
// 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
|
||
}
|