mirror of
https://github.com/tiennm99/ai-coding-workflow-labs.git
synced 2026-09-17 20:18:58 +00:00
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.
187 lines
4.4 KiB
JavaScript
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;
|
|
}
|