M10d: toaletter innanför och utanför spärrarna
All checks were successful
release / build-release (push) Successful in 1m38s

Björns tillägg 2026-10-24. Generatorn har nu ett betalt område per perrong
(paidDepth) och toilets inside/outside/both: 005 Ueno och 008b Nippori har
toalett bara innanför, 007b Ueno på båda sidor. Perrongceller görs till
återvändsgränder (bara passagen mot starten behålls, labyrinten repareras)
och det betalda området krymper om det skulle bli genomfart; regeln att
varje perrong nås utan att passera ett annat betalt område testas i
level och levelcheck. level.PathOutside används av testbotar och shots.
Alla genererade banor utom 003 omgjorda, skärmdumpar omtagna.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 23:09:27 +02:00
parent f04d6a88f9
commit 46bd330dfc
33 changed files with 419 additions and 209 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -135,6 +135,8 @@ att bygga på Linux som på Windows.
| `internal/minigame` | `TestPayerPicksSourceAndRefusesPoor` | **M10c:** markören på första som har råd, nekande utan avslut, avbrott utan köp | | `internal/minigame` | `TestPayerPicksSourceAndRefusesPoor` | **M10c:** markören på första som har råd, nekande utan avslut, avbrott utan köp |
| `internal/sim` | `TestKioskAsksWhoPays`, `TestOneWayGatesNeedStaff` | **M10c:** kiosken frågar vem som betalar med egna kort, vald följares kort dras, kontant fungerar, avbrott drar inget; avgift vid inpassering i betalt område, spärren släpper inte ut incheckad, personal låser upp efter 10 s, ny inpassering kostar igen | | `internal/sim` | `TestKioskAsksWhoPays`, `TestOneWayGatesNeedStaff` | **M10c:** kiosken frågar vem som betalar med egna kort, vald följares kort dras, kontant fungerar, avbrott drar inget; avgift vid inpassering i betalt område, spärren släpper inte ut incheckad, personal låser upp efter 10 s, ny inpassering kostar igen |
| `internal/level` | `TestOneWayGatesAndInside` | **M10c:** `oneWayGates` läses, `InsideGates` = rutorna innanför spärrarna | | `internal/level` | `TestOneWayGatesAndInside` | **M10c:** `oneWayGates` läses, `InsideGates` = rutorna innanför spärrarna |
| `internal/level` | `TestPathOutsideAvoidsOtherPaidAreas` + regel i `TestAllShippedLevels` | **M10d:** vägar som undviker andra perrongers betalda områden; varje perrong nås utan att gå genom ett annat betalt område |
| `internal/level` | `TestToiletSides` | **M10d:** 005 och 008b har toaletter bara innanför, 007b på båda sidor, 007 och 008 bara utanför; innanför bara på banor med spärrar utan återvändo |
| `internal/sim` | `TestJourneyWithTransfer`, `TestJourneyWrongTrainOnSecondLeg`, `TestChainLevelsLoad` | **M10:** bytet fortsätter omgången på nästa station med klocka, plånbok, sällskap och par-summa; vinst först på sista delbanan; fel håll på bytesstationen förlorar; delbanor laddas enskilt | | `internal/sim` | `TestJourneyWithTransfer`, `TestJourneyWrongTrainOnSecondLeg`, `TestChainLevelsLoad` | **M10:** bytet fortsätter omgången på nästa station med klocka, plånbok, sällskap och par-summa; vinst först på sista delbanan; fel håll på bytesstationen förlorar; delbanor laddas enskilt |
| `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/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` | `TestNPCsWanderStayOnFloorAndBlockPlayer`, `TestAlightingNPCsSpawnAndLeave` | Folk rör sig på golv, seedat, blockerar spelaren; avstigande spawnar när dörrarna öppnas och lämnar via utgången |
@@ -159,6 +161,7 @@ att bygga på Linux som på Windows.
| ~~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 | | ~~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~~ ✓ | Kedjorna valideras, byten hänger ihop, storyn börjar och slutar på flygplatsen, bytet för med sig tid/plånbok/sällskap – finns, se tabellen ovan; `tools/levelcheck` som manuell kontroll | | ~~M10 Story-innehåll~~ ✓ | Kedjorna valideras, byten hänger ihop, storyn börjar och slutar på flygplatsen, bytet för med sig tid/plånbok/sällskap – finns, se tabellen ovan; `tools/levelcheck` som manuell kontroll |
| ~~M10c Vem betalar, spärrar utan återvändo~~ ✓ | Väljaren betalar från rätt källa och nekar den utan pengar; spärren håller kvar, personalen låser upp, ny inpassering kostar – finns, se tabellen ovan | | ~~M10c Vem betalar, spärrar utan återvändo~~ ✓ | Väljaren betalar från rätt källa och nekar den utan pengar; spärren håller kvar, personalen låser upp, ny inpassering kostar – finns, se tabellen ovan |
| ~~M10d Toaletter innanför/utanför~~ ✓ | Toaletterna ligger på rätt sida av spärrarna per bana – finns, se tabellen ovan |
| M10b Bussar och vägar | Bussar följer tidtabell och vägrutor; hållplatser bedöms som perronger; överstimulerande UI-reglaget påverkar bara presentationen | | M10b Bussar och vägar | Bussar följer tidtabell och vägrutor; hållplatser bedöms som perronger; överstimulerande UI-reglaget påverkar bara presentationen |
| M11 Oändligt läge & NG+ | Målsekvens deterministisk per seed; NG+ behåller skills men nollställer items | | 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) | | M12 Ljud & polish | Alla ljudfiler som refereras finns och går att avkoda; Windows-binär startar (manuellt) |

View File

@@ -612,6 +612,40 @@ på **brasse-linux01-runnern** (label `linux-amd64`), aldrig på Pi5:n.
- **Kvar till senare:** ett kontor (egen ruta) som alternativ till knappen - **Kvar till senare:** ett kontor (egen ruta) som alternativ till knappen
vid spärren; personal som också kan hjälpa med tomt kort. vid spärren; personal som också kan hjälpa med tomt kort.
### M10d – Toaletter innanför och utanför spärrarna ✓ (klar 2026-10-24)
- **Mål:** Björns tillägg 2026-10-24: toaletter finns ibland bara
innanför spärrarna, ibland bara utanför, ibland på båda sidor – så att
man måste tänka på *när* sällskapet går på toa, särskilt där spärren inte
släpper ut en (M10c).
- **Teknik:** generatorn får ett **betalt område** (`paidDepth`: hur många
steg från perrongen spärrarna sätts) och `toilets: inside|outside|both`
som fäster en toalettnisch i en vägg innanför spärrarna och/eller lägger
en i en återvändsgränd utanför. `tools/levelcheck` rapporterar var
toaletterna ligger. Banorna 005 Ueno (bara innanför), 007b Ueno (båda)
och 008b Nippori (bara innanför) görs om; 007 och 008 har bara utanför.
- **Så blev det:** generatorn räknar ett betalt område med BFS från
perrongerna (`paidDepth` steg) och sätter spärrar på alla öppna rutor
precis utanför; `toilets: inside` fäster en toalettnisch i en vägg intill
en betald ruta (där rutan bortom också är vägg, så inget nytt öppnas),
`both` lägger dessutom en i en återvändsgränd utanför, automat och kiosk
ligger alltid utanför. 005 Ueno (betalt område 12 rutor, toalett bara
innanför), 007b Ueno (22 rutor, båda) och 008b Nippori (16 rutor, bara
innanför) är omgjorda; 007 och 008 har bara utanför. `tools/levelcheck`
rapporterar betalt område och toaletter per sida. **Perrongerna är nu
återvändsgränder:** generatorn behåller bara passagen mot starten in till
perrongens cell (rum i den breda labyrinten), murar igen de andra och
reparerar labyrinten med nya passager där den annars delas; det betalda
området krymper automatiskt om det skulle bli en genomfart. Regeln "varje
perrong nås utan att gå genom en annan perrongs betalda område" testas
(`level.PathOutside`, i `TestAllShippedLevels` och levelcheck) – annars
skulle spärrar utan återvändo göra fällperrongerna onåbara. Alla
genererade banor utom 003 (som redan klarade regeln) gjordes om.
- **Klar när:** banorna har toaletter på rätt sida (`level.TestToiletSides`),
spärrtestet fungerar med ett större betalt område
(`sim.TestOneWayGatesNeedStaff` går vägen till första spärren i stället
för ett fast steg), och levelcheck är grön. ✓
### M11 – Oändligt läge och New Game+ ### M11 – Oändligt läge och New Game+
- Slumpade mål från seed, poäng per mål, egen poänglista. NG+ behåller skills. - Slumpade mål från seed, poäng per mål, egen poänglista. NG+ behåller skills.

