Files
ai-coding-workflow-labs/superpowers/src/game/board.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

134 lines
3.3 KiB
JavaScript

import { DIFFICULTY_CONFIGS } from "./constants";
import { getEmojisForDifficulty } from "./emoji";
import { hasAnyValidMove } from "./pathfinder";
/**
* @typedef {import("../types").Board} Board
* @typedef {import("../types").Difficulty} Difficulty
* @typedef {import("../types").TileData} TileData
* @typedef {import("../types").Point} Point
*/
let nextTileId = 0;
/**
* @param {Difficulty} difficulty
* @returns {Board}
*/
export function createBoard(difficulty) {
const config = DIFFICULTY_CONFIGS[difficulty];
const { rows, cols } = config;
const emojis = getEmojisForDifficulty(difficulty);
// Create pairs
/** @type {TileData[]} */
const tiles = [];
nextTileId = 0;
for (const emoji of emojis) {
tiles.push({ emoji, id: nextTileId++ });
tiles.push({ emoji, id: nextTileId++ });
}
// Fisher-Yates shuffle
for (let i = tiles.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[tiles[i], tiles[j]] = [tiles[j], tiles[i]];
}
// Place into grid
/** @type {Board} */
const board = [];
let idx = 0;
for (let r = 0; r < rows; r++) {
/** @type {(TileData | null)[]} */
const row = [];
for (let c = 0; c < cols; c++) {
row.push(tiles[idx++]);
}
board.push(row);
}
// Validate at least one valid move exists; reshuffle if not
while (!hasAnyValidMove(board)) {
const allTiles = board.flat().filter((t) => t !== null);
for (let i = allTiles.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[allTiles[i], allTiles[j]] = [allTiles[j], allTiles[i]];
}
let idx2 = 0;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
board[r][c] = allTiles[idx2++];
}
}
}
return board;
}
/**
* @param {Board} board
* @returns {{ tile: TileData; pos: Point }[]}
*/
export function getRemainingTiles(board) {
/** @type {{ tile: TileData; pos: Point }[]} */
const result = [];
for (let r = 0; r < board.length; r++) {
for (let c = 0; c < board[r].length; c++) {
const cell = board[r][c];
if (cell !== null) {
result.push({ tile: cell, pos: { row: r, col: c } });
}
}
}
return result;
}
/**
* @param {Board} board
* @returns {Board}
*/
export function shuffleBoard(board) {
const rows = board.length;
const cols = board[0].length;
/** @type {TileData[]} */
const tiles = [];
/** @type {Point[]} */
const occupiedPositions = [];
/** @type {Point[]} */
const emptyPositions = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (board[r][c] !== null) {
tiles.push(/** @type {TileData} */ (board[r][c]));
occupiedPositions.push({ row: r, col: c });
} else {
emptyPositions.push({ row: r, col: c });
}
}
}
// Fisher-Yates shuffle the tiles
for (let i = tiles.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[tiles[i], tiles[j]] = [tiles[j], tiles[i]];
}
// Create new board with nulls
/** @type {Board} */
const newBoard = [];
for (let r = 0; r < rows; r++) {
newBoard.push(new Array(cols).fill(null));
}
// Place shuffled tiles back into occupied positions
for (let i = 0; i < tiles.length; i++) {
const pos = occupiedPositions[i];
newBoard[pos.row][pos.col] = tiles[i];
}
return newBoard;
}