M9a: stationer i flera våningar med trappor
All checks were successful
release / build-release (push) Successful in 1m34s
All checks were successful
release / build-release (push) Successful in 1m34s
- level: Point har Z; banor kan ha floors (lika stora rutnät med namn), gamla grid-banor läses som en våning; trappor U/D på samma x,y en våning upp/ner valideras parvis; Neighbors ger grannar över våningar så att Path, lösbarhet, spärregel och Unreachable fungerar; skyltar har z - sim: att kliva på en trappa flyttar spelaren till andra änden (FloorChanges); dimman är per våning och avslöjar trappans andra ände; tåg hörs/syns bara på egen våning - game: bara spelarens våning ritas (följare på andra våningar döljs), HUD och minikarta visar våningens namn, trappgrafik i genart och färg på minikartan - bana 004 Tokyo (svårighet 3, tre i sällskapet): start under mark på B1, trappa upp till 1F med perronger; generatorn gör våningar med trappor som flaskhalsar; ordboken utökad - testbotar och tools/shots förstår trappor; två våningsbilder - docs/README/wiki uppdaterade Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -9,23 +9,24 @@ const FogRadius = 3
|
||||
// Fog håller reda på vilka rutor spelaren har sett i den här omgången.
|
||||
// Minikartan ritar bara sedda rutor plus riktmärken som alltid syns.
|
||||
type Fog struct {
|
||||
w, h int
|
||||
w, h, floors int
|
||||
seen []bool
|
||||
Radius int
|
||||
count int
|
||||
ExtraLandmarks bool // toaletter, kiosker och automater syns alltid (skillen Lokalkännedom)
|
||||
}
|
||||
|
||||
// NewFog skapar en tom dimma för banan.
|
||||
// NewFog skapar en tom dimma för banan (alla våningar).
|
||||
func NewFog(l *level.Level, radius int) *Fog {
|
||||
return &Fog{w: l.Width(), h: l.Height(), seen: make([]bool, l.Width()*l.Height()), Radius: radius}
|
||||
n := max(1, l.NumFloors())
|
||||
return &Fog{w: l.Width(), h: l.Height(), floors: n, seen: make([]bool, l.Width()*l.Height()*n), Radius: radius}
|
||||
}
|
||||
|
||||
func (f *Fog) idx(p level.Point) (int, bool) {
|
||||
if p.X < 0 || p.Y < 0 || p.X >= f.w || p.Y >= f.h {
|
||||
if p.X < 0 || p.Y < 0 || p.Z < 0 || p.X >= f.w || p.Y >= f.h || p.Z >= f.floors {
|
||||
return 0, false
|
||||
}
|
||||
return p.Y*f.w + p.X, true
|
||||
return (p.Z*f.h+p.Y)*f.w + p.X, true
|
||||
}
|
||||
|
||||
// Seen rapporterar om rutan har setts.
|
||||
@@ -41,8 +42,9 @@ func (f *Fog) mark(p level.Point) {
|
||||
}
|
||||
}
|
||||
|
||||
// Reveal avslöjar allt inom Radius steg från from via passerbara rutor,
|
||||
// samt väggarna som gränsar till dem (så att gångarnas form syns).
|
||||
// Reveal avslöjar allt inom Radius steg från from via passerbara rutor på
|
||||
// samma våning, samt väggarna som gränsar till dem (så att gångarnas form
|
||||
// syns). Trappans andra ände avslöjas också.
|
||||
func (f *Fog) Reveal(l *level.Level, from level.Point) {
|
||||
dist := map[level.Point]int{from: 0}
|
||||
queue := []level.Point{from}
|
||||
@@ -50,10 +52,16 @@ func (f *Fog) Reveal(l *level.Level, from level.Point) {
|
||||
p := queue[0]
|
||||
queue = queue[1:]
|
||||
f.mark(p)
|
||||
switch l.At(p) {
|
||||
case level.StairsUp:
|
||||
f.mark(p.Up())
|
||||
case level.StairsDown:
|
||||
f.mark(p.Down())
|
||||
}
|
||||
for dx := -1; dx <= 1; dx++ {
|
||||
for dy := -1; dy <= 1; dy++ {
|
||||
n := level.Point{X: p.X + dx, Y: p.Y + dy}
|
||||
if !l.At(n.X, n.Y).Passable() {
|
||||
n := level.Point{X: p.X + dx, Y: p.Y + dy, Z: p.Z}
|
||||
if !l.At(n).Passable() {
|
||||
f.mark(n) // gränsande vägg
|
||||
}
|
||||
}
|
||||
@@ -62,8 +70,8 @@ func (f *Fog) Reveal(l *level.Level, from level.Point) {
|
||||
continue
|
||||
}
|
||||
for _, d := range [...]level.Point{{X: 1}, {X: -1}, {Y: 1}, {Y: -1}} {
|
||||
n := level.Point{X: p.X + d.X, Y: p.Y + d.Y}
|
||||
if _, ok := dist[n]; ok || !l.At(n.X, n.Y).Passable() {
|
||||
n := level.Point{X: p.X + d.X, Y: p.Y + d.Y, Z: p.Z}
|
||||
if _, ok := dist[n]; ok || !l.At(n).Passable() {
|
||||
continue
|
||||
}
|
||||
dist[n] = dist[p] + 1
|
||||
@@ -75,8 +83,8 @@ func (f *Fog) Reveal(l *level.Level, from level.Point) {
|
||||
// Landmark är rutor som alltid syns på minikartan: perrongerna (avgångarna),
|
||||
// starten och spärrarna – det man orienterar sig efter.
|
||||
func Landmark(l *level.Level, p level.Point) bool {
|
||||
switch l.At(p.X, p.Y) {
|
||||
case level.Platform, level.WrongPlatform, level.Spawn, level.Gate:
|
||||
switch l.At(p) {
|
||||
case level.Platform, level.WrongPlatform, level.Spawn, level.Gate, level.StairsUp, level.StairsDown:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -88,7 +96,7 @@ func (f *Fog) Visible(l *level.Level, p level.Point) bool {
|
||||
return true
|
||||
}
|
||||
if f.ExtraLandmarks {
|
||||
switch l.At(p.X, p.Y) {
|
||||
switch l.At(p) {
|
||||
case level.Toilet, level.Kiosk, level.TicketMachine:
|
||||
return true
|
||||
}
|
||||
@@ -98,9 +106,11 @@ func (f *Fog) Visible(l *level.Level, p level.Point) bool {
|
||||
|
||||
// RevealAll avslöjar hela banan (kartan köpt som item).
|
||||
func (f *Fog) RevealAll(l *level.Level) {
|
||||
for y := 0; y < l.Height(); y++ {
|
||||
for x := 0; x < l.Width(); x++ {
|
||||
f.mark(level.Point{X: x, Y: y})
|
||||
for z := 0; z < l.NumFloors(); z++ {
|
||||
for y := 0; y < l.Height(); y++ {
|
||||
for x := 0; x < l.Width(); x++ {
|
||||
f.mark(level.Point{X: x, Y: y, Z: z})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,14 +121,17 @@ func (f *Fog) Count() int { return f.count }
|
||||
// Explored är andelen passerbara rutor som setts, 0..1.
|
||||
func (f *Fog) Explored(l *level.Level) float64 {
|
||||
total, seen := 0, 0
|
||||
for y := 0; y < l.Height(); y++ {
|
||||
for x := 0; x < l.Width(); x++ {
|
||||
if !l.At(x, y).Passable() {
|
||||
continue
|
||||
}
|
||||
total++
|
||||
if f.Seen(level.Point{X: x, Y: y}) {
|
||||
seen++
|
||||
for z := 0; z < l.NumFloors(); z++ {
|
||||
for y := 0; y < l.Height(); y++ {
|
||||
for x := 0; x < l.Width(); x++ {
|
||||
p := level.Point{X: x, Y: y, Z: z}
|
||||
if !l.At(p).Passable() {
|
||||
continue
|
||||
}
|
||||
total++
|
||||
if f.Seen(p) {
|
||||
seen++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func (m *Mover) Step(dt float64, want Dir, canEnter func(level.Point) bool) int
|
||||
}
|
||||
m.Facing = want
|
||||
dx, dy := want.Delta()
|
||||
next := level.Point{X: m.Tile.X + dx, Y: m.Tile.Y + dy}
|
||||
next := level.Point{X: m.Tile.X + dx, Y: m.Tile.Y + dy, Z: m.Tile.Z}
|
||||
if !canEnter(next) {
|
||||
return arrived
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ type Run struct {
|
||||
Progressed []string // ord som fick framsteg vid senaste läsningen, "ord n/3"
|
||||
ProgressedAt float64 // < 0 = aldrig
|
||||
|
||||
FloorChanges int // antal trappbyten (statistik)
|
||||
|
||||
// Resultat när banan vunnits.
|
||||
Stars int // 1–3 efter tid mot par
|
||||
PointsEarned int // språkpoäng
|
||||
@@ -266,7 +268,7 @@ func (r *Run) StarsFor(t float64) int {
|
||||
// kräver biljett (på biljettbanor) eller ett användbart kort med saldo (att
|
||||
// gå mellan två spärrutor är gratis).
|
||||
func (r *Run) CanEnter(p level.Point) bool {
|
||||
t := r.Level.At(p.X, p.Y)
|
||||
t := r.Level.At(p)
|
||||
if !t.Passable() {
|
||||
return false
|
||||
}
|
||||
@@ -315,7 +317,7 @@ func (r *Run) MembersWithoutFare() []*Member {
|
||||
}
|
||||
|
||||
func (r *Run) inGate() bool {
|
||||
return r.Level.At(r.Player.Tile.X, r.Player.Tile.Y) == level.Gate
|
||||
return r.Level.At(r.Player.Tile) == level.Gate
|
||||
}
|
||||
|
||||
// Step för banan framåt dt sekunder. want är hållen riktning, board=true
|
||||
@@ -342,6 +344,18 @@ func (r *Run) Step(dt float64, want Dir, board bool) {
|
||||
}
|
||||
r.Steps += r.Player.Step(dt, want, r.CanEnter)
|
||||
if r.Player.Tile != before {
|
||||
// Trappa: hamnar man på en trappruta flyttas man till andra änden
|
||||
// (samma x, y, en våning upp eller ner) och fortsätter därifrån.
|
||||
switch r.Level.At(r.Player.Tile) {
|
||||
case level.StairsUp:
|
||||
r.Player.Tile = r.Player.Tile.Up()
|
||||
r.Player.Target = r.Player.Tile
|
||||
r.FloorChanges++
|
||||
case level.StairsDown:
|
||||
r.Player.Tile = r.Player.Tile.Down()
|
||||
r.Player.Target = r.Player.Tile
|
||||
r.FloorChanges++
|
||||
}
|
||||
r.Party.OnLeaderArrived(r.Player.Tile)
|
||||
r.Fog.Reveal(r.Level, r.Player.Tile)
|
||||
}
|
||||
@@ -372,7 +386,7 @@ func (r *Run) Step(dt float64, want Dir, board bool) {
|
||||
}
|
||||
}
|
||||
if board && !r.Player.Moving() {
|
||||
switch r.Level.At(r.Player.Tile.X, r.Player.Tile.Y) {
|
||||
switch r.Level.At(r.Player.Tile) {
|
||||
case level.Toilet:
|
||||
r.useToilet()
|
||||
case level.Kiosk:
|
||||
@@ -652,7 +666,7 @@ func (r *Run) FacilityProgress() float64 {
|
||||
// spelaren inte står på en perrong), om det är inne, och sekunder kvar
|
||||
// till avgång respektive ankomst.
|
||||
func (r *Run) PlatformHere() (tr *level.Train, atPlatform bool, seconds float64) {
|
||||
t := r.Level.At(r.Player.Tile.X, r.Player.Tile.Y)
|
||||
t := r.Level.At(r.Player.Tile)
|
||||
if t != level.Platform && t != level.WrongPlatform {
|
||||
return nil, false, 0
|
||||
}
|
||||
@@ -679,6 +693,9 @@ func (r *Run) NearTrack(maxDist int) (tr *level.Train, platform level.Point, pha
|
||||
best := maxDist + 1
|
||||
for _, t := range []level.Tile{level.Platform, level.WrongPlatform} {
|
||||
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
|
||||
}
|
||||
d := max(abs(p.X-r.Player.Tile.X), abs(p.Y-r.Player.Tile.Y))
|
||||
if d < best {
|
||||
if cand := r.Level.TrainAt(t); cand != nil {
|
||||
|
||||
@@ -85,21 +85,29 @@ func dirTo(from, to level.Point) Dir {
|
||||
// walk låter en bot följa en väg ruta för ruta, en bildruta i taget. Tangenten
|
||||
// släpps så fort steget mot nästa ruta påbörjats, så att boten stannar exakt
|
||||
// där även när farten inte är bildruteexakt (telefon- eller sällskapsfaktor).
|
||||
// Trappor: att kliva på en trappruta flyttar en till andra änden i samma
|
||||
// steg, så vägens nästa punkt (motparten) är redan nådd.
|
||||
func walk(t *testing.T, r *Run, path []level.Point) {
|
||||
t.Helper()
|
||||
for _, next := range path[1:] {
|
||||
d := dirTo(r.Player.Tile, next)
|
||||
for i := 0; i < 600; i++ {
|
||||
if r.Player.Tile == next && !r.Player.Moving() {
|
||||
break
|
||||
}
|
||||
if r.Player.Target == next || r.Player.Tile == next {
|
||||
for idx := 1; idx < len(path); idx++ {
|
||||
next := path[idx]
|
||||
cur := r.Player.Tile
|
||||
if next.Z != cur.Z && next.X == cur.X && next.Y == cur.Y {
|
||||
continue // våningsbytet skedde när vi klev på trappan
|
||||
}
|
||||
d := dirTo(cur, next)
|
||||
arrived := func() bool {
|
||||
p := r.Player.Tile
|
||||
return !r.Player.Moving() && (p == next || (r.Level.At(next).IsStairs() && p.X == next.X && p.Y == next.Y))
|
||||
}
|
||||
for i := 0; i < 600 && !arrived(); i++ {
|
||||
if r.Player.Target == next || r.Player.Tile == next || (r.Player.Tile.X == next.X && r.Player.Tile.Y == next.Y) {
|
||||
r.Step(frame, None, false)
|
||||
} else {
|
||||
r.Step(frame, d, false)
|
||||
}
|
||||
}
|
||||
if r.Player.Tile != next || r.Player.Moving() {
|
||||
if !arrived() {
|
||||
t.Fatalf("boten fastnade på %v på väg mot %v", r.Player.Tile, next)
|
||||
}
|
||||
}
|
||||
@@ -203,7 +211,7 @@ func TestGateNeedsCardBalance(t *testing.T) {
|
||||
r.Player.Tile, r.Player.Target = gate, gate
|
||||
r.Wallet.Card = 0
|
||||
other := level.Point{X: gate.X + 1, Y: gate.Y}
|
||||
if l.At(other.X, other.Y) == level.Gate && !r.CanEnter(other) {
|
||||
if l.At(other) == level.Gate && !r.CanEnter(other) {
|
||||
t.Fatal("att gå mellan två spärrutor ska inte kosta")
|
||||
}
|
||||
}
|
||||
@@ -635,7 +643,7 @@ func TestPhoneSpeedRescanAndBattery(t *testing.T) {
|
||||
gate := l.Find(level.Gate)[0]
|
||||
path := l.Path(r.Player.Tile, gate, r.CanEnter)
|
||||
for i, p := range path { // stanna på den första spärrutan – där sker skanningen
|
||||
if l.At(p.X, p.Y) == level.Gate {
|
||||
if l.At(p) == level.Gate {
|
||||
path = path[:i+1]
|
||||
break
|
||||
}
|
||||
@@ -810,7 +818,7 @@ func TestIndividualCardsAtGateAndMachine(t *testing.T) {
|
||||
before := r.Wallet.Spent
|
||||
path := l1.Path(r.Player.Tile, gate, r.CanEnter)
|
||||
for i, p := range path {
|
||||
if l1.At(p.X, p.Y) == level.Gate {
|
||||
if l1.At(p) == level.Gate {
|
||||
path = path[:i+1]
|
||||
break
|
||||
}
|
||||
@@ -981,7 +989,7 @@ func TestRunOptionsFromProgression(t *testing.T) {
|
||||
}
|
||||
path := l.Path(r.Player.Tile, gate, r.CanEnter)
|
||||
for i, p := range path {
|
||||
if l.At(p.X, p.Y) == level.Gate {
|
||||
if l.At(p) == level.Gate {
|
||||
path = path[:i+1]
|
||||
break
|
||||
}
|
||||
@@ -1001,3 +1009,58 @@ func TestRunOptionsFromProgression(t *testing.T) {
|
||||
t.Fatal("landmärken från skillen")
|
||||
}
|
||||
}
|
||||
|
||||
const stairsLevel = `{
|
||||
"id": "stairs", "name": {"sv": "Trappor"}, "difficulty": 1, "destination": {"sv": "x", "ja": "新宿"},
|
||||
"floors": [
|
||||
{"name": "B1", "grid": ["#######", "#S..U.#", "#######"]},
|
||||
{"name": "1F", "grid": ["#######", "#.P.D.#", "#######"]}
|
||||
],
|
||||
"trains": [{"platform": "P", "track": "1", "line": {"en": "A"}, "destination": {"en": "B", "ja": "新宿"}, "correct": true}]
|
||||
}`
|
||||
|
||||
func TestStairsMoveBetweenFloors(t *testing.T) {
|
||||
l, err := level.Parse([]byte(stairsLevel))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := NewRun(l, RunOptions{Seed: rng.FromInt(1)})
|
||||
if r.Player.Tile.Z != 0 || r.Fog.Seen(level.Point{X: 2, Y: 1, Z: 1}) {
|
||||
t.Fatal("startar på B1; 1F är inte sedd än (utom trappans ände)")
|
||||
}
|
||||
walk(t, r, []level.Point{pt(1, 1), pt(2, 1), pt(3, 1)})
|
||||
if !r.Fog.Seen(level.Point{X: 4, Y: 1, Z: 1}) {
|
||||
t.Fatal("nära trappan avslöjas dess andra ände")
|
||||
}
|
||||
// Gå in på trappan: hamnar på 1F i trappan ner.
|
||||
for i := 0; i < 120 && r.Player.Tile.Z == 0; i++ {
|
||||
r.Step(frame, Right, false)
|
||||
}
|
||||
if r.Player.Tile != (level.Point{X: 4, Y: 1, Z: 1}) || r.Player.Moving() || r.FloorChanges != 1 {
|
||||
t.Fatalf("efter trappan: %v moving=%v changes=%d", r.Player.Tile, r.Player.Moving(), r.FloorChanges)
|
||||
}
|
||||
if _, _, _, _, _, ok := r.NearTrack(2); !ok {
|
||||
t.Fatal("på 1F är perrongen nära")
|
||||
}
|
||||
// Dimman på 1F är nu avslöjad runt trappan.
|
||||
if !r.Fog.Seen(level.Point{X: 2, Y: 1, Z: 1}) {
|
||||
t.Fatal("dimman på 1F ska lätta runt trappan")
|
||||
}
|
||||
// Tillbaka ner: kliv av trappan och gå på den igen.
|
||||
walk(t, r, []level.Point{r.Player.Tile, {X: 3, Y: 1, Z: 1}})
|
||||
walk(t, r, []level.Point{{X: 3, Y: 1, Z: 1}, {X: 4, Y: 1, Z: 1}})
|
||||
if r.Player.Tile != (level.Point{X: 4, Y: 1, Z: 0}) || r.FloorChanges != 2 {
|
||||
t.Fatalf("tillbaka till B1: %v changes=%d", r.Player.Tile, r.FloorChanges)
|
||||
}
|
||||
// Hela vägen till perrongen med BFS över våningar. Man står i trappans
|
||||
// nedre ände, så kliv av först – trappan tar en när man kliver PÅ den.
|
||||
walk(t, r, []level.Point{r.Player.Tile, {X: 3, Y: 1, Z: 0}})
|
||||
path := l.Path(r.Player.Tile, l.CorrectPlatform(), r.CanEnter)
|
||||
if path == nil {
|
||||
t.Fatal("ingen väg")
|
||||
}
|
||||
walk(t, r, path)
|
||||
if r.Player.Tile != l.CorrectPlatform() {
|
||||
t.Fatalf("boten nådde inte perrongen: %v", r.Player.Tile)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user