View File

@@ -122,6 +122,10 @@ otydligt står tolkningen i [§15](#15-tolkningar-av-originalet).
- Folk måste gå på toa, köpa dricka och kan bli distraherade. - Folk måste gå på toa, köpa dricka och kan bli distraherade.
- **Ju fler i lämmeltåget, desto svårare** blir det och desto oftare händer - **Ju fler i lämmeltåget, desto svårare** blir det och desto oftare händer
specialhändelser (dricka, mat, toa m.m.). specialhändelser (dricka, mat, toa m.m.).
- **Toaletter innanför och utanför** (tillägg 2026-10-24): toaletter kan
ibland finnas **bara innanför** spärrarna, ibland **bara utanför** och
ibland **på båda sidor**. Tillsammans med spärrar utan återvändo (§8)
avgör det om man bör gå på toa före eller efter spärren.
## 6. Japanska, skyltar och hjälp ## 6. Japanska, skyltar och hjälp

View File

@@ -645,6 +645,32 @@ func (l *Level) InsideGates() map[Point]bool {
return seen return seen
} }
// PathOutside är Path som inte går genom betalda områden (innanför
// spärrarna) utom dem som start eller mål ligger i – så att en bot inte
// checkar in vid fel perrong på vägen (spärrar utan återvändo).
func (l *Level) PathOutside(from, to Point, canEnter func(Point) bool) []Point {
inside := l.InsideGates()
allowed := map[Point]bool{}
for _, seed := range []Point{from, to} {
if !inside[seed] || allowed[seed] {
continue
}
allowed[seed] = true
queue := []Point{seed}
for len(queue) > 0 {
p := queue[0]
queue = queue[1:]
for _, n := range l.Neighbors(p) {
if inside[n] && !allowed[n] {
allowed[n] = true
queue = append(queue, n)
}
}
}
}
return l.Path(from, to, func(p Point) bool { return canEnter(p) && (!inside[p] || allowed[p]) })
}
// UngatedPlatforms listar perronger som går att nå från start utan att // UngatedPlatforms listar perronger som går att nå från start utan att
// passera någon spärr. Speldesignen kräver att alla perronger ligger // passera någon spärr. Speldesignen kräver att alla perronger ligger
// innanför spärrar, så listan ska vara tom för skeppade banor. // innanför spärrar, så listan ska vara tom för skeppade banor.

View File

