80f59fd3b3
- Extract pure game logic functions to game-logic.js for testability - Add Jest testing framework with 57 tests covering: * Unit tests for pure functions (createEmptyBoard, checkBoardOverflow, etc.) * Integration tests for garbage system (addGarbageToPlayer, sendGarbage) * Socket.io integration tests for multiplayer flow - Refactor index.js to use extracted functions - Tests verify garbage elimination bug fix: players only eliminated when board overflows
270 lines
8.0 KiB
JavaScript
270 lines
8.0 KiB
JavaScript
// 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
|
|
});
|
|
});
|