Files
ai-coding-workflow-labs/superpowers/src/game/pathfinder.js
T
tiennm99 51a4b90163 refactor: migrate quiz-project, gstack, and superpowers to JS+JSDoc
Convert all 41 TypeScript source/config files in cc4e-course/quiz-project
(Next.js), gstack (Astro), and superpowers (Vite/React/Phaser) to plain
JavaScript with JSDoc type annotations, replacing tsconfig.json with
jsconfig.json (strict + checkJs) in each subproject. No behavior changes:
lint, typecheck, test, and build gates match their TypeScript baselines
exactly in all three subprojects. Update stack references in the affected
READMEs accordingly.
2026-08-18 09:49:42 +07:00

187 lines
4.4 KiB
JavaScript

/**
* @typedef {import("../types").Board} Board
* @typedef {import("../types").Point} Point
* @typedef {import("../types").TileData} TileData
*/
/**
* @param {Board} board
* @param {number} row
* @param {number} col
* @returns {boolean}
*/
function isEmpty(board, row, col) {
const rows = board.length;
const cols = board[0].length;
if (row < 0 || row >= rows || col < 0 || col >= cols) {
return row >= -1 && row <= rows && col >= -1 && col <= cols;
}
return board[row][col] === null;
}
/** @type {[number, number][]} */
const DIRS = [
[0, 1],
[0, -1],
[1, 0],
[-1, 0],
];
/**
* @param {Board} board
* @param {number} row
* @param {number} col
* @param {number} dr
* @param {number} dc
* @returns {Point[]}
*/
function raycast(board, row, col, dr, dc) {
/** @type {Point[]} */
const points = [];
let r = row + dr;
let c = col + dc;
const rows = board.length;
const cols = board[0].length;
while (r >= -1 && r <= rows && c >= -1 && c <= cols && isEmpty(board, r, c)) {
points.push({ row: r, col: c });
r += dr;
c += dc;
}
return points;
}
/**
* @param {Board} board
* @param {Point} a
* @param {Point} b
* @returns {boolean}
*/
function canConnectStraight(board, a, b) {
if (a.row === b.row) {
const minC = Math.min(a.col, b.col);
const maxC = Math.max(a.col, b.col);
for (let c = minC + 1; c < maxC; c++) {
if (!isEmpty(board, a.row, c)) return false;
}
return true;
}
if (a.col === b.col) {
const minR = Math.min(a.row, b.row);
const maxR = Math.max(a.row, b.row);
for (let r = minR + 1; r < maxR; r++) {
if (!isEmpty(board, r, a.col)) return false;
}
return true;
}
return false;
}
/**
* @param {Board} board
* @param {Point} a
* @param {Point} b
* @returns {Point[] | null}
*/
export function findPath(board, a, b) {
const tileA = board[a.row]?.[a.col];
const tileB = board[b.row]?.[b.col];
if (!tileA || !tileB || tileA.emoji !== tileB.emoji) return null;
if (a.row === b.row && a.col === b.col) return null;
// 0 bends: direct straight line
if (canConnectStraight(board, a, b)) {
return [a, b];
}
// 1 bend: check two possible corners
/** @type {Point} */
const corner1 = { row: a.row, col: b.col };
if (
isEmpty(board, corner1.row, corner1.col) &&
canConnectStraight(board, a, corner1) &&
canConnectStraight(board, corner1, b)
) {
return [a, corner1, b];
}
/** @type {Point} */
const corner2 = { row: b.row, col: a.col };
if (
isEmpty(board, corner2.row, corner2.col) &&
canConnectStraight(board, a, corner2) &&
canConnectStraight(board, corner2, b)
) {
return [a, corner2, b];
}
// 2 bends: try all lines from A, for each reachable cell, try 1-bend to B
for (const [dr, dc] of DIRS) {
const reachable = raycast(board, a.row, a.col, dr, dc);
for (const mid of reachable) {
/** @type {Point} */
const cornerA = { row: mid.row, col: b.col };
if (
isEmpty(board, cornerA.row, cornerA.col) &&
canConnectStraight(board, mid, cornerA) &&
canConnectStraight(board, cornerA, b)
) {
return [a, mid, cornerA, b];
}
/** @type {Point} */
const cornerB = { row: b.row, col: mid.col };
if (
isEmpty(board, cornerB.row, cornerB.col) &&
canConnectStraight(board, mid, cornerB) &&
canConnectStraight(board, cornerB, b)
) {
return [a, mid, cornerB, b];
}
if (canConnectStraight(board, mid, b)) {
return [a, mid, b];
}
}
}
return null;
}
/**
* @param {Board} board
* @returns {boolean}
*/
export function hasAnyValidMove(board) {
/** @type {{ pos: Point; emoji: string }[]} */
const tiles = [];
for (let r = 0; r < board.length; r++) {
for (let c = 0; c < board[r].length; c++) {
if (board[r][c] !== null) {
tiles.push({
pos: { row: r, col: c },
emoji: /** @type {TileData} */ (board[r][c]).emoji,
});
}
}
}
/** @type {Map<string, Point[]>} */
const groups = new Map();
for (const t of tiles) {
if (!groups.has(t.emoji)) groups.set(t.emoji, []);
/** @type {Point[]} */ (groups.get(t.emoji)).push(t.pos);
}
for (const [, positions] of groups) {
for (let i = 0; i < positions.length; i++) {
for (let j = i + 1; j < positions.length; j++) {
if (findPath(board, positions[i], positions[j]) !== null) {
return true;
}
}
}
}
return false;
}