@@ -138,6 +138,12 @@ func TestAllShippedLevels(t *testing.T) {
if u := l.UngatedPlatforms(); len(u) > 0 { if u := l.UngatedPlatforms(); len(u) > 0 {
t.Errorf("perronger som nås utan att passera en spärr: %v – spärrarna måste vara flaskhalsar", u) t.Errorf("perronger som nås utan att passera en spärr: %v – spärrarna måste vara flaskhalsar", u)
} }
for _, tr := range l.Trains { // varje perrong ska nås utan att gå genom en annan perrongs betalda område
pt := l.Find(Tile(tr.Platform[0]))[0]
if l.PathOutside(l.SpawnPoint(), pt, l.CanEnter) == nil {
t.Errorf("perrong %s nås bara genom ett annat betalt område", tr.Platform)
}
}
for i, tr := range l.Trains { for i, tr := range l.Trains {
if tr.Track == "" { if tr.Track == "" {
t.Errorf("tåg %d saknar spårnummer", i) t.Errorf("tåg %d saknar spårnummer", i)
@@ -733,6 +739,43 @@ func TestStoryChains(t *testing.T) {
} }
} }
func TestPathOutsideAvoidsOtherPaidAreas(t *testing.T) {
// Två perronger med betalda områden; den raka vägen till Q går genom
// P:s område, PathOutside tar omvägen via korridoren ovanför.
src := `{"id":"po","name":{"sv":"x"},"difficulty":1,"destination":{"sv":"x"},"oneWayGates":true,
"grid":["###########","#S........#","#.#######.#","#G#######G#","#.G.P.G.Q.#","###########"],
"trains":[{"platform":"P","track":"1","line":{"en":"A"},"destination":{"en":"B"},"correct":true},
{"platform":"Q","track":"2","line":{"en":"A"},"destination":{"en":"C"}}]}`
l, err := Parse([]byte(src))
if err != nil {
t.Fatal(err)
}
q := l.Find(Platform2)[0]
direct := l.Path(l.SpawnPoint(), q, l.CanEnter)
outside := l.PathOutside(l.SpawnPoint(), q, l.CanEnter)
if direct == nil || outside == nil {
t.Fatal("vägar saknas")
}
p := l.Find(Platform)[0]
pRegion := map[Point]bool{{X: 3, Y: 4}: true, p: true, {X: 5, Y: 4}: true}
through := false
for _, pt := range direct {
through = through || pRegion[pt]
}
if !through {
t.Fatal("den raka vägen borde gå genom P:s område – annars testar vi inget")
}
for _, pt := range outside {
if pRegion[pt] {
t.Fatalf("PathOutside går genom P:s område vid %v", pt)
}
}
// Från P (inne) och ut till starten: det egna området får användas.
if back := l.PathOutside(p, l.SpawnPoint(), l.CanEnter); back == nil {
t.Fatal("vägen ut från P ska finnas")
}
}
func TestOneWayGatesAndInside(t *testing.T) { func TestOneWayGatesAndInside(t *testing.T) {
src := `{"id":"ow","name":{"sv":"x"},"difficulty":1,"destination":{"sv":"x"},"oneWayGates":true, src := `{"id":"ow","name":{"sv":"x"},"difficulty":1,"destination":{"sv":"x"},"oneWayGates":true,
"grid":["#########","#S.G.P.G#","#########"], "grid":["#########","#S.G.P.G#","#########"],
@@ -758,3 +801,40 @@ func TestOneWayGatesAndInside(t *testing.T) {
t.Fatal("spärren och starten är inte innanför") t.Fatal("spärren och starten är inte innanför")
} }
} }
// TestToiletSides: toaletterna ligger på den sida av spärrarna som banan
// är gjord för – bara innanför, bara utanför eller båda.
func TestToiletSides(t *testing.T) {
dir, err := res.Path("data", "levels")
if err != nil {
t.Fatal(err)
}
want := map[string][2]bool{ // id -> {innanför, utanför}
"005-ueno": {true, false},
"007-akihabara": {false, true},
"007b-ueno": {true, true},
"008-tokyo-hemresa": {false, true},
"008b-nippori": {true, false},
}
for id, w := range want {
l, err := LoadFile(filepath.Join(dir, id+".json"))
if err != nil {
t.Fatal(err)
}
inside := l.InsideGates()
in, out := false, false
for _, p := range l.Find(Toilet) {
if inside[p] {
in = true
} else {
out = true
}
}
if in != w[0] || out != w[1] {
t.Errorf("%s: toaletter innanför=%v utanför=%v, vill ha %v/%v", id, in, out, w[0], w[1])
}
if in && !l.OneWayGates {
t.Errorf("%s: toaletter innanför är bara intressant med spärrar utan återvändo", id)
}
}
}

View File

@@ -11,7 +11,7 @@ import (
// pathTo är testbotens väg till goal: som PathCanEnter, men andra perronger // pathTo är testbotens väg till goal: som PathCanEnter, men andra perronger
// används inte som genvägar (på spärrar utan återvändo skulle man fastna). // används inte som genvägar (på spärrar utan återvändo skulle man fastna).
func pathTo(r *Run, goal level.Point) []level.Point { func pathTo(r *Run, goal level.Point) []level.Point {
return r.Level.Path(r.Player.Tile, goal, func(p level.Point) bool { return r.Level.PathOutside(r.Player.Tile, goal, func(p level.Point) bool {
return r.PathCanEnter(p) && (!r.Level.At(p).IsPlatform() || p == goal) return r.PathCanEnter(p) && (!r.Level.At(p).IsPlatform() || p == goal)
}) })
} }
@@ -100,12 +100,22 @@ func TestOneWayGatesNeedStaff(t *testing.T) {
t.Fatalf("på perrongen ska man vara incheckad och ha betalat en gång: checkedIn=%v passes=%d card=%d", r.CheckedIn(), r.GatePasses, r.Wallet.Card) t.Fatalf("på perrongen ska man vara incheckad och ha betalat en gång: checkedIn=%v passes=%d card=%d", r.CheckedIn(), r.GatePasses, r.Wallet.Card)
} }
card := r.Wallet.Card card := r.Wallet.Card
stepUntilStill(r, Left) // tillbaka på spärren: kostar inget // Tillbaka mot utgången: vägen till starten, kapad vid första spärren.
back := l.PathOutside(plat, l.SpawnPoint(), r.PathCanEnter)
gi := 0
for gi < len(back) && l.At(back[gi]) != level.Gate {
gi++
}
if gi >= len(back)-1 {
t.Fatal("ingen spärr på vägen ut")
}
walk(t, r, back[:gi+1]) // inifrån och ut på spärren: kostar inget
if !r.inGate() || r.GatePasses != 1 || r.Wallet.Card != card { if !r.inGate() || r.GatePasses != 1 || r.Wallet.Card != card {
t.Fatalf("spärren igen: inGate=%v passes=%d card=%d", r.inGate(), r.GatePasses, r.Wallet.Card) t.Fatalf("spärren igen: inGate=%v passes=%d card=%d", r.inGate(), r.GatePasses, r.Wallet.Card)
} }
gate := r.Player.Tile gate, outside := r.Player.Tile, back[gi+1]
stepUntilStill(r, Left) // ut? nej out := dirTo(gate, outside)
stepUntilStill(r, out) // ut? nej
if r.Player.Tile != gate || r.NoExitAt < 0 { if r.Player.Tile != gate || r.NoExitAt < 0 {
t.Fatalf("spärren ska inte släppa ut: tile=%v noExitAt=%.1f", r.Player.Tile, r.NoExitAt) t.Fatalf("spärren ska inte släppa ut: tile=%v noExitAt=%.1f", r.Player.Tile, r.NoExitAt)
} }
@@ -119,15 +129,15 @@ func TestOneWayGatesNeedStaff(t *testing.T) {
if r.Staff.Active || !r.StaffUnlocked || r.StaffCalls != 1 || r.FlashKey != FlashStaff { if r.Staff.Active || !r.StaffUnlocked || r.StaffCalls != 1 || r.FlashKey != FlashStaff {
t.Fatalf("personalen ska ha låst upp: active=%v unlocked=%v calls=%d", r.Staff.Active, r.StaffUnlocked, r.StaffCalls) t.Fatalf("personalen ska ha låst upp: active=%v unlocked=%v calls=%d", r.Staff.Active, r.StaffUnlocked, r.StaffCalls)
} }
stepUntilStill(r, Left) stepUntilStill(r, out)
if r.Player.Tile == gate || r.CheckedIn() || r.StaffUnlocked { if r.Player.Tile != outside || r.CheckedIn() || r.StaffUnlocked {
t.Fatalf("ute igen: tile=%v checkedIn=%v unlocked=%v", r.Player.Tile, r.CheckedIn(), r.StaffUnlocked) t.Fatalf("ute igen: tile=%v checkedIn=%v unlocked=%v", r.Player.Tile, r.CheckedIn(), r.StaffUnlocked)
} }
stepUntilStill(r, Right) // på spärren utifrån: inget dras än stepUntilStill(r, dirTo(outside, gate)) // på spärren utifrån: inget dras än
if !r.inGate() || r.CheckedIn() || r.GatePasses != 1 { if !r.inGate() || r.CheckedIn() || r.GatePasses != 1 {
t.Fatalf("spärren utifrån: inGate=%v checkedIn=%v passes=%d", r.inGate(), r.CheckedIn(), r.GatePasses) t.Fatalf("spärren utifrån: inGate=%v checkedIn=%v passes=%d", r.inGate(), r.CheckedIn(), r.GatePasses)
} }
stepUntilStill(r, Right) // in igen: kostar igen walk(t, r, pathTo(r, plat)) // in igen: kostar igen
if r.Player.Tile != plat || !r.CheckedIn() || r.GatePasses != 2 || r.Wallet.Card != card-l.FareYen { if r.Player.Tile != plat || !r.CheckedIn() || r.GatePasses != 2 || r.Wallet.Card != card-l.FareYen {
t.Fatalf("ny inpassering: tile=%v checkedIn=%v passes=%d card=%d (var %d)", r.Player.Tile, r.CheckedIn(), r.GatePasses, r.Wallet.Card, card) t.Fatalf("ny inpassering: tile=%v checkedIn=%v passes=%d card=%d (var %d)", r.Player.Tile, r.CheckedIn(), r.GatePasses, r.Wallet.Card, card)
} }

View File

@@ -1,40 +1,19 @@
{ {
"id": "002-shinjuku",
"kind": "main",
"order": 2,
"name": {
"en": "Shinjuku Station",
"ja": "新宿駅",
"sv": "Shinjuku station"
},
"difficulty": 2,
"party": 3,
"ticketRequired": true,
"seatSelection": true,
"rideStops": 4,
"destination": { "destination": {
"en": "Shibuya", "en": "Shibuya",
"ja": "渋谷", "ja": "渋谷",
"romaji": "Shibuya", "romaji": "Shibuya",
"sv": "Shibuya" "sv": "Shibuya"
}, },
"timeLimitSec": 420, "difficulty": 2,
"maxMissedTrains": 2,
"fareYen": 200, "fareYen": 200,
"shop": [
"cap_red",
"water_bottle",
"mask_surgical",
"map_shinjuku",
"sneakers"
],
"grid": [ "grid": [
"#################################", "#################################",
"#S#..i..#K#...........#.........#", "#S#..i..#K#...........#.........#",
"#.#.#.#.#.#.#######.#.#########.#", "#.#.#.#.#.#.#######.#.#########.#",
"#.#W#.#.#.#.#.....#.#...........#", "#.#W#.#.#.#.#.....#.#...........#",
"#.###.#.#.#.#####.#.###########.#", "#.###.#.#.#.#####.#.###########.#",
"#.#...#.#..i......#.#.......#...#", "#.#...#.#.........#.#.......#...#",
"#.#.###.###########.#.#.###.#.###", "#.#.###.###########.#.#.###.#.###",
"#.....#.......#...#.#.#.........#", "#.....#.......#...#.#.#.........#",
"#####.#######.#.#.#.#.#########.#", "#####.#######.#.#.#.#.#########.#",
@@ -46,11 +25,66 @@
"#.#.###.###.#.###.#.#.#######.#.#", "#.#.###.###.#.###.#.#.#######.#.#",
"#...#T#.#..i#.#.#...#.....#...#.#", "#...#T#.#..i#.#.#...#.....#...#.#",
"#####.#.#.#.#.#.#####.#####.###.#", "#####.#.#.#.#.#.#####.#####.###.#",
"#.....#.#.#.#.......#.......#...#", "#.....#.#.#.#..i....#.......#...#",
"#G#.#.#.#.#########.#########.#G#", "#G#.#.#.#.#########.#########.#G#",
"#pG.#...#.....................#P#", "#p#.#...#.....................#P#",
"#################################" "#################################"
], ],
"id": "002-shinjuku",
"kind": "main",
"maxMissedTrains": 2,
"name": {
"en": "Shinjuku Station",
"ja": "新宿駅",
"sv": "Shinjuku station"
},
"order": 2,
"party": 3,
"rideStops": 4,
"seatSelection": true,
"shop": [
"cap_red",
"water_bottle",
"mask_surgical",
"map_shinjuku",
"sneakers"
],
"signs": [
{
"text": {
"en": "Shinjuku Station. Track 14 Yamanote Line for Shibuya: far bottom right. Track 8 Chuo Line for Tokyo: far bottom left.",
"ja": "新宿駅 14番線 山手線 渋谷方面 → 南東 8番線 中央線 東京方面 → 南西",
"sv": "Shinjuku station. Spår 14 Yamanote-linjen mot Shibuya: längst ner till höger. Spår 8 Chuo-linjen mot Tokyo: längst ner till vänster."
},
"x": 5,
"y": 1,
"z": 0
},
{
"text": {
"en": "\u003c- Track 8 Chuo Line (Tokyo) Track 14 Yamanote Line (Shibuya) -\u003e",
"ja": "← 8番線 中央線 東京方面 14番線 山手線 渋谷方面 →",
"sv": "\u003c- Spår 8 Chuo-linjen (Tokyo) Spår 14 Yamanote-linjen (Shibuya) -\u003e"
},
"x": 11,
"y": 15,
"z": 0
},
{
"text": {
"en": "Track 14 Yamanote Line for Shibuya: the gate in the south-east corner.",
"ja": "14番線 山手線 渋谷方面 → 南東口 改札",
"sv": "Spår 14 Yamanote-linjen mot Shibuya: spärren i sydöstra hörnet."
},
"x": 15,
"y": 17,
"z": 0
}
],
"station": "shinjuku",
"ticketRequired": true,
"timeLimitSec": 420,
"to": "shibuya",
"trains": [ "trains": [
{ {
"crowded": true, "crowded": true,
@@ -64,15 +98,15 @@
"ja": "山手線", "ja": "山手線",
"romaji": "Yamanote-sen" "romaji": "Yamanote-sen"
}, },
"lineId": "yamanote",
"platform": "P", "platform": "P",
"schedule": { "schedule": {
"dwellSec": 8, "dwellSec": 8,
"intervalSec": 20, "intervalSec": 20,
"offsetSec": 3 "offsetSec": 3
}, },
"track": "14", "terminus": "shibuya",
"lineId": "yamanote", "track": "14"
"terminus": "shibuya"
}, },
{ {
"destination": { "destination": {
@@ -85,46 +119,15 @@
"ja": "中央線", "ja": "中央線",
"romaji": "Chuo-sen" "romaji": "Chuo-sen"
}, },
"lineId": "chuo",
"platform": "p", "platform": "p",
"schedule": { "schedule": {
"dwellSec": 8, "dwellSec": 8,
"intervalSec": 25, "intervalSec": 25,
"offsetSec": 10 "offsetSec": 10
}, },
"track": "8", "terminus": "tokyo",
"lineId": "chuo", "track": "8"
"terminus": "tokyo"
} }
], ]
"signs": [
{
"text": {
"en": "Shinjuku Station. Track 14 Yamanote Line for Shibuya: far bottom right. Track 8 Chuo Line for Tokyo: far bottom left.",
"ja": "新宿駅 14番線 山手線 渋谷方面 → 南東 8番線 中央線 東京方面 → 南西",
"sv": "Shinjuku station. Spår 14 Yamanote-linjen mot Shibuya: längst ner till höger. Spår 8 Chuo-linjen mot Tokyo: längst ner till vänster."
},
"x": 5,
"y": 1
},
{
"text": {
"en": "<- Track 8 Chuo Line (Tokyo) Track 14 Yamanote Line (Shibuya) ->",
"ja": "← 8番線 中央線 東京方面 14番線 山手線 渋谷方面 →",
"sv": "<- Spår 8 Chuo-linjen (Tokyo) Spår 14 Yamanote-linjen (Shibuya) ->"
},
"x": 11,
"y": 15
},
{
"text": {
"en": "Track 14 Yamanote Line for Shibuya: the gate in the south-east corner.",
"ja": "14番線 山手線 渋谷方面 → 南東口 改札",
"sv": "Spår 14 Yamanote-linjen mot Shibuya: spärren i sydöstra hörnet."
},
"x": 11,
"y": 5
}
],
"station": "shinjuku",
"to": "shibuya"
} }

View File

@@ -9,25 +9,25 @@
"fareYen": 200, "fareYen": 200,
"grid": [ "grid": [
"#################################", "#################################",
"#S....#....K#.....#.............#", "#S..........#.....#.............#",
"#.###.#.#####.###.#.#.#########.#", "#.###.#.#####.###.#.#.#########.#",
"#...#.#.........#.#.#.....#.....#", "#...#.#......i.W#.#.#.....#K....#",
"#.###.###########.#.#####.#######", "#.###.###########.#.#####.#######",
"#...#.#.....#...#.#.....#.#.....#", "#...#.#.....#...#.#i...T#.#.....#",
"#.#.#.#.###.#.#.#.#.#####.#.###.#", "#.#.#.#.###.#.#.#.#.#####.#.###.#",
"#.#...#.#T....#.#.#.#...#.....#.#", "#.#...#.#.....#.#.#.#...#..i..#.#",
"#.#.###.#######.#.#.#.#.#####.#.#", "#.#.###.#######.#.#.#.#.#####.#.#",
"#.#....i......#.#...#.#.....#.#.#", "#.#...........#.#...#.#.....#.#.#",
"#.#####.#####.#.#####.#####.#.#.#", "#.#####.#####.#.#####.#####.#.#.#",
"#.....#....W#.#.....#.#...#.#.#.#", "#.....#.....#.#.....#.#...#.#.#.#",
"#####.###.###.#####.#.#.#.#.###.#", "#####.###.###.#####.#.#.#.#.###.#",
"#...#.#...#...#...#.#.#.#...#...#", "#...#.#...#...#...#.#.#.#...#...#",
"#.#.#.###.#.###.#.#.#.#.#####.###", "#.#.#.###.#.###.#.#.#.#.#####.###",
"#.#.#...#.#.#.....#.#.#...#.....#", "#.#.#...#.#.#.....#.#.#...#.....#",
"#.#.###.#.#.#.#####.#.###.#.#.#.#", "#.#.###.#.#.#.#####.#.###.#.#.#.#",
"#.#i..#.#.#.#.#...#...#...#...#.#", "#.#...#.#.#.#.#...#...#...#...#.#",
"#G#.#.#.###.#.#.#.#####.#######G#", "###.#.#.###.#.#.#.#####.#########",
"#pG.#.......#...#......i......GP#", "#pG.#.......#...#.............GP#",
"#################################" "#################################"
], ],
"id": "002b-kabukicho", "id": "002b-kabukicho",
@@ -54,8 +54,9 @@
"ja": "歌舞伎町 1番線 山手線 新宿方面 → 南東 3番線 中央線 東京方面", "ja": "歌舞伎町 1番線 山手線 新宿方面 → 南東 3番線 中央線 東京方面",
"sv": "Sidospår: Kabukicho. Tillbaka till Shinjuku med Yamanote-linjen, spår 1 i sydost. Chuo-linjen går åt fel håll." "sv": "Sidospår: Kabukicho. Tillbaka till Shinjuku med Yamanote-linjen, spår 1 i sydost. Chuo-linjen går åt fel håll."
}, },
"x": 7, "x": 13,
"y": 9 "y": 3,
"z": 0
}, },
{ {
"text": { "text": {
@@ -63,8 +64,9 @@
"ja": "← 3番線 中央線 東京方面 1番線 山手線 新宿方面 →", "ja": "← 3番線 中央線 東京方面 1番線 山手線 新宿方面 →",
"sv": "\u003c- Spår 3 Chuo-linjen (Tokyo) Spår 1 Yamanote-linjen (Shinjuku) -\u003e" "sv": "\u003c- Spår 3 Chuo-linjen (Tokyo) Spår 1 Yamanote-linjen (Shinjuku) -\u003e"
}, },
"x": 3, "x": 19,
"y": 17 "y": 5,
"z": 0
}, },
{ {
"text": { "text": {
@@ -72,8 +74,9 @@
"ja": "1番線 山手線 新宿方面 → 南東口 改札", "ja": "1番線 山手線 新宿方面 → 南東口 改札",
"sv": "Spår 1 Yamanote-linjen: spärren i sydöstra hörnet." "sv": "Spår 1 Yamanote-linjen: spärren i sydöstra hörnet."
}, },
"x": 23, "x": 27,
"y": 19 "y": 7,
"z": 0
} }
], ],
"timeLimitSec": 240, "timeLimitSec": 240,

