All checks were successful
release / build-release (push) Successful in 1m42s
- level.Schedule.Phase: away/approaching/arriving/dwelling/closing/leaving ur tidtabellen med progress; Boardable (öppna dörrar) och Present; validering av minsta uppehåll/borta-tid; TrackTile för rälsen - sim.Run: ombordstigning bara med öppna dörrar, TrainVisible via Present, PlatformPhase och NearTrack (närmaste perrong inom två rutor) - game/trains.go: rälsrutor längs kantraden (roterade för vertikala spår), tre vagnar som bromsar in och accelererar ut längs rälsen, strålkastare under inrullningen, blinkande lampor vid kanten för den som står nära, dammpartiklar längs perrongen; kameran får en rutas marginal så att kanträlsen syns - game/audio.go: ebiten/v2/audio med avgångsmelodi (approaching) och dörrsignal (closing), volym efter avstånd, -mute; tools/gensound syntetiserar placeholder-WAV:ar - HUD: fasberoende perrongtext och "tåg på väg in" nära rälsen; skriptkommandot waitphase; skärmdump av ankomsten - plan (Björn 2026-10-24): räls/väg-grafik och melodierna i M7b; folk/NPC:er, vänster-/högertrafik, bredare gångar, reklam- och ljudröra som nytt steg M9b – i spelbeskrivning, roadmap, testplan - docs/README/wiki uppdaterade Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
550 lines
17 KiB
Go
550 lines
17 KiB
Go
// Package level definierar banformatet och ren spellogik kring det
|
||
// (validering, framkomlighet). Paketet importerar inte Ebitengine och
|
||
// kan därför testas utan fönster.
|
||
//
|
||
// Banor lagras som JSON i resources/data/levels/. Rutnätet är en lista av
|
||
// lika långa strängar där varje tecken är en ruta (se Tile-konstanterna).
|
||
package level
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"math"
|
||
"os"
|
||
"path/filepath"
|
||
"sort"
|
||
"strings"
|
||
)
|
||
|
||
// Tile är ett tecken i rutnätet.
|
||
type Tile byte
|
||
|
||
// Tecknen som får förekomma i ett rutnät.
|
||
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
|
||
Gate Tile = 'G' // spärr (kräver kort/biljett)
|
||
TicketMachine Tile = 'T' // biljett-/påfyllningsautomat
|
||
Sign Tile = 'i' // skylt
|
||
Toilet Tile = 'W' // toalett (sällskapets toamätare)
|
||
Kiosk Tile = 'K' // kiosk/automat för mat och dryck
|
||
)
|
||
|
||
var knownTiles = map[Tile]bool{
|
||
Wall: true, Floor: true, Spawn: true, Platform: true, WrongPlatform: true,
|
||
Gate: true, TicketMachine: true, Sign: true, Toilet: true, Kiosk: true,
|
||
}
|
||
|
||
// Passable rapporterar om en ruta går att gå på (utan hänsyn till spärrar).
|
||
func (t Tile) Passable() bool { return t != Wall }
|
||
|
||
// Text är en flerspråkig sträng, t.ex. {"sv": "Flygplatsen", "en": "The Airport", "ja": "空港"}.
|
||
type Text map[string]string
|
||
|
||
// Get hämtar texten på ett språk med fallback till engelska, sedan vilket som helst.
|
||
func (t Text) Get(lang string) string {
|
||
if s, ok := t[lang]; ok {
|
||
return s
|
||
}
|
||
if s, ok := t["en"]; ok {
|
||
return s
|
||
}
|
||
for _, s := range t {
|
||
return s
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// Schedule är ett tågs tidtabell i sekunder från banstart: första ankomst
|
||
// vid OffsetSec, därefter var IntervalSec; tåget står DwellSec vid perrongen.
|
||
type Schedule struct {
|
||
OffsetSec float64 `json:"offsetSec"`
|
||
IntervalSec float64 `json:"intervalSec"`
|
||
DwellSec float64 `json:"dwellSec"`
|
||
}
|
||
|
||
// Standardvärden som fylls i av Validate när fältet är 0.
|
||
const (
|
||
DefaultIntervalSec = 30.0
|
||
DefaultDwellSec = 10.0
|
||
DefaultFareYen = 200
|
||
MaxParty = 5
|
||
DefaultRideStops = 3
|
||
)
|
||
|
||
// Status returnerar om tåget står vid perrongen vid tiden t, och hur många
|
||
// sekunder det är kvar – till avgång om det står där, annars till ankomst.
|
||
func (s Schedule) Status(t float64) (atPlatform bool, seconds float64) {
|
||
if t < s.OffsetSec {
|
||
return false, s.OffsetSec - t
|
||
}
|
||
phase := math.Mod(t-s.OffsetSec, s.IntervalSec)
|
||
if phase < s.DwellSec {
|
||
return true, s.DwellSec - phase
|
||
}
|
||
return false, s.IntervalSec - phase
|
||
}
|
||
|
||
// Phase är var i cykeln ett tåg befinner sig.
|
||
type Phase int
|
||
|
||
// Tågfaser i ordning över en cykel.
|
||
const (
|
||
Away Phase = iota // borta
|
||
Approaching // på väg in: melodi, lampor, vind (ApproachSec före ankomst)
|
||
Arriving // rullar in och bromsar (ArriveSec), dörrarna stängda
|
||
Dwelling // står inne med öppna dörrar
|
||
Closing // strax avgång: dörrsignal (sista WarnSec av uppehållet)
|
||
Leaving // rullar ut (LeaveSec efter avgång)
|
||
)
|
||
|
||
func (p Phase) String() string {
|
||
return [...]string{"away", "approaching", "arriving", "dwelling", "closing", "leaving"}[p]
|
||
}
|
||
|
||
// Fasernas längder i sekunder.
|
||
const (
|
||
ApproachSec = 5.0
|
||
ArriveSec = 1.5
|
||
WarnSec = 3.0
|
||
LeaveSec = 1.5
|
||
MinDwellSec = ArriveSec + WarnSec + 1 // minst en sekund öppna dörrar utan signal
|
||
)
|
||
|
||
// Phase returnerar fasen vid tiden t och hur långt in i fasen man är (0..1).
|
||
func (s Schedule) Phase(t float64) (Phase, float64) {
|
||
var phi float64 // tid sedan senaste ankomst, kan vara negativ före första
|
||
if t < s.OffsetSec {
|
||
phi = t - s.OffsetSec
|
||
if -phi <= ApproachSec {
|
||
return Approaching, 1 + phi/ApproachSec
|
||
}
|
||
return Away, 0
|
||
} else {
|
||
phi = math.Mod(t-s.OffsetSec, s.IntervalSec)
|
||
}
|
||
switch {
|
||
case phi < ArriveSec:
|
||
return Arriving, phi / ArriveSec
|
||
case phi < s.DwellSec-WarnSec:
|
||
return Dwelling, (phi - ArriveSec) / max(0.001, s.DwellSec-WarnSec-ArriveSec)
|
||
case phi < s.DwellSec:
|
||
return Closing, (phi - (s.DwellSec - WarnSec)) / WarnSec
|
||
case phi < s.DwellSec+LeaveSec:
|
||
return Leaving, (phi - s.DwellSec) / LeaveSec
|
||
case phi >= s.IntervalSec-ApproachSec:
|
||
return Approaching, (phi - (s.IntervalSec - ApproachSec)) / ApproachSec
|
||
default:
|
||
return Away, (phi - s.DwellSec - LeaveSec) / max(0.001, s.IntervalSec-ApproachSec-s.DwellSec-LeaveSec)
|
||
}
|
||
}
|
||
|
||
// Boardable rapporterar om dörrarna är öppna så att man kan kliva på.
|
||
func (s Schedule) Boardable(t float64) bool {
|
||
p, _ := s.Phase(t)
|
||
return p == Dwelling || p == Closing
|
||
}
|
||
|
||
// Present rapporterar om tåget syns vid perrongen (rullar in, står, rullar ut).
|
||
func (s Schedule) Present(t float64) bool {
|
||
p, _ := s.Phase(t)
|
||
return p == Arriving || p == Dwelling || p == Closing || p == Leaving
|
||
}
|
||
|
||
// Train är ett tåg (eller en buss) som avgår från en perrong i banan.
|
||
type Train struct {
|
||
Platform string `json:"platform"` // vilket perrong-tecken (P/p) tåget står vid
|
||
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
|
||
Crowded bool `json:"crowded"` // fullt tåg: minispel för att få plats
|
||
Schedule Schedule `json:"schedule"`
|
||
}
|
||
|
||
// SignText är texten på en skyltruta ('i').
|
||
type SignText struct {
|
||
X int `json:"x"`
|
||
Y int `json:"y"`
|
||
Text Text `json:"text"`
|
||
}
|
||
|
||
// Level är en bana.
|
||
type Level struct {
|
||
ID string `json:"id"`
|
||
Name Text `json:"name"`
|
||
Difficulty int `json:"difficulty"` // 1 (lätt) … 5 (svår)
|
||
Destination Text `json:"destination"` // dit spelaren ska
|
||
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)
|
||
TicketRequired bool `json:"ticketRequired"` // spärren kräver pappersbiljett (köps i automaten) i stället för kort
|
||
RideStops int `json:"rideStops"` // antal stationer på tågresan (0 = DefaultRideStops)
|
||
MaxMissedTrains int `json:"maxMissedTrains"` // >0: fler missade tåg än så förlorar banan
|
||
IndividualCards bool `json:"individualCards"` // alla i sällskapet har egna kort (och biljetter) som spärren kräver
|
||
SeatSelection bool `json:"seatSelection"` // biljettköpet fortsätter med sittplatsval efter sällskapets önskemål
|
||
FareYen int `json:"fareYen"` // vad spärren drar från kortet (0 = standard)
|
||
Grid []string `json:"grid"`
|
||
Trains []Train `json:"trains"`
|
||
Signs []SignText `json:"signs"`
|
||
}
|
||
|
||
// Point är en rutkoordinat.
|
||
type Point struct{ X, Y int }
|
||
|
||
// Parse avkodar och validerar en bana.
|
||
func Parse(data []byte) (*Level, error) {
|
||
var l Level
|
||
if err := json.Unmarshal(data, &l); err != nil {
|
||
return nil, fmt.Errorf("level: ogiltig JSON: %w", err)
|
||
}
|
||
if err := l.Validate(); err != nil {
|
||
return nil, err
|
||
}
|
||
return &l, nil
|
||
}
|
||
|
||
// Width och Height i rutor.
|
||
func (l *Level) Width() int {
|
||
if len(l.Grid) == 0 {
|
||
return 0
|
||
}
|
||
return len(l.Grid[0])
|
||
}
|
||
func (l *Level) Height() int { return len(l.Grid) }
|
||
|
||
// At returnerar rutan på (x, y); utanför kartan räknas som vägg.
|
||
func (l *Level) At(x, y int) Tile {
|
||
if y < 0 || y >= len(l.Grid) || x < 0 || x >= len(l.Grid[y]) {
|
||
return Wall
|
||
}
|
||
return Tile(l.Grid[y][x])
|
||
}
|
||
|
||
// Find returnerar alla rutor av en viss typ.
|
||
func (l *Level) Find(t Tile) []Point {
|
||
var pts []Point
|
||
for y, row := range l.Grid {
|
||
for x := 0; x < len(row); x++ {
|
||
if Tile(row[x]) == t {
|
||
pts = append(pts, Point{x, y})
|
||
}
|
||
}
|
||
}
|
||
return pts
|
||
}
|
||
|
||
// SpawnPoint returnerar startrutan (valideringen garanterar att den finns).
|
||
func (l *Level) SpawnPoint() Point { return l.Find(Spawn)[0] }
|
||
|
||
// Validate kontrollerar att banan är rimlig: rektangulär, bara kända tecken,
|
||
// exakt en start, minst en perrong, och att varje tåg står vid ett
|
||
// perrong-tecken som finns i rutnätet.
|
||
func (l *Level) Validate() error {
|
||
if l.ID == "" {
|
||
return fmt.Errorf("level: saknar id")
|
||
}
|
||
if len(l.Grid) == 0 {
|
||
return fmt.Errorf("level %s: tomt rutnät", l.ID)
|
||
}
|
||
w := len(l.Grid[0])
|
||
spawns, platforms := 0, 0
|
||
for y, row := range l.Grid {
|
||
if len(row) != w {
|
||
return fmt.Errorf("level %s: rad %d har längd %d, förväntade %d", l.ID, y, len(row), w)
|
||
}
|
||
for x := 0; x < len(row); x++ {
|
||
t := Tile(row[x])
|
||
if !knownTiles[t] {
|
||
return fmt.Errorf("level %s: okänt tecken %q på (%d,%d)", l.ID, row[x], x, y)
|
||
}
|
||
switch t {
|
||
case Spawn:
|
||
spawns++
|
||
case Platform, WrongPlatform:
|
||
platforms++
|
||
}
|
||
}
|
||
}
|
||
if spawns != 1 {
|
||
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)
|
||
}
|
||
if len(l.Trains) == 0 {
|
||
return fmt.Errorf("level %s: saknar tåg", l.ID)
|
||
}
|
||
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) {
|
||
return fmt.Errorf("level %s: tåg %d har ogiltig perrong %q", l.ID, i, tr.Platform)
|
||
}
|
||
if !strings.Contains(strings.Join(l.Grid, ""), tr.Platform) {
|
||
return fmt.Errorf("level %s: tåg %d står vid perrong %q som inte finns i rutnätet", l.ID, i, tr.Platform)
|
||
}
|
||
if tr.Correct {
|
||
correct++
|
||
}
|
||
sc := &tr.Schedule
|
||
if sc.IntervalSec == 0 {
|
||
sc.IntervalSec = DefaultIntervalSec
|
||
}
|
||
if sc.DwellSec == 0 {
|
||
sc.DwellSec = DefaultDwellSec
|
||
}
|
||
if sc.OffsetSec < 0 || sc.IntervalSec <= 0 || sc.DwellSec <= 0 || sc.DwellSec >= sc.IntervalSec {
|
||
return fmt.Errorf("level %s: tåg %d har orimlig tidtabell %+v (kräver offset ≥ 0, 0 < dwell < interval)", l.ID, i, *sc)
|
||
}
|
||
if sc.DwellSec < MinDwellSec || sc.IntervalSec-sc.DwellSec < LeaveSec+ApproachSec+1 {
|
||
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 {
|
||
return fmt.Errorf("level %s: inget tåg är markerat som rätt", l.ID)
|
||
}
|
||
if l.FareYen == 0 {
|
||
l.FareYen = DefaultFareYen
|
||
}
|
||
if l.FareYen < 0 || l.ParSec < 0 || l.Party < 0 {
|
||
return fmt.Errorf("level %s: negativ avgift/partid/sällskap", l.ID)
|
||
}
|
||
if l.Party == 0 {
|
||
l.Party = 1
|
||
}
|
||
if l.RideStops == 0 {
|
||
l.RideStops = DefaultRideStops
|
||
}
|
||
if l.RideStops < 2 || l.RideStops > 8 {
|
||
return fmt.Errorf("level %s: rideStops måste vara 2–8", l.ID)
|
||
}
|
||
if l.TicketRequired && len(l.Find(TicketMachine)) == 0 {
|
||
return fmt.Errorf("level %s: biljettkrav utan biljettautomat (T)", l.ID)
|
||
}
|
||
if l.SeatSelection && !l.TicketRequired {
|
||
return fmt.Errorf("level %s: sittplatsval kräver biljettbana", l.ID)
|
||
}
|
||
if l.MaxMissedTrains < 0 || l.TimeLimitSec < 0 {
|
||
return fmt.Errorf("level %s: negativ tidsgräns/maxantal missade tåg", l.ID)
|
||
}
|
||
if l.Party > MaxParty {
|
||
return fmt.Errorf("level %s: sällskapet får vara högst %d", l.ID, MaxParty)
|
||
}
|
||
// Skyltar: varje post måste stå på en 'i'-ruta och varje 'i'-ruta ha text.
|
||
seen := map[Point]bool{}
|
||
for i, sg := range l.Signs {
|
||
p := Point{sg.X, sg.Y}
|
||
if l.At(p.X, p.Y) != Sign {
|
||
return fmt.Errorf("level %s: skylt %d på (%d,%d) står inte på en skyltruta", l.ID, i, p.X, p.Y)
|
||
}
|
||
if seen[p] {
|
||
return fmt.Errorf("level %s: två skyltar på (%d,%d)", l.ID, p.X, p.Y)
|
||
}
|
||
if len(sg.Text) == 0 {
|
||
return fmt.Errorf("level %s: skylt på (%d,%d) saknar text", l.ID, p.X, p.Y)
|
||
}
|
||
seen[p] = true
|
||
}
|
||
for _, p := range l.Find(Sign) {
|
||
if !seen[p] {
|
||
return fmt.Errorf("level %s: skyltrutan (%d,%d) saknar text i \"signs\"", l.ID, p.X, p.Y)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// SignAt returnerar skylten på en ruta, eller nil.
|
||
func (l *Level) SignAt(p Point) *SignText {
|
||
for i := range l.Signs {
|
||
if l.Signs[i].X == p.X && l.Signs[i].Y == p.Y {
|
||
return &l.Signs[i]
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Path returnerar kortaste vägen från from till to (båda inkluderade) via
|
||
// rutor som canEnter godkänner, eller nil om ingen väg finns.
|
||
func (l *Level) Path(from, to Point, canEnter func(Point) bool) []Point {
|
||
prev := map[Point]Point{from: from}
|
||
queue := []Point{from}
|
||
for len(queue) > 0 {
|
||
p := queue[0]
|
||
queue = queue[1:]
|
||
if p == to {
|
||
var path []Point
|
||
for ; p != from; p = prev[p] {
|
||
path = append(path, p)
|
||
}
|
||
path = append(path, from)
|
||
for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 {
|
||
path[i], path[j] = path[j], path[i]
|
||
}
|
||
return path
|
||
}
|
||
for _, d := range [...]Point{{1, 0}, {-1, 0}, {0, 1}, {0, -1}} {
|
||
n := Point{p.X + d.X, p.Y + d.Y}
|
||
if _, ok := prev[n]; ok || !canEnter(n) {
|
||
continue
|
||
}
|
||
prev[n] = p
|
||
queue = append(queue, n)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Reachable returnerar alla rutor som nås från start via passerbara rutor
|
||
// (4-riktningars BFS). Spärrar räknas som passerbara.
|
||
func (l *Level) Reachable() map[Point]bool {
|
||
start := l.SpawnPoint()
|
||
seen := map[Point]bool{start: true}
|
||
queue := []Point{start}
|
||
for len(queue) > 0 {
|
||
p := queue[0]
|
||
queue = queue[1:]
|
||
for _, d := range [...]Point{{1, 0}, {-1, 0}, {0, 1}, {0, -1}} {
|
||
n := Point{p.X + d.X, p.Y + d.Y}
|
||
if seen[n] || !l.At(n.X, n.Y).Passable() {
|
||
continue
|
||
}
|
||
seen[n] = true
|
||
queue = append(queue, n)
|
||
}
|
||
}
|
||
return seen
|
||
}
|
||
|
||
// Solvable rapporterar om minst en perrong med ett rätt tåg kan nås från start.
|
||
func (l *Level) Solvable() bool {
|
||
reach := l.Reachable()
|
||
for _, tr := range l.Trains {
|
||
if !tr.Correct {
|
||
continue
|
||
}
|
||
for _, p := range l.Find(Tile(tr.Platform[0])) {
|
||
if reach[p] {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// UngatedPlatforms listar perronger som går att nå från start utan att
|
||
// passera någon spärr. Speldesignen kräver att alla perronger ligger
|
||
// innanför spärrar, så listan ska vara tom för skeppade banor.
|
||
func (l *Level) UngatedPlatforms() []Point {
|
||
start := l.SpawnPoint()
|
||
seen := map[Point]bool{start: true}
|
||
queue := []Point{start}
|
||
for len(queue) > 0 {
|
||
p := queue[0]
|
||
queue = queue[1:]
|
||
for _, d := range [...]Point{{1, 0}, {-1, 0}, {0, 1}, {0, -1}} {
|
||
n := Point{p.X + d.X, p.Y + d.Y}
|
||
t := l.At(n.X, n.Y)
|
||
if seen[n] || !t.Passable() || t == Gate {
|
||
continue
|
||
}
|
||
seen[n] = true
|
||
queue = append(queue, n)
|
||
}
|
||
}
|
||
var out []Point
|
||
for _, t := range []Tile{Platform, WrongPlatform} {
|
||
for _, p := range l.Find(t) {
|
||
if seen[p] {
|
||
out = append(out, p)
|
||
}
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// Unreachable listar interaktiva rutor (perronger, automater, skyltar) som
|
||
// inte går att nå – praktiskt som varning när man bygger banor.
|
||
func (l *Level) Unreachable() []Point {
|
||
reach := l.Reachable()
|
||
var out []Point
|
||
for _, t := range []Tile{Platform, WrongPlatform, TicketMachine, Sign, Gate, Toilet, Kiosk} {
|
||
for _, p := range l.Find(t) {
|
||
if !reach[p] {
|
||
out = append(out, p)
|
||
}
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// BoardText är avgångstavlans text på japanska, t.ex. "2番線 成田エクスプレス 新宿方面".
|
||
func (tr *Train) BoardText() string {
|
||
return tr.Track + "番線 " + tr.Line.Get("ja") + " " + tr.Destination.Get("ja") + "方面"
|
||
}
|
||
|
||
// TrackTile är rutan "bortom perrongkanten" där rälsen går: första väggruta
|
||
// nedanför, till höger, till vänster eller ovanför perrongen. horizontal
|
||
// anger om rälsen (och tågets rörelse) går i sidled.
|
||
func (l *Level) TrackTile(p Point) (tile Point, horizontal bool) {
|
||
for _, d := range [...]Point{{X: 0, Y: 1}, {X: 1, Y: 0}, {X: -1, Y: 0}, {X: 0, Y: -1}} {
|
||
n := Point{X: p.X + d.X, Y: p.Y + d.Y}
|
||
if l.At(n.X, n.Y) == Wall {
|
||
return n, d.Y != 0
|
||
}
|
||
}
|
||
return p, true
|
||
}
|
||
|
||
// CorrectPlatform returnerar en perrongruta med rätt tåg (valideringen
|
||
// garanterar att det finns minst ett rätt tåg vid en befintlig perrong).
|
||
func (l *Level) CorrectPlatform() Point {
|
||
for _, tr := range l.Trains {
|
||
if tr.Correct {
|
||
if pts := l.Find(Tile(tr.Platform[0])); len(pts) > 0 {
|
||
return pts[0]
|
||
}
|
||
}
|
||
}
|
||
return l.SpawnPoint()
|
||
}
|
||
|
||
// CanEnter rapporterar om rutan går att gå in på (för sim.Mover).
|
||
func (l *Level) CanEnter(p Point) bool { return l.At(p.X, p.Y).Passable() }
|
||
|
||
// TrainAt returnerar tåget vid en perrongruta, eller nil.
|
||
func (l *Level) TrainAt(t Tile) *Train {
|
||
for i := range l.Trains {
|
||
if Tile(l.Trains[i].Platform[0]) == t {
|
||
return &l.Trains[i]
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// LoadFile läser och validerar en bana från disk.
|
||
func LoadFile(path string) (*Level, error) {
|
||
b, err := os.ReadFile(path)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("level: %w", err)
|
||
}
|
||
return Parse(b)
|
||
}
|
||
|
||
// List returnerar id:n (filnamn utan .json) för alla banor i en mapp, sorterade.
|
||
func List(dir string) ([]string, error) {
|
||
files, err := filepath.Glob(filepath.Join(dir, "*.json"))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
ids := make([]string, 0, len(files))
|
||
for _, f := range files {
|
||
ids = append(ids, strings.TrimSuffix(filepath.Base(f), ".json"))
|
||
}
|
||
sort.Strings(ids)
|
||
return ids, nil
|
||
}
|