Add unit and integration tests for garbage system
- 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
This commit is contained in:
+28
-107
@@ -3,6 +3,25 @@ const http = require('http');
|
||||
const { Server } = require('socket.io');
|
||||
const path = require('path');
|
||||
|
||||
// Import pure game logic functions
|
||||
const {
|
||||
BOARD_WIDTH,
|
||||
BOARD_HEIGHT,
|
||||
GARBAGE_COLOR,
|
||||
TETROMINOS,
|
||||
TETROMINO_KEYS,
|
||||
createEmptyBoard,
|
||||
getPieceFromType,
|
||||
isValidPosition,
|
||||
getGhostY,
|
||||
checkBoardOverflow,
|
||||
addGarbageRow,
|
||||
lockPieceToBoard,
|
||||
clearRows: clearRowsFromBoard,
|
||||
generatePieceBag,
|
||||
createPieceQueue
|
||||
} = require('./game-logic');
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const io = new Server(server);
|
||||
@@ -21,54 +40,8 @@ const lobby = {
|
||||
};
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Tetromino definitions
|
||||
const TETROMINOS = {
|
||||
I: { shape: [[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]], color: '#00ffff' },
|
||||
O: { shape: [[1,1],[1,1]], color: '#ffff00' },
|
||||
T: { shape: [[0,1,0],[1,1,1],[0,0,0]], color: '#800080' },
|
||||
S: { shape: [[0,1,1],[1,1,0],[0,0,0]], color: '#00ff00' },
|
||||
Z: { shape: [[1,1,0],[0,1,1],[0,0,0]], color: '#ff0000' },
|
||||
J: { shape: [[1,0,0],[1,1,1],[0,0,0]], color: '#0000ff' },
|
||||
L: { shape: [[0,0,1],[1,1,1],[0,0,0]], color: '#ffa500' }
|
||||
};
|
||||
|
||||
const TETROMINO_KEYS = Object.keys(TETROMINOS);
|
||||
const BOARD_WIDTH = 10;
|
||||
const BOARD_HEIGHT = 20;
|
||||
const GARBAGE_COLOR = '#666666';
|
||||
const LOBBY_ROOM = 'global-lobby';
|
||||
|
||||
// 7-bag piece generation system
|
||||
function generatePieceBag() {
|
||||
const bag = [...TETROMINO_KEYS];
|
||||
// Fisher-Yates shuffle
|
||||
for (let i = bag.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[bag[i], bag[j]] = [bag[j], bag[i]];
|
||||
}
|
||||
return bag;
|
||||
}
|
||||
|
||||
function createPieceQueue(bagCount = 14) {
|
||||
let queue = [];
|
||||
for (let i = 0; i < bagCount; i++) {
|
||||
queue = queue.concat(generatePieceBag());
|
||||
}
|
||||
return queue;
|
||||
}
|
||||
|
||||
function getPieceFromType(pieceType) {
|
||||
const tetromino = TETROMINOS[pieceType];
|
||||
return {
|
||||
type: pieceType,
|
||||
shape: JSON.parse(JSON.stringify(tetromino.shape)),
|
||||
color: tetromino.color,
|
||||
x: Math.floor(BOARD_WIDTH / 2) - Math.floor(tetromino.shape[0].length / 2),
|
||||
y: 0
|
||||
};
|
||||
}
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
console.log('Player connected:', socket.id);
|
||||
|
||||
@@ -331,10 +304,6 @@ io.on('connection', (socket) => {
|
||||
});
|
||||
});
|
||||
|
||||
function createEmptyBoard() {
|
||||
return Array(BOARD_HEIGHT).fill(null).map(() => Array(BOARD_WIDTH).fill(0));
|
||||
}
|
||||
|
||||
function getPlayersList() {
|
||||
return Array.from(lobby.players.values()).map(p => ({
|
||||
id: p.id,
|
||||
@@ -354,21 +323,6 @@ function getSpectatorsList() {
|
||||
}));
|
||||
}
|
||||
|
||||
function isValidPosition(piece, x, y, board, shape = null) {
|
||||
const s = shape || piece.shape;
|
||||
for (let row = 0; row < s.length; row++) {
|
||||
for (let col = 0; col < s[row].length; col++) {
|
||||
if (s[row][col]) {
|
||||
const newX = x + col;
|
||||
const newY = y + row;
|
||||
if (newX < 0 || newX >= BOARD_WIDTH || newY < 0 || newY >= BOARD_HEIGHT) return false;
|
||||
if (board[newY][newX]) return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function getRandomPiece() {
|
||||
const key = TETROMINO_KEYS[Math.floor(Math.random() * TETROMINO_KEYS.length)];
|
||||
const tetromino = TETROMINOS[key];
|
||||
@@ -421,17 +375,7 @@ function lockPiece(player) {
|
||||
const rowsCleared = clearRows(player);
|
||||
|
||||
// Check if board overflowed (top 2 rows have blocks) - this means player is eliminated
|
||||
let boardOverflowed = false;
|
||||
for (let row = 0; row < 2; row++) {
|
||||
for (let col = 0; col < BOARD_WIDTH; col++) {
|
||||
if (player.board[row][col] !== 0) {
|
||||
boardOverflowed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (boardOverflowed) break;
|
||||
}
|
||||
if (boardOverflowed) {
|
||||
if (checkBoardOverflow(player.board)) {
|
||||
player.eliminated = true;
|
||||
console.log(`[ELIMINATION] ${player.name} eliminated - board overflowed`);
|
||||
broadcastState();
|
||||
@@ -455,15 +399,10 @@ function lockPiece(player) {
|
||||
}
|
||||
|
||||
function clearRows(player) {
|
||||
let rowsCleared = 0;
|
||||
for (let row = BOARD_HEIGHT - 1; row >= 0; row--) {
|
||||
if (player.board[row].every(cell => cell !== 0)) {
|
||||
player.board.splice(row, 1);
|
||||
player.board.unshift(Array(BOARD_WIDTH).fill(0));
|
||||
rowsCleared++;
|
||||
row++;
|
||||
}
|
||||
}
|
||||
const result = clearRowsFromBoard(player.board);
|
||||
const rowsCleared = result.rowsCleared;
|
||||
player.board = result.board;
|
||||
|
||||
if (rowsCleared > 0) {
|
||||
player.lines += rowsCleared;
|
||||
const points = [0, 100, 300, 500, 800];
|
||||
@@ -496,31 +435,13 @@ function sendGarbage(sender, rowsCleared) {
|
||||
}
|
||||
|
||||
function addGarbageToPlayer(player, senderName) {
|
||||
// Remove top row and add garbage to bottom
|
||||
player.board.shift();
|
||||
const garbageRow = Array(BOARD_WIDTH).fill(GARBAGE_COLOR);
|
||||
const gap = Math.floor(Math.random() * BOARD_WIDTH);
|
||||
garbageRow[gap] = 0;
|
||||
player.board.push(garbageRow);
|
||||
// Use the pure function to add garbage row
|
||||
const result = addGarbageRow(player.board, player.currentPiece);
|
||||
player.board = result.board;
|
||||
player.currentPiece = result.currentPiece;
|
||||
|
||||
// Track garbage received
|
||||
player.garbageReceived.push({ rows: 1, sender: senderName });
|
||||
|
||||
// Push current piece up by 1 row if it exists (y decreases when moving up)
|
||||
// Don't eliminate here - let the piece lock and check board overflow then
|
||||
if (player.currentPiece) {
|
||||
player.currentPiece.y--;
|
||||
}
|
||||
}
|
||||
|
||||
function getGhostY(piece, board) {
|
||||
if (!piece) return null;
|
||||
|
||||
let ghostY = piece.y;
|
||||
while (isValidPosition(piece, piece.x, ghostY + 1, board)) {
|
||||
ghostY++;
|
||||
}
|
||||
return ghostY;
|
||||
}
|
||||
|
||||
function startGame() {
|
||||
|
||||
Reference in New Issue
Block a user