View File

@@ -54,7 +54,7 @@
"#...#.....#.........#.#...#.....#", "#...#.....#.........#.#...#.....#",
"###.#.#.###.###########.#.#.###.#", "###.#.#.###.###########.#.#.###.#",
"#...#.#...#.....#.......#.#...#.#", "#...#.#...#.....#.......#.#...#.#",
"#G###.###.#####.#.#.#####.###.#G#", "#####.###.#####.#.#.#####.###.#G#",
"#pG...D.#...........#.........#P#", "#pG...D.#...........#.........#P#",
"#################################" "#################################"
], ],
@@ -92,9 +92,9 @@
}, },
{ {
"text": { "text": {
"en": "<- Track 7 Tokaido Line (Shinagawa) Track 4 Yamanote Line (Ueno) ->", "en": "\u003c- Track 7 Tokaido Line (Shinagawa) Track 4 Yamanote Line (Ueno) -\u003e",
"ja": "← 7番線 東海道線 品川方面 4番線 山手線 上野方面 →", "ja": "← 7番線 東海道線 品川方面 4番線 山手線 上野方面 →",
"sv": "<- Spår 7 Tokaido-linjen (Shinagawa) Spår 4 Yamanote-linjen (Ueno) ->" "sv": "\u003c- Spår 7 Tokaido-linjen (Shinagawa) Spår 4 Yamanote-linjen (Ueno) -\u003e"
}, },
"x": 3, "x": 3,
"y": 3, "y": 3,
@@ -111,8 +111,10 @@
"z": 1 "z": 1
} }
], ],
"station": "tokyo",
"ticketRequired": false, "ticketRequired": false,
"timeLimitSec": 420, "timeLimitSec": 420,
"to": "ueno",
"trains": [ "trains": [
{ {
"crowded": true, "crowded": true,
@@ -126,15 +128,15 @@
"ja": "山手線", "ja": "山手線",
"romaji": "Yamanote-sen" "romaji": "Yamanote-sen"
}, },
"lineId": "yamanote",
"platform": "P", "platform": "P",
"schedule": { "schedule": {
"dwellSec": 8, "dwellSec": 8,
"intervalSec": 24, "intervalSec": 24,
"offsetSec": 5 "offsetSec": 5
}, },
"track": "4", "terminus": "ueno",
"lineId": "yamanote", "track": "4"
"terminus": "ueno"
}, },
{ {
"destination": { "destination": {
@@ -147,17 +149,15 @@
"ja": "東海道線", "ja": "東海道線",
"romaji": "Tokaido-sen" "romaji": "Tokaido-sen"
}, },
"lineId": "tokaido",
"platform": "p", "platform": "p",
"schedule": { "schedule": {
"dwellSec": 8, "dwellSec": 8,
"intervalSec": 28, "intervalSec": 28,
"offsetSec": 11 "offsetSec": 11
}, },
"track": "7", "terminus": "shinagawa",
"lineId": "tokaido", "track": "7"
"terminus": "shinagawa"
} }
], ]
"station": "tokyo",
"to": "ueno"
} }

