mirror of
https://github.com/tiennm99/try-superpowers.git
synced 2026-08-10 08:24:19 +00:00
feat: add board generation, remaining tiles, and shuffle
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user