// Unit tests for pure game logic functions const { BOARD_WIDTH, BOARD_HEIGHT, GARBAGE_COLOR, createEmptyBoard, getPieceFromType, isValidPosition, getGhostY, checkBoardOverflow, addGarbageRow, lockPieceToBoard, clearRows, generatePieceBag, createPieceQueue } = require('../game-logic'); describe('createEmptyBoard', () => { test('creates a 20x10 board', () => { const board = createEmptyBoard(); expect(board.length).toBe(BOARD_HEIGHT); expect(board[0].length).toBe(BOARD_WIDTH); }); test('all cells are initialized to 0', () => { const board = createEmptyBoard(); for (let row = 0; row < BOARD_HEIGHT; row++) { for (let col = 0; col < BOARD_WIDTH; col++) { expect(board[row][col]).toBe(0); } } }); }); describe('getPieceFromType', () => { test('returns correct piece for I tetromino', () => { const piece = getPieceFromType('I'); expect(piece.type).toBe('I'); expect(piece.color).toBe('#00ffff'); expect(piece.y).toBe(0); }); test('returns correct piece for O tetromino', () => { const piece = getPieceFromType('O'); expect(piece.type).toBe('O'); expect(piece.color).toBe('#ffff00'); }); test('centers piece horizontally', () => { const piece = getPieceFromType('I'); expect(piece.x).toBe(Math.floor(BOARD_WIDTH / 2) - Math.floor(4 / 2)); }); }); describe('isValidPosition', () => { test('returns true for valid position', () => { const board = createEmptyBoard(); const piece = getPieceFromType('I'); expect(isValidPosition(piece, 3, 5, board)).toBe(true); }); test('returns false for position out of bounds (left)', () => { const board = createEmptyBoard(); const piece = getPieceFromType('I'); expect(isValidPosition(piece, -1, 5, board)).toBe(false); }); test('returns false for position out of bounds (right)', () => { const board = createEmptyBoard(); const piece = getPieceFromType('I'); expect(isValidPosition(piece, 10, 5, board)).toBe(false); }); test('returns false for position out of bounds (bottom)', () => { const board = createEmptyBoard(); const piece = getPieceFromType('I'); expect(isValidPosition(piece, 3, 20, board)).toBe(false); }); test('returns false for collision with existing block', () => { const board = createEmptyBoard(); board[10][5] = '#ff0000'; // Place a block const piece = getPieceFromType('O'); expect(isValidPosition(piece, 4, 9, board)).toBe(false); }); }); describe('getGhostY', () => { test('returns null for no piece', () => { expect(getGhostY(null, createEmptyBoard())).toBe(null); }); test('returns current Y when piece is at bottom', () => { const board = createEmptyBoard(); const piece = { ...getPieceFromType('I'), y: 15 }; const ghostY = getGhostY(piece, board); expect(ghostY).toBeLessThanOrEqual(19); }); test('returns lower Y when piece can fall', () => { const board = createEmptyBoard(); const piece = getPieceFromType('I'); const ghostY = getGhostY(piece, board); expect(ghostY).toBeGreaterThan(piece.y); }); }); describe('checkBoardOverflow', () => { test('returns false for empty board', () => { const board = createEmptyBoard(); expect(checkBoardOverflow(board)).toBe(false); }); test('returns false when only bottom rows have blocks', () => { const board = createEmptyBoard(); board[19][5] = '#ff0000'; board[18][5] = '#ff0000'; expect(checkBoardOverflow(board)).toBe(false); }); test('returns true when row 0 has a block', () => { const board = createEmptyBoard(); board[0][5] = '#ff0000'; expect(checkBoardOverflow(board)).toBe(true); }); test('returns true when row 1 has a block', () => { const board = createEmptyBoard(); board[1][5] = '#ff0000'; expect(checkBoardOverflow(board)).toBe(true); }); test('returns false when only row 2 has blocks', () => { const board = createEmptyBoard(); board[2][5] = '#ff0000'; expect(checkBoardOverflow(board)).toBe(false); }); }); describe('addGarbageRow', () => { test('maintains board height', () => { const board = createEmptyBoard(); const result = addGarbageRow(board, null); expect(result.board.length).toBe(BOARD_HEIGHT); }); test('adds garbage row at bottom', () => { const board = createEmptyBoard(); const result = addGarbageRow(board, null); const bottomRow = result.board[BOARD_HEIGHT - 1]; expect(bottomRow.some(cell => cell === GARBAGE_COLOR)).toBe(true); }); test('garbage row has exactly one gap', () => { const board = createEmptyBoard(); const result = addGarbageRow(board, null); const bottomRow = result.board[BOARD_HEIGHT - 1]; const gaps = bottomRow.filter(cell => cell === 0).length; expect(gaps).toBe(1); }); test('pushes current piece up by 1', () => { const board = createEmptyBoard(); const piece = { ...getPieceFromType('I'), y: 5 }; const result = addGarbageRow(board, piece); expect(result.currentPiece.y).toBe(4); }); test('handles null current piece', () => { const board = createEmptyBoard(); const result = addGarbageRow(board, null); expect(result.currentPiece).toBe(null); }); }); describe('lockPieceToBoard', () => { test('locks piece to board', () => { const board = createEmptyBoard(); const piece = { ...getPieceFromType('O'), x: 4, y: 5 }; const newBoard = lockPieceToBoard(piece, board); // O piece is 2x2, so it should fill 4 cells expect(newBoard[5][4]).toBe(piece.color); expect(newBoard[5][5]).toBe(piece.color); expect(newBoard[6][4]).toBe(piece.color); expect(newBoard[6][5]).toBe(piece.color); }); test('does not modify original board', () => { const board = createEmptyBoard(); const piece = { ...getPieceFromType('O'), x: 4, y: 5 }; lockPieceToBoard(piece, board); // Original board should still be empty expect(board[5][4]).toBe(0); }); }); describe('clearRows', () => { test('returns 0 rows cleared for empty board', () => { const board = createEmptyBoard(); const result = clearRows(board); expect(result.rowsCleared).toBe(0); }); test('clears one complete row', () => { const board = createEmptyBoard(); board[10].fill('#ff0000'); // Fill row 10 const result = clearRows(board); expect(result.rowsCleared).toBe(1); }); test('clears multiple complete rows', () => { const board = createEmptyBoard(); board[10].fill('#ff0000'); board[11].fill('#00ff00'); board[12].fill('#0000ff'); const result = clearRows(board); expect(result.rowsCleared).toBe(3); }); test('adds empty rows at top after clearing', () => { const board = createEmptyBoard(); board[19].fill('#ff0000'); // Fill bottom row const result = clearRows(board); expect(result.board[0].every(cell => cell === 0)).toBe(true); }); test('maintains board height after clearing', () => { const board = createEmptyBoard(); board[18].fill('#ff0000'); board[19].fill('#00ff00'); const result = clearRows(board); expect(result.board.length).toBe(BOARD_HEIGHT); }); }); describe('generatePieceBag', () => { test('returns array of 7 pieces', () => { const bag = generatePieceBag(); expect(bag.length).toBe(7); }); test('contains all 7 tetromino types', () => { const bag = generatePieceBag(); const types = new Set(bag); expect(types.size).toBe(7); expect(types.has('I')).toBe(true); expect(types.has('O')).toBe(true); expect(types.has('T')).toBe(true); expect(types.has('S')).toBe(true); expect(types.has('Z')).toBe(true); expect(types.has('J')).toBe(true); expect(types.has('L')).toBe(true); }); }); describe('createPieceQueue', () => { test('creates queue with correct length', () => { const queue = createPieceQueue(2); expect(queue.length).toBe(14); // 2 bags * 7 pieces }); test('uses default bag count of 14', () => { const queue = createPieceQueue(); expect(queue.length).toBe(98); // 14 bags * 7 pieces }); });