feat: add board generation, remaining tiles, and shuffle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-07 20:53:10 +07:00
co-authored by Claude Sonnet 4.6
parent bcc457b713
commit 75c0351d44
2 changed files with 182 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
import { Board, Difficulty, TileData, Point } from "../types";
import { DIFFICULTY_CONFIGS } from "./constants";
import { getEmojisForDifficulty } from "./emoji";
let nextTileId = 0;
export function createBoard(difficulty: Difficulty): Board {
const config = DIFFICULTY_CONFIGS[difficulty];
const { rows, cols } = config;
const emojis = getEmojisForDifficulty(difficulty);
// Create pairs
const tiles: TileData[] = [];
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
const board: Board = [];
let idx = 0;
for (let r = 0; r < rows; r++) {
const row: (TileData | null)[] = [];
for (let c = 0; c < cols; c++) {
row.push(tiles[idx++]);
}
board.push(row);
}
return board;
}
export function getRemainingTiles(board: Board): { tile: TileData; pos: Point }[] {
const result: { tile: TileData; pos: Point }[] = [];
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;
}
export function shuffleBoard(board: Board): Board {
const rows = board.length;
const cols = board[0].length;
const tiles: TileData[] = [];
const occupiedPositions: Point[] = [];
const emptyPositions: Point[] = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (board[r][c] !== null) {
tiles.push(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
const newBoard: Board = [];
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;
}
+91
View File
@@ -0,0 +1,91 @@
import { describe, it, expect } from "vitest";
import { createBoard, getRemainingTiles, shuffleBoard } from "../../src/game/board";
describe("createBoard", () => {
it("creates a board with correct dimensions for easy", () => {
const board = createBoard("easy");
expect(board.length).toBe(4);
expect(board[0].length).toBe(6);
});
it("creates a board with correct dimensions for medium", () => {
const board = createBoard("medium");
expect(board.length).toBe(6);
expect(board[0].length).toBe(8);
});
it("creates a board with correct dimensions for hard", () => {
const board = createBoard("hard");
expect(board.length).toBe(8);
expect(board[0].length).toBe(10);
});
it("every tile has a matching pair", () => {
const board = createBoard("easy");
const emojiCounts = new Map<string, number>();
for (const row of board) {
for (const cell of row) {
if (cell) {
emojiCounts.set(cell.emoji, (emojiCounts.get(cell.emoji) ?? 0) + 1);
}
}
}
for (const [, count] of emojiCounts) {
expect(count).toBe(2);
}
});
it("all tiles have unique ids", () => {
const board = createBoard("medium");
const ids = new Set<number>();
for (const row of board) {
for (const cell of row) {
if (cell) {
expect(ids.has(cell.id)).toBe(false);
ids.add(cell.id);
}
}
}
});
});
describe("getRemainingTiles", () => {
it("returns all tiles from a full board", () => {
const board = createBoard("easy");
const remaining = getRemainingTiles(board);
expect(remaining.length).toBe(24);
});
it("excludes null cells", () => {
const board = createBoard("easy");
board[0][0] = null;
board[0][1] = null;
const remaining = getRemainingTiles(board);
expect(remaining.length).toBe(22);
});
});
describe("shuffleBoard", () => {
it("preserves tile count after shuffle", () => {
const board = createBoard("easy");
board[0][0] = null;
board[0][1] = null;
const shuffled = shuffleBoard(board);
const remaining = getRemainingTiles(shuffled);
expect(remaining.length).toBe(22);
});
it("keeps null positions as occupied cells and vice versa", () => {
const board = createBoard("easy");
board[0][0] = null;
board[1][2] = null;
const shuffled = shuffleBoard(board);
let nullCount = 0;
for (const row of shuffled) {
for (const cell of row) {
if (cell === null) nullCount++;
}
}
expect(nullCount).toBe(2);
});
});