@@ 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
+}
@@ 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)
+ }
+}