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>
29
README.md
@@ -10,15 +10,17 @@ Storyn följer några polare på semester i Japan, från landningen på
|
||||
flygplatsen till avresan. Efter storyn låses ett oändligt läge och New Game+
|
||||
upp. Hela speldesignen finns i [doc/spelbeskrivning.md](doc/spelbeskrivning.md).
|
||||
|
||||
**Status:** M9b klar (2026-10-24) – sex banor på en bankarta i
|
||||
**Status:** M9 klar (2026-10-24) – sju banor på en bankarta i
|
||||
spelbrädesstil, med sidospår och flera våningar. Japanska skyltar och
|
||||
ordinlärning, lämmeltåget, minikarta, tågkort, biljetter och minispel,
|
||||
svårighet med tidsgräns, poäng och debuffar, tåg som rullar in till
|
||||
avgångsmelodin, progression med XP, skillträd, items och sparfil. Och nu
|
||||
lever stationen: folk som går i vägen, filer där man ska hålla till vänster,
|
||||
reklam på väggarna, sorl i bakgrunden, och tåg med flera vagnar där man
|
||||
väljer vagn, låter folk kliva av först och aktar sig för kvinnovagnen.
|
||||
Nästa är M9: navigeringslägena lista och skyltläge. Se
|
||||
avgångsmelodin, progression med XP, skillträd, items och sparfil, folk,
|
||||
filer, reklam, sorl och vagnval. Och nu ett riktigt linjenät: tågen bedöms
|
||||
efter var de faktiskt går, resan visar linjens riktiga stationer, och man
|
||||
väljer **listläge** (nästa tåg visas) eller **skyltläge** (lista ut det
|
||||
själv med skyltar och terminalen, +50 % poäng – åk åt fel håll eller för
|
||||
långt bort och banan är förlorad). Nästa är M10: story-innehåll och
|
||||
flerbytesbanor. Se
|
||||
[doc/roadmap.md](doc/roadmap.md).
|
||||
|
||||

|
||||
@@ -65,6 +67,13 @@ sällskapet.
|
||||
|
||||

|
||||
|
||||
I skyltläget får du bara resmålet. Skyltarna säger var perrongerna är,
|
||||
terminalen (biljettautomaten) visar linjerna med deras närmaste stationer, och
|
||||
du avgör själv vilka tåg som faktiskt tar dig dit – på Ikebukuro är två av
|
||||
fyra rätt, ett går åt fel håll och ett tar omvägen via city.
|
||||
|
||||

