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