M10d: toaletter innanför och utanför spärrarna
All checks were successful
release / build-release (push) Successful in 1m38s
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:
@@ -645,6 +645,32 @@ func (l *Level) InsideGates() map[Point]bool {
|
||||
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
|
||||
// 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.
|
||||
|
||||
@@ -138,6 +138,12 @@ func TestAllShippedLevels(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
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 {
|
||||
if tr.Track == "" {
|
||||
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) {
|
||||
src := `{"id":"ow","name":{"sv":"x"},"difficulty":1,"destination":{"sv":"x"},"oneWayGates":true,
|
||||
"grid":["#########","#S.G.P.G#","#########"],
|
||||
@@ -758,3 +801,40 @@ func TestOneWayGatesAndInside(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
// 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).
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
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 {
|
||||
t.Fatalf("spärren igen: inGate=%v passes=%d card=%d", r.inGate(), r.GatePasses, r.Wallet.Card)
|
||||
}
|
||||
gate := r.Player.Tile
|
||||
stepUntilStill(r, Left) // ut? nej
|
||||
gate, outside := r.Player.Tile, back[gi+1]
|
||||
out := dirTo(gate, outside)
|
||||
stepUntilStill(r, out) // ut? nej
|
||||
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)
|
||||
}
|
||||
@@ -119,15 +129,15 @@ func TestOneWayGatesNeedStaff(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
stepUntilStill(r, Left)
|
||||
if r.Player.Tile == gate || r.CheckedIn() || r.StaffUnlocked {
|
||||
stepUntilStill(r, out)
|
||||
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)
|
||||
}
|
||||
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 {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user