pathfinding.test.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import { describe, expect, it } from "vitest";
  2. import { findPath, isWalkableTile, type MapDef } from "../src/index.js";
  3. function map(layers: string[][], walkable?: Record<string, boolean>): MapDef {
  4. return {
  5. id: "farm",
  6. width: layers[0]?.length ?? 0,
  7. height: layers.length,
  8. tilesetRef: "tiles",
  9. spawn: { x: 0, y: 0 },
  10. layers: [{ name: "ground", data: layers }],
  11. ...(walkable !== undefined ? { walkable } : {}),
  12. };
  13. }
  14. const GRASS: MapDef = map([
  15. ["grass", "grass", "water", "water", "water"],
  16. ["grass", "grass", "water", "water", "water"],
  17. ["grass", "grass", "grass", "grass", "grass"],
  18. ["grass", "grass", "grass", "grass", "grass"],
  19. ["grass", "grass", "grass", "grass", "grass"],
  20. ]);
  21. const OPEN: MapDef = map([
  22. ["grass", "grass", "grass", "grass", "grass"],
  23. ["grass", "grass", "grass", "grass", "grass"],
  24. ["grass", "grass", "grass", "grass", "grass"],
  25. ["grass", "grass", "grass", "grass", "grass"],
  26. ["grass", "grass", "grass", "grass", "grass"],
  27. ]);
  28. const noBlock = (): boolean => false;
  29. describe("findPath (A* grid pathfinding)", () => {
  30. it("returns a straight-line path including start and goal", () => {
  31. const path = findPath(OPEN, { x: 0, y: 0 }, { x: 1, y: 1 }, noBlock);
  32. expect(path).toEqual([
  33. { x: 0, y: 0 },
  34. { x: 1, y: 0 },
  35. { x: 1, y: 1 },
  36. ]);
  37. });
  38. it("detours around a single blocked tile", () => {
  39. const path = findPath(OPEN, { x: 0, y: 0 }, { x: 1, y: 1 }, (x, y) => x === 1 && y === 0);
  40. expect(path).toEqual([
  41. { x: 0, y: 0 },
  42. { x: 0, y: 1 },
  43. { x: 1, y: 1 },
  44. ]);
  45. });
  46. it("finds the shortest detour around multiple obstacles", () => {
  47. const blocked = (x: number, y: number): boolean =>
  48. (x === 1 && y === 0) || (x === 2 && y === 0) || (x === 1 && y === 1);
  49. const path = findPath(OPEN, { x: 0, y: 0 }, { x: 3, y: 0 }, blocked);
  50. expect(path).not.toBeNull();
  51. expect(path![0]).toEqual({ x: 0, y: 0 });
  52. expect(path![path!.length - 1]).toEqual({ x: 3, y: 0 });
  53. expect(path).toHaveLength(8);
  54. });
  55. it("returns null for a U-shaped unreachable goal", () => {
  56. const box: MapDef = map([
  57. ["grass", "grass", "grass", "grass", "grass"],
  58. ["grass", "block", "block", "block", "grass"],
  59. ["grass", "block", "grass", "block", "grass"],
  60. ["grass", "block", "block", "block", "grass"],
  61. ["grass", "grass", "grass", "grass", "grass"],
  62. ]);
  63. const blocked = (x: number, y: number): boolean => {
  64. const tile = box.layers[0]?.data[y]?.[x];
  65. return tile === "block";
  66. };
  67. expect(findPath(box, { x: 0, y: 0 }, { x: 2, y: 2 }, blocked)).toBeNull();
  68. });
  69. it("returns null when the goal is on a blocked tile", () => {
  70. expect(findPath(OPEN, { x: 0, y: 0 }, { x: 1, y: 0 }, (x, y) => x === 1 && y === 0)).toBeNull();
  71. });
  72. it("returns a single-element path when start equals goal", () => {
  73. expect(findPath(OPEN, { x: 2, y: 2 }, { x: 2, y: 2 }, noBlock)).toEqual([{ x: 2, y: 2 }]);
  74. });
  75. it("returns null for an out-of-bounds goal", () => {
  76. expect(findPath(OPEN, { x: 0, y: 0 }, { x: 5, y: 0 }, noBlock)).toBeNull();
  77. expect(findPath(OPEN, { x: 0, y: 0 }, { x: -1, y: 0 }, noBlock)).toBeNull();
  78. });
  79. it("returns null when the goal is not walkable (water fallback)", () => {
  80. expect(findPath(GRASS, { x: 0, y: 0 }, { x: 2, y: 0 }, noBlock)).toBeNull();
  81. });
  82. it("treats a walkable-declared water tile as passable", () => {
  83. const declared: MapDef = map(
  84. [
  85. ["grass", "grass", "water", "grass", "grass"],
  86. ["grass", "grass", "water", "grass", "grass"],
  87. ["grass", "grass", "water", "grass", "grass"],
  88. ["grass", "grass", "water", "grass", "grass"],
  89. ["grass", "grass", "grass", "grass", "grass"],
  90. ],
  91. { water: true },
  92. );
  93. const path = findPath(declared, { x: 0, y: 0 }, { x: 4, y: 0 }, noBlock);
  94. expect(path).not.toBeNull();
  95. expect(path![0]).toEqual({ x: 0, y: 0 });
  96. expect(path![path!.length - 1]).toEqual({ x: 4, y: 0 });
  97. });
  98. it("returns null when no map is loaded and never mutates inputs", () => {
  99. expect(findPath(undefined, { x: 0, y: 0 }, { x: 1, y: 1 }, noBlock)).toBeNull();
  100. const start = { x: 0, y: 0 };
  101. const goal = { x: 2, y: 2 };
  102. findPath(GRASS, start, goal, noBlock);
  103. expect(start).toEqual({ x: 0, y: 0 });
  104. expect(goal).toEqual({ x: 2, y: 2 });
  105. expect(GRASS.layers[0]?.data[0]?.[2]).toBe("water");
  106. });
  107. });
  108. describe("isWalkableTile (map-grid walkability resolution)", () => {
  109. it("declared-false blocks a tile", () => {
  110. const m = map([["grass", "water"]], { water: false });
  111. expect(isWalkableTile(m, 0, 0)).toBe(true);
  112. expect(isWalkableTile(m, 1, 0)).toBe(false);
  113. });
  114. it("undeclared water is blocked via the UNWALKABLE_TILES fallback", () => {
  115. const m = map([["grass", "water"]]);
  116. expect(isWalkableTile(m, 1, 0)).toBe(false);
  117. });
  118. it("undeclared grass is walkable", () => {
  119. expect(isWalkableTile(GRASS, 0, 0)).toBe(true);
  120. });
  121. it("declared-true overrides the fallback", () => {
  122. const m = map([["grass", "water"]], { water: true });
  123. expect(isWalkableTile(m, 1, 0)).toBe(true);
  124. });
  125. it("out-of-bounds and unknown tiles are not walkable", () => {
  126. expect(isWalkableTile(GRASS, -1, 0)).toBe(false);
  127. expect(isWalkableTile(GRASS, 0, -1)).toBe(false);
  128. expect(isWalkableTile(GRASS, 5, 0)).toBe(false);
  129. expect(isWalkableTile(GRASS, 0, 5)).toBe(false);
  130. });
  131. });