View File

@@ -11,23 +11,23 @@
"fareYen": 200, "fareYen": 200,
"grid": [ "grid": [
"#################################", "#################################",
"#S.<..<..#..<..<..<..<..#..<..###", "#S.\u003c..\u003c..#..\u003c..\u003c..\u003cK.\u003c..#..\u003c..###",
"#..>..>..#..>..>..>..>..#..>..###", "#..\u003e..\u003e..#..\u003e..\u003e..\u003e..\u003e..#.i\u003e..###",
"#######<>#<>#<>####<>#<>####<>###", "#######\u003c\u003e#\u003c\u003e#\u003c\u003e####\u003c\u003e#\u003c\u003e####\u003c\u003e###",
"#..<..<..#.W<..<..<..#..<..<..###", "#..\u003c..\u003cT.#..\u003c..\u003c..\u003c..#..\u003c..\u003c..###",
"#.i>..>..#..>..>..>..#..>K.>..###", "#.i\u003e..\u003e..#..\u003e..\u003e..\u003e..#..\u003e..\u003e..###",
"#<>#############<>#######<>#<>###", "#\u003c\u003e#############\u003c\u003e#######\u003c\u003e#\u003c\u003e###",
"#..#..<..<..<.i<..<..<..#..<..###", "#..#..\u003c..\u003c..\u003c..\u003c..\u003c..\u003c..#..\u003c..###",
"#..#..>..>..>..>..>..>..#.T>i.###", "#..#..\u003e..\u003e..\u003e..\u003e..\u003e..\u003e..#..\u003e..###",
"#<>####<>##########<>#<>#<>######", "#\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..###",
"#..>..#..#..#..>..#..>..>..>..###", "#G.\u003e..#..#..#..\u003e..#..\u003e..\u003e..\u003e..###",
"#<>#######<>####<>###############", "#\u003cG#######\u003c\u003e####\u003c\u003e###############",
"#G.<..<..<..#..<..<..<..<..<.G###", "#..#..\u003c..\u003c.i#..\u003c..\u003c..\u003c..\u003c..G..W##",
"#pG>..>..>..#..>..>..>..>..>GP###", "#p.#..\u003e..\u003e..#..\u003e..\u003e..\u003e..\u003e.G\u003e.P###",
"#################################", "#################################",
"#################################", "#################################",
"#################################" "#################################"
@@ -41,6 +41,7 @@
"sv": "Ueno station" "sv": "Ueno station"
}, },
"noise": 2, "noise": 2,
"oneWayGates": true,
"order": 5, "order": 5,
"party": 3, "party": 3,
"rideStops": 5, "rideStops": 5,
@@ -64,12 +65,12 @@
}, },
{ {
"text": { "text": {
"en": "<- Track 9 Keihin-Tohoku Line (Omiya) Track 3 Yamanote Line (Akihabara) ->", "en": "\u003c- Track 9 Keihin-Tohoku Line (Omiya) Track 3 Yamanote Line (Akihabara) -\u003e",
"ja": "← 9番線 京浜東北線 大宮方面 3番線 山手線 秋葉原方面 →", "ja": "← 9番線 京浜東北線 大宮方面 3番線 山手線 秋葉原方面 →",
"sv": "<- Spår 9 Keihin-Tohoku-linjen (Omiya) Spår 3 Yamanote-linjen (Akihabara) ->" "sv": "\u003c- Spår 9 Keihin-Tohoku-linjen (Omiya) Spår 3 Yamanote-linjen (Akihabara) -\u003e"
}, },
"x": 14, "x": 11,
"y": 7, "y": 16,
"z": 0 "z": 0
}, },
{ {
@@ -78,12 +79,14 @@
"ja": "3番線 1号車 女性専用車 ラッシュ 改札 → 南東口", "ja": "3番線 1号車 女性専用車 ラッシュ 改札 → 南東口",
"sv": "Spår 3: vagn 1 är kvinnovagn under rusningen (de första 2,5 minuterna). Köa vid markeringarna och låt folk kliva av först." "sv": "Spår 3: vagn 1 är kvinnovagn under rusningen (de första 2,5 minuterna). Köa vid markeringarna och låt folk kliva av först."
}, },
"x": 28, "x": 26,
"y": 8, "y": 2,
"z": 0 "z": 0
} }
], ],
"station": "ueno",
"timeLimitSec": 420, "timeLimitSec": 420,
"to": "akihabara",
"trains": [ "trains": [
{ {
"cars": 4, "cars": 4,
@@ -97,18 +100,18 @@
"ja": "山手線", "ja": "山手線",
"romaji": "Yamanote-sen" "romaji": "Yamanote-sen"
}, },
"lineId": "yamanote",
"platform": "P", "platform": "P",
"schedule": { "schedule": {
"dwellSec": 9, "dwellSec": 9,
"intervalSec": 26, "intervalSec": 26,
"offsetSec": 6 "offsetSec": 6
}, },
"terminus": "tokyo",
"track": "3", "track": "3",
"womenOnlyCar": 1, "womenOnlyCar": 1,
"womenOnlyFrom": 0, "womenOnlyFrom": 0,
"womenOnlyTo": 150, "womenOnlyTo": 150
"lineId": "yamanote",
"terminus": "tokyo"
}, },
{ {
"cars": 3, "cars": 3,
@@ -122,18 +125,15 @@
"ja": "京浜東北線", "ja": "京浜東北線",
"romaji": "Keihin-Tohoku-sen" "romaji": "Keihin-Tohoku-sen"
}, },
"lineId": "keihin-tohoku",
"platform": "p", "platform": "p",
"schedule": { "schedule": {
"dwellSec": 8, "dwellSec": 8,
"intervalSec": 30, "intervalSec": 30,
"offsetSec": 13 "offsetSec": 13
}, },
"track": "9", "terminus": "omiya",
"lineId": "keihin-tohoku", "track": "9"
"terminus": "omiya"
} }
], ]
"station": "ueno",
"to": "akihabara",
"oneWayGates": true
} }