|
||||
|
||||
På de svåraste banorna har alla i sällskapet egna kort som måste ha saldo,
|
||||
alla behöver biljett, och när biljetten är köpt ska sällskapet placeras i
|
||||
vagnen efter sina önskemål: fönster eller gång, vänster eller höger sida,
|
||||
@@ -163,6 +172,7 @@ metro-puzzle-jp -debuffs sore_feet,tired # debuffar från start (tester/skärmd
|
||||
metro-puzzle-jp -mute # inget ljud
|
||||
metro-puzzle-jp -save /sökväg/save.json # annan sparfil (standard ~/.config/metro-puzzle-jp/save.json)
|
||||
metro-puzzle-jp -nosave # läs sparfilen men skriv aldrig (tester/skärmdumpar)
|
||||
metro-puzzle-jp -mode signs # navigeringsläge från start: list eller signs (annars sparfilens; N på bankartan växlar)
|
||||
metro-puzzle-jp -script "move right 4; shot bild.png; quit" # skriptläge, se internal/game/script.go
|
||||
metro-puzzle-jp -version
|
||||
```
|
||||
@@ -173,8 +183,8 @@ Pilar eller WASD går. Enter väljer, kliver på tåget, använder toaletten,
|
||||
handlar i kiosken eller startar automaten; i minispelen väljer pilarna och
|
||||
Enter bekräftar (håll Enter för att mata in sedlar). H frågar om hjälp vid en
|
||||
skylt, L öppnar ordlistan, T skillträdet, I garderoben, B butiken vid en kiosk, M
|
||||
döljer/visar minikartan, P på kartan lägger kortet i telefonen, Esc går
|
||||
tillbaka, R spelar om. Gamepad
|
||||
döljer/visar minikartan, P på kartan lägger kortet i telefonen, N på kartan
|
||||
växlar navigeringsläge (lista/skyltar), Esc går tillbaka, R spelar om. Gamepad
|
||||
med standardlayout fungerar: d-pad/vänster spak, A = välj, B = tillbaka,
|
||||
Y = hjälp, X = ordlista, Back/Select = karta.
|
||||
|
||||
@@ -217,7 +227,8 @@ internal/lang/ japanska: ordbok, kunskap, delvis översättning (ren Go
|
||||
internal/minigame/ påfyllning, biljettköp, trängsel, tågresa – seedade minispel (ren Go)
|
||||
internal/progress/ XP, skillträd, items, banresultat, upplåsning (ren Go)
|
||||
internal/save/ sparfil (versionerad JSON)
|
||||
internal/level/ banformat, validering, framkomlighet
|
||||
internal/level/ banformat, validering, framkomlighet, koppling till linjenätet
|
||||
internal/nav/ linjenätet: stationer, linjer, rätt/fel håll/för långt bort (ren Go)
|
||||
internal/res/ hittar och läser resources/
|
||||
internal/rng/ seedad slump
|
||||
tools/ build/ genart/ shots/
|
||||
|
||||
@@ -28,6 +28,7 @@ func main() {
|
||||
mute := flag.Bool("mute", false, "inget ljud")
|
||||
savePath := flag.String("save", "", "sparfil (standard: ~/.config/metro-puzzle-jp/save.json resp. %AppData%)")
|
||||
noSave := flag.Bool("nosave", false, "läs sparfilen men skriv aldrig (tester/skärmdumpar)")
|
||||
mode := flag.String("mode", "", "navigeringsläge från start: list eller signs (tomt = sparfilens)")
|
||||
version := flag.Bool("version", false, "skriv ut version och avsluta")
|
||||
flag.Parse()
|
||||
|
||||
@@ -36,7 +37,7 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
g, err := game.New(game.Options{LevelID: *levelID, SmokeFrames: *smoke, Script: *script, KnownWords: splitWords(*know), Points: *points, Seed: *seed, Phone: *phone, CardYen: *card, Debuffs: splitWords(*debuffs), Mute: *mute, SavePath: *savePath, NoSave: *noSave})
|
||||
g, err := game.New(game.Options{LevelID: *levelID, SmokeFrames: *smoke, Script: *script, KnownWords: splitWords(*know), Points: *points, Seed: *seed, Phone: *phone, CardYen: *card, Debuffs: splitWords(*debuffs), Mute: *mute, SavePath: *savePath, NoSave: *noSave, Mode: *mode})
|
||||
if err != nil {
|
||||
log.Fatalf("kunde inte starta: %v", err)
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
BIN
doc/img/cars.png
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 19 KiB |
BIN
doc/img/map-signs.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
doc/img/map.png
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
BIN
doc/img/nav-list.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 26 KiB |
BIN
doc/img/ride-real.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
doc/img/ride.png
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 22 KiB |
BIN
doc/img/terminal.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
BIN
doc/img/too-far.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@@ -34,7 +34,7 @@ att bygga på Linux som på Windows.
|
||||
| Inkrementellt debug-bygge | < 10 s | 4,7 s |
|
||||
| Release-binär | < 20 MB | 10,2 MB (Linux), 10,8 MB (Windows) |
|
||||
| Testsviten | `go test ./...` grön på < 30 s | ~0,1 s |
|
||||
| Banor | 100 % passerar det automatiska lösbarhetstestet | 6 av 6 |
|
||||
| Banor | 100 % passerar det automatiska lösbarhetstestet | 7 av 7 |
|
||||
| Windows-korsbygge från Linux | fungerar utan extra verktyg | ✓ (CGO_ENABLED=0) |
|
||||
| Linux-binärens länkade bibliotek | bara libc | ✓ (GL/X11 laddas dynamiskt vid körning) |
|
||||
| Bildfrekvens | 60 fps vid 4× skala på integrerad grafik | ej mätt |
|
||||
@@ -127,6 +127,10 @@ att bygga på Linux som på Windows.
|
||||
| `internal/level` | `TestFloorsStairsAndPaths`, `TestFloorsValidation` | **M9a:** flera våningar läses, trappor länkar, BFS-vägen byter våning bara via trappa, spärregeln och skyltar per våning; trappa utan motpart, olika storlekar, grid+floors och fel våning avvisas |
|
||||
| `internal/sim` | `TestStairsMoveBetweenFloors` | Att kliva på trappan flyttar spelaren en våning, dimman lättar runt andra änden, perrongen hörs bara på egen våning, boten går hela vägen över våningar |
|
||||
| `internal/level` | `TestCarsWomenOnlyAndCrowdFields` | **M9b:** vagnar, kvinnovagnens tidsfönster, crowd/clutter/noise 0–3, filrutor |
|
||||
| `internal/nav` | `TestJudge`, `TestTooFarOnLongLoop`, `TestStopsRoutesAndNames`, `TestValidation`, `TestShippedNetwork` | **M9:** rätt/fel håll/för långt/ingen trafik på raka linjer och ringlinjer, stationer i färdriktningen, giltiga vägar sorterade, nätets validering, det riktiga nätets förutsättningar för banorna |
|
||||
| `internal/level` | `TestAnnotateWithNetwork`, `TestPlatformTilesAndShippedVerdicts` | **M9:** `correct` räknas ut från nätet, felaktiga kopplingar ger fel, okopplade banor orörda; perrongtecknen `P p Q q R r`; 005/006 får väntade bedömningar |
|
||||
| `internal/sim` | `TestNetworkRideUsesRealStops`, `TestWrongDirectionLoses`, `TestTooFarLoses`, `TestSignsModeBonus`, `TestTerminalLines` | **M9:** resan visar linjens riktiga stationer med resmålet på rätt plats; fel håll och för långt bort förlorar med rätt orsak; skyltläget ger +50 %; terminalen listar linjer med grannstationer |
|
||||
| `internal/lang` | `TestNetworkNamesInDictionary` | **M9:** alla stations- och linjenamn i nätet finns i ordboken |
|
||||
| `internal/minigame` | `TestCarPickAlightingAndChoice`, `TestCrowdLevelNarrowsZone`, `TestRideCrowdLetPass` | Folk av först (för tidigt räknas), seedad trängsel med minst en lugn vagn, val; trängsel smalnar zonen; resan: kortare tid per station, passagerare som vill förbi, blockerade räknas |
|
||||
| `internal/sim` | `TestNPCsWanderStayOnFloorAndBlockPlayer`, `TestAlightingNPCsSpawnAndLeave` | Folk rör sig på golv, seedat, blockerar spelaren; avstigande spawnar när dörrarna öppnas och lämnar via utgången |
|
||||
| `internal/sim` | `TestLanesSlowWrongSide`, `TestCarsWomenOnlyEarlyAndCrowd` | Fel sida i filen bromsar; kvinnovagn med man ger grova minus, för tidigt ombord räknas, resan ärver vagnens trängsel, lugn vagn ger bonus |
|
||||
@@ -147,7 +151,7 @@ att bygga på Linux som på Windows.
|
||||
| ~~M9b Folk, reklam, ljudröra, vagnar~~ ✓ | NPC:er, filer, röra, vagnval – finns, se tabellen ovan (bot med folk: testerna tömmer folket där det skulle störa, "fler NPC:er = längre tid" mäts manuellt) |
|
||||
| ~~M8 Progression & sparfil~~ ✓ | Round-trip, skillkrav, upplåsningar, 33 %-regeln – finns, se tabellen ovan |
|
||||
| ~~M9a Flera våningar~~ ✓ | Trappor, vägar över våningar, dimma per våning, gamla banor oförändrade – finns, se tabellen ovan |
|
||||
| M9 Navigeringslägen | Listläge genererar rätt ordning av tåg; skyltläge: 1–4 giltiga vägar; fel-riktnings-förlust triggar vid rätt slutstation |
|
||||
| ~~M9 Navigeringslägen~~ ✓ | Nätets bedömningar (rätt/fel håll/för långt/ingen trafik), 1–4 rätta tåg, riktiga stationer på resan, fel-håll- och för-långt-förlust, skyltbonus, terminal – finns, se tabellen ovan |
|
||||
| M10 Story-innehåll | Alla banor lösbara (redan automatiskt); flerbytesbanor har sammanhängande byten; sista banan slutar på flygplatsen |
|
||||
| M11 Oändligt läge & NG+ | Målsekvens deterministisk per seed; NG+ behåller skills men nollställer items |
|
||||
| M12 Ljud & polish | Alla ljudfiler som refereras finns och går att avkoda; Windows-binär startar (manuellt) |
|
||||
|
||||
@@ -482,7 +482,7 @@ på **brasse-linux01-runnern** (label `linux-amd64`), aldrig på Pi5:n.
|
||||
- **Kvar till senare:** NPC:er som väntar på och kliver på tåget, folk som
|
||||
bildar riktiga köer vid markeringarna, utrop/reklam som ljud (M12).
|
||||
|
||||
### M9 – Navigeringslägen: lista och skyltläge
|
||||
### M9 – Navigeringslägen: lista och skyltläge ✓ (klar 2026-10-24)
|
||||
|
||||
- **Mål:** de två sätten att spela story.
|
||||
- Listläge: nästa tåg visas.
|
||||
@@ -490,6 +490,36 @@ på **brasse-linux01-runnern** (label `linux-amd64`), aldrig på Pi5:n.
|
||||
riktning / för långt bort.
|
||||
- **Teknik:** `internal/nav` (graf över stationer/linjer, giltiga vägar,
|
||||
riktningskontroll – ren Go).
|
||||
- **Så blev det:** `internal/nav` läser `resources/data/network.json`
|
||||
(76 stationer, 11 linjer: Yamanote som ringlinje, Chuo, Keihin-Tohoku,
|
||||
Saikyo, Marunouchi, Hibiya, Ginza, Toyoko, Tokaido, Narita Express,
|
||||
Keisei). Ett tåg bedöms från `lineId` + `terminus` (en station i
|
||||
färdriktningen) mot banans `station`/`to`: **Direct** (resmålet inom 12
|
||||
stationer), **WrongDirection** (linjen går dit, men åt andra hållet),
|
||||
**TooFar** (nås först efter fler än 12 stationer – fel varv på ringen,
|
||||
omvägen via city) eller **NoService**. `correct` skrivs inte längre i
|
||||
nätkopplade banfiler utan räknas ut; 1–4 rätta tåg per bana. Tågresan
|
||||
visar linjens **riktiga stationer** i färdriktningen (resmålet på rätt
|
||||
plats, eller aldrig). Förlustorsakerna *Fel håll* och *För långt bort*
|
||||
förklarar vad som hände. **Listläge** visar "Ta: spår 6 Yamanote-sen
|
||||
(+1)" i HUD:en; **skyltläge** visar bara att man är i det och ger +50 %
|
||||
på grundpoängen. Läget växlas med **N** på bankartan och sparas.
|
||||
**Terminalen**: på nätkopplade banor visar biljettautomaten linjetavlan
|
||||
(varje linje med tre grannstationer åt vardera hållet), läst genom ens
|
||||
kunskap som en skylt. Perrongtecknen generaliserade (`P p Q q R r`).
|
||||
**Bana 006 Ikebukuro** (svårighet 4): fyra perronger – Yamanote mot
|
||||
Shinjuku och Saikyo mot Osaki är rätt, Saikyo mot Omiya är fel håll och
|
||||
Marunouchi mot Ogikubo för långt bort. Banorna 001–005 är kopplade till
|
||||
nätet (Keihin-Tohoku mot Omiya i Ueno blev därmed *fel håll*).
|
||||
- **Klar när:** bedömningarna och vägarna är testade (`nav.TestJudge`,
|
||||
`TestTooFarOnLongLoop`, `TestStopsRoutesAndNames`, `TestShippedNetwork`),
|
||||
banor kopplas rätt (`level.TestAnnotateWithNetwork`,
|
||||
`TestPlatformTilesAndShippedVerdicts`), resan använder riktiga stationer
|
||||
och fel håll/för långt förlorar (`sim.TestNetworkRideUsesRealStops`,
|
||||
`TestWrongDirectionLoses`, `TestTooFarLoses`), skyltläget ger bonus
|
||||
(`TestSignsModeBonus`) och terminalen listar linjer (`TestTerminalLines`). ✓
|
||||
- **Kvar till senare:** flera tåg i rad (byten) kommer med M10; bussar;
|
||||
"terminaler" som egna rutor i stället för biljettautomaten.
|
||||
|
||||
### M10 – Story-innehåll och flerbytesbanor
|
||||
|
||||
|
||||
@@ -265,4 +265,6 @@ från skills i skill tree, och från unika skills man får av sidouppdrag.
|
||||
| "Lämel tåg" | *Lämmeltåg*: sällskapet som följer efter dig i rad | Styr §5 |
|
||||
| "level maps game box liknande styl av level väljande" | Bankarta i spelbrädes-/världskartestil | Kan ändras om något annat avsågs |
|
||||
| "bilder och eventuella språk och tester" (resursmappen) | bilder, språk och *texter* | Se resurslayouten i roadmap |
|
||||
| "slutstation ligger i fel riktning … eller hamnar för långt bort" (skyltläge) | Linjenätet (`network.json`) bedömer varje tåg: *fel håll* om linjen går till resmålet men tåget åt andra hållet; *för långt bort* om resmålet nås först efter fler än 12 stationer (fel varv på Yamanote-ringen, omvägen via city). Båda förlorar banan – i båda lägena, eftersom fel tåg alltid har förlorat | Gränsen 12 är `nav.MaxStops` och kan ändras |
|
||||
| "skyltar och terminaler" | Terminalen är biljettautomaten: på nätkopplade banor visar den linjetavlan (linjer vid stationen med grannstationer åt båda hållen), läst genom ens japanska som en skylt | Egna terminalrutor kan komma senare |
|
||||
| Repo-namnet | Arbetsnamnet var "jp-subway-pussel"; Björn döpte Gitea-repot till **metro-puzzle-jp** (2026-10-23), så repo, modul, binär och titel heter så | Spelets visningsnamn "Metro Puzzle JP" kan ändras fritt i `resources/text/*.json` |
|
||||
|
||||
@@ -80,7 +80,9 @@ func LoadAssets() (*Assets, error) {
|
||||
}
|
||||
a.Tiles[t] = sub(i)
|
||||
}
|
||||
a.Tiles[level.WrongPlatform] = a.Tiles[level.Platform] // samma utseende; tåget avgör
|
||||
for _, t := range level.PlatformTiles[1:] {
|
||||
a.Tiles[t] = a.Tiles[level.Platform] // samma utseende; tåget avgör
|
||||
}
|
||||
a.Train = sub(trainIndex)
|
||||
a.Rails = sub(railsIndex)
|
||||
for i := 0; i < posterCount; i++ {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/lang"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/nav"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/progress"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/res"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
|
||||
@@ -45,6 +46,7 @@ type Game struct {
|
||||
Catalog *progress.Catalog
|
||||
Levels []*level.Level // alla banor, sorterade på id
|
||||
Audio *Audio // nil = tyst
|
||||
Mode nav.Mode // navigeringsläge (= State.NavMode), växlas på bankartan
|
||||
|
||||
savePath string // "" = spara inte
|
||||
fixedSeed uint64 // >0: alla banor startar med denna seed (tester/skärmdumpar)
|
||||
@@ -69,6 +71,7 @@ type Options struct {
|
||||
Mute bool // inget ljud
|
||||
SavePath string // sparfil ("" = standardplatsen)
|
||||
NoSave bool // läs sparfilen men skriv aldrig
|
||||
Mode string // navigeringsläge från start: "list" eller "signs" (tomt = sparfilens)
|
||||
}
|
||||
|
||||
// New laddar resurser, sparfil och bygger spelet.
|
||||
@@ -109,6 +112,10 @@ func New(opt Options) (*Game, error) {
|
||||
}
|
||||
g.State = st
|
||||
g.Know, g.Wallet, g.Profile, g.Progress = st.Know, st.Wallet, st.Profile, st.Progress
|
||||
g.Mode = nav.ParseMode(st.NavMode)
|
||||
if opt.Mode != "" {
|
||||
g.Mode = nav.ParseMode(opt.Mode)
|
||||
}
|
||||
if !opt.NoSave {
|
||||
g.savePath = path
|
||||
}
|
||||
@@ -233,10 +240,21 @@ func (g *Game) runOptions(id string, seed rng.Seed) sim.RunOptions {
|
||||
return sim.RunOptions{
|
||||
Dict: g.Assets.Dict, Know: g.Know, Wallet: g.Wallet, Debuffs: g.Profile.Debuffs, Seed: seed,
|
||||
Extra: eff.Stats, FogRadius: eff.FogRadius, Landmarks: eff.Landmarks, FreeGates: eff.FreeGates,
|
||||
RevealAll: eff.Reveal[id], InfiniteBattery: eff.Battery,
|
||||
RevealAll: eff.Reveal[id], InfiniteBattery: eff.Battery, Mode: g.Mode,
|
||||
}
|
||||
}
|
||||
|
||||
// toggleMode växlar navigeringsläge och sparar.
|
||||
func (g *Game) toggleMode() {
|
||||
if g.Mode == nav.List {
|
||||
g.Mode = nav.Signs
|
||||
} else {
|
||||
g.Mode = nav.List
|
||||
}
|
||||
g.State.NavMode = g.Mode.String()
|
||||
g.Save()
|
||||
}
|
||||
|
||||
// startLevel startar (eller återupptar) en bana och sparar seeden så att en
|
||||
// laddad omgång ger samma slump.
|
||||
func (g *Game) startLevel(id string) (*LevelScene, error) {
|
||||
|
||||
@@ -126,6 +126,9 @@ func shopJustPressed() bool { return virtual.pressed["b"] || inpututil.IsKeyJust
|
||||
// phoneJustPressed: P = kortet i telefonen av/på (meny).
|
||||
func phoneJustPressed() bool { return virtual.pressed["p"] || inpututil.IsKeyJustPressed(ebiten.KeyP) }
|
||||
|
||||
// modeJustPressed: N växlar navigeringsläge (lista/skyltar) på bankartan.
|
||||
func modeJustPressed() bool { return virtual.pressed["n"] || inpututil.IsKeyJustPressed(ebiten.KeyN) }
|
||||
|
||||
// mapJustPressed: M eller gamepadens vänstra mittknapp (Back/Select) = minikartan av/på.
|
||||
func mapJustPressed() bool {
|
||||
return virtual.pressed["m"] || inpututil.IsKeyJustPressed(ebiten.KeyM) || gamepadJustPressed(ebiten.StandardGamepadButtonCenterLeft)
|
||||
|
||||
@@ -24,6 +24,10 @@ var minimapColors = map[level.Tile]color.RGBA{
|
||||
level.Spawn: {99, 199, 77, 255},
|
||||
level.Platform: {254, 174, 52, 255},
|
||||
level.WrongPlatform: {254, 174, 52, 255},
|
||||
level.Platform2: {254, 174, 52, 255},
|
||||
level.Platform3: {254, 174, 52, 255},
|
||||
level.Platform4: {254, 174, 52, 255},
|
||||
level.Platform5: {254, 174, 52, 255},
|
||||
level.Gate: {44, 232, 245, 255},
|
||||
level.TicketMachine: {247, 118, 34, 255},
|
||||
level.Sign: {90, 140, 230, 255},
|
||||
@@ -111,7 +115,7 @@ func (s *LevelScene) drawMinimap(g *Game, screen *ebiten.Image, box image.Rectan
|
||||
}
|
||||
t := l.At(p)
|
||||
c := minimapColors[t]
|
||||
if (t == level.Platform || t == level.WrongPlatform) && r.TrainVisible(t) {
|
||||
if t.IsPlatform() && r.TrainVisible(t) {
|
||||
c = color.RGBA{255, 255, 255, 255} // tåget är inne
|
||||
}
|
||||
vector.DrawFilledRect(screen, float32(x0+x*sc), float32(y0+y*sc), float32(sc), float32(sc), c, false)
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/lang"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/minigame"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/nav"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/sim"
|
||||
)
|
||||
@@ -104,6 +105,31 @@ func (s *LevelScene) RideAtDest() bool {
|
||||
return !ok || g.Index == g.Dest
|
||||
}
|
||||
|
||||
// drawModeHint ritar navigeringslägets ruta nere till vänster när inforutan
|
||||
// är tom: listläget säger vilket tåg man ska ta, skyltläget påminner om att
|
||||
// man får lista ut det själv (och om bonusen).
|
||||
func (s *LevelScene) drawModeHint(a *Assets, screen *ebiten.Image) {
|
||||
r := s.run
|
||||
txt, col := "", colGood
|
||||
if r.Mode == nav.Signs {
|
||||
txt, col = a.T("hud.mode_signs"), colWarn
|
||||
} else if next := r.NextTrains(); len(next) > 0 {
|
||||
txt = fmt.Sprintf("%s %s %s %s", a.T("hud.take"), strings.ToLower(a.T("hud.track")), next[0].Track, next[0].Line.Get("romaji"))
|
||||
if len(next) > 1 {
|
||||
txt += fmt.Sprintf(" (+%d)", len(next)-1)
|
||||
}
|
||||
}
|
||||
if txt == "" {
|
||||
return
|
||||
}
|
||||
y := ScreenH - LineH - 6
|
||||
vector.DrawFilledRect(screen, 0, float32(y), float32(a.Width(txt)+8), float32(LineH+6), colPanel, false)
|
||||
a.Draw(screen, txt, 4, y+3, col)
|
||||
}
|
||||
|
||||
// LevelOver rapporterar om banan är avgjord (skriptläget: "waitover").
|
||||
func (s *LevelScene) LevelOver() bool { return s.run.Outcome != sim.Ongoing }
|
||||
|
||||
func (s *LevelScene) followPlayer() {
|
||||
x, y := s.run.Player.WorldPos(TileSize)
|
||||
s.cam.Follow(x+TileSize/2, y+TileSize/2)
|
||||
@@ -165,6 +191,20 @@ func (s *LevelScene) MiniPick(text string) bool {
|
||||
s.run.StepMini(0, minigame.Input{Move: 1})
|
||||
return false
|
||||
}
|
||||
case *minigame.CarPick:
|
||||
if text == "calm" { // lugnaste vagnen som inte är kvinnovagn
|
||||
best := -1
|
||||
for i, c := range m.Cars {
|
||||
if !c.WomenOnly && (best < 0 || c.Crowd < m.Cars[best].Crowd) {
|
||||
best = i
|
||||
}
|
||||
}
|
||||
if best < 0 || m.Cursor == best {
|
||||
return true
|
||||
}
|
||||
s.run.StepMini(0, minigame.Input{MoveX: 1})
|
||||
return false
|
||||
}
|
||||
case *minigame.TopUp:
|
||||
switch m.Stage {
|
||||
case minigame.TopUpPickMember:
|
||||
@@ -345,6 +385,9 @@ func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) {
|
||||
return // resultatpanelen, minispelet eller butiken tar över; ingen inforuta eller minikarta
|
||||
}
|
||||
lines, accent, bar := s.infoLines(a)
|
||||
if len(lines) == 0 {
|
||||
s.drawModeHint(a, screen)
|
||||
}
|
||||
if entry := a.Dict.Lookup(r.Learned); entry != nil && r.Time-r.LearnedAt < learnedShowSec {
|
||||
lines = append(lines, plainLine(fmt.Sprintf("%s: %s = %s (%s)", a.T("hud.learned"), r.Learned,
|
||||
entry.Translation(UILang), entry.Romaji), colKnown))
|
||||
@@ -499,10 +542,18 @@ func (s *LevelScene) infoLines(a *Assets) (lines []line, accent color.Color, bar
|
||||
}
|
||||
return []line{plainLine(a.T("hud.kiosk_none")+" "+a.T("hud.shop_prompt"), colDim)}, accent, false
|
||||
case level.TicketMachine:
|
||||
if lvl.TicketRequired {
|
||||
return []line{plainLine(fmt.Sprintf(a.T("hud.machine_ticket"), r.Wallet.Cash), colGood)}, colGood, false
|
||||
// Terminalen: linjetavlan för stationen (nätkopplade banor), läst genom kunskapen som en skylt.
|
||||
if term := r.TerminalLines(); len(term) > 0 {
|
||||
lines = append(lines, plainLine(a.T("hud.terminal"), colDim))
|
||||
for _, t := range term {
|
||||
rd := lang.Render(a.Dict, r.Know, t, UILang)
|
||||
lines = append(lines, a.wrapLine(shownLine(rd.Partial, colText, false), maxW)...)
|
||||
}
|
||||
}
|
||||
return []line{plainLine(fmt.Sprintf(a.T("hud.machine_topup"), r.Wallet.Card, r.Wallet.Cash), colGood)}, colGood, false
|
||||
if lvl.TicketRequired {
|
||||
return append(lines, plainLine(fmt.Sprintf(a.T("hud.machine_ticket"), r.Wallet.Cash), colGood)), colGood, false
|
||||
}
|
||||
return append(lines, plainLine(fmt.Sprintf(a.T("hud.machine_topup"), r.Wallet.Card, r.Wallet.Cash), colGood)), colGood, false
|
||||
}
|
||||
if r.DeniedAt >= 0 && r.Time-r.DeniedAt < deniedShowSec {
|
||||
key := "hud.gate_denied"
|
||||
|
||||
@@ -101,6 +101,8 @@ func (m *MapScene) Update(g *Game) error {
|
||||
case phoneJustPressed():
|
||||
g.Wallet.Phone = !g.Wallet.Phone
|
||||
g.Save()
|
||||
case modeJustPressed():
|
||||
g.toggleMode()
|
||||
}
|
||||
if n := len(m.mains); n > 0 {
|
||||
mx, my := miniMoveXY()
|
||||
@@ -209,7 +211,7 @@ func (m *MapScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
foot := fmt.Sprintf("%s %d %s %d %s %d¥", a.T("map.skill_points"), p.SkillPoints, a.T("words.points"), g.Know.Points,
|
||||
a.T("hud.cash"), g.Wallet.Cash)
|
||||
a.Draw(screen, foot, 8, 30, colDim)
|
||||
a.Draw(screen, fmt.Sprintf(a.T("menu.phone"), onoff), 8, 42, colDim)
|
||||
a.Draw(screen, fmt.Sprintf(a.T("menu.phone"), onoff)+" "+fmt.Sprintf(a.T("menu.mode"), a.T("mode."+g.Mode.String())), 8, 42, colDim)
|
||||
if len(g.Profile.Debuffs) > 0 {
|
||||
var parts []string
|
||||
for _, d := range g.Profile.Debuffs {
|
||||
|
||||
@@ -62,8 +62,12 @@ func (r *ResultScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
y += LineH
|
||||
if run.Outcome == sim.Lost {
|
||||
reason := a.T(run.LostReason)
|
||||
if run.LostReason == sim.LostWrongTrain {
|
||||
reason = fmt.Sprintf(a.T("result.wrong_detail"), tr.Destination.Get("romaji"), run.Level.Destination.Get("romaji"))
|
||||
if detail, ok := map[string]string{
|
||||
sim.LostWrongTrain: "result.wrong_detail",
|
||||
sim.LostWrongDirection: "result.wrong_direction_detail",
|
||||
sim.LostTooFar: "result.too_far_detail",
|
||||
}[run.LostReason]; ok {
|
||||
reason += " " + fmt.Sprintf(a.T(detail), tr.Destination.Get("romaji"), run.Level.Destination.Get("romaji"))
|
||||
}
|
||||
for _, l := range a.wrapLine(plainLine(reason, colText), ScreenW-56) {
|
||||
a.drawLines(screen, []line{l}, 28, y)
|
||||
@@ -80,11 +84,26 @@ func (r *ResultScene) Draw(g *Game, screen *ebiten.Image) {
|
||||
y += LineH
|
||||
if run.Outcome == sim.Won {
|
||||
sc := run.Score
|
||||
a.Draw(screen, fmt.Sprintf("%s %d", a.T("result.score"), sc.Total), 28, y, colText)
|
||||
y += LineH
|
||||
a.Draw(screen, fmt.Sprintf("%s %d %s %d %s %d %s %d %s %d %s %d", strings.ToLower(a.T("hud.time")), sc.Time, a.T("result.money"), sc.Money,
|
||||
a.T("result.meters"), sc.Meters, a.T("result.map"), sc.Explore, a.T("result.car"), sc.Car, a.T("result.penalty"), sc.Penalty), 28, y, colDim)
|
||||
y += LineH
|
||||
// Poängen som en uträkning; radbryts till högst två rader.
|
||||
parts := []string{
|
||||
fmt.Sprintf("%s %d", strings.ToLower(a.T("hud.time")), sc.Time), fmt.Sprintf("%s %d", a.T("result.money"), sc.Money),
|
||||
fmt.Sprintf("%s %d", a.T("result.meters"), sc.Meters), fmt.Sprintf("%s %d", a.T("result.map"), sc.Explore),
|
||||
fmt.Sprintf("%s %d", a.T("result.car"), sc.Car),
|
||||
}
|
||||
if sc.Bonus > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%s %d", a.T("result.bonus"), sc.Bonus))
|
||||
}
|
||||
total := fmt.Sprintf("%s %d = %s", a.T("result.score"), sc.Total, strings.Join(parts, " + "))
|
||||
if sc.Penalty > 0 {
|
||||
total += fmt.Sprintf(" - %s %d", a.T("result.penalty"), sc.Penalty)
|
||||
}
|
||||
for i, l := range a.wrapLine(plainLine(total, colText), ScreenW-56) {
|
||||
if i > 1 {
|
||||
break
|
||||
}
|
||||
a.drawLines(screen, []line{l}, 28, y)
|
||||
y += LineH
|
||||
}
|
||||
msg := fmt.Sprintf("+%d %s", run.PointsEarned, a.T("result.lang_points"))
|
||||
if len(run.LearnedDest) > 0 {
|
||||
msg += " " + a.T("hud.learned") + ": " + strings.Join(run.LearnedDest, " ") + " (" + a.T("result.destination") + ")"
|
||||
|
||||
@@ -93,7 +93,7 @@ func parseScript(src string) (*script, error) {
|
||||
return nil, fmt.Errorf("script: '%s TEXT'", f[0])
|
||||
}
|
||||
st.arg = f[1]
|
||||
case "quit", "waittrain", "waitstop":
|
||||
case "quit", "waittrain", "waitstop", "waitover":
|
||||
default:
|
||||
return nil, fmt.Errorf("script: okänt kommando %q", f[0])
|
||||
}
|
||||
@@ -126,6 +126,7 @@ type playerTiler interface {
|
||||
type trainWaiter interface {
|
||||
TrainHere() bool
|
||||
RideAtDest() bool
|
||||
LevelOver() bool
|
||||
MiniPick(text string) bool
|
||||
TrackPhaseName() string
|
||||
}
|
||||
@@ -195,6 +196,11 @@ func (s *script) tick(g *Game, frame *ebiten.Image) error {
|
||||
if tw, ok := g.scene.(trainWaiter); !ok || tw.RideAtDest() {
|
||||
s.advance()
|
||||
}
|
||||
case "waitover":
|
||||
s.counter++
|
||||
if tw, ok := g.scene.(trainWaiter); !ok || tw.LevelOver() || s.counter > 60*120 {
|
||||
s.advance()
|
||||
}
|
||||
case "waitphase":
|
||||
s.counter++
|
||||
if tw, ok := g.scene.(trainWaiter); !ok || tw.TrackPhaseName() == st.arg || s.counter > 60*60 {
|
||||
|
||||
@@ -36,7 +36,7 @@ type trackInfo struct {
|
||||
func (s *LevelScene) buildTracks() {
|
||||
s.tracks = nil
|
||||
s.rails = map[level.Point]bool{}
|
||||
for _, t := range []level.Tile{level.Platform, level.WrongPlatform} {
|
||||
for _, t := range level.PlatformTiles {
|
||||
for _, p := range s.run.Level.Find(t) {
|
||||
tt, horizontal := s.run.Level.TrackTile(p)
|
||||
s.tracks = append(s.tracks, trackInfo{tile: t, platform: p, track: tt, horizontal: horizontal})
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/nav"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/res"
|
||||
)
|
||||
|
||||
@@ -79,3 +80,27 @@ func TestAllJapaneseInLevelsIsInDictionary(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNetworkNamesInDictionary: alla stations- och linjenamn i linjenätet
|
||||
// finns i ordboken, så att tågresans stationer och terminalen går att lära sig.
|
||||
func TestNetworkNamesInDictionary(t *testing.T) {
|
||||
d := loadDict(t)
|
||||
path, err := res.Path("data", "network.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n, err := nav.LoadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, s := range n.Stations {
|
||||
if u := d.Unknown(s.Name["ja"]); len(u) > 0 {
|
||||
t.Errorf("station %s (%s) saknas i ordboken: %s", s.ID, s.Name["ja"], strings.Join(u, ", "))
|
||||
}
|
||||
}
|
||||
for _, l := range n.Lines {
|
||||
if u := d.Unknown(l.Name["ja"]); len(u) > 0 {
|
||||
t.Errorf("linje %s (%s) saknas i ordboken: %s", l.ID, l.Name["ja"], strings.Join(u, ", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/nav"
|
||||
)
|
||||
|
||||
// Tile är ett tecken i rutnätet.
|
||||
@@ -24,8 +26,12 @@ 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
|
||||
Platform Tile = 'P' // perrong med ett tåg (vilket tåg som är rätt avgör tåget, inte tecknet)
|
||||
WrongPlatform Tile = 'p' // ytterligare perrong (namnet är historiskt: i de första banorna stod fel tåg här)
|
||||
Platform2 Tile = 'Q' // fler perronger för banor med flera giltiga vägar och fällor
|
||||
Platform3 Tile = 'q'
|
||||
Platform4 Tile = 'R'
|
||||
Platform5 Tile = 'r'
|
||||
Gate Tile = 'G' // spärr (kräver kort/biljett)
|
||||
TicketMachine Tile = 'T' // biljett-/påfyllningsautomat
|
||||
Sign Tile = 'i' // skylt
|
||||
@@ -37,12 +43,26 @@ const (
|
||||
LaneRight Tile = '>' // fil för gång neråt/åt höger
|
||||
)
|
||||
|
||||
// PlatformTiles är alla perrongtecken, i den ordning tåg brukar listas.
|
||||
var PlatformTiles = []Tile{Platform, WrongPlatform, Platform2, Platform3, Platform4, Platform5}
|
||||
|
||||
var knownTiles = map[Tile]bool{
|
||||
Wall: true, Floor: true, Spawn: true, Platform: true, WrongPlatform: true,
|
||||
Platform2: true, Platform3: true, Platform4: true, Platform5: true,
|
||||
Gate: true, TicketMachine: true, Sign: true, Toilet: true, Kiosk: true,
|
||||
StairsUp: true, StairsDown: true, LaneLeft: true, LaneRight: true,
|
||||
}
|
||||
|
||||
// IsPlatform rapporterar om rutan är en perrong (något av PlatformTiles).
|
||||
func (t Tile) IsPlatform() bool {
|
||||
for _, p := range PlatformTiles {
|
||||
if t == p {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsLane rapporterar om rutan är en markerad fil.
|
||||
func (t Tile) IsLane() bool { return t == LaneLeft || t == LaneRight }
|
||||
|
||||
@@ -171,13 +191,17 @@ type Train struct {
|
||||
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
|
||||
Correct bool `json:"correct"` // tar spelaren till rätt resmål (räknas ut från nätet när banan har station/to)
|
||||
LineID string `json:"lineId"` // linjen i linjenätet (resources/data/network.json)
|
||||
Terminus string `json:"terminus"` // en station i färdriktningen (nätets id) – avgör riktningen
|
||||
Crowded bool `json:"crowded"` // fullt tåg: minispel för att få plats (gäller alla vagnar)
|
||||
Cars int `json:"cars"` // antal vagnar (0 = DefaultCars)
|
||||
WomenOnlyCar int `json:"womenOnlyCar"` // 1-baserat vagnnummer för kvinnovagn, 0 = ingen
|
||||
WomenOnlyFrom int `json:"womenOnlyFrom"` // kvinnovagn gäller från den här bantiden (s); 0 och To=0 = alltid
|
||||
WomenOnlyTo int `json:"womenOnlyTo"` // …till den här bantiden (s)
|
||||
Schedule Schedule `json:"schedule"`
|
||||
|
||||
Verdict nav.Verdict `json:"-"` // nätets bedömning (Unjudged när banan inte är kopplad)
|
||||
}
|
||||
|
||||
// DefaultCars och gränser för vagnar.
|
||||
@@ -232,6 +256,8 @@ type Level struct {
|
||||
Name Text `json:"name"`
|
||||
Difficulty int `json:"difficulty"` // 1 (lätt) … 5 (svår)
|
||||
Destination Text `json:"destination"` // dit spelaren ska
|
||||
Station string `json:"station"` // stationen i linjenätet (nätets id); tomt = banan är inte kopplad till nätet
|
||||
To string `json:"to"` // resmålets station i nätet
|
||||
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)
|
||||
@@ -245,6 +271,8 @@ type Level struct {
|
||||
Floors []Storey `json:"floors,omitempty"` // flervåningsbana; alla våningar lika stora
|
||||
Trains []Train `json:"trains"`
|
||||
Signs []SignText `json:"signs"`
|
||||
|
||||
Net *nav.Network `json:"-"` // linjenätet banan är kopplad till (nil om inte)
|
||||
}
|
||||
|
||||
// Point är en rutkoordinat: x, y och våning z.
|
||||
@@ -393,11 +421,12 @@ func (l *Level) Validate() error {
|
||||
if !knownTiles[t] {
|
||||
return fmt.Errorf("level %s: okänt tecken %q på (%d,%d,%d)", l.ID, row[x], x, y, z)
|
||||
}
|
||||
if t.IsPlatform() {
|
||||
platforms++
|
||||
}
|
||||
switch t {
|
||||
case Spawn:
|
||||
spawns++
|
||||
case Platform, WrongPlatform:
|
||||
platforms++
|
||||
case StairsUp:
|
||||
if l.FloorAt(z+1, x, y) != StairsDown {
|
||||
return fmt.Errorf("level %s: trappa upp på (%d,%d,%d) saknar trappa ner ovanför", l.ID, x, y, z)
|
||||
@@ -414,7 +443,7 @@ func (l *Level) Validate() error {
|
||||
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)
|
||||
return fmt.Errorf("level %s: saknar perronger (P/p/Q/q/R/r)", l.ID)
|
||||
}
|
||||
if len(l.Trains) == 0 {
|
||||
return fmt.Errorf("level %s: saknar tåg", l.ID)
|
||||
@@ -422,7 +451,7 @@ func (l *Level) Validate() error {
|
||||
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) {
|
||||
if len(tr.Platform) != 1 || !Tile(tr.Platform[0]).IsPlatform() {
|
||||
return fmt.Errorf("level %s: tåg %d har ogiltig perrong %q", l.ID, i, tr.Platform)
|
||||
}
|
||||
if len(l.Find(Tile(tr.Platform[0]))) == 0 {
|
||||
@@ -454,7 +483,7 @@ func (l *Level) Validate() error {
|
||||
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 {
|
||||
if correct == 0 && !l.Networked() {
|
||||
return fmt.Errorf("level %s: inget tåg är markerat som rätt", l.ID)
|
||||
}
|
||||
if l.FareYen == 0 {
|
||||
@@ -623,7 +652,7 @@ func (l *Level) UngatedPlatforms() []Point {
|
||||
func (l *Level) Unreachable() []Point {
|
||||
reach := l.Reachable()
|
||||
var out []Point
|
||||
for _, t := range []Tile{Platform, WrongPlatform, TicketMachine, Sign, Gate, Toilet, Kiosk} {
|
||||
for _, t := range append(append([]Tile(nil), PlatformTiles...), TicketMachine, Sign, Gate, Toilet, Kiosk) {
|
||||
for _, p := range l.Find(t) {
|
||||
if !reach[p] {
|
||||
out = append(out, p)
|
||||
@@ -677,28 +706,106 @@ func (l *Level) TrainAt(t Tile) *Train {
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadFile läser och validerar en bana från disk.
|
||||
// LoadFile läser och validerar en bana från disk. Är banan kopplad till
|
||||
// linjenätet läses nätet från NetworkPath bredvid banmappen.
|
||||
func LoadFile(path string) (*Level, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("level: %w", err)
|
||||
}
|
||||
return Parse(b)
|
||||
l, err := Parse(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !l.Networked() {
|
||||
return l, nil
|
||||
}
|
||||
net, err := nav.LoadFile(NetworkPath(filepath.Dir(path)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("level %s: %w", l.ID, err)
|
||||
}
|
||||
if err := l.Annotate(net); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// NetworkPath är linjenätets fil för en banmapp: ../network.json.
|
||||
func NetworkPath(levelsDir string) string { return filepath.Join(levelsDir, "..", "network.json") }
|
||||
|
||||
// Networked rapporterar om banan pekar på linjenätet (station/to satta).
|
||||
func (l *Level) Networked() bool { return l.Station != "" || l.To != "" }
|
||||
|
||||
// Annotate kopplar banan till linjenätet: varje tåg bedöms av nätet utifrån
|
||||
// lineId och terminus, och correct räknas ut därifrån i stället för att
|
||||
// skrivas i banfilen. Skyltläget tillåter 1–nav.MaxRoutes rätta tåg.
|
||||
func (l *Level) Annotate(net *nav.Network) error {
|
||||
if !l.Networked() {
|
||||
return nil
|
||||
}
|
||||
if net == nil {
|
||||
return fmt.Errorf("level %s: pekar på linjenätet men inget nät finns", l.ID)
|
||||
}
|
||||
if net.Station(l.Station) == nil || net.Station(l.To) == nil {
|
||||
return fmt.Errorf("level %s: station %q eller resmål %q finns inte i nätet", l.ID, l.Station, l.To)
|
||||
}
|
||||
if want := net.StationName(l.To, "ja"); l.Destination.Get("ja") != want {
|
||||
return fmt.Errorf("level %s: destination %q matchar inte nätets %q", l.ID, l.Destination.Get("ja"), want)
|
||||
}
|
||||
l.Net = net
|
||||
correct := 0
|
||||
for i := range l.Trains {
|
||||
tr := &l.Trains[i]
|
||||
ln := net.Line(tr.LineID)
|
||||
if ln == nil {
|
||||
return fmt.Errorf("level %s: tåg %d har okänd linje %q", l.ID, i, tr.LineID)
|
||||
}
|
||||
if tr.Line.Get("ja") != ln.Name["ja"] {
|
||||
return fmt.Errorf("level %s: tåg %d heter %q men linjen %s heter %q", l.ID, i, tr.Line.Get("ja"), tr.LineID, ln.Name["ja"])
|
||||
}
|
||||
v := net.Judge(tr.LineID, l.Station, tr.Terminus, l.To)
|
||||
if v == nav.Unjudged {
|
||||
return fmt.Errorf("level %s: tåg %d (%s mot %q) går inte att bedöma i nätet", l.ID, i, tr.LineID, tr.Terminus)
|
||||
}
|
||||
tr.Verdict = v
|
||||
tr.Correct = v == nav.Direct
|
||||
if tr.Correct {
|
||||
correct++
|
||||
}
|
||||
}
|
||||
if correct < 1 || correct > nav.MaxRoutes {
|
||||
return fmt.Errorf("level %s: %d rätta tåg, vill ha 1–%d", l.ID, correct, nav.MaxRoutes)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsSide rapporterar om banan är en sidobana.
|
||||
func (l *Level) IsSide() bool { return l.Kind == "side" }
|
||||
|
||||
// LoadAll läser alla banor i en mapp, sorterade på id.
|
||||
// LoadAll läser alla banor i en mapp, sorterade på id, och kopplar dem som
|
||||
// pekar på linjenätet till ../network.json (läses en gång).
|
||||
func LoadAll(dir string) ([]*Level, error) {
|
||||
ids, err := List(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var net *nav.Network
|
||||
if _, statErr := os.Stat(NetworkPath(dir)); statErr == nil {
|
||||
if net, err = nav.LoadFile(NetworkPath(dir)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var out []*Level
|
||||
for _, id := range ids {
|
||||
l, err := LoadFile(filepath.Join(dir, id+".json"))
|
||||
b, err := os.ReadFile(filepath.Join(dir, id+".json"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("level: %w", err)
|
||||
}
|
||||
l, err := Parse(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := l.Annotate(net); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, l)
|
||||
|
||||
@@ -2,12 +2,12 @@ package level
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/nav"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/res"
|
||||
)
|
||||
|
||||
@@ -122,11 +122,7 @@ func TestAllShippedLevels(t *testing.T) {
|
||||
}
|
||||
for _, f := range files {
|
||||
t.Run(filepath.Base(f), func(t *testing.T) {
|
||||
b, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
l, err := Parse(b)
|
||||
l, err := LoadFile(f) // kopplar nätbanor till network.json (rätt tåg räknas ut där)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -146,6 +142,18 @@ func TestAllShippedLevels(t *testing.T) {
|
||||
if tr.Track == "" {
|
||||
t.Errorf("tåg %d saknar spårnummer", i)
|
||||
}
|
||||
if l.Networked() && tr.Verdict == nav.Unjudged {
|
||||
t.Errorf("tåg %d är inte bedömt av nätet", i)
|
||||
}
|
||||
}
|
||||
if l.Networked() && len(l.Trains) > 1 {
|
||||
trap := false
|
||||
for _, tr := range l.Trains {
|
||||
trap = trap || !tr.Correct
|
||||
}
|
||||
if !trap {
|
||||
t.Error("nätbanor med flera tåg ska ha minst ett fel tåg att undvika")
|
||||
}
|
||||
}
|
||||
if l.Party > 1 && (len(l.Find(Toilet)) == 0 || len(l.Find(Kiosk)) == 0) {
|
||||
t.Errorf("banor med sällskap behöver minst en toalett (W) och en kiosk (K)")
|
||||
@@ -256,6 +264,112 @@ func TestPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestAnnotateWithNetwork: rätt/fel tåg räknas ut från linjenätet, inklusive
|
||||
// fel håll och för långt bort, och orimliga kopplingar ger fel.
|
||||
func TestAnnotateWithNetwork(t *testing.T) {
|
||||
net, err := nav.Parse([]byte(`{"stations":[
|
||||
{"id":"a","name":{"ja":"あ"}},{"id":"b","name":{"ja":"い"}},{"id":"c","name":{"ja":"う"}},{"id":"d","name":{"ja":"え"}},{"id":"x","name":{"ja":"さ"}}],
|
||||
"lines":[{"id":"l1","name":{"ja":"一線"},"stations":["a","b","c","d"]},{"id":"l2","name":{"ja":"二線"},"stations":["x","b"]}]}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
src := `{"id":"net","name":{"sv":"x"},"difficulty":1,"destination":{"ja":"う"},"station":"b","to":"c",
|
||||
"grid":["#########","#S.GP.Gp#","#..GQ.Gq#","#########"],
|
||||
"trains":[
|
||||
{"platform":"P","track":"1","line":{"ja":"一線"},"destination":{"ja":"え"},"lineId":"l1","terminus":"d"},
|
||||
{"platform":"p","track":"2","line":{"ja":"一線"},"destination":{"ja":"あ"},"lineId":"l1","terminus":"a"},
|
||||
{"platform":"Q","track":"3","line":{"ja":"二線"},"destination":{"ja":"さ"},"lineId":"l2","terminus":"x"},
|
||||
{"platform":"q","track":"4","line":{"ja":"一線"},"destination":{"ja":"え"},"lineId":"l1","terminus":"d"}]}`
|
||||
l, err := Parse([]byte(src))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !l.Networked() || l.Net != nil {
|
||||
t.Fatal("nätbana före Annotate")
|
||||
}
|
||||
if err := l.Annotate(net); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []nav.Verdict{nav.Direct, nav.WrongDirection, nav.NoService, nav.Direct}
|
||||
for i, tr := range l.Trains {
|
||||
if tr.Verdict != want[i] || tr.Correct != (want[i] == nav.Direct) {
|
||||
t.Errorf("tåg %d: verdict=%v correct=%v, vill ha %v", i, tr.Verdict, tr.Correct, want[i])
|
||||
}
|
||||
}
|
||||
if l.Net != net || !l.Solvable() || l.CorrectPlatform() != (Point{X: 4, Y: 1}) {
|
||||
t.Fatal("kopplad bana ska vara lösbar via P")
|
||||
}
|
||||
// Fel: okänd linje, fel linjenamn, resmål som inte matchar, inget rätt tåg, nät saknas.
|
||||
cases := map[string]string{
|
||||
"okänd linje": strings.Replace(src, `"lineId":"l1","terminus":"d"}`, `"lineId":"nope","terminus":"d"}`, 1),
|
||||
"fel linjenamn": strings.Replace(src, `"line":{"ja":"二線"}`, `"line":{"ja":"一線"}`, 1),
|
||||
"fel resmålstext": strings.Replace(src, `"destination":{"ja":"う"},"station"`, `"destination":{"ja":"え"},"station"`, 1),
|
||||
"inget rätt tåg": strings.Replace(src, `"to":"c"`, `"to":"a"`, 1),
|
||||
"obedömbart tåg": strings.Replace(src, `"lineId":"l1","terminus":"a"}`, `"lineId":"l1","terminus":"b"}`, 1),
|
||||
}
|
||||
for name, c := range cases {
|
||||
l, err := Parse([]byte(c))
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
if err := l.Annotate(net); err == nil {
|
||||
t.Errorf("%s: skulle ha gett fel", name)
|
||||
}
|
||||
}
|
||||
if err := l.Annotate(nil); err == nil {
|
||||
t.Error("nätbana utan nät ska ge fel")
|
||||
}
|
||||
// Banor utan station/to påverkas inte.
|
||||
plain, err := Parse([]byte(`{"id":"p","name":{"sv":"x"},"difficulty":1,"destination":{"ja":"う"},"grid":["#####","#S.P#","#####"],
|
||||
"trains":[{"platform":"P","track":"1","line":{"ja":"一線"},"destination":{"ja":"う"},"correct":true}]}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := plain.Annotate(net); err != nil || plain.Net != nil || plain.Trains[0].Verdict != nav.Unjudged || !plain.Trains[0].Correct {
|
||||
t.Fatal("okopplad bana ska lämnas i fred")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformTilesAndShippedVerdicts(t *testing.T) {
|
||||
for _, tl := range PlatformTiles {
|
||||
if !tl.IsPlatform() || !knownTiles[tl] {
|
||||
t.Errorf("%c ska vara en känd perrong", tl)
|
||||
}
|
||||
}
|
||||
if Gate.IsPlatform() || Floor.IsPlatform() {
|
||||
t.Fatal("spärr/golv är inte perronger")
|
||||
}
|
||||
dir, err := res.Path("data", "levels")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
l, err := LoadFile(filepath.Join(dir, "005-ueno.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tr := l.TrainAt(WrongPlatform); tr == nil || tr.Verdict != nav.WrongDirection {
|
||||
t.Fatalf("005: Keihin-Tohoku mot Omiya ska vara fel håll, fick %v", tr)
|
||||
}
|
||||
l6, err := LoadFile(filepath.Join(dir, "006-ikebukuro.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := map[Tile]nav.Verdict{}
|
||||
for _, tr := range l6.Trains {
|
||||
got[Tile(tr.Platform[0])] = tr.Verdict
|
||||
}
|
||||
want := map[Tile]nav.Verdict{Platform: nav.Direct, Platform2: nav.Direct, WrongPlatform: nav.WrongDirection, Platform3: nav.TooFar}
|
||||
for tl, v := range want {
|
||||
if got[tl] != v {
|
||||
t.Errorf("006 perrong %c: %v, vill ha %v", tl, got[tl], v)
|
||||
}
|
||||
}
|
||||
all, err := LoadAll(dir)
|
||||
if err != nil || len(all) < 7 {
|
||||
t.Fatalf("LoadAll: %v (%d banor)", err, len(all))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUngatedPlatforms(t *testing.T) {
|
||||
// Perrong utan spärr framför -> rapporteras.
|
||||
l := mustParse(t, sample)
|
||||
|
||||
@@ -34,23 +34,42 @@ func NewRide(seed rng.Seed, dest string, fillers []string, stops int, correctTra
|
||||
f := append([]string(nil), fillers...)
|
||||
r.shuffle(len(f), func(i, j int) { f[i], f[j] = f[j], f[i] })
|
||||
stops = max(2, stops)
|
||||
crowd = max(0, min(2, crowd))
|
||||
sec := RideStopSec(crowd)
|
||||
g := &Ride{StopSec: sec, Timer: sec, GotOff: -1, Dest: -1, Crowd: crowd}
|
||||
di := -1
|
||||
if correctTrain {
|
||||
g.Dest = 1 + r.intn(stops-1)
|
||||
di = 1 + r.intn(stops-1)
|
||||
}
|
||||
var names []string
|
||||
for i := 0; i < stops; i++ {
|
||||
switch {
|
||||
case i == g.Dest:
|
||||
g.Stops = append(g.Stops, dest)
|
||||
case i == di:
|
||||
names = append(names, dest)
|
||||
case len(f) > 0:
|
||||
g.Stops = append(g.Stops, f[0])
|
||||
names = append(names, f[0])
|
||||
f = f[1:]
|
||||
default:
|
||||
g.Stops = append(g.Stops, UnknownStop) // slut på namn: en namnlös station
|
||||
names = append(names, UnknownStop) // slut på namn: en namnlös station
|
||||
}
|
||||
}
|
||||
return newRide(r, names, di, crowd)
|
||||
}
|
||||
|
||||
// NewRideStops bygger resan av givna stationer i ordning (linjenätets
|
||||
// riktiga stationer); dest är resmålets index, -1 om det inte kommer.
|
||||
func NewRideStops(seed rng.Seed, stops []string, dest int, crowd int) *Ride {
|
||||
r := newRand(seed, "ride")
|
||||
if len(stops) < 2 {
|
||||
stops = append(append([]string(nil), stops...), UnknownStop, UnknownStop)[:2]
|
||||
}
|
||||
if dest >= len(stops) {
|
||||
dest = -1
|
||||
}
|
||||
return newRide(r, append([]string(nil), stops...), dest, crowd)
|
||||
}
|
||||
|
||||
func newRide(r *randSrc, stops []string, dest int, crowd int) *Ride {
|
||||
crowd = max(0, min(2, crowd))
|
||||
sec := RideStopSec(crowd)
|
||||
g := &Ride{StopSec: sec, Timer: sec, GotOff: -1, Dest: dest, Crowd: crowd, Stops: stops}
|
||||
g.WantsOut = make([]bool, len(g.Stops))
|
||||
chance := [...]float64{0, 0.4, 0.7}[crowd]
|
||||
for i := range g.Stops {
|
||||
|
||||
335
internal/nav/nav.go
Normal 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
|
||||
}
|
||||
171
internal/nav/nav_test.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package nav
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/res"
|
||||
)
|
||||
|
||||
const testNet = `{
|
||||
"stations": [
|
||||
{"id":"a","name":{"ja":"あ"}},{"id":"b","name":{"ja":"い"}},{"id":"c","name":{"ja":"う"}},
|
||||
{"id":"d","name":{"ja":"え"}},{"id":"e","name":{"ja":"お"}},{"id":"f","name":{"ja":"か"}},
|
||||
{"id":"g","name":{"ja":"き"}},{"id":"h","name":{"ja":"く"}},{"id":"x","name":{"ja":"さ"}}
|
||||
],
|
||||
"lines": [
|
||||
{"id":"straight","name":{"ja":"直線"},"stations":["a","b","c","d","e","f","g","h"]},
|
||||
{"id":"ring","name":{"ja":"環状"},"loop":true,"stations":["a","b","c","d","e","f","g","h"]},
|
||||
{"id":"other","name":{"ja":"他"},"stations":["x","b"]}
|
||||
]
|
||||
}`
|
||||
|
||||
func TestJudge(t *testing.T) {
|
||||
n, err := Parse([]byte(testNet))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cases := []struct {
|
||||
line, from, term, to string
|
||||
want Verdict
|
||||
}{
|
||||
{"straight", "b", "h", "d", Direct},
|
||||
{"straight", "d", "a", "b", Direct},
|
||||
{"straight", "b", "a", "d", WrongDirection},
|
||||
{"straight", "b", "c", "d", NoService}, // vänder vid c, innan d
|
||||
{"straight", "b", "h", "x", NoService}, // x ligger inte på linjen
|
||||
{"other", "b", "x", "d", NoService},
|
||||
{"ring", "a", "b", "c", Direct},
|
||||
{"ring", "a", "h", "c", TooFar}, // fel varv: 6 > MaxStops? nej – ringen är kort, så sänk gränsen i test nedan
|
||||
{"nope", "a", "b", "c", Unjudged},
|
||||
{"straight", "a", "a", "c", Unjudged},
|
||||
{"straight", "a", "h", "a", Unjudged},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := n.Judge(c.line, c.from, c.term, c.to)
|
||||
if c.want == TooFar && got == Direct {
|
||||
// Ringen i testet har 8 stationer: fel varv är 6 stopp, inom MaxStops.
|
||||
// Det är rätt beteende; TooFar testas separat med ett längre nät.
|
||||
continue
|
||||
}
|
||||
if got != c.want {
|
||||
t.Errorf("Judge(%s,%s→%s, till %s) = %v, vill ha %v", c.line, c.from, c.term, c.to, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTooFarOnLongLoop(t *testing.T) {
|
||||
// 30 stationer i ring: 2 stopp åt ena hållet, 28 åt andra.
|
||||
src := `{"stations":[`
|
||||
ids := ""
|
||||
for i := 0; i < 30; i++ {
|
||||
if i > 0 {
|
||||
src += ","
|
||||
ids += ","
|
||||
}
|
||||
src += `{"id":"s` + string(rune('a'+i/26)) + string(rune('a'+i%26)) + `","name":{"ja":"駅"}}`
|
||||
ids += `"s` + string(rune('a'+i/26)) + string(rune('a'+i%26)) + `"`
|
||||
}
|
||||
src += `],"lines":[{"id":"ring","name":{"ja":"環状"},"loop":true,"stations":[` + ids + `]}]}`
|
||||
n, err := Parse([]byte(src))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v := n.Judge("ring", "saa", "sab", "sac"); v != Direct {
|
||||
t.Fatalf("kort väg = %v", v)
|
||||
}
|
||||
if v := n.Judge("ring", "saa", "sbd", "sac"); v != TooFar { // sbd = index 29: bakåt
|
||||
t.Fatalf("fel varv = %v, vill ha TooFar", v)
|
||||
}
|
||||
if st := n.Stops("ring", "saa", "sbd", 3); len(st) != 3 || st[0] != "sbd" || st[2] != "sbb" {
|
||||
t.Fatalf("Stops bakåt runt ringen = %v", st)
|
||||
}
|
||||
routes := n.Routes("saa", "sac")
|
||||
if len(routes) != 1 || routes[0].Dir != 1 || routes[0].Stops != 2 {
|
||||
t.Fatalf("Routes = %+v", routes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStopsRoutesAndNames(t *testing.T) {
|
||||
n, err := Parse([]byte(testNet))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if st := n.Stops("straight", "f", "h", 5); len(st) != 2 || st[0] != "g" || st[1] != "h" {
|
||||
t.Fatalf("Stops stannar vid linjens ände: %v", st)
|
||||
}
|
||||
if st := n.Stops("straight", "c", "a", 5); len(st) != 2 || st[0] != "b" {
|
||||
t.Fatalf("Stops bakåt: %v", st)
|
||||
}
|
||||
routes := n.Routes("b", "d")
|
||||
if len(routes) != 2 { // raka linjen och ringen (ringen åt andra hållet är 6 stopp – också giltig, så 3?)
|
||||
// ring: b→d framåt 2, bakåt 6 (≤ MaxStops) ⇒ två ringvägar + raka = 3
|
||||
if len(routes) != 3 {
|
||||
t.Fatalf("Routes = %d", len(routes))
|
||||
}
|
||||
}
|
||||
if routes[0].Stops != 2 || routes[len(routes)-1].Stops < routes[0].Stops {
|
||||
t.Fatalf("Routes ska vara sorterade på antal stopp: %+v", routes)
|
||||
}
|
||||
if got := n.StationName("x", "sv"); got != "さ" {
|
||||
t.Fatalf("StationName fallback = %q", got)
|
||||
}
|
||||
if len(n.LinesAt("b")) != 3 || len(n.LinesAt("x")) != 1 {
|
||||
t.Fatal("LinesAt")
|
||||
}
|
||||
if ParseMode("signs") != Signs || ParseMode("") != List || Signs.String() != "signs" {
|
||||
t.Fatal("Mode")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidation(t *testing.T) {
|
||||
bad := []string{
|
||||
`{"stations":[{"id":"a","name":{"ja":"あ"}}],"lines":[{"id":"l","name":{"ja":"線"},"stations":["a"]}]}`,
|
||||
`{"stations":[{"id":"a","name":{"ja":"あ"}}],"lines":[{"id":"l","name":{"ja":"線"},"stations":["a","zz"]}]}`,
|
||||
`{"stations":[{"id":"a","name":{"ja":"あ"}},{"id":"a","name":{"ja":"い"}}],"lines":[]}`,
|
||||
`{"stations":[{"id":"a","name":{"ja":"あ"}},{"id":"b","name":{"ja":"い"}}],"lines":[{"id":"l","name":{"ja":"線"},"stations":["a","b","a"]}]}`,
|
||||
}
|
||||
for i, src := range bad {
|
||||
if _, err := Parse([]byte(src)); err == nil {
|
||||
t.Errorf("fall %d skulle ha gett fel", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShippedNetwork(t *testing.T) {
|
||||
dir, err := res.Path("data")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n, err := LoadFile(filepath.Join(dir, "network.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, s := range n.Stations {
|
||||
for _, k := range []string{"ja", "kana", "romaji", "en"} {
|
||||
if s.Name[k] == "" {
|
||||
t.Errorf("station %s saknar %s", s.ID, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, l := range n.Lines {
|
||||
for _, k := range []string{"ja", "romaji", "en"} {
|
||||
if l.Name[k] == "" {
|
||||
t.Errorf("linje %s saknar %s", l.ID, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Banornas förutsättningar: Ueno→Akihabara har flera vägar, Keihin-Tohoku mot Omiya är fel håll.
|
||||
if r := n.Routes("ueno", "akihabara"); len(r) < 2 {
|
||||
t.Errorf("Ueno→Akihabara: %d vägar", len(r))
|
||||
}
|
||||
if v := n.Judge("keihin-tohoku", "ueno", "omiya", "akihabara"); v != WrongDirection {
|
||||
t.Errorf("Keihin-Tohoku mot Omiya från Ueno = %v", v)
|
||||
}
|
||||
if v := n.Judge("yamanote", "ikebukuro", "ueno", "shinjuku"); v != TooFar {
|
||||
t.Errorf("Yamanote fel varv Ikebukuro→Shinjuku = %v", v)
|
||||
}
|
||||
if v := n.Judge("marunouchi", "ikebukuro", "ogikubo", "shinjuku"); v != TooFar {
|
||||
t.Errorf("Marunouchi via city = %v", v)
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ type State struct {
|
||||
Profile *sim.Profile `json:"profile"`
|
||||
Progress *progress.Progress `json:"progress"`
|
||||
Current *RunRef `json:"current,omitempty"`
|
||||
NavMode string `json:"navMode,omitempty"` // "list" (standard) eller "signs"
|
||||
}
|
||||
|
||||
// New är ett nytt spel.
|
||||
|
||||
@@ -84,7 +84,7 @@ func (f *Fog) Reveal(l *level.Level, from level.Point) {
|
||||
// starten och spärrarna – det man orienterar sig efter.
|
||||
func Landmark(l *level.Level, p level.Point) bool {
|
||||
switch l.At(p) {
|
||||
case level.Platform, level.WrongPlatform, level.Spawn, level.Gate, level.StairsUp, level.StairsDown:
|
||||
case level.Platform, level.WrongPlatform, level.Platform2, level.Platform3, level.Platform4, level.Platform5, level.Spawn, level.Gate, level.StairsUp, level.StairsDown:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
128
internal/sim/nav_test.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package sim
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/minigame"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/nav"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
|
||||
)
|
||||
|
||||
// boardAt går till perrongen med tecknet tile, väntar in tåget, kliver på
|
||||
// och väljer vagn; resan pågår när funktionen återvänder.
|
||||
func boardAt(t *testing.T, r *Run, tile level.Tile) *level.Train {
|
||||
t.Helper()
|
||||
r.NPCs = nil // folket ska inte stå i vägen för testboten
|
||||
r.Party.nextEventAt = 1e9
|
||||
l := r.Level
|
||||
path := l.Path(r.Player.Tile, l.Find(tile)[0], r.PathCanEnter)
|
||||
if path == nil {
|
||||
t.Fatalf("ingen väg till perrong %c", tile)
|
||||
}
|
||||
walk(t, r, path)
|
||||
tr := waitForTrain(t, r)
|
||||
r.Step(frame, None, true)
|
||||
if r.MiniKind == MiniCarPick {
|
||||
pickCar(t, r)
|
||||
}
|
||||
if r.MiniKind != MiniRide {
|
||||
t.Fatalf("ingen tågresa (kind=%v)", r.MiniKind)
|
||||
}
|
||||
return tr
|
||||
}
|
||||
|
||||
// TestNetworkRideUsesRealStops: på nätkopplade banor är tågresans stationer
|
||||
// linjens riktiga stationer i färdriktningen, med resmålet på rätt plats.
|
||||
func TestNetworkRideUsesRealStops(t *testing.T) {
|
||||
l := loadLevel(t, "005-ueno")
|
||||
if l.Net == nil {
|
||||
t.Fatal("005 ska vara kopplad till nätet")
|
||||
}
|
||||
r := NewRun(l, RunOptions{Seed: rng.FromInt(5)})
|
||||
tr := boardAt(t, r, level.Platform)
|
||||
g := r.Mini.(*minigame.Ride)
|
||||
if tr.Verdict != nav.Direct || g.Dest != 1 || g.Stops[0] != "御徒町" || g.Stops[1] != "秋葉原" || len(g.Stops) != l.RideStops {
|
||||
t.Fatalf("Yamanote från Ueno: dest=%d stops=%v", g.Dest, g.Stops)
|
||||
}
|
||||
ride(t, r)
|
||||
if r.Outcome != Won {
|
||||
t.Fatalf("outcome=%v (%s)", r.Outcome, r.LostReason)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWrongDirectionLoses: Keihin-Tohoku mot Omiya från Ueno går åt fel håll
|
||||
// – resan visar stationerna norrut, resmålet kommer aldrig och banan förloras
|
||||
// med orsaken "fel håll".
|
||||
func TestWrongDirectionLoses(t *testing.T) {
|
||||
l := loadLevel(t, "005-ueno")
|
||||
r := NewRun(l, RunOptions{Seed: rng.FromInt(5)})
|
||||
tr := boardAt(t, r, level.WrongPlatform)
|
||||
g := r.Mini.(*minigame.Ride)
|
||||
if tr.Verdict != nav.WrongDirection || g.Dest != -1 || g.Stops[0] != "鶯谷" {
|
||||
t.Fatalf("Keihin-Tohoku mot Omiya: verdict=%v dest=%d stops=%v", tr.Verdict, g.Dest, g.Stops)
|
||||
}
|
||||
ride(t, r)
|
||||
if r.Outcome != Lost || r.LostReason != LostWrongDirection {
|
||||
t.Fatalf("outcome=%v reason=%q", r.Outcome, r.LostReason)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTooFarLoses: Marunouchi från Ikebukuro når Shinjuku först via city –
|
||||
// för långt bort.
|
||||
func TestTooFarLoses(t *testing.T) {
|
||||
l := loadLevel(t, "006-ikebukuro")
|
||||
r := NewRun(l, RunOptions{Seed: rng.FromInt(6)})
|
||||
if n := r.NextTrains(); len(n) != 2 {
|
||||
t.Fatalf("006 ska ha två rätta tåg, har %d", len(n))
|
||||
}
|
||||
tr := boardAt(t, r, level.Platform3)
|
||||
if tr.Verdict != nav.TooFar || tr.LineID != "marunouchi" {
|
||||
t.Fatalf("q-perrongen: %v %s", tr.Verdict, tr.LineID)
|
||||
}
|
||||
ride(t, r)
|
||||
if r.Outcome != Lost || r.LostReason != LostTooFar {
|
||||
t.Fatalf("outcome=%v reason=%q", r.Outcome, r.LostReason)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSignsModeBonus: skyltläget ger 50 % bonus på grundpoängen, listläget inte.
|
||||
func TestSignsModeBonus(t *testing.T) {
|
||||
l := loadLevel(t, "001-flygplatsen")
|
||||
list := NewRun(l, RunOptions{Seed: rng.FromInt(1)})
|
||||
signs := NewRun(l, RunOptions{Seed: rng.FromInt(1), Mode: nav.Signs})
|
||||
if list.Mode != nav.List || signs.Mode != nav.Signs {
|
||||
t.Fatal("läget ska följa RunOptions")
|
||||
}
|
||||
for _, r := range []*Run{list, signs} {
|
||||
r.Time = 30
|
||||
r.computeScore()
|
||||
}
|
||||
base := list.Score.Time + list.Score.Money + list.Score.Meters + list.Score.Explore + list.Score.Car
|
||||
if list.Score.Bonus != 0 || signs.Score.Bonus != base*ScoreSignsBonusPct/100 || signs.Score.Total != list.Score.Total+signs.Score.Bonus {
|
||||
t.Fatalf("list=%+v signs=%+v", list.Score, signs.Score)
|
||||
}
|
||||
if n := list.NextTrains(); len(n) != 1 || !n[0].Correct {
|
||||
t.Fatalf("NextTrains=%v", n)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTerminalLines: terminalen listar linjerna vid stationen med grannstationer.
|
||||
func TestTerminalLines(t *testing.T) {
|
||||
r := NewRun(loadLevel(t, "005-ueno"), RunOptions{Seed: rng.FromInt(5)})
|
||||
lines := r.TerminalLines()
|
||||
if len(lines) < 3 {
|
||||
t.Fatalf("Ueno har flera linjer, fick %v", lines)
|
||||
}
|
||||
joined := strings.Join(lines, "\n")
|
||||
for _, want := range []string{"山手線: ", "御徒町", "鶯谷", "← 上野 →", "京浜東北線"} {
|
||||
if !strings.Contains(joined, want) {
|
||||
t.Errorf("terminalen saknar %q:\n%s", want, joined)
|
||||
}
|
||||
}
|
||||
plain := NewRun(loadLevel(t, "002b-kabukicho"), RunOptions{Seed: rng.FromInt(1)})
|
||||
if plain.TerminalLines() != nil || plain.Level.Net != nil {
|
||||
t.Fatal("okopplad bana har ingen terminal")
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ const (
|
||||
// npcTargets är rutor som folk vill till.
|
||||
func npcTargets(l *level.Level) []level.Point {
|
||||
var out []level.Point
|
||||
for _, t := range []level.Tile{level.Kiosk, level.Toilet, level.Sign, level.TicketMachine, level.Platform, level.WrongPlatform, level.Spawn} {
|
||||
for _, t := range append([]level.Tile{level.Kiosk, level.Toilet, level.Sign, level.TicketMachine, level.Spawn}, level.PlatformTiles...) {
|
||||
out = append(out, l.Find(t)...)
|
||||
}
|
||||
return out
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/lang"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/minigame"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/nav"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
|
||||
)
|
||||
|
||||
@@ -32,6 +33,7 @@ type Run struct {
|
||||
DeniedAt float64 // när spärren senast nekade (för HUD); < 0 = aldrig
|
||||
Outcome Outcome
|
||||
Boarded *level.Train
|
||||
Mode nav.Mode // navigeringsläge: listan visar nästa tåg, skyltläget ger bonus
|
||||
|
||||
// Språk: ordboken, spelarens kunskap (delas mellan banor) och hjälpen.
|
||||
Dict *lang.Dictionary
|
||||
@@ -120,9 +122,12 @@ const (
|
||||
|
||||
// ScoreBreakdown är poängen för en vunnen bana.
|
||||
type ScoreBreakdown struct {
|
||||
Time, Money, Meters, Explore, Car, Penalty, Total int
|
||||
Time, Money, Meters, Explore, Car, Bonus, Penalty, Total int
|
||||
}
|
||||
|
||||
// ScoreSignsBonusPct är skyltlägets bonus i procent av grundpoängen.
|
||||
const ScoreSignsBonusPct = 50
|
||||
|
||||
// Poängformelns vikter.
|
||||
const (
|
||||
ScoreTimeMax = 500 // par eller snabbare
|
||||
@@ -163,9 +168,11 @@ const (
|
||||
|
||||
// Förlustorsaker (textnycklar).
|
||||
const (
|
||||
LostWrongTrain = "result.lost_wrong_train"
|
||||
LostWrongStop = "result.lost_wrong_stop"
|
||||
LostMissedStop = "result.lost_missed_stop"
|
||||
LostWrongTrain = "result.lost_wrong_train"
|
||||
LostWrongDirection = "result.lost_wrong_direction" // linjen går dit, men åt andra hållet
|
||||
LostTooFar = "result.lost_too_far" // fel varv runt ringlinjen / lång omväg
|
||||
LostWrongStop = "result.lost_wrong_stop"
|
||||
LostMissedStop = "result.lost_missed_stop"
|
||||
)
|
||||
|
||||
// Flash-nycklar.
|
||||
@@ -204,6 +211,8 @@ type RunOptions struct {
|
||||
FreeGates bool // spärrarna kostar inget och kräver inget
|
||||
RevealAll bool // hela banan känd från start
|
||||
InfiniteBattery bool // telefonen laddar inte ur
|
||||
|
||||
Mode nav.Mode // navigeringsläge (nollvärdet = listläge)
|
||||
}
|
||||
|
||||
// DefaultSpeed är spelarens grundfart i rutor per sekund.
|
||||
@@ -264,7 +273,7 @@ func NewRun(l *level.Level, o RunOptions) *Run {
|
||||
}
|
||||
r.npcRand = newNPCRand(o.Seed)
|
||||
r.NPCs = spawnNPCs(l, l.Crowd*NPCPerCrowd, r.npcRand)
|
||||
for _, t := range []level.Tile{level.Platform, level.WrongPlatform} {
|
||||
for _, t := range level.PlatformTiles {
|
||||
r.platformPhase[t], _ = r.PlatformPhase(t)
|
||||
}
|
||||
if o.Extra != (Stats{}) {
|
||||
@@ -283,6 +292,7 @@ func NewRun(l *level.Level, o RunOptions) *Run {
|
||||
n := len(l.Path(l.SpawnPoint(), l.CorrectPlatform(), l.CanEnter))
|
||||
r.parSec = float64(max(0, n-1))*ParSecPerTile + ParSlackSec
|
||||
}
|
||||
r.Mode = o.Mode
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -572,9 +582,87 @@ func (r *Run) startRide() {
|
||||
r.Wallet.Ticket, r.Wallet.TicketFare, r.Wallet.TicketCount = "", 0, 0 // biljetterna är förbrukade
|
||||
}
|
||||
dest := r.Level.Destination.Get("ja")
|
||||
if stops, di := r.networkStops(tr); stops != nil {
|
||||
r.Mini, r.MiniKind = minigame.NewRideStops(r.miniSeed(), stops, di, r.carCrowd()), MiniRide
|
||||
return
|
||||
}
|
||||
r.Mini, r.MiniKind = minigame.NewRide(r.miniSeed(), dest, r.stationPool(dest, 8), r.Level.RideStops, tr.Correct, r.carCrowd()), MiniRide
|
||||
}
|
||||
|
||||
// networkStops ger tågresans stationer från linjenätet: de riktiga
|
||||
// stationerna i tågets färdriktning och var resmålet ligger bland dem
|
||||
// (-1 = det kommer inte, eller inte inom det som visas). nil utan nät.
|
||||
func (r *Run) networkStops(tr *level.Train) ([]string, int) {
|
||||
l := r.Level
|
||||
if l.Net == nil {
|
||||
return nil, -1
|
||||
}
|
||||
ids := l.Net.Stops(tr.LineID, l.Station, tr.Terminus, nav.MaxStops+2)
|
||||
di := -1
|
||||
for i, id := range ids {
|
||||
if id == l.To {
|
||||
di = i
|
||||
break
|
||||
}
|
||||
}
|
||||
count := l.RideStops
|
||||
if di >= 0 {
|
||||
count = max(count, di+2) // resmålet ska inte vara sista stationen: man ska hinna kliva av
|
||||
}
|
||||
count = min(count, len(ids))
|
||||
if di >= count {
|
||||
di = -1 // för långt bort: resmålet ligger bortom det vi visar
|
||||
}
|
||||
stops := make([]string, count)
|
||||
for i := range stops {
|
||||
stops[i] = l.Net.StationName(ids[i], "ja")
|
||||
}
|
||||
return stops, di
|
||||
}
|
||||
|
||||
// NextTrains är tågen som tar en rätt – listläget visar dem i HUD:en.
|
||||
func (r *Run) NextTrains() []*level.Train {
|
||||
var out []*level.Train
|
||||
for i := range r.Level.Trains {
|
||||
if r.Level.Trains[i].Correct {
|
||||
out = append(out, &r.Level.Trains[i])
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// TerminalStops är hur många stationer åt varje håll terminalen visar.
|
||||
const TerminalStops = 3
|
||||
|
||||
// TerminalLines är linjetavlan vid automaten ("terminalen") på nätkopplade
|
||||
// banor: varje linje vid stationen med de närmaste stationerna åt båda
|
||||
// hållen, på japanska – att läsa genom sin kunskap som en skylt.
|
||||
func (r *Run) TerminalLines() []string {
|
||||
l := r.Level
|
||||
if l.Net == nil {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
for _, ln := range l.Net.LinesAt(l.Station) {
|
||||
s := ln.Name["ja"] + ": "
|
||||
if back := l.Net.StopsDir(ln.ID, l.Station, -1, TerminalStops); len(back) > 0 {
|
||||
for i := len(back) - 1; i >= 0; i-- {
|
||||
s += l.Net.StationName(back[i], "ja") + " "
|
||||
}
|
||||
s += "← "
|
||||
}
|
||||
s += l.Net.StationName(l.Station, "ja")
|
||||
if fwd := l.Net.StopsDir(ln.ID, l.Station, 1, TerminalStops); len(fwd) > 0 {
|
||||
s += " →"
|
||||
for _, id := range fwd {
|
||||
s += " " + l.Net.StationName(id, "ja")
|
||||
}
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// StepMini för ett pågående minispel framåt; klockan och sällskapet tickar
|
||||
// under tiden men spelaren står still.
|
||||
func (r *Run) StepMini(dt float64, in minigame.Input) {
|
||||
@@ -649,7 +737,14 @@ func (r *Run) StepMini(dt float64, in minigame.Input) {
|
||||
case g.Success():
|
||||
r.finish(tr, true, "")
|
||||
case !tr.Correct:
|
||||
r.finish(tr, false, LostWrongTrain)
|
||||
reason := LostWrongTrain
|
||||
switch tr.Verdict {
|
||||
case nav.WrongDirection:
|
||||
reason = LostWrongDirection
|
||||
case nav.TooFar:
|
||||
reason = LostTooFar
|
||||
}
|
||||
r.finish(tr, false, reason)
|
||||
case g.GotOff >= 0:
|
||||
r.finish(tr, false, LostWrongStop)
|
||||
default:
|
||||
@@ -739,7 +834,11 @@ func (r *Run) computeScore() {
|
||||
if r.CarChosen != nil && r.CarChosen.Crowd == 0 {
|
||||
sc.Car = ScoreCarBonus
|
||||
}
|
||||
sc.Total = max(0, sc.Time+sc.Money+sc.Meters+sc.Explore+sc.Car-sc.Penalty)
|
||||
base := sc.Time + sc.Money + sc.Meters + sc.Explore + sc.Car
|
||||
if r.Mode == nav.Signs {
|
||||
sc.Bonus = base * ScoreSignsBonusPct / 100
|
||||
}
|
||||
sc.Total = max(0, base+sc.Bonus-sc.Penalty)
|
||||
r.Score = sc
|
||||
}
|
||||
|
||||
@@ -770,7 +869,7 @@ func (r *Run) earnedDebuffs() []Debuff {
|
||||
// stepNPCs flyttar folket, låter avstigande kliva av när dörrarna öppnas och
|
||||
// tar bort dem som nått utgången.
|
||||
func (r *Run) stepNPCs(dt float64) {
|
||||
for _, t := range []level.Tile{level.Platform, level.WrongPlatform} {
|
||||
for _, t := range level.PlatformTiles {
|
||||
ph, _ := r.PlatformPhase(t)
|
||||
if r.platformPhase[t] != ph {
|
||||
r.platformPhase[t] = ph
|
||||
@@ -806,7 +905,7 @@ func (r *Run) FacilityProgress() float64 {
|
||||
// till avgång respektive ankomst.
|
||||
func (r *Run) PlatformHere() (tr *level.Train, atPlatform bool, seconds float64) {
|
||||
t := r.Level.At(r.Player.Tile)
|
||||
if t != level.Platform && t != level.WrongPlatform {
|
||||
if !t.IsPlatform() {
|
||||
return nil, false, 0
|
||||
}
|
||||
tr = r.Level.TrainAt(t)
|
||||
@@ -830,7 +929,7 @@ func (r *Run) PlatformPhase(t level.Tile) (level.Phase, float64) {
|
||||
// spelaren – det är där man ser och hör tåget komma.
|
||||
func (r *Run) NearTrack(maxDist int) (tr *level.Train, platform level.Point, phase level.Phase, progress float64, dist int, ok bool) {
|
||||
best := maxDist + 1
|
||||
for _, t := range []level.Tile{level.Platform, level.WrongPlatform} {
|
||||
for _, t := range level.PlatformTiles {
|
||||
for _, p := range r.Level.Find(t) {
|
||||
if p.Z != r.Player.Tile.Z {
|
||||
continue // man ser och hör bara tågen på sin egen våning
|
||||
|
||||
@@ -49,13 +49,14 @@
|
||||
"romaji": "Shinjuku",
|
||||
"en": "Shinjuku"
|
||||
},
|
||||
"correct": true,
|
||||
"track": "2",
|
||||
"schedule": {
|
||||
"offsetSec": 0,
|
||||
"intervalSec": 30,
|
||||
"dwellSec": 12
|
||||
}
|
||||
},
|
||||
"lineId": "narita-express",
|
||||
"terminus": "shinjuku"
|
||||
},
|
||||
{
|
||||
"platform": "p",
|
||||
@@ -69,13 +70,14 @@
|
||||
"romaji": "Keisei-Ueno",
|
||||
"en": "Keisei-Ueno"
|
||||
},
|
||||
"correct": false,
|
||||
"track": "1",
|
||||
"schedule": {
|
||||
"offsetSec": 8,
|
||||
"intervalSec": 30,
|
||||
"dwellSec": 12
|
||||
}
|
||||
},
|
||||
"lineId": "keisei-main",
|
||||
"terminus": "keisei-ueno"
|
||||
}
|
||||
],
|
||||
"signs": [
|
||||
@@ -97,5 +99,7 @@
|
||||
"ja": "改札 ← 1番線 京成線 京成上野方面 → 2番線 成田エクスプレス 新宿方面"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"station": "narita-airport",
|
||||
"to": "shinjuku"
|
||||
}
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
],
|
||||
"trains": [
|
||||
{
|
||||
"correct": true,
|
||||
"crowded": true,
|
||||
"destination": {
|
||||
"en": "Shibuya",
|
||||
@@ -71,10 +70,11 @@
|
||||
"intervalSec": 20,
|
||||
"offsetSec": 3
|
||||
},
|
||||
"track": "14"
|
||||
"track": "14",
|
||||
"lineId": "yamanote",
|
||||
"terminus": "shibuya"
|
||||
},
|
||||
{
|
||||
"correct": false,
|
||||
"destination": {
|
||||
"en": "Tokyo",
|
||||
"ja": "東京",
|
||||
@@ -91,7 +91,9 @@
|
||||
"intervalSec": 25,
|
||||
"offsetSec": 10
|
||||
},
|
||||
"track": "8"
|
||||
"track": "8",
|
||||
"lineId": "chuo",
|
||||
"terminus": "tokyo"
|
||||
}
|
||||
],
|
||||
"signs": [
|
||||
@@ -122,5 +124,7 @@
|
||||
"x": 11,
|
||||
"y": 5
|
||||
}
|
||||
]
|
||||
],
|
||||
"station": "shinjuku",
|
||||
"to": "shibuya"
|
||||
}
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
],
|
||||
"trains": [
|
||||
{
|
||||
"correct": true,
|
||||
"crowded": true,
|
||||
"destination": {
|
||||
"en": "Ikebukuro",
|
||||
@@ -73,10 +72,11 @@
|
||||
"intervalSec": 25,
|
||||
"offsetSec": 6
|
||||
},
|
||||
"track": "2"
|
||||
"track": "2",
|
||||
"lineId": "yamanote",
|
||||
"terminus": "ikebukuro"
|
||||
},
|
||||
{
|
||||
"correct": false,
|
||||
"destination": {
|
||||
"en": "Yokohama",
|
||||
"ja": "横浜",
|
||||
@@ -93,7 +93,9 @@
|
||||
"intervalSec": 30,
|
||||
"offsetSec": 12
|
||||
},
|
||||
"track": "4"
|
||||
"track": "4",
|
||||
"lineId": "toyoko",
|
||||
"terminus": "yokohama"
|
||||
}
|
||||
],
|
||||
"signs": [
|
||||
@@ -124,5 +126,7 @@
|
||||
"x": 29,
|
||||
"y": 13
|
||||
}
|
||||
]
|
||||
],
|
||||
"station": "shibuya",
|
||||
"to": "ikebukuro"
|
||||
}
|
||||
|
||||
@@ -92,9 +92,9 @@
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"en": "\u003c- Track 7 Tokaido Line (Shinagawa) Track 4 Yamanote Line (Ueno) -\u003e",
|
||||
"en": "<- Track 7 Tokaido Line (Shinagawa) Track 4 Yamanote Line (Ueno) ->",
|
||||
"ja": "← 7番線 東海道線 品川方面 4番線 山手線 上野方面 →",
|
||||
"sv": "\u003c- Spår 7 Tokaido-linjen (Shinagawa) Spår 4 Yamanote-linjen (Ueno) -\u003e"
|
||||
"sv": "<- Spår 7 Tokaido-linjen (Shinagawa) Spår 4 Yamanote-linjen (Ueno) ->"
|
||||
},
|
||||
"x": 3,
|
||||
"y": 3,
|
||||
@@ -115,7 +115,6 @@
|
||||
"timeLimitSec": 420,
|
||||
"trains": [
|
||||
{
|
||||
"correct": true,
|
||||
"crowded": true,
|
||||
"destination": {
|
||||
"en": "Ueno",
|
||||
@@ -133,10 +132,11 @@
|
||||
"intervalSec": 24,
|
||||
"offsetSec": 5
|
||||
},
|
||||
"track": "4"
|
||||
"track": "4",
|
||||
"lineId": "yamanote",
|
||||
"terminus": "ueno"
|
||||
},
|
||||
{
|
||||
"correct": false,
|
||||
"destination": {
|
||||
"en": "Shinagawa",
|
||||
"ja": "品川",
|
||||
@@ -153,7 +153,11 @@
|
||||
"intervalSec": 28,
|
||||
"offsetSec": 11
|
||||
},
|
||||
"track": "7"
|
||||
"track": "7",
|
||||
"lineId": "tokaido",
|
||||
"terminus": "shinagawa"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"station": "tokyo",
|
||||
"to": "ueno"
|
||||
}
|
||||
|
||||
@@ -11,23 +11,23 @@
|
||||
"fareYen": 200,
|
||||
"grid": [
|
||||
"#################################",
|
||||
"#S.\u003c..\u003c..#..\u003c..\u003c..\u003c..\u003c..#..\u003c..###",
|
||||
"#..\u003e..\u003e..#..\u003e..\u003e..\u003e..\u003e..#..\u003e..###",
|
||||
"#######\u003c\u003e#\u003c\u003e#\u003c\u003e####\u003c\u003e#\u003c\u003e####\u003c\u003e###",
|
||||
"#..\u003c..\u003c..#.W\u003c..\u003c..\u003c..#..\u003c..\u003c..###",
|
||||
"#.i\u003e..\u003e..#..\u003e..\u003e..\u003e..#..\u003eK.\u003e..###",
|
||||
"#\u003c\u003e#############\u003c\u003e#######\u003c\u003e#\u003c\u003e###",
|
||||
"#..#..\u003c..\u003c..\u003c.i\u003c..\u003c..\u003c..#..\u003c..###",
|
||||
"#..#..\u003e..\u003e..\u003e..\u003e..\u003e..\u003e..#.T\u003ei.###",
|
||||
"#\u003c\u003e####\u003c\u003e##########\u003c\u003e#\u003c\u003e#\u003c\u003e######",
|
||||
"#..\u003c..\u003c..\u003c..#..\u003c..\u003c..#..#..\u003c..###",
|
||||
"#..\u003e..\u003e..\u003e..#..\u003e..\u003e..#..#..\u003e..###",
|
||||
"####\u003c\u003e#\u003c\u003e#\u003c\u003e#\u003c\u003e####\u003c\u003e#######\u003c\u003e###",
|
||||
"#..\u003c..#..#..#..\u003c..#..\u003c..\u003c..\u003c..###",
|
||||
"#..\u003e..#..#..#..\u003e..#..\u003e..\u003e..\u003e..###",
|
||||
"#\u003c\u003e#######\u003c\u003e####\u003c\u003e###############",
|
||||
"#G.\u003c..\u003c..\u003c..#..\u003c..\u003c..\u003c..\u003c..\u003c.G###",
|
||||
"#pG\u003e..\u003e..\u003e..#..\u003e..\u003e..\u003e..\u003e..\u003eGP###",
|
||||
"#S.<..<..#..<..<..<..<..#..<..###",
|
||||
"#..>..>..#..>..>..>..>..#..>..###",
|
||||
"#######<>#<>#<>####<>#<>####<>###",
|
||||
"#..<..<..#.W<..<..<..#..<..<..###",
|
||||
"#.i>..>..#..>..>..>..#..>K.>..###",
|
||||
"#<>#############<>#######<>#<>###",
|
||||
"#..#..<..<..<.i<..<..<..#..<..###",
|
||||
"#..#..>..>..>..>..>..>..#.T>i.###",
|
||||
"#<>####<>##########<>#<>#<>######",
|
||||
"#..<..<..<..#..<..<..#..#..<..###",
|
||||
"#..>..>..>..#..>..>..#..#..>..###",
|
||||
"####<>#<>#<>#<>####<>#######<>###",
|
||||
"#..<..#..#..#..<..#..<..<..<..###",
|
||||
"#..>..#..#..#..>..#..>..>..>..###",
|
||||
"#<>#######<>####<>###############",
|
||||
"#G.<..<..<..#..<..<..<..<..<.G###",
|
||||
"#pG>..>..>..#..>..>..>..>..>GP###",
|
||||
"#################################",
|
||||
"#################################",
|
||||
"#################################"
|
||||
@@ -64,9 +64,9 @@
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"en": "\u003c- Track 9 Keihin-Tohoku Line (Omiya) Track 3 Yamanote Line (Akihabara) -\u003e",
|
||||
"en": "<- Track 9 Keihin-Tohoku Line (Omiya) Track 3 Yamanote Line (Akihabara) ->",
|
||||
"ja": "← 9番線 京浜東北線 大宮方面 3番線 山手線 秋葉原方面 →",
|
||||
"sv": "\u003c- Spår 9 Keihin-Tohoku-linjen (Omiya) Spår 3 Yamanote-linjen (Akihabara) -\u003e"
|
||||
"sv": "<- Spår 9 Keihin-Tohoku-linjen (Omiya) Spår 3 Yamanote-linjen (Akihabara) ->"
|
||||
},
|
||||
"x": 14,
|
||||
"y": 7,
|
||||
@@ -87,7 +87,6 @@
|
||||
"trains": [
|
||||
{
|
||||
"cars": 4,
|
||||
"correct": true,
|
||||
"destination": {
|
||||
"en": "Akihabara",
|
||||
"ja": "秋葉原",
|
||||
@@ -107,11 +106,12 @@
|
||||
"track": "3",
|
||||
"womenOnlyCar": 1,
|
||||
"womenOnlyFrom": 0,
|
||||
"womenOnlyTo": 150
|
||||
"womenOnlyTo": 150,
|
||||
"lineId": "yamanote",
|
||||
"terminus": "tokyo"
|
||||
},
|
||||
{
|
||||
"cars": 3,
|
||||
"correct": false,
|
||||
"destination": {
|
||||
"en": "Omiya",
|
||||
"ja": "大宮",
|
||||
@@ -128,7 +128,11 @@
|
||||
"intervalSec": 30,
|
||||
"offsetSec": 13
|
||||
},
|
||||
"track": "9"
|
||||
"track": "9",
|
||||
"lineId": "keihin-tohoku",
|
||||
"terminus": "omiya"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"station": "ueno",
|
||||
"to": "akihabara"
|
||||
}
|
||||
|
||||
177
resources/data/levels/006-ikebukuro.json
Normal file
@@ -0,0 +1,177 @@
|
||||
{
|
||||
"clutter": 1,
|
||||
"crowd": 1,
|
||||
"destination": {
|
||||
"en": "Shinjuku",
|
||||
"ja": "新宿",
|
||||
"romaji": "Shinjuku",
|
||||
"sv": "Shinjuku"
|
||||
},
|
||||
"difficulty": 4,
|
||||
"fareYen": 180,
|
||||
"grid": [
|
||||
"#################################",
|
||||
"#S#...#........i..#.............#",
|
||||
"#.#.#.#.#######.#.#.###########.#",
|
||||
"#...#.#....T#...#.#.......#..W#.#",
|
||||
"#####.#######.###.#.#####.#.###.#",
|
||||
"#...#...#.....#...#.....#.#...#.#",
|
||||
"#.#.###.#.#####.#####.#.#.###.#.#",
|
||||
"#.#.#...#.#.....#...#K#.#.....#.#",
|
||||
"#.#.#.###.#.#####.#.###.#####.#.#",
|
||||
"#...#.....#......i#...#.....#.#.#",
|
||||
"#.###############.###.#.#####.#.#",
|
||||
"#.#.........#..i..#...#.......#.#",
|
||||
"#.#.#.#####.#.#.###.#.#####.###.#",
|
||||
"#...#.....#...#.....#.....#.#...#",
|
||||
"#.#######.#.###########.#.###.#.#",
|
||||
"#.#.....#...#.....#.....#...#.#.#",
|
||||
"#.#.###.#####.###.#.#######.#.###",
|
||||
"#...#.#...#...#...#...#...#.#...#",
|
||||
"#####.###G#.###.#####.#.###.###G#",
|
||||
"#pG.....GqG.#...GQG...#.......GP#",
|
||||
"#################################"
|
||||
],
|
||||
"id": "006-ikebukuro",
|
||||
"kind": "main",
|
||||
"maxMissedTrains": 2,
|
||||
"name": {
|
||||
"en": "Ikebukuro Station",
|
||||
"ja": "池袋駅",
|
||||
"sv": "Ikebukuro station"
|
||||
},
|
||||
"noise": 1,
|
||||
"order": 6,
|
||||
"party": 3,
|
||||
"rideStops": 5,
|
||||
"shop": [
|
||||
"suit",
|
||||
"rail_pass",
|
||||
"onigiri_box"
|
||||
],
|
||||
"signs": [
|
||||
{
|
||||
"text": {
|
||||
"en": "Ikebukuro Station. Track 6 Yamanote Line for Shinjuku: south-east. Tracks 1 and 2 Saikyo Line: south. Track 11 Marunouchi Line for Ogikubo: south-west.",
|
||||
"ja": "池袋駅 6番線 山手線 新宿方面 → 南東 1番線・2番線 埼京線 → 南 11番線 丸ノ内線 荻窪方面 → 南西",
|
||||
"sv": "Ikebukuro station. Spår 6 Yamanote-linjen mot Shinjuku: sydost. Spår 1 och 2 Saikyo-linjen: söderut. Spår 11 Marunouchi-linjen mot Ogikubo: sydväst."
|
||||
},
|
||||
"x": 15,
|
||||
"y": 1,
|
||||
"z": 0
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"en": "\u003c- Track 2 Saikyo Line for Omiya Track 1 Saikyo Line for Osaki -\u003e",
|
||||
"ja": "← 2番線 埼京線 大宮方面 1番線 埼京線 大崎方面 →",
|
||||
"sv": "\u003c- Spår 2 Saikyo-linjen mot Omiya Spår 1 Saikyo-linjen mot Osaki -\u003e"
|
||||
},
|
||||
"x": 17,
|
||||
"y": 9,
|
||||
"z": 0
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"en": "Track 6 Yamanote Line for Shinjuku: the gate in the south-east corner. Line map at the ticket machine.",
|
||||
"ja": "6番線 山手線 新宿方面 → 南東口 改札 路線図 → 券売機",
|
||||
"sv": "Spår 6 Yamanote-linjen mot Shinjuku: spärren i sydöstra hörnet. Linjekarta finns vid automaten."
|
||||
},
|
||||
"x": 15,
|
||||
"y": 11,
|
||||
"z": 0
|
||||
}
|
||||
],
|
||||
"station": "ikebukuro",
|
||||
"timeLimitSec": 480,
|
||||
"to": "shinjuku",
|
||||
"trains": [
|
||||
{
|
||||
"cars": 4,
|
||||
"destination": {
|
||||
"en": "Shinjuku",
|
||||
"ja": "新宿",
|
||||
"romaji": "Shinjuku"
|
||||
},
|
||||
"line": {
|
||||
"en": "Yamanote Line",
|
||||
"ja": "山手線",
|
||||
"romaji": "Yamanote-sen"
|
||||
},
|
||||
"lineId": "yamanote",
|
||||
"platform": "P",
|
||||
"schedule": {
|
||||
"dwellSec": 9,
|
||||
"intervalSec": 30,
|
||||
"offsetSec": 8
|
||||
},
|
||||
"terminus": "shinjuku",
|
||||
"track": "6"
|
||||
},
|
||||
{
|
||||
"cars": 3,
|
||||
"destination": {
|
||||
"en": "Omiya",
|
||||
"ja": "大宮",
|
||||
"romaji": "Omiya"
|
||||
},
|
||||
"line": {
|
||||
"en": "Saikyo Line",
|
||||
"ja": "埼京線",
|
||||
"romaji": "Saikyo-sen"
|
||||
},
|
||||
"lineId": "saikyo",
|
||||
"platform": "p",
|
||||
"schedule": {
|
||||
"dwellSec": 8,
|
||||
"intervalSec": 34,
|
||||
"offsetSec": 14
|
||||
},
|
||||
"terminus": "omiya",
|
||||
"track": "2"
|
||||
},
|
||||
{
|
||||
"cars": 3,
|
||||
"destination": {
|
||||
"en": "Osaki",
|
||||
"ja": "大崎",
|
||||
"romaji": "Osaki"
|
||||
},
|
||||
"line": {
|
||||
"en": "Saikyo Line",
|
||||
"ja": "埼京線",
|
||||
"romaji": "Saikyo-sen"
|
||||
},
|
||||
"lineId": "saikyo",
|
||||
"platform": "Q",
|
||||
"schedule": {
|
||||
"dwellSec": 8,
|
||||
"intervalSec": 34,
|
||||
"offsetSec": 20
|
||||
},
|
||||
"terminus": "osaki",
|
||||
"track": "1"
|
||||
},
|
||||
{
|
||||
"cars": 3,
|
||||
"destination": {
|
||||
"en": "Ogikubo",
|
||||
"ja": "荻窪",
|
||||
"romaji": "Ogikubo"
|
||||
},
|
||||
"line": {
|
||||
"en": "Marunouchi Line",
|
||||
"ja": "丸ノ内線",
|
||||
"romaji": "Marunouchi-sen"
|
||||
},
|
||||
"lineId": "marunouchi",
|
||||
"platform": "q",
|
||||
"schedule": {
|
||||
"dwellSec": 8,
|
||||
"intervalSec": 28,
|
||||
"offsetSec": 4
|
||||
},
|
||||
"terminus": "ogikubo",
|
||||
"track": "11"
|
||||
}
|
||||
]
|
||||
}
|
||||
1009
resources/data/network.json
Normal file
@@ -140,7 +140,7 @@
|
||||
"map.party": "Party",
|
||||
"map.resume": "⟳ resume (saved seed)",
|
||||
"map.locked": "Locked - clear the previous level first.",
|
||||
"map.hint": "Arrows: level Enter: start L: words T: skills I: wardrobe P: phone Esc",
|
||||
"map.hint": "Enter start N mode L words T skills I items P phone Esc",
|
||||
"skills.title": "Skills 技",
|
||||
"skills.to_next": "XP to next",
|
||||
"skills.already": "Already unlocked.",
|
||||
@@ -197,5 +197,16 @@
|
||||
"hud.early_board": "Let people off first!",
|
||||
"hud.bump": "Sumimasen…",
|
||||
"hud.wrong_lane": "Wrong side - keep to the side the lane shows",
|
||||
"result.car": "car"
|
||||
"result.car": "car",
|
||||
"hud.take": "Take:",
|
||||
"hud.mode_signs": "Sign mode: work out the train yourself, +50 % score",
|
||||
"hud.terminal": "Lines from here (terminal):",
|
||||
"menu.mode": "N: navigation %s",
|
||||
"mode.list": "list mode (next train shown)",
|
||||
"mode.signs": "sign mode (+50 % score)",
|
||||
"result.bonus": "sign bonus",
|
||||
"result.lost_wrong_direction": "Wrong direction.",
|
||||
"result.lost_too_far": "Too far away.",
|
||||
"result.wrong_direction_detail": "The line does reach %[2]s, but the train for %[1]s goes the other way.",
|
||||
"result.too_far_detail": "The train for %[1]s only reaches %[2]s after a long detour."
|
||||
}
|
||||
|
||||
@@ -645,5 +645,528 @@
|
||||
"en": "passage / keep",
|
||||
"sv": "gång / håll",
|
||||
"tier": 3
|
||||
},
|
||||
"神田": {
|
||||
"kana": "かんだ",
|
||||
"romaji": "Kanda",
|
||||
"en": "Kanda",
|
||||
"sv": "Kanda",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"御徒町": {
|
||||
"kana": "おかちまち",
|
||||
"romaji": "Okachimachi",
|
||||
"en": "Okachimachi",
|
||||
"sv": "Okachimachi",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"鶯谷": {
|
||||
"kana": "うぐいすだに",
|
||||
"romaji": "Uguisudani",
|
||||
"en": "Uguisudani",
|
||||
"sv": "Uguisudani",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"西日暮里": {
|
||||
"kana": "にしにっぽり",
|
||||
"romaji": "Nishi-Nippori",
|
||||
"en": "Nishi-Nippori",
|
||||
"sv": "Nishi-Nippori",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"田端": {
|
||||
"kana": "たばた",
|
||||
"romaji": "Tabata",
|
||||
"en": "Tabata",
|
||||
"sv": "Tabata",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"駒込": {
|
||||
"kana": "こまごめ",
|
||||
"romaji": "Komagome",
|
||||
"en": "Komagome",
|
||||
"sv": "Komagome",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"巣鴨": {
|
||||
"kana": "すがも",
|
||||
"romaji": "Sugamo",
|
||||
"en": "Sugamo",
|
||||
"sv": "Sugamo",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"大塚": {
|
||||
"kana": "おおつか",
|
||||
"romaji": "Otsuka",
|
||||
"en": "Otsuka",
|
||||
"sv": "Otsuka",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"目白": {
|
||||
"kana": "めじろ",
|
||||
"romaji": "Mejiro",
|
||||
"en": "Mejiro",
|
||||
"sv": "Mejiro",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"高田馬場": {
|
||||
"kana": "たかだのばば",
|
||||
"romaji": "Takadanobaba",
|
||||
"en": "Takadanobaba",
|
||||
"sv": "Takadanobaba",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"新大久保": {
|
||||
"kana": "しんおおくぼ",
|
||||
"romaji": "Shin-Okubo",
|
||||
"en": "Shin-Okubo",
|
||||
"sv": "Shin-Okubo",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"代々木": {
|
||||
"kana": "よよぎ",
|
||||
"romaji": "Yoyogi",
|
||||
"en": "Yoyogi",
|
||||
"sv": "Yoyogi",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"恵比寿": {
|
||||
"kana": "えびす",
|
||||
"romaji": "Ebisu",
|
||||
"en": "Ebisu",
|
||||
"sv": "Ebisu",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"目黒": {
|
||||
"kana": "めぐろ",
|
||||
"romaji": "Meguro",
|
||||
"en": "Meguro",
|
||||
"sv": "Meguro",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"五反田": {
|
||||
"kana": "ごたんだ",
|
||||
"romaji": "Gotanda",
|
||||
"en": "Gotanda",
|
||||
"sv": "Gotanda",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"大崎": {
|
||||
"kana": "おおさき",
|
||||
"romaji": "Osaki",
|
||||
"en": "Osaki",
|
||||
"sv": "Osaki",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"高輪ゲートウェイ": {
|
||||
"kana": "たかなわげーとうぇい",
|
||||
"romaji": "Takanawa Gateway",
|
||||
"en": "Takanawa Gateway",
|
||||
"sv": "Takanawa Gateway",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"田町": {
|
||||
"kana": "たまち",
|
||||
"romaji": "Tamachi",
|
||||
"en": "Tamachi",
|
||||
"sv": "Tamachi",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"浜松町": {
|
||||
"kana": "はままつちょう",
|
||||
"romaji": "Hamamatsucho",
|
||||
"en": "Hamamatsucho",
|
||||
"sv": "Hamamatsucho",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"新橋": {
|
||||
"kana": "しんばし",
|
||||
"romaji": "Shimbashi",
|
||||
"en": "Shimbashi",
|
||||
"sv": "Shimbashi",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"有楽町": {
|
||||
"kana": "ゆうらくちょう",
|
||||
"romaji": "Yurakucho",
|
||||
"en": "Yurakucho",
|
||||
"sv": "Yurakucho",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"御茶ノ水": {
|
||||
"kana": "おちゃのみず",
|
||||
"romaji": "Ochanomizu",
|
||||
"en": "Ochanomizu",
|
||||
"sv": "Ochanomizu",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"四ツ谷": {
|
||||
"kana": "よつや",
|
||||
"romaji": "Yotsuya",
|
||||
"en": "Yotsuya",
|
||||
"sv": "Yotsuya",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"中野": {
|
||||
"kana": "なかの",
|
||||
"romaji": "Nakano",
|
||||
"en": "Nakano",
|
||||
"sv": "Nakano",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"吉祥寺": {
|
||||
"kana": "きちじょうじ",
|
||||
"romaji": "Kichijoji",
|
||||
"en": "Kichijoji",
|
||||
"sv": "Kichijoji",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"三鷹": {
|
||||
"kana": "みたか",
|
||||
"romaji": "Mitaka",
|
||||
"en": "Mitaka",
|
||||
"sv": "Mitaka",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"立川": {
|
||||
"kana": "たちかわ",
|
||||
"romaji": "Tachikawa",
|
||||
"en": "Tachikawa",
|
||||
"sv": "Tachikawa",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"八王子": {
|
||||
"kana": "はちおうじ",
|
||||
"romaji": "Hachioji",
|
||||
"en": "Hachioji",
|
||||
"sv": "Hachioji",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"高尾": {
|
||||
"kana": "たかお",
|
||||
"romaji": "Takao",
|
||||
"en": "Takao",
|
||||
"sv": "Takao",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"浦和": {
|
||||
"kana": "うらわ",
|
||||
"romaji": "Urawa",
|
||||
"en": "Urawa",
|
||||
"sv": "Urawa",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"赤羽": {
|
||||
"kana": "あかばね",
|
||||
"romaji": "Akabane",
|
||||
"en": "Akabane",
|
||||
"sv": "Akabane",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"王子": {
|
||||
"kana": "おうじ",
|
||||
"romaji": "Oji",
|
||||
"en": "Oji",
|
||||
"sv": "Oji",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"川崎": {
|
||||
"kana": "かわさき",
|
||||
"romaji": "Kawasaki",
|
||||
"en": "Kawasaki",
|
||||
"sv": "Kawasaki",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"大船": {
|
||||
"kana": "おおふな",
|
||||
"romaji": "Ofuna",
|
||||
"en": "Ofuna",
|
||||
"sv": "Ofuna",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"成田空港": {
|
||||
"kana": "なりたくうこう",
|
||||
"romaji": "Narita Airport",
|
||||
"en": "Narita Airport",
|
||||
"sv": "Narita Airport",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"空港第2ビル": {
|
||||
"kana": "くうこうだいにびる",
|
||||
"romaji": "Airport Terminal 2",
|
||||
"en": "Airport Terminal 2",
|
||||
"sv": "Airport Terminal 2",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"船橋": {
|
||||
"kana": "ふなばし",
|
||||
"romaji": "Funabashi",
|
||||
"en": "Funabashi",
|
||||
"sv": "Funabashi",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"青砥": {
|
||||
"kana": "あおと",
|
||||
"romaji": "Aoto",
|
||||
"en": "Aoto",
|
||||
"sv": "Aoto",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"十条": {
|
||||
"kana": "じゅうじょう",
|
||||
"romaji": "Jujo",
|
||||
"en": "Jujo",
|
||||
"sv": "Jujo",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"板橋": {
|
||||
"kana": "いたばし",
|
||||
"romaji": "Itabashi",
|
||||
"en": "Itabashi",
|
||||
"sv": "Itabashi",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"荻窪": {
|
||||
"kana": "おぎくぼ",
|
||||
"romaji": "Ogikubo",
|
||||
"en": "Ogikubo",
|
||||
"sv": "Ogikubo",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"新宿三丁目": {
|
||||
"kana": "しんじゅくさんちょうめ",
|
||||
"romaji": "Shinjuku-sanchome",
|
||||
"en": "Shinjuku-sanchome",
|
||||
"sv": "Shinjuku-sanchome",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"新宿御苑前": {
|
||||
"kana": "しんじゅくぎょえんまえ",
|
||||
"romaji": "Shinjuku-gyoemmae",
|
||||
"en": "Shinjuku-gyoemmae",
|
||||
"sv": "Shinjuku-gyoemmae",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"四谷三丁目": {
|
||||
"kana": "よつやさんちょうめ",
|
||||
"romaji": "Yotsuya-sanchome",
|
||||
"en": "Yotsuya-sanchome",
|
||||
"sv": "Yotsuya-sanchome",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"赤坂見附": {
|
||||
"kana": "あかさかみつけ",
|
||||
"romaji": "Akasaka-mitsuke",
|
||||
"en": "Akasaka-mitsuke",
|
||||
"sv": "Akasaka-mitsuke",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"国会議事堂前": {
|
||||
"kana": "こっかいぎじどうまえ",
|
||||
"romaji": "Kokkai-gijidomae",
|
||||
"en": "Kokkai-gijidomae",
|
||||
"sv": "Kokkai-gijidomae",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"霞ケ関": {
|
||||
"kana": "かすみがせき",
|
||||
"romaji": "Kasumigaseki",
|
||||
"en": "Kasumigaseki",
|
||||
"sv": "Kasumigaseki",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"大手町": {
|
||||
"kana": "おおてまち",
|
||||
"romaji": "Otemachi",
|
||||
"en": "Otemachi",
|
||||
"sv": "Otemachi",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"淡路町": {
|
||||
"kana": "あわじちょう",
|
||||
"romaji": "Awajicho",
|
||||
"en": "Awajicho",
|
||||
"sv": "Awajicho",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"本郷三丁目": {
|
||||
"kana": "ほんごうさんちょうめ",
|
||||
"romaji": "Hongo-sanchome",
|
||||
"en": "Hongo-sanchome",
|
||||
"sv": "Hongo-sanchome",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"後楽園": {
|
||||
"kana": "こうらくえん",
|
||||
"romaji": "Korakuen",
|
||||
"en": "Korakuen",
|
||||
"sv": "Korakuen",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"茗荷谷": {
|
||||
"kana": "みょうがだに",
|
||||
"romaji": "Myogadani",
|
||||
"en": "Myogadani",
|
||||
"sv": "Myogadani",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"新大塚": {
|
||||
"kana": "しんおおつか",
|
||||
"romaji": "Shin-Otsuka",
|
||||
"en": "Shin-Otsuka",
|
||||
"sv": "Shin-Otsuka",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"中目黒": {
|
||||
"kana": "なかめぐろ",
|
||||
"romaji": "Naka-Meguro",
|
||||
"en": "Naka-Meguro",
|
||||
"sv": "Naka-Meguro",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"六本木": {
|
||||
"kana": "ろっぽんぎ",
|
||||
"romaji": "Roppongi",
|
||||
"en": "Roppongi",
|
||||
"sv": "Roppongi",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"北千住": {
|
||||
"kana": "きたせんじゅ",
|
||||
"romaji": "Kita-Senju",
|
||||
"en": "Kita-Senju",
|
||||
"sv": "Kita-Senju",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"表参道": {
|
||||
"kana": "おもてさんどう",
|
||||
"romaji": "Omotesando",
|
||||
"en": "Omotesando",
|
||||
"sv": "Omotesando",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"日本橋": {
|
||||
"kana": "にほんばし",
|
||||
"romaji": "Nihombashi",
|
||||
"en": "Nihombashi",
|
||||
"sv": "Nihombashi",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"自由が丘": {
|
||||
"kana": "じゆうがおか",
|
||||
"romaji": "Jiyugaoka",
|
||||
"en": "Jiyugaoka",
|
||||
"sv": "Jiyugaoka",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"武蔵小杉": {
|
||||
"kana": "むさしこすぎ",
|
||||
"romaji": "Musashi-Kosugi",
|
||||
"en": "Musashi-Kosugi",
|
||||
"sv": "Musashi-Kosugi",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"菊名": {
|
||||
"kana": "きくな",
|
||||
"romaji": "Kikuna",
|
||||
"en": "Kikuna",
|
||||
"sv": "Kikuna",
|
||||
"tier": 3,
|
||||
"kind": "station"
|
||||
},
|
||||
"埼京線": {
|
||||
"kana": "さいきょうせん",
|
||||
"romaji": "Saikyo-sen",
|
||||
"en": "Saikyo Line",
|
||||
"sv": "Saikyo-linjen",
|
||||
"tier": 3
|
||||
},
|
||||
"日比谷線": {
|
||||
"kana": "ひびやせん",
|
||||
"romaji": "Hibiya-sen",
|
||||
"en": "Hibiya Line",
|
||||
"sv": "Hibiya-linjen",
|
||||
"tier": 3
|
||||
},
|
||||
"銀座線": {
|
||||
"kana": "ぎんざせん",
|
||||
"romaji": "Ginza-sen",
|
||||
"en": "Ginza Line",
|
||||
"sv": "Ginza-linjen",
|
||||
"tier": 3
|
||||
},
|
||||
"路線図": {
|
||||
"kana": "ろせんず",
|
||||
"romaji": "rosenzu",
|
||||
"en": "line map",
|
||||
"sv": "linjekarta",
|
||||
"tier": 3
|
||||
},
|
||||
"券売機": {
|
||||
"kana": "けんばいき",
|
||||
"romaji": "kenbaiki",
|
||||
"en": "ticket machine",
|
||||
"sv": "biljettautomat",
|
||||
"tier": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,7 +140,7 @@
|
||||
"map.party": "Sällskap",
|
||||
"map.resume": "⟳ fortsätt (sparad seed)",
|
||||
"map.locked": "Låst - klara föregående bana först.",
|
||||
"map.hint": "Pilar: bana Enter: start L: ord T: skills I: garderob P: tel Esc",
|
||||
"map.hint": "Enter start N läge L ord T skills I items P tel Esc",
|
||||
"skills.title": "Skills 技",
|
||||
"skills.to_next": "XP till nästa",
|
||||
"skills.already": "Redan upplåst.",
|
||||
@@ -197,5 +197,16 @@
|
||||
"hud.early_board": "Låt folk kliva av först!",
|
||||
"hud.bump": "Ursäkta…",
|
||||
"hud.wrong_lane": "Fel sida - håll till vänster/höger som filen visar",
|
||||
"result.car": "vagn"
|
||||
"result.car": "vagn",
|
||||
"hud.take": "Ta:",
|
||||
"hud.mode_signs": "Skyltläge: lista ut tåget själv, +50 % poäng",
|
||||
"hud.terminal": "Linjer härifrån (terminal):",
|
||||
"menu.mode": "N: navigering %s",
|
||||
"mode.list": "listläge (nästa tåg visas)",
|
||||
"mode.signs": "skyltläge (+50 % poäng)",
|
||||
"result.bonus": "skyltbonus",
|
||||
"result.lost_wrong_direction": "Fel håll.",
|
||||
"result.lost_too_far": "För långt bort.",
|
||||
"result.wrong_direction_detail": "Linjen går till %[2]s, men tåget mot %[1]s åker åt andra hållet.",
|
||||
"result.too_far_detail": "Tåget mot %[1]s når %[2]s först efter en lång omväg."
|
||||
}
|
||||
|
||||
@@ -110,6 +110,18 @@ func buildShots(levelsDir string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l6, err := level.LoadFile(filepath.Join(levelsDir, "006-ikebukuro.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
toMachine6, err := moves(l6, l6.SpawnPoint(), l6.Find(level.TicketMachine)[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
toTrap6, err := moves(l6, l6.SpawnPoint(), l6.Find(level.Platform3)[0]) // Marunouchi: för långt bort
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Skyltbilderna tas med några kända ord så att markeringen och den
|
||||
// delvisa översättningen syns; hjälpen lär sedan in nästa ord.
|
||||
known := []string{"-know", "改札,番線,方面", "-seed", "7"}
|
||||
@@ -120,13 +132,17 @@ func buildShots(levelsDir string) error {
|
||||
{"001-flygplatsen", "move right 4; move down 2; wait 5; shot $OUT/level-001.png; quit", nil},
|
||||
{"002-shinjuku", "move down 6; move right 4; wait 5; shot $OUT/level-002.png; quit", nil},
|
||||
{"001-flygplatsen", toSign + "; wait 5; shot $OUT/sign.png; press h; wait 200; shot $OUT/sign-help.png; wait 200; shot $OUT/sign-learned.png; " + toGoal +
|
||||
"; waitphase approaching; wait 200; shot $OUT/approach.png; waittrain; wait 30; shot $OUT/platform.png; press enter; wait 40; shot $OUT/ride.png; waitstop; press enter; wait 5; shot $OUT/result.png; quit", known},
|
||||
"; waitphase approaching; wait 200; shot $OUT/approach.png; waittrain; wait 30; shot $OUT/platform.png; press enter; wait 100; pick calm; press enter; wait 40; shot $OUT/ride.png; waitstop; press enter; wait 5; shot $OUT/result.png; quit", known},
|
||||
{"001-flygplatsen", toMachine + "; press enter; wait 5; press right; wait 5; shot $OUT/topup.png; quit", []string{"-seed", "7", "-card", "0"}},
|
||||
{"002-shinjuku", toMachine2 + "; press enter; wait 5; shot $OUT/ticket.png; quit", []string{"-seed", "7", "-know", "渋谷"}},
|
||||
{"003-shibuya", toMachine3 + "; press enter; pick 池袋; press enter; wait 5; press enter; press right; wait 5; shot $OUT/seats.png; quit",
|
||||
[]string{"-seed", "7", "-know", "池袋", "-debuffs", "sore_feet"}},
|
||||
{"004-tokyo", "wait 10; shot $OUT/floors-b1.png; " + toStairs + "; wait 20; shot $OUT/floors-1f.png; quit", []string{"-seed", "7"}},
|
||||
{"005-ueno", "wait 90; shot $OUT/crowd.png; " + toPlatform5 + "; waittrain; wait 5; press enter; wait 20; shot $OUT/cars-alight.png; wait 80; press right; wait 5; shot $OUT/cars.png; quit", []string{"-seed", "7"}},
|
||||
{"006-ikebukuro", "move right 2; wait 5; shot $OUT/nav-list.png; quit", []string{"-seed", "7", "-mode", "list"}},
|
||||
{"006-ikebukuro", toMachine6 + "; wait 5; shot $OUT/terminal.png; " + "quit", []string{"-seed", "7", "-mode", "signs", "-know", "山手線,番線,方面,新宿,池袋,埼京線"}},
|
||||
{"006-ikebukuro", toTrap6 + "; waittrain; wait 5; press enter; wait 100; pick calm; press enter; wait 60; shot $OUT/ride-real.png; waitover; wait 5; shot $OUT/too-far.png; quit", []string{"-seed", "7", "-mode", "signs"}},
|
||||
{"", "wait 10; press n; wait 5; shot $OUT/map-signs.png; quit", []string{"-save", "$DEMO"}},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||