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:
@@ -31,13 +31,19 @@ const (
|
||||
Sign Tile = 'i' // skylt
|
||||
Toilet Tile = 'W' // toalett (sällskapets toamätare)
|
||||
Kiosk Tile = 'K' // kiosk/automat för mat och dryck
|
||||
StairsUp Tile = 'U' // trappa upp: leder till 'D' på samma x,y en våning upp
|
||||
StairsDown Tile = 'D' // trappa ner: leder till 'U' på samma x,y en våning ner
|
||||
)
|
||||
|
||||
var knownTiles = map[Tile]bool{
|
||||
Wall: true, Floor: true, Spawn: true, Platform: true, WrongPlatform: true,
|
||||
Gate: true, TicketMachine: true, Sign: true, Toilet: true, Kiosk: true,
|
||||
StairsUp: true, StairsDown: true,
|
||||
}
|
||||
|
||||
// IsStairs rapporterar om rutan är en trappa.
|
||||
func (t Tile) IsStairs() bool { return t == StairsUp || t == StairsDown }
|
||||
|
||||
// Passable rapporterar om en ruta går att gå på (utan hänsyn till spärrar).
|
||||
func (t Tile) Passable() bool { return t != Wall }
|
||||
|
||||
@@ -169,9 +175,17 @@ type Train struct {
|
||||
type SignText struct {
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
Z int `json:"z"` // våning (0 = första i floors)
|
||||
Text Text `json:"text"`
|
||||
}
|
||||
|
||||
// Storey är en våning: ett rutnät med namn ("B1", "1F", "2F").
|
||||
type Storey struct {
|
||||
Name string `json:"name"`
|
||||
Grid []string `json:"grid"`
|
||||
Floors []Storey `json:"floors,omitempty"` // flervåningsbana; alla våningar lika stora
|
||||
}
|
||||
|
||||
// Rewards är vad en klarad bana ger utöver poäng.
|
||||
type Rewards struct {
|
||||
Items []string `json:"items"` // item-id:n (t.ex. skins) som delas ut första gången
|
||||
@@ -187,24 +201,32 @@ type Level struct {
|
||||
Rewards Rewards `json:"rewards"`
|
||||
Shop []string `json:"shop"` // items som kiosken säljer på banan (tom = seedat urval)
|
||||
Name Text `json:"name"`
|
||||
Difficulty int `json:"difficulty"` // 1 (lätt) … 5 (svår)
|
||||
Destination Text `json:"destination"` // dit spelaren ska
|
||||
TimeLimitSec int `json:"timeLimitSec"` // 0 = ingen tidsgräns
|
||||
ParSec int `json:"parSec"` // "bra tid" för tre stjärnor (0 = räknas ut från kortaste vägen)
|
||||
Party int `json:"party"` // antal i sällskapet inkl. spelaren (0 = 1)
|
||||
TicketRequired bool `json:"ticketRequired"` // spärren kräver pappersbiljett (köps i automaten) i stället för kort
|
||||
RideStops int `json:"rideStops"` // antal stationer på tågresan (0 = DefaultRideStops)
|
||||
MaxMissedTrains int `json:"maxMissedTrains"` // >0: fler missade tåg än så förlorar banan
|
||||
IndividualCards bool `json:"individualCards"` // alla i sällskapet har egna kort (och biljetter) som spärren kräver
|
||||
SeatSelection bool `json:"seatSelection"` // biljettköpet fortsätter med sittplatsval efter sällskapets önskemål
|
||||
FareYen int `json:"fareYen"` // vad spärren drar från kortet (0 = standard)
|
||||
Grid []string `json:"grid"`
|
||||
Difficulty int `json:"difficulty"` // 1 (lätt) … 5 (svår)
|
||||
Destination Text `json:"destination"` // dit spelaren ska
|
||||
TimeLimitSec int `json:"timeLimitSec"` // 0 = ingen tidsgräns
|
||||
ParSec int `json:"parSec"` // "bra tid" för tre stjärnor (0 = räknas ut från kortaste vägen)
|
||||
Party int `json:"party"` // antal i sällskapet inkl. spelaren (0 = 1)
|
||||
TicketRequired bool `json:"ticketRequired"` // spärren kräver pappersbiljett (köps i automaten) i stället för kort
|
||||
RideStops int `json:"rideStops"` // antal stationer på tågresan (0 = DefaultRideStops)
|
||||
MaxMissedTrains int `json:"maxMissedTrains"` // >0: fler missade tåg än så förlorar banan
|
||||
IndividualCards bool `json:"individualCards"` // alla i sällskapet har egna kort (och biljetter) som spärren kräver
|
||||
SeatSelection bool `json:"seatSelection"` // biljettköpet fortsätter med sittplatsval efter sällskapets önskemål
|
||||
FareYen int `json:"fareYen"` // vad spärren drar från kortet (0 = standard)
|
||||
Grid []string `json:"grid,omitempty"` // envåningsbana (kompatibilitet) – blir Floors[0]
|
||||
Floors []Storey `json:"floors,omitempty"` // flervåningsbana; alla våningar lika stora
|
||||
Trains []Train `json:"trains"`
|
||||
Signs []SignText `json:"signs"`
|
||||
}
|
||||
|
||||
// Point är en rutkoordinat.
|
||||
type Point struct{ X, Y int }
|
||||
// Point är en rutkoordinat: x, y och våning z.
|
||||
type Point struct{ X, Y, Z int }
|
||||
|
||||
// Up/Down ger rutan rakt ovanför/under (via trappa).
|
||||
func (p Point) Up() Point { return Point{X: p.X, Y: p.Y, Z: p.Z + 1} }
|
||||
func (p Point) Down() Point { return Point{X: p.X, Y: p.Y, Z: p.Z - 1} }
|
||||
|
||||
// dirs är de fyra grannriktningarna på samma våning.
|
||||
var dirs = [...]Point{{X: 1}, {X: -1}, {Y: 1}, {Y: -1}}
|
||||
|
||||
// Parse avkodar och validerar en bana.
|
||||
func Parse(data []byte) (*Level, error) {
|
||||
@@ -218,36 +240,77 @@ func Parse(data []byte) (*Level, error) {
|
||||
return &l, nil
|
||||
}
|
||||
|
||||
// Width och Height i rutor.
|
||||
// Width och Height i rutor (alla våningar är lika stora).
|
||||
func (l *Level) Width() int {
|
||||
if len(l.Grid) == 0 {
|
||||
if len(l.Floors) == 0 || len(l.Floors[0].Grid) == 0 {
|
||||
return 0
|
||||
}
|
||||
return len(l.Grid[0])
|
||||
return len(l.Floors[0].Grid[0])
|
||||
}
|
||||
func (l *Level) Height() int {
|
||||
if len(l.Floors) == 0 {
|
||||
return 0
|
||||
}
|
||||
return len(l.Floors[0].Grid)
|
||||
}
|
||||
func (l *Level) Height() int { return len(l.Grid) }
|
||||
|
||||
// At returnerar rutan på (x, y); utanför kartan räknas som vägg.
|
||||
func (l *Level) At(x, y int) Tile {
|
||||
if y < 0 || y >= len(l.Grid) || x < 0 || x >= len(l.Grid[y]) {
|
||||
// NumFloors är antalet våningar.
|
||||
func (l *Level) NumFloors() int { return len(l.Floors) }
|
||||
|
||||
// FloorName är våningens namn ("1F" om det saknas).
|
||||
func (l *Level) FloorName(z int) string {
|
||||
if z < 0 || z >= len(l.Floors) || l.Floors[z].Name == "" {
|
||||
return "1F"
|
||||
}
|
||||
return l.Floors[z].Name
|
||||
}
|
||||
|
||||
// At returnerar rutan på p; utanför kartan (eller våningarna) räknas som vägg.
|
||||
func (l *Level) At(p Point) Tile { return l.FloorAt(p.Z, p.X, p.Y) }
|
||||
|
||||
// FloorAt returnerar rutan (x, y) på våning z; utanför kartan räknas som vägg.
|
||||
func (l *Level) FloorAt(z, x, y int) Tile {
|
||||
if z < 0 || z >= len(l.Floors) {
|
||||
return Wall
|
||||
}
|
||||
return Tile(l.Grid[y][x])
|
||||
g := l.Floors[z].Grid
|
||||
if y < 0 || y >= len(g) || x < 0 || x >= len(g[y]) {
|
||||
return Wall
|
||||
}
|
||||
return Tile(g[y][x])
|
||||
}
|
||||
|
||||
// Find returnerar alla rutor av en viss typ.
|
||||
// Find returnerar alla rutor av en viss typ på alla våningar.
|
||||
func (l *Level) Find(t Tile) []Point {
|
||||
var pts []Point
|
||||
for y, row := range l.Grid {
|
||||
for x := 0; x < len(row); x++ {
|
||||
if Tile(row[x]) == t {
|
||||
pts = append(pts, Point{x, y})
|
||||
for z, f := range l.Floors {
|
||||
for y, row := range f.Grid {
|
||||
for x := 0; x < len(row); x++ {
|
||||
if Tile(row[x]) == t {
|
||||
pts = append(pts, Point{X: x, Y: y, Z: z})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return pts
|
||||
}
|
||||
|
||||
// Neighbors är rutorna man kan gå till från p: de fyra grannarna på samma
|
||||
// våning samt trappans andra ände. Passerbarhet avgörs av anroparen.
|
||||
func (l *Level) Neighbors(p Point) []Point {
|
||||
out := make([]Point, 0, 5)
|
||||
for _, d := range dirs {
|
||||
out = append(out, Point{X: p.X + d.X, Y: p.Y + d.Y, Z: p.Z})
|
||||
}
|
||||
switch l.At(p) {
|
||||
case StairsUp:
|
||||
out = append(out, p.Up())
|
||||
case StairsDown:
|
||||
out = append(out, p.Down())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// SpawnPoint returnerar startrutan (valideringen garanterar att den finns).
|
||||
func (l *Level) SpawnPoint() Point { return l.Find(Spawn)[0] }
|
||||
|
||||
@@ -274,25 +337,47 @@ func (l *Level) Validate() error {
|
||||
if l.Order < 0 {
|
||||
return fmt.Errorf("level %s: negativ order", l.ID)
|
||||
}
|
||||
if len(l.Grid) == 0 {
|
||||
return fmt.Errorf("level %s: tomt rutnät", l.ID)
|
||||
}
|
||||
w := len(l.Grid[0])
|
||||
spawns, platforms := 0, 0
|
||||
for y, row := range l.Grid {
|
||||
if len(row) != w {
|
||||
return fmt.Errorf("level %s: rad %d har längd %d, förväntade %d", l.ID, y, len(row), w)
|
||||
if len(l.Floors) == 0 {
|
||||
if len(l.Grid) == 0 {
|
||||
return fmt.Errorf("level %s: tomt rutnät", l.ID)
|
||||
}
|
||||
for x := 0; x < len(row); x++ {
|
||||
t := Tile(row[x])
|
||||
if !knownTiles[t] {
|
||||
return fmt.Errorf("level %s: okänt tecken %q på (%d,%d)", l.ID, row[x], x, y)
|
||||
l.Floors = []Storey{{Name: "1F", Grid: l.Grid}}
|
||||
} else if len(l.Grid) > 0 {
|
||||
return fmt.Errorf("level %s: ange antingen grid eller floors, inte båda", l.ID)
|
||||
}
|
||||
l.Grid = l.Floors[0].Grid // kompatibilitet: Grid = första våningen
|
||||
if len(l.Floors[0].Grid) == 0 {
|
||||
return fmt.Errorf("level %s: tom första våning", l.ID)
|
||||
}
|
||||
w, h := len(l.Floors[0].Grid[0]), len(l.Floors[0].Grid)
|
||||
spawns, platforms := 0, 0
|
||||
for z, f := range l.Floors {
|
||||
if len(f.Grid) != h {
|
||||
return fmt.Errorf("level %s: våning %d har %d rader, förväntade %d", l.ID, z, len(f.Grid), h)
|
||||
}
|
||||
for y, row := range f.Grid {
|
||||
if len(row) != w {
|
||||
return fmt.Errorf("level %s: våning %d rad %d har längd %d, förväntade %d", l.ID, z, y, len(row), w)
|
||||
}
|
||||
switch t {
|
||||
case Spawn:
|
||||
spawns++
|
||||
case Platform, WrongPlatform:
|
||||
platforms++
|
||||
for x := 0; x < len(row); x++ {
|
||||
t := Tile(row[x])
|
||||
if !knownTiles[t] {
|
||||
return fmt.Errorf("level %s: okänt tecken %q på (%d,%d,%d)", l.ID, row[x], x, y, z)
|
||||
}
|
||||
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)
|
||||
}
|
||||
case StairsDown:
|
||||
if l.FloorAt(z-1, x, y) != StairsUp {
|
||||
return fmt.Errorf("level %s: trappa ner på (%d,%d,%d) saknar trappa upp under", l.ID, x, y, z)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -311,7 +396,7 @@ func (l *Level) Validate() error {
|
||||
if len(tr.Platform) != 1 || (Tile(tr.Platform[0]) != Platform && Tile(tr.Platform[0]) != WrongPlatform) {
|
||||
return fmt.Errorf("level %s: tåg %d har ogiltig perrong %q", l.ID, i, tr.Platform)
|
||||
}
|
||||
if !strings.Contains(strings.Join(l.Grid, ""), tr.Platform) {
|
||||
if len(l.Find(Tile(tr.Platform[0]))) == 0 {
|
||||
return fmt.Errorf("level %s: tåg %d står vid perrong %q som inte finns i rutnätet", l.ID, i, tr.Platform)
|
||||
}
|
||||
if tr.Correct {
|
||||
@@ -364,9 +449,9 @@ func (l *Level) Validate() error {
|
||||
// Skyltar: varje post måste stå på en 'i'-ruta och varje 'i'-ruta ha text.
|
||||
seen := map[Point]bool{}
|
||||
for i, sg := range l.Signs {
|
||||
p := Point{sg.X, sg.Y}
|
||||
if l.At(p.X, p.Y) != Sign {
|
||||
return fmt.Errorf("level %s: skylt %d på (%d,%d) står inte på en skyltruta", l.ID, i, p.X, p.Y)
|
||||
p := Point{X: sg.X, Y: sg.Y, Z: sg.Z}
|
||||
if l.At(p) != Sign {
|
||||
return fmt.Errorf("level %s: skylt %d på (%d,%d,%d) står inte på en skyltruta", l.ID, i, p.X, p.Y, p.Z)
|
||||
}
|
||||
if seen[p] {
|
||||
return fmt.Errorf("level %s: två skyltar på (%d,%d)", l.ID, p.X, p.Y)
|
||||
@@ -378,7 +463,7 @@ func (l *Level) Validate() error {
|
||||
}
|
||||
for _, p := range l.Find(Sign) {
|
||||
if !seen[p] {
|
||||
return fmt.Errorf("level %s: skyltrutan (%d,%d) saknar text i \"signs\"", l.ID, p.X, p.Y)
|
||||
return fmt.Errorf("level %s: skyltrutan (%d,%d,%d) saknar text i \"signs\"", l.ID, p.X, p.Y, p.Z)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -387,7 +472,7 @@ func (l *Level) Validate() error {
|
||||
// SignAt returnerar skylten på en ruta, eller nil.
|
||||
func (l *Level) SignAt(p Point) *SignText {
|
||||
for i := range l.Signs {
|
||||
if l.Signs[i].X == p.X && l.Signs[i].Y == p.Y {
|
||||
if l.Signs[i].X == p.X && l.Signs[i].Y == p.Y && l.Signs[i].Z == p.Z {
|
||||
return &l.Signs[i]
|
||||
}
|
||||
}
|
||||
@@ -413,8 +498,7 @@ func (l *Level) Path(from, to Point, canEnter func(Point) bool) []Point {
|
||||
}
|
||||
return path
|
||||
}
|
||||
for _, d := range [...]Point{{1, 0}, {-1, 0}, {0, 1}, {0, -1}} {
|
||||
n := Point{p.X + d.X, p.Y + d.Y}
|
||||
for _, n := range l.Neighbors(p) {
|
||||
if _, ok := prev[n]; ok || !canEnter(n) {
|
||||
continue
|
||||
}
|
||||
@@ -434,9 +518,8 @@ func (l *Level) Reachable() map[Point]bool {
|
||||
for len(queue) > 0 {
|
||||
p := queue[0]
|
||||
queue = queue[1:]
|
||||
for _, d := range [...]Point{{1, 0}, {-1, 0}, {0, 1}, {0, -1}} {
|
||||
n := Point{p.X + d.X, p.Y + d.Y}
|
||||
if seen[n] || !l.At(n.X, n.Y).Passable() {
|
||||
for _, n := range l.Neighbors(p) {
|
||||
if seen[n] || !l.At(n).Passable() {
|
||||
continue
|
||||
}
|
||||
seen[n] = true
|
||||
@@ -472,9 +555,8 @@ func (l *Level) UngatedPlatforms() []Point {
|
||||
for len(queue) > 0 {
|
||||
p := queue[0]
|
||||
queue = queue[1:]
|
||||
for _, d := range [...]Point{{1, 0}, {-1, 0}, {0, 1}, {0, -1}} {
|
||||
n := Point{p.X + d.X, p.Y + d.Y}
|
||||
t := l.At(n.X, n.Y)
|
||||
for _, n := range l.Neighbors(p) {
|
||||
t := l.At(n)
|
||||
if seen[n] || !t.Passable() || t == Gate {
|
||||
continue
|
||||
}
|
||||
@@ -518,8 +600,8 @@ func (tr *Train) BoardText() string {
|
||||
// anger om rälsen (och tågets rörelse) går i sidled.
|
||||
func (l *Level) TrackTile(p Point) (tile Point, horizontal bool) {
|
||||
for _, d := range [...]Point{{X: 0, Y: 1}, {X: 1, Y: 0}, {X: -1, Y: 0}, {X: 0, Y: -1}} {
|
||||
n := Point{X: p.X + d.X, Y: p.Y + d.Y}
|
||||
if l.At(n.X, n.Y) == Wall {
|
||||
n := Point{X: p.X + d.X, Y: p.Y + d.Y, Z: p.Z}
|
||||
if l.At(n) == Wall {
|
||||
return n, d.Y != 0
|
||||
}
|
||||
}
|
||||
@@ -540,7 +622,7 @@ func (l *Level) CorrectPlatform() Point {
|
||||
}
|
||||
|
||||
// CanEnter rapporterar om rutan går att gå in på (för sim.Mover).
|
||||
func (l *Level) CanEnter(p Point) bool { return l.At(p.X, p.Y).Passable() }
|
||||
func (l *Level) CanEnter(p Point) bool { return l.At(p).Passable() }
|
||||
|
||||
// TrainAt returnerar tåget vid en perrongruta, eller nil.
|
||||
func (l *Level) TrainAt(t Tile) *Train {
|
||||
|
||||
@@ -40,7 +40,7 @@ func TestParseAndSolve(t *testing.T) {
|
||||
if l.Width() != 7 || l.Height() != 5 {
|
||||
t.Fatalf("storlek %dx%d", l.Width(), l.Height())
|
||||
}
|
||||
if l.SpawnPoint() != (Point{1, 1}) {
|
||||
if l.SpawnPoint() != (Point{X: 1, Y: 1}) {
|
||||
t.Fatalf("spawn %v", l.SpawnPoint())
|
||||
}
|
||||
if !l.Solvable() {
|
||||
@@ -56,14 +56,14 @@ func TestUnsolvableWhenWalledOff(t *testing.T) {
|
||||
if l.Solvable() {
|
||||
t.Fatal("banan ska INTE vara lösbar när perrongen är inmurad")
|
||||
}
|
||||
if u := l.Unreachable(); len(u) != 1 || u[0] != (Point{5, 1}) {
|
||||
if u := l.Unreachable(); len(u) != 1 || u[0] != (Point{X: 5, Y: 1}) {
|
||||
t.Fatalf("Unreachable = %v, vill ha [(5,1)]", u)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutsideGridIsWall(t *testing.T) {
|
||||
l := mustParse(t, sample)
|
||||
if l.At(-1, 0).Passable() || l.At(0, 99).Passable() {
|
||||
if l.FloorAt(0, -1, 0).Passable() || l.FloorAt(0, 0, 99).Passable() || l.At(Point{Z: 5}).Passable() {
|
||||
t.Fatal("utanför kartan ska räknas som vägg")
|
||||
}
|
||||
}
|
||||
@@ -227,19 +227,19 @@ func TestSignAt(t *testing.T) {
|
||||
src := strings.Replace(strings.Replace(sample, `"#S..#P#"`, `"#Si.#P#"`, 1),
|
||||
`"trains"`, `"signs": [{"x": 2, "y": 1, "text": {"sv": "Hej"}}], "trains"`, 1)
|
||||
l := mustParse(t, src)
|
||||
if sg := l.SignAt(Point{2, 1}); sg == nil || sg.Text.Get("sv") != "Hej" {
|
||||
if sg := l.SignAt(Point{X: 2, Y: 1}); sg == nil || sg.Text.Get("sv") != "Hej" {
|
||||
t.Fatalf("SignAt = %v", sg)
|
||||
}
|
||||
if l.SignAt(Point{1, 1}) != nil {
|
||||
if l.SignAt(Point{X: 1, Y: 1}) != nil {
|
||||
t.Fatal("ingen skylt på startrutan")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPath(t *testing.T) {
|
||||
l := mustParse(t, sample)
|
||||
path := l.Path(l.SpawnPoint(), Point{5, 1}, l.CanEnter)
|
||||
path := l.Path(l.SpawnPoint(), Point{X: 5, Y: 1}, l.CanEnter)
|
||||
// S(1,1) -> (1,2) -> (1,3) -> (2,3) -> (3,3) -> (4,3) -> (5,3) -> (5,2) -> (5,1)
|
||||
if len(path) != 9 || path[0] != (Point{1, 1}) || path[len(path)-1] != (Point{5, 1}) {
|
||||
if len(path) != 9 || path[0] != (Point{X: 1, Y: 1}) || path[len(path)-1] != (Point{X: 5, Y: 1}) {
|
||||
t.Fatalf("path = %v", path)
|
||||
}
|
||||
for i := 1; i < len(path); i++ {
|
||||
@@ -248,7 +248,7 @@ func TestPath(t *testing.T) {
|
||||
t.Fatalf("steg %d är inte en grannruta: %v -> %v", i, path[i-1], path[i])
|
||||
}
|
||||
}
|
||||
if l.Path(l.SpawnPoint(), Point{0, 0}, l.CanEnter) != nil {
|
||||
if l.Path(l.SpawnPoint(), Point{X: 0, Y: 0}, l.CanEnter) != nil {
|
||||
t.Fatal("väg in i en vägg ska vara nil")
|
||||
}
|
||||
if p := l.Path(l.SpawnPoint(), l.SpawnPoint(), l.CanEnter); len(p) != 1 {
|
||||
@@ -259,7 +259,7 @@ func TestPath(t *testing.T) {
|
||||
func TestUngatedPlatforms(t *testing.T) {
|
||||
// Perrong utan spärr framför -> rapporteras.
|
||||
l := mustParse(t, sample)
|
||||
if u := l.UngatedPlatforms(); len(u) != 1 || u[0] != (Point{5, 1}) {
|
||||
if u := l.UngatedPlatforms(); len(u) != 1 || u[0] != (Point{X: 5, Y: 1}) {
|
||||
t.Fatalf("UngatedPlatforms = %v, vill ha [(5,1)]", u)
|
||||
}
|
||||
// Spärr i enda passagen -> inget rapporteras, men banan är fortfarande lösbar.
|
||||
@@ -280,14 +280,14 @@ func TestPartyAndParDefaults(t *testing.T) {
|
||||
if _, err := Parse([]byte(strings.Replace(sample, `"difficulty": 1`, `"difficulty": 1, "party": 9`, 1))); err == nil {
|
||||
t.Fatal("för stort sällskap ska ge fel")
|
||||
}
|
||||
if l.CorrectPlatform() != (Point{5, 1}) {
|
||||
if l.CorrectPlatform() != (Point{X: 5, Y: 1}) {
|
||||
t.Fatalf("CorrectPlatform = %v", l.CorrectPlatform())
|
||||
}
|
||||
if got := l.Trains[0].BoardText(); got != "番線 A B方面" {
|
||||
t.Fatalf("BoardText = %q", got)
|
||||
}
|
||||
l2 := mustParse(t, strings.Replace(sample, `"#S..#P#"`, `"#SW.KP#"`, 1))
|
||||
if l2.At(2, 1) != Toilet || l2.At(4, 1) != Kiosk || !l2.Solvable() {
|
||||
if l2.FloorAt(0, 2, 1) != Toilet || l2.FloorAt(0, 4, 1) != Kiosk || !l2.Solvable() {
|
||||
t.Fatal("W och K ska vara passerbara rutor")
|
||||
}
|
||||
}
|
||||
@@ -378,12 +378,12 @@ func TestScheduleValidationForPhases(t *testing.T) {
|
||||
|
||||
func TestTrackTile(t *testing.T) {
|
||||
l := mustParse(t, sample)
|
||||
tile, horizontal := l.TrackTile(Point{5, 1}) // P uppe till höger: vägg ovanför? nej, (5,0) är vägg -> men ordningen är ner, höger, vänster, upp
|
||||
if tile != (Point{5, 2}) && tile != (Point{6, 1}) && tile != (Point{5, 0}) {
|
||||
tile, horizontal := l.TrackTile(Point{X: 5, Y: 1}) // ordningen är ner, höger, vänster, upp
|
||||
if tile != (Point{X: 5, Y: 2}) && tile != (Point{X: 6, Y: 1}) && tile != (Point{X: 5, Y: 0}) {
|
||||
t.Fatalf("TrackTile = %v", tile)
|
||||
}
|
||||
// (5,2) är golv, (6,1) är vägg -> höger: vertikal räls
|
||||
if tile == (Point{6, 1}) && horizontal {
|
||||
if tile == (Point{X: 6, Y: 1}) && horizontal {
|
||||
t.Fatal("räls till höger om perrongen är vertikal")
|
||||
}
|
||||
}
|
||||
@@ -430,3 +430,88 @@ func TestKindOrderParentAndLoadAll(t *testing.T) {
|
||||
t.Fatal("det ska finnas minst en sidobana")
|
||||
}
|
||||
}
|
||||
|
||||
const floorsSample = `{
|
||||
"id": "two-floors", "name": {"sv": "Två våningar"}, "difficulty": 2, "destination": {"sv": "x"},
|
||||
"floors": [
|
||||
{"name": "B1", "grid": [
|
||||
"#######",
|
||||
"#S..U.#",
|
||||
"#.###.#",
|
||||
"#.....#",
|
||||
"#######"
|
||||
]},
|
||||
{"name": "1F", "grid": [
|
||||
"#######",
|
||||
"#...D.#",
|
||||
"#.#####",
|
||||
"#G..i.#",
|
||||
"#P#####"
|
||||
]}
|
||||
],
|
||||
"signs": [{"x": 4, "y": 3, "z": 1, "text": {"sv": "Upp", "en": "Up"}}],
|
||||
"trains": [{"platform": "P", "track": "1", "line": {"en": "A"}, "destination": {"en": "B"}, "correct": true}]
|
||||
}`
|
||||
|
||||
func TestFloorsStairsAndPaths(t *testing.T) {
|
||||
l := mustParse(t, floorsSample)
|
||||
if l.NumFloors() != 2 || l.FloorName(0) != "B1" || l.FloorName(1) != "1F" || l.FloorName(9) != "1F" {
|
||||
t.Fatalf("våningar: %d %s %s", l.NumFloors(), l.FloorName(0), l.FloorName(1))
|
||||
}
|
||||
if l.Width() != 7 || l.Height() != 5 || len(l.Grid) != 5 {
|
||||
t.Fatal("storlek/kompatibilitet")
|
||||
}
|
||||
if l.SpawnPoint() != (Point{X: 1, Y: 1, Z: 0}) || l.CorrectPlatform() != (Point{X: 1, Y: 4, Z: 1}) {
|
||||
t.Fatalf("spawn=%v platform=%v", l.SpawnPoint(), l.CorrectPlatform())
|
||||
}
|
||||
if l.At(Point{X: 4, Y: 1}) != StairsUp || l.At(Point{X: 4, Y: 1, Z: 1}) != StairsDown || !StairsUp.IsStairs() || !StairsUp.Passable() {
|
||||
t.Fatal("trappor")
|
||||
}
|
||||
up := Point{X: 4, Y: 1}
|
||||
n := l.Neighbors(up)
|
||||
if len(n) != 5 || n[4] != up.Up() {
|
||||
t.Fatalf("grannar vid trappa: %v", n)
|
||||
}
|
||||
if !l.Solvable() {
|
||||
t.Fatal("banan ska vara lösbar via trappan")
|
||||
}
|
||||
path := l.Path(l.SpawnPoint(), l.CorrectPlatform(), l.CanEnter)
|
||||
if path == nil || path[0].Z != 0 || path[len(path)-1].Z != 1 {
|
||||
t.Fatalf("vägen ska byta våning: %v", path)
|
||||
}
|
||||
climbed := false
|
||||
for i := 1; i < len(path); i++ {
|
||||
if path[i].Z != path[i-1].Z {
|
||||
climbed = true
|
||||
if path[i].X != path[i-1].X || path[i].Y != path[i-1].Y || !l.At(path[i-1]).IsStairs() {
|
||||
t.Fatalf("våningsbyte utan trappa: %v -> %v", path[i-1], path[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
if !climbed {
|
||||
t.Fatal("vägen använde ingen trappa")
|
||||
}
|
||||
if u := l.UngatedPlatforms(); len(u) != 0 {
|
||||
t.Fatalf("perrongen ligger innanför spärren: %v", u)
|
||||
}
|
||||
if sg := l.SignAt(Point{X: 4, Y: 3, Z: 1}); sg == nil || l.SignAt(Point{X: 4, Y: 3}) != nil {
|
||||
t.Fatal("skylten hör till våning 1")
|
||||
}
|
||||
if u := l.Unreachable(); len(u) != 0 {
|
||||
t.Fatalf("onåbart: %v", u)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFloorsValidation(t *testing.T) {
|
||||
bad := map[string]string{
|
||||
"trappa utan motpart": strings.Replace(floorsSample, `"#...D.#"`, `"#.....#"`, 1),
|
||||
"olika storlek": strings.Replace(floorsSample, `"#P#####"`, `"#P######"`, 1),
|
||||
"både grid och floors": strings.Replace(floorsSample, `"floors"`, `"grid": ["###","#S#","###"], "floors"`, 1),
|
||||
"skylt på fel våning": strings.Replace(floorsSample, `"z": 1`, `"z": 0`, 1),
|
||||
}
|
||||
for name, src := range bad {
|
||||
if _, err := Parse([]byte(src)); err == nil {
|
||||
t.Errorf("%s: förväntade fel", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user