View File

@@ -22,14 +22,14 @@
"#...#.....#......i#...#.....#.#.#", "#...#.....#......i#...#.....#.#.#",
"#.###############.###.#.#####.#.#", "#.###############.###.#.#####.#.#",
"#.#.........#..i..#...#.......#.#", "#.#.........#..i..#...#.......#.#",
"#.#.#.#####.#.#.###.#.#####.###.#", "#.#.#.#####.#.#.###.#.#.###.###.#",
"#...#.....#...#.....#.....#.#...#", "#...#.....#...#.....#.....#.#...#",
"#.#######.#.###########.#.###.#.#", "#.#######.#.#####.#####.#.###.#.#",
"#.#.....#...#.....#.....#...#.#.#", "#.#.....#...#.....#.....#...#.#.#",
"#.#.###.#####.###.#.#######.#.###", "#.#.#.#.#####.###.#.#######.#.###",
"#...#.#...#...#...#...#...#.#...#", "#...#.#...#...#...#...#...#.#...#",
"#####.###G#.###.#####.#.###.###G#", "#####.###G#.###.#####.#.###.#####",
"#pG.....GqG.#...GQG...#.......GP#", "#pG.....#q#.#...#QG...#.......GP#",
"#################################" "#################################"
], ],
"id": "006-ikebukuro", "id": "006-ikebukuro",

View File

@@ -17,7 +17,7 @@
"#.#####.#.#.#.#.#.#.#.#.#.#.###.#", "#.#####.#.#.#.#.#.#.#.#.#.#.###.#",
"#i..#.....#...#.#.#.#.#.#.#.#...#", "#i..#.....#...#.#.#.#.#.#.#.#...#",
"#.#.#.#########.#.#.#.#.#.#.#.###", "#.#.#.#########.#.#.#.#.#.#.#.###",
"#.#...#.......#.#...#..i#.#.#...#", "#.#...#.......#.#...#...#.#.#...#",
"#.#####.#####.#.#######.#.#.#####", "#.#####.#####.#.#######.#.#.#####",
"#.......#T#...#.......#.#K#.#...#", "#.......#T#...#.......#.#K#.#...#",
"#########.#.#####.###.#.###.#.#.#", "#########.#.#####.###.#.###.#.#.#",
@@ -27,9 +27,9 @@
"#.#.#####.#.###.#####.#######.#.#", "#.#.#####.#.###.#####.#######.#.#",
"#.#.....#.#.#...#...#.........#.#", "#.#.....#.#.#...#...#.........#.#",
"#.#####.#.#.#.###.#.###.#######.#", "#.#####.#.#.#.###.#.###.#######.#",
"#.......#.#.#.#...#...#.#.......#", "#i......#.#.#.#...#...#.#.......#",
"#G###.###G#.#.#.#####.###.#####G#", "#G###.#####.#.#.#####.###.#####G#",
"#pG.....Gq#.....#QG.......#...GP#", "#p#.....Gq#.....#QG...........#P#",
"#################################" "#################################"
], ],
"id": "007-akihabara", "id": "007-akihabara",
@@ -43,6 +43,7 @@
}, },
"next": "007b-ueno", "next": "007b-ueno",
"noise": 1, "noise": 1,
"oneWayGates": true,
"order": 7, "order": 7,
"party": 4, "party": 4,
"rideStops": 3, "rideStops": 3,
@@ -64,9 +65,9 @@
}, },
{ {
"text": { "text": {
"en": "<- Track 3 Hibiya Line for Naka-Meguro Track 4 Hibiya Line for Kita-Senju ->", "en": "\u003c- Track 3 Hibiya Line for Naka-Meguro Track 4 Hibiya Line for Kita-Senju -\u003e",
"ja": "← 3番線 日比谷線 中目黒方面 4番線 日比谷線 北千住方面 →", "ja": "← 3番線 日比谷線 中目黒方面 4番線 日比谷線 北千住方面 →",
"sv": "<- Spår 3 Hibiya-linjen mot Naka-Meguro Spår 4 Hibiya-linjen mot Kita-Senju ->" "sv": "\u003c- Spår 3 Hibiya-linjen mot Naka-Meguro Spår 4 Hibiya-linjen mot Kita-Senju -\u003e"
}, },
"x": 9, "x": 9,
"y": 11, "y": 11,
@@ -78,8 +79,8 @@
"ja": "上野 → 乗り換え 銀座線 浅草方面", "ja": "上野 → 乗り換え 銀座線 浅草方面",
"sv": "Ueno: byte till Ginza-linjen mot Asakusa." "sv": "Ueno: byte till Ginza-linjen mot Asakusa."
}, },
"x": 23, "x": 1,
"y": 7, "y": 17,
"z": 0 "z": 0
} }
], ],
@@ -175,6 +176,5 @@
"terminus": "tokyo", "terminus": "tokyo",
"track": "1" "track": "1"
} }
], ]
"oneWayGates": true
} }

View File

@@ -11,24 +11,24 @@
"fareYen": 170, "fareYen": 170,
"grid": [ "grid": [
"#################################", "#################################",
"#S.#..<..<..<..<..<..<..<..<..###", "#S.#.i\u003c..\u003c..\u003c..\u003c..\u003c..\u003c..\u003c..\u003c..###",
"#..#..>..>..>..>..>..>..>..>..###", "#..#..\u003e..\u003e..\u003e..\u003e..\u003e..\u003e..\u003e..\u003e..###",
"#<>################<>#######<>###", "#\u003c\u003e################\u003c\u003e#######\u003c\u003e###",
"#..#..<i.<..<..<..#..#..<..#..###", "#..#..\u003c..\u003c..\u003c..\u003c..#..#..\u003c..#..###",
"#..#..>..>..>..>W.#..#..>..#..###", "#..#..\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#\u003c\u003e#\u003c\u003e#\u003c\u003e#\u003c\u003e#########",
"#..#..<..<..#..#..#..<..#..<..###", "#..\u003c..\u003ci.\u003cT.#..#..#..\u003c..#..\u003c..###",
"#..#..>..>..#..#..#..>..#..>..###", "#..\u003e..\u003e.K\u003e..#..#..#..\u003e..#..\u003e..###",
"#<>#<>#######<>#<>#<>#<>####<>###", "#\u003c\u003e#\u003c\u003e#######\u003c\u003e#\u003c\u003e#\u003c\u003e#\u003c\u003e####\u003c\u003e###",
"#..#K.<..<..#..#..<.i#.T<..<..###", "#..#..\u003c..\u003c..#..#..\u003c.i#..\u003c..\u003c..###",
"#..#i.>..>..#..#..>..#..>..>..###", "#..#..\u003e..\u003e..#..#..\u003e..#..\u003e..\u003e..###",
"#<>#<>####<>#<>#<>##########<>###", "####\u003c\u003e####\u003c\u003e#\u003c\u003e#G\u003e##########\u003c\u003e###",
"#G.<..<..<..#..<G.<..<..<..<.G###", "#.G\u003cW.\u003c..\u003c..#..#.G#..\u003c..\u003c..\u003c.G###",
"#pG>..>..>..#..GQG>..>..>..>GP###", "#p.G..\u003e..\u003e..#..#Q.#..\u003e..\u003e..\u003eGP###",
"#################################", "##W##############################",
"#################################", "#################################",
"#################################" "#################################"
], ],
@@ -41,6 +41,7 @@
"sv": "Ueno station" "sv": "Ueno station"
}, },
"noise": 2, "noise": 2,
"oneWayGates": true,
"party": 4, "party": 4,
"rideStops": 3, "rideStops": 3,
"signs": [ "signs": [
@@ -51,14 +52,14 @@
"sv": "Ueno station. Spår 1 Ginza-linjen mot Asakusa: sydost. Spår 2 Ginza-linjen mot Shibuya: sydväst. Spår 3 Hibiya-linjen mot Kita-Senju: söderut." "sv": "Ueno station. Spår 1 Ginza-linjen mot Asakusa: sydost. Spår 2 Ginza-linjen mot Shibuya: sydväst. Spår 3 Hibiya-linjen mot Kita-Senju: söderut."
}, },
"x": 7, "x": 7,
"y": 4, "y": 10,
"z": 0 "z": 0
}, },
{ {
"text": { "text": {
"en": "<- Track 2 Ginza Line for Shibuya Track 1 Ginza Line for Asakusa ->", "en": "\u003c- Track 2 Ginza Line for Shibuya Track 1 Ginza Line for Asakusa -\u003e",
"ja": "← 2番線 銀座線 渋谷方面 1番線 銀座線 浅草方面 →", "ja": "← 2番線 銀座線 渋谷方面 1番線 銀座線 浅草方面 →",
"sv": "<- Spår 2 Ginza-linjen mot Shibuya Spår 1 Ginza-linjen mot Asakusa ->" "sv": "\u003c- Spår 2 Ginza-linjen mot Shibuya Spår 1 Ginza-linjen mot Asakusa -\u003e"
}, },
"x": 20, "x": 20,
"y": 13, "y": 13,
@@ -70,8 +71,8 @@
"ja": "1番線 銀座線 浅草方面 → 南東口 改札", "ja": "1番線 銀座線 浅草方面 → 南東口 改札",
"sv": "Spår 1 Ginza-linjen mot Asakusa: spärren i sydöstra hörnet." "sv": "Spår 1 Ginza-linjen mot Asakusa: spärren i sydöstra hörnet."
}, },
"x": 4, "x": 5,
"y": 14, "y": 1,
"z": 0 "z": 0
} }
], ],
@@ -144,6 +145,5 @@
"terminus": "kita-senju", "terminus": "kita-senju",
"track": "3" "track": "3"
} }
], ]
"oneWayGates": true
} }

View File

@@ -55,9 +55,9 @@
"#.#######.#######.#.###.#.#.###.#", "#.#######.#######.#.###.#.#.###.#",
"#.....#..........i..#...#...#...#", "#.....#..........i..#...#...#...#",
"#####.###.###########.#######.###", "#####.###.###########.#######.###",
"#...#...#.#...........#.....#...#", "#...#...#.#.................#...#",
"#G#.###.###.###########.#######G#", "#G#.###.###.#.#########.#######G#",
"#p#....iGqG.#...GQG...........GP#", "#p#....iGq#.#...#QG...........#P#",
"#################################" "#################################"
], ],
"name": "1F" "name": "1F"
@@ -74,6 +74,7 @@
}, },
"next": "008b-nippori", "next": "008b-nippori",
"noise": 2, "noise": 2,
"oneWayGates": true,
"order": 8, "order": 8,
"party": 4, "party": 4,
"rewards": { "rewards": {
@@ -99,9 +100,9 @@
}, },
{ {
"text": { "text": {
"en": "<- Track 5 Yamanote Line for Shinagawa Track 4 Yamanote Line for Ueno ->", "en": "\u003c- Track 5 Yamanote Line for Shinagawa Track 4 Yamanote Line for Ueno -\u003e",
"ja": "← 5番線 山手線 品川方面 4番線 山手線 上野方面 →", "ja": "← 5番線 山手線 品川方面 4番線 山手線 上野方面 →",
"sv": "<- Spår 5 Yamanote-linjen mot Shinagawa Spår 4 Yamanote-linjen mot Ueno ->" "sv": "\u003c- Spår 5 Yamanote-linjen mot Shinagawa Spår 4 Yamanote-linjen mot Ueno -\u003e"
}, },
"x": 7, "x": 7,
"y": 19, "y": 19,
@@ -210,6 +211,5 @@
"terminus": "takao", "terminus": "takao",
"track": "1" "track": "1"
} }
], ]
"oneWayGates": true
} }

View File

@@ -11,25 +11,25 @@
"fareYen": 500, "fareYen": 500,
"grid": [ "grid": [
"#################################", "#################################",
"#S#W....#.....#.....#...#.......#", "#S#.....#.....#.....#...#.......#",
"#.#####.#.###.#.#.#.#.#.#.#.#.#.#", "#.#####.#.###.#.#.#.#.#.#.#.#.#.#",
"#.....#.#...#...#.....#...#.#...#", "#.....#.#...#...#.....#...#.#...#",
"#####.#.###.###############.#####", "#####.#.###.###############.#####",
"#K#...#...#...#.......#...#.....#", "#T#...#...#...#.......#...#.....#",
"#.#.#####.###.#.#.###.#.#.#####.#", "#.#.#####.###.#.#.###.#.#.#####.#",
"#.#.#...#.....#.#....i#.#...#...#", "#.#.#...#.....#.#.....#.#...#...#",
"#.#.#.#.###.###.#####.#.###.#.#.#", "#.#.#.#.###.###.#####.#.###.#.#.#",
"#.#...#...#...#.....#...#.#...#.#", "#.#..i#...#...#.....#...#K#...#.#",
"#.#######.#######.#.#####.#####.#", "#.###.###.#######.#.#####.#####.#",
"#...#...#.#.....#.#.#.#.......#.#", "#...#...#.#.....#.#.#.#..i....#.#",
"#.#.###.#.#.###.#.#.#.#.#####.#.#", "#.#.###.#.#.###.#.#.#.#.#####.#.#",
"#.#.....#...#T#i..#.#...#.....#.#", "#.#.....#...#.#...#.....#.....#.#",
"#.#.#########.#.###.#####.#####.#", "#.#.#########.#.###.#####.#####.#",
"#.#.#.....#..i..#...#.....#.....#", "#.#.#.....#.....#...#.....#.....#",
"#.###.#.#.#.#####.###.###.#.#####", "#.###.#.#.#.#####.###.###.#.#####",
"#.....#.#.#.......#...#...#.....#", "#i....#.#.#.......#...#...#...G.#",
"#G#####.#.#########.#.#########G#", "#G#####.#.#########G#.########W.#",
"#pG...#.........GQG.#.........GP#", "#p#.............#Q..#.........#P#",
"#################################" "#################################"
], ],
"id": "008b-nippori", "id": "008b-nippori",
@@ -41,6 +41,7 @@
"sv": "Nippori station" "sv": "Nippori station"
}, },
"noise": 1, "noise": 1,
"oneWayGates": true,
"party": 4, "party": 4,
"rideStops": 4, "rideStops": 4,
"seatSelection": true, "seatSelection": true,
@@ -51,18 +52,18 @@
"ja": "日暮里駅 1番線 京成本線 成田空港方面 → 南東 2番線 京成本線 京成上野方面 → 南西 11番線 山手線 上野方面 → 南", "ja": "日暮里駅 1番線 京成本線 成田空港方面 → 南東 2番線 京成本線 京成上野方面 → 南西 11番線 山手線 上野方面 → 南",
"sv": "Nippori station. Spår 1 Keisei-linjen mot Narita flygplats: sydost. Spår 2 Keisei-linjen mot Keisei-Ueno: sydväst. Spår 11 Yamanote-linjen mot Ueno: söderut. Biljett krävs." "sv": "Nippori station. Spår 1 Keisei-linjen mot Narita flygplats: sydost. Spår 2 Keisei-linjen mot Keisei-Ueno: sydväst. Spår 11 Yamanote-linjen mot Ueno: söderut. Biljett krävs."
}, },
"x": 15, "x": 5,
"y": 13, "y": 9,
"z": 0 "z": 0
}, },
{ {
"text": { "text": {
"en": "<- Track 2 Keisei Line for Keisei-Ueno Track 1 Keisei Line for Narita Airport ->", "en": "\u003c- Track 2 Keisei Line for Keisei-Ueno Track 1 Keisei Line for Narita Airport -\u003e",
"ja": "← 2番線 京成本線 京成上野方面 1番線 京成本線 成田空港方面 →", "ja": "← 2番線 京成本線 京成上野方面 1番線 京成本線 成田空港方面 →",
"sv": "<- Spår 2 Keisei-linjen mot Keisei-Ueno Spår 1 Keisei-linjen mot Narita flygplats ->" "sv": "\u003c- Spår 2 Keisei-linjen mot Keisei-Ueno Spår 1 Keisei-linjen mot Narita flygplats -\u003e"
}, },
"x": 13, "x": 1,
"y": 15, "y": 17,
"z": 0 "z": 0
}, },
{ {
@@ -71,8 +72,8 @@
"ja": "成田空港 → 1番線 切符 → 券売機", "ja": "成田空港 → 1番線 切符 → 券売機",
"sv": "Narita flygplats: spår 1. Biljetter i automaten." "sv": "Narita flygplats: spår 1. Biljetter i automaten."
}, },
"x": 21, "x": 25,
"y": 7, "y": 11,
"z": 0 "z": 0
} }
], ],
@@ -146,6 +147,5 @@
"terminus": "ueno", "terminus": "ueno",
"track": "11" "track": "11"
} }
], ]
"oneWayGates": true
} }

View File

@@ -87,6 +87,10 @@ func report(l *level.Level, all []*level.Level, out string) (problems int) {
for _, tr := range l.Trains { for _, tr := range l.Trains {
pts := l.Find(level.Tile(tr.Platform[0])) pts := l.Find(level.Tile(tr.Platform[0]))
dist := len(l.Path(l.SpawnPoint(), pts[0], l.CanEnter)) - 1 dist := len(l.Path(l.SpawnPoint(), pts[0], l.CanEnter)) - 1
if l.PathOutside(l.SpawnPoint(), pts[0], l.CanEnter) == nil {
fmt.Printf(" FEL: perrong %s nås bara genom ett annat betalt område\n", tr.Platform)
problems++
}
v := "correct=" + fmt.Sprint(tr.Correct) v := "correct=" + fmt.Sprint(tr.Correct)
if tr.Verdict != nav.Unjudged { if tr.Verdict != nav.Unjudged {
v = tr.Verdict.String() v = tr.Verdict.String()
@@ -100,7 +104,16 @@ func report(l *level.Level, all []*level.Level, out string) (problems int) {
} }
fmt.Printf(" resa: %s\n", strings.Join(ids, " → ")) fmt.Printf(" resa: %s\n", strings.Join(ids, " → "))
} }
fmt.Printf(" skyltar %d, utforskbart %d rutor\n", len(l.Signs), len(reach)) inside := l.InsideGates()
tin, tout := 0, 0
for _, p := range l.Find(level.Toilet) {
if inside[p] {
tin++
} else {
tout++
}
}
fmt.Printf(" skyltar %d, utforskbart %d rutor, betalt område %d rutor, toaletter innanför %d / utanför %d\n", len(l.Signs), len(reach), len(inside), tin, tout)
for z := 0; z < l.NumFloors(); z++ { for z := 0; z < l.NumFloors(); z++ {
name := fmt.Sprintf("%s/%s-%d.png", out, l.ID, z) name := fmt.Sprintf("%s/%s-%d.png", out, l.ID, z)
fail(savePNG(name, draw(l, z, reach))) fail(savePNG(name, draw(l, z, reach)))

View File

@@ -134,6 +134,12 @@ func buildShots(levelsDir string) error {
if err != nil { if err != nil {
return err return err
} }
// Från perrongen i Ueno tillbaka till första spärren, och riktningen ut
// genom den (som spärren utan återvändo ska vägra).
toGateBack, pushOut, err := backToGate(l5, l5.CorrectPlatform())
if err != nil {
return err
}
// Skyltbilderna tas med några kända ord så att markeringen och den // 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. // delvisa översättningen syns; hjälpen lär sedan in nästa ord.
known := []string{"-know", "改札,番線,方面", "-seed", "7"} known := []string{"-know", "改札,番線,方面", "-seed", "7"}
@@ -159,14 +165,42 @@ func buildShots(levelsDir string) error {
[]string{"-seed", "7", "-mode", "list", "-know", "上野,番線,方面,乗り換え"}}, []string{"-seed", "7", "-mode", "list", "-know", "上野,番線,方面,乗り換え"}},
{"", "wait 150; shot $OUT/ending.png; press enter; wait 5; shot $OUT/ending-summary.png; press enter; wait 250; shot $OUT/credits.png; quit", []string{"-ending", "-save", "$DEMO"}}, {"", "wait 150; shot $OUT/ending.png; press enter; wait 5; shot $OUT/ending-summary.png; press enter; wait 250; shot $OUT/credits.png; quit", []string{"-ending", "-save", "$DEMO"}},
{"007-akihabara", toKiosk7 + "; urge Ville; press enter; wait 5; press right; wait 3; shot $OUT/payer.png; quit", []string{"-seed", "7"}}, {"007-akihabara", toKiosk7 + "; urge Ville; press enter; wait 5; press right; wait 3; shot $OUT/payer.png; quit", []string{"-seed", "7"}},
{"005-ueno", toPlatform5 + "; move left 1; wait 5; push left 30; wait 3; shot $OUT/no-exit.png; press enter; wait 150; shot $OUT/staff.png; quit", []string{"-seed", "7"}}, {"005-ueno", toPlatform5 + "; " + toGateBack + "; wait 5; push " + pushOut + " 30; wait 3; shot $OUT/no-exit.png; press enter; wait 150; shot $OUT/staff.png; quit", []string{"-seed", "7"}},
} }
return nil return nil
} }
// moves gör om kortaste vägen mellan två rutor till "move DIR N"-steg. // moves gör om kortaste vägen mellan två rutor till "move DIR N"-steg.
// backToGate ger stegen från perrongen till första spärren på vägen ut och
// riktningen vidare ut genom den.
func backToGate(l *level.Level, from level.Point) (string, string, error) {
path := l.PathOutside(from, l.SpawnPoint(), l.CanEnter)
gi := 0
for gi < len(path) && l.At(path[gi]) != level.Gate {
gi++
}
if gi >= len(path)-1 {
return "", "", fmt.Errorf("ingen spärr på vägen ut från %v i %s", from, l.ID)
}
m, err := moves(l, from, path[gi])
if err != nil {
return "", "", err
}
g, o := path[gi], path[gi+1]
dir := "right"
switch {
case o.X < g.X:
dir = "left"
case o.Y < g.Y:
dir = "up"
case o.Y > g.Y:
dir = "down"
}
return m, dir, nil
}
func moves(l *level.Level, from, to level.Point) (string, error) { func moves(l *level.Level, from, to level.Point) (string, error) {
path := l.Path(from, to, func(p level.Point) bool { // andra perronger är inga genvägar (spärrar utan återvändo) path := l.PathOutside(from, to, func(p level.Point) bool { // andra perronger och deras betalda områden är inga genvägar
return l.CanEnter(p) && (!l.At(p).IsPlatform() || p == to) return l.CanEnter(p) && (!l.At(p).IsPlatform() || p == to)
}) })
if path == nil { if path == nil {