From 8170df95ade2fe233b86c4405bd92e28fc95c7ff Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Tue, 28 Apr 2026 03:05:31 +0300 Subject: [PATCH] crew-movement: walkability + BFS pathfinding with tests --- tiles.go | 59 +++++++++++++++++++++++++++++ tiles_test.go | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 tiles.go create mode 100644 tiles_test.go diff --git a/tiles.go b/tiles.go new file mode 100644 index 0000000000000000000000000000000000000000..d0a346836acca1dc5a58aa14ba576d8dcb7746b4 --- /dev/null +++ b/tiles.go @@ -0,0 +1,59 @@ +package main + +func walkableTiles(s Ship) map[[2]int]bool { + walk := make(map[[2]int]bool) + for _, r := range s.Rooms { + for y := r.GridY; y < r.GridY+r.GridH; y++ { + for x := r.GridX; x < r.GridX+r.GridW; x++ { + walk[[2]int{x, y}] = true + } + } + } + return walk +} + +func bfsPath(walk map[[2]int]bool, from, to [2]int) [][2]int { + if from == to { + return nil + } + if !walk[to] { + return nil + } + + neighbours := [4][2]int{{1, 0}, {-1, 0}, {0, 1}, {0, -1}} + visited := map[[2]int]bool{from: true} + parent := map[[2]int][2]int{} + queue := [][2]int{from} + + for len(queue) > 0 { + cur := queue[0] + queue = queue[1:] + for _, d := range neighbours { + next := [2]int{cur[0] + d[0], cur[1] + d[1]} + if !walk[next] || visited[next] { + continue + } + visited[next] = true + parent[next] = cur + if next == to { + return reconstructPath(parent, from, to) + } + queue = append(queue, next) + } + } + return nil +} + +func reconstructPath(parent map[[2]int][2]int, from, to [2]int) [][2]int { + var path [][2]int + cur := to + for cur != from { + path = append(path, cur) + cur = parent[cur] + } + // reverse + for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 { + path[i], path[j] = path[j], path[i] + } + return path +} diff --git a/tiles_test.go b/tiles_test.go new file mode 100644 index 0000000000000000000000000000000000000000..52848fe7c0a9a275c9fcf895601d6401639af549 --- /dev/null +++ b/tiles_test.go @@ -0,0 +1,103 @@ +package main + +import ( + "reflect" + "testing" +) + +func TestWalkableTiles_coversAllRooms(t *testing.T) { + s := NewPlayerShip() + walk := walkableTiles(s) + if len(walk) != 54 { + t.Fatalf("expected 54 walkable tiles, got %d", len(walk)) + } + // Spot-check one tile from each room + checks := [][2]int{ + {4, 10}, // Pilot center + {7, 7}, // Weapons + {7, 10}, // Shields + {7, 13}, // MedBay + {11, 10}, // Engines center + } + for _, tc := range checks { + if !walk[tc] { + t.Errorf("expected tile %v to be walkable", tc) + } + } + // Off-ship tile must be absent + if walk[[2]int{0, 0}] { + t.Error("tile (0,0) must not be walkable") + } +} + +func TestWalkableTiles_ignoresOutside(t *testing.T) { + s := NewPlayerShip() + walk := walkableTiles(s) + if walk[[2]int{0, 0}] { + t.Error("tile (0,0) must not be walkable") + } + if walk[[2]int{20, 20}] { + t.Error("tile (20,20) must not be walkable") + } +} + +func TestBFSPath_sameTile(t *testing.T) { + s := NewPlayerShip() + walk := walkableTiles(s) + path := bfsPath(walk, [2]int{4, 10}, [2]int{4, 10}) + if len(path) != 0 { + t.Errorf("expected empty path for same-tile, got %v", path) + } +} + +func TestBFSPath_adjacentTile(t *testing.T) { + s := NewPlayerShip() + walk := walkableTiles(s) + path := bfsPath(walk, [2]int{4, 10}, [2]int{5, 10}) + if len(path) != 1 { + t.Fatalf("expected path of length 1, got %d: %v", len(path), path) + } + if path[0] != ([2]int{5, 10}) { + t.Errorf("expected path[0] == {5,10}, got %v", path[0]) + } +} + +func TestBFSPath_unreachable(t *testing.T) { + s := NewPlayerShip() + walk := walkableTiles(s) + path := bfsPath(walk, [2]int{4, 10}, [2]int{0, 0}) + if len(path) != 0 { + t.Errorf("expected empty path for unreachable target, got %v", path) + } +} + +func TestBFSPath_throughShields(t *testing.T) { + s := NewPlayerShip() + walk := walkableTiles(s) + from := [2]int{4, 10} + to := [2]int{11, 10} + path := bfsPath(walk, from, to) + if len(path) != 7 { + t.Fatalf("expected path length 7, got %d: %v", len(path), path) + } + if path[len(path)-1] != to { + t.Errorf("expected last tile %v, got %v", to, path[len(path)-1]) + } + for _, tile := range path { + if !walk[tile] { + t.Errorf("path contains non-walkable tile %v", tile) + } + } +} + +func TestBFSPath_deterministic(t *testing.T) { + s := NewPlayerShip() + walk := walkableTiles(s) + from := [2]int{4, 10} + to := [2]int{11, 10} + p1 := bfsPath(walk, from, to) + p2 := bfsPath(walk, from, to) + if !reflect.DeepEqual(p1, p2) { + t.Errorf("non-deterministic: first=%v second=%v", p1, p2) + } +}