// Integration tests for garbage system const { BOARD_WIDTH, BOARD_HEIGHT, GARBAGE_COLOR, createEmptyBoard, getPieceFromType, checkBoardOverflow, addGarbageRow, lockPieceToBoard } = require('../game-logic'); // Mock player object function createMockPlayer(name, pieceY = 5) { return { id: `player-${name}`, name, board: createEmptyBoard(), currentPiece: pieceY !== null ? { ...getPieceFromType('I'), y: pieceY } : null, nextPiece: null, holdPiece: null, canHold: true, score: 0, lines: 0, level: 1, eliminated: false, dropCounter: 0, dropInterval: 1000, garbageReceived: [] }; } // Mock lobby object function createMockLobby(players) { const lobby = { players: new Map() }; players.forEach(p => lobby.players.set(p.id, p)); return lobby; } // Simulate addGarbageToPlayer (from index.js) function addGarbageToPlayer(player, senderName) { const result = addGarbageRow(player.board, player.currentPiece); player.board = result.board; player.currentPiece = result.currentPiece; player.garbageReceived.push({ rows: 1, sender: senderName }); } // Simulate sendGarbage (from index.js) function sendGarbage(lobby, sender, rowsCleared) { const garbageLog = []; for (let i = 0; i < rowsCleared; i++) { const opponents = Array.from(lobby.players.values()).filter( p => p.id !== sender.id && !p.eliminated ); if (opponents.length === 0) { break; } const target = opponents[Math.floor(Math.random() * opponents.length)]; garbageLog.push(target.name); addGarbageToPlayer(target, sender.name); } return garbageLog; } // Simulate lockPiece with elimination check (from index.js) function lockPiece(player) { if (!player.currentPiece) return 0; // Lock piece into board player.board = lockPieceToBoard(player.currentPiece, player.board); // Check board overflow for elimination if (checkBoardOverflow(player.board)) { player.eliminated = true; return 0; } // Clear rows 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++; } } return rowsCleared; } describe('addGarbageToPlayer', () => { test('adds exactly 1 garbage row per call', () => { const player = createMockPlayer('Player1', 5); const initialGarbageCount = player.garbageReceived.length; addGarbageToPlayer(player, 'Enemy'); expect(player.garbageReceived.length).toBe(initialGarbageCount + 1); expect(player.garbageReceived[0].rows).toBe(1); }); test('does NOT eliminate player when receiving garbage', () => { const player = createMockPlayer('Player1', 5); addGarbageToPlayer(player, 'Enemy'); expect(player.eliminated).toBe(false); }); test('pushes current piece up by 1 row', () => { const player = createMockPlayer('Player1', 10); addGarbageToPlayer(player, 'Enemy'); expect(player.currentPiece.y).toBe(9); }); test('tracks sender of garbage', () => { const player = createMockPlayer('Player1', 5); addGarbageToPlayer(player, 'Attacker'); expect(player.garbageReceived[0].sender).toBe('Attacker'); }); test('player survives multiple garbage rows if board not full', () => { const player = createMockPlayer('Player1', 10); // Receive 5 garbage rows for (let i = 0; i < 5; i++) { addGarbageToPlayer(player, 'Enemy'); } // Player should still be alive expect(player.eliminated).toBe(false); expect(player.currentPiece.y).toBe(5); // 10 - 5 = 5 }); test('handles player with no current piece', () => { const player = createMockPlayer('Player1', null); addGarbageToPlayer(player, 'Enemy'); expect(player.currentPiece).toBe(null); expect(player.garbageReceived.length).toBe(1); }); test('multiple garbage from different senders tracked separately', () => { const player = createMockPlayer('Player1', 10); addGarbageToPlayer(player, 'Enemy1'); addGarbageToPlayer(player, 'Enemy2'); addGarbageToPlayer(player, 'Enemy1'); expect(player.garbageReceived).toHaveLength(3); expect(player.garbageReceived[0].sender).toBe('Enemy1'); expect(player.garbageReceived[1].sender).toBe('Enemy2'); expect(player.garbageReceived[2].sender).toBe('Enemy1'); }); }); describe('sendGarbage', () => { test('sends garbage equal to rows cleared', () => { const sender = createMockPlayer('Sender', 5); const target = createMockPlayer('Target', 10); const lobby = createMockLobby([sender, target]); const garbageLog = sendGarbage(lobby, sender, 3); expect(garbageLog.length).toBe(3); expect(target.garbageReceived.length).toBe(3); }); test('excludes sender from garbage targets', () => { const sender = createMockPlayer('Sender', 5); const target = createMockPlayer('Target', 10); const lobby = createMockLobby([sender, target]); sendGarbage(lobby, sender, 2); expect(sender.garbageReceived.length).toBe(0); expect(target.garbageReceived.length).toBe(2); }); test('excludes eliminated players from garbage targets', () => { const sender = createMockPlayer('Sender', 5); const eliminated = createMockPlayer('Eliminated', 10); eliminated.eliminated = true; const target = createMockPlayer('Target', 10); const lobby = createMockLobby([sender, eliminated, target]); sendGarbage(lobby, sender, 3); expect(eliminated.garbageReceived.length).toBe(0); expect(target.garbageReceived.length).toBe(3); }); test('stops sending when all opponents eliminated', () => { const sender = createMockPlayer('Sender', 5); const eliminated = createMockPlayer('Eliminated', 10); eliminated.eliminated = true; const lobby = createMockLobby([sender, eliminated]); const garbageLog = sendGarbage(lobby, sender, 4); expect(garbageLog.length).toBe(0); }); test('distributes garbage to multiple opponents', () => { const sender = createMockPlayer('Sender', 5); const target1 = createMockPlayer('Target1', 10); const target2 = createMockPlayer('Target2', 10); const lobby = createMockLobby([sender, target1, target2]); sendGarbage(lobby, sender, 4); // Total garbage sent should equal rows cleared expect(target1.garbageReceived.length + target2.garbageReceived.length).toBe(4); }); test('garbage distribution is deterministic with mocked random', () => { const sender = createMockPlayer('Sender', 5); const target1 = createMockPlayer('Target1', 10); const target2 = createMockPlayer('Target2', 10); const lobby = createMockLobby([sender, target1, target2]); // Mock Math.random to always pick first opponent (index 0) const originalRandom = Math.random; Math.random = () => 0.1; sendGarbage(lobby, sender, 4); Math.random = originalRandom; // All garbage should go to target1 (first in array) expect(target1.garbageReceived.length).toBe(4); expect(target2.garbageReceived.length).toBe(0); }); test('1 row cleared sends 1 garbage row', () => { const sender = createMockPlayer('Sender', 5); const target = createMockPlayer('Target', 10); const lobby = createMockLobby([sender, target]); const garbageLog = sendGarbage(lobby, sender, 1); expect(garbageLog.length).toBe(1); expect(target.garbageReceived.length).toBe(1); }); test('2 rows cleared sends 2 garbage rows', () => { const sender = createMockPlayer('Sender', 5); const target = createMockPlayer('Target', 10); const lobby = createMockLobby([sender, target]); const garbageLog = sendGarbage(lobby, sender, 2); expect(garbageLog.length).toBe(2); }); test('4 rows (tetris) sends 4 garbage rows', () => { const sender = createMockPlayer('Sender', 5); const target = createMockPlayer('Target', 10); const lobby = createMockLobby([sender, target]); const garbageLog = sendGarbage(lobby, sender, 4); expect(garbageLog.length).toBe(4); }); }); describe('lockPiece elimination', () => { test('piece locking does not eliminate when board not overflowed', () => { const player = createMockPlayer('Player1', 10); const rowsCleared = lockPiece(player); expect(player.eliminated).toBe(false); expect(rowsCleared).toBe(0); }); test('piece locking eliminates when board overflowed', () => { const player = createMockPlayer('Player1', 0); // Fill top 2 rows with garbage player.board[0].fill(GARBAGE_COLOR); player.board[0][5] = 0; // Gap lockPiece(player); expect(player.eliminated).toBe(true); }); test('piece at y=0 eliminates when it locks into top rows', () => { const player = createMockPlayer('Player1', 0); // I piece at y=0 locks into rows 0,1,2,3 // This causes overflow since row 0 and 1 will have blocks lockPiece(player); expect(player.eliminated).toBe(true); }); test('piece at y=5 does not eliminate if top rows empty', () => { const player = createMockPlayer('Player1', 5); // Top rows are empty, piece locks at rows 5,6,7,8 lockPiece(player); expect(player.eliminated).toBe(false); }); test('piece locks and does not clear rows when no complete rows', () => { const player = createMockPlayer('Player1', 10); // Piece locks at rows 10,11,12,13 - no complete rows const rowsCleared = lockPiece(player); expect(rowsCleared).toBe(0); }); }); describe('checkBoardOverflow', () => { test('returns false when top 2 rows are empty', () => { const board = createEmptyBoard(); expect(checkBoardOverflow(board)).toBe(false); }); test('returns true when row 0 has any block', () => { const board = createEmptyBoard(); board[0][5] = '#ff0000'; expect(checkBoardOverflow(board)).toBe(true); }); test('returns true when row 1 has any 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); }); test('returns false when garbage is at bottom rows only', () => { const board = createEmptyBoard(); board[19].fill(GARBAGE_COLOR); board[18].fill(GARBAGE_COLOR); expect(checkBoardOverflow(board)).toBe(false); }); test('returns true when both row 0 and row 1 have blocks', () => { const board = createEmptyBoard(); board[0][3] = '#ff0000'; board[1][7] = '#ff0000'; expect(checkBoardOverflow(board)).toBe(true); }); test('returns true when row 0 has garbage with gap', () => { const board = createEmptyBoard(); board[0].fill(GARBAGE_COLOR); board[0][5] = 0; // Gap in row 0 expect(checkBoardOverflow(board)).toBe(true); }); }); describe('Garbage Elimination Bug Fix', () => { test('player NOT eliminated immediately after receiving garbage', () => { // This tests the bug fix: players should NOT be eliminated when garbage is received // Elimination should only happen when piece locks and board overflows const player = createMockPlayer('Player1', 5); // Receive garbage - player should NOT be eliminated addGarbageToPlayer(player, 'Enemy'); expect(player.eliminated).toBe(false); }); test('player survives garbage when piece is pushed but still visible', () => { // Player with piece at y=5 receives 3 garbage rows // Piece should be at y=2, still visible, player not eliminated const player = createMockPlayer('Player1', 5); for (let i = 0; i < 3; i++) { addGarbageToPlayer(player, 'Enemy'); } expect(player.eliminated).toBe(false); expect(player.currentPiece.y).toBe(2); }); test('multiple garbage rows do not cause premature elimination', () => { // The original bug caused elimination even when board wasn't full // This test verifies that garbage alone doesn't eliminate const player = createMockPlayer('Player1', 8); // Receive 5 garbage rows - piece pushed to y=3 for (let i = 0; i < 5; i++) { addGarbageToPlayer(player, 'Enemy'); } // Board has 5 garbage rows at bottom, piece at y=3 // Player should NOT be eliminated expect(player.eliminated).toBe(false); expect(checkBoardOverflow(player.board)).toBe(false); }); test('sendGarbage does not eliminate opponents', () => { // When sender clears rows, opponents receive garbage but should not be eliminated const sender = createMockPlayer('Sender', 5); const target = createMockPlayer('Target', 10); const lobby = createMockLobby([sender, target]); // Sender clears 4 rows (tetris) sendGarbage(lobby, sender, 4); // Target received 4 garbage rows but should NOT be eliminated expect(target.eliminated).toBe(false); expect(target.garbageReceived.length).toBe(4); }); test('elimination only happens when piece locks with overflow', () => { // Complete scenario: garbage received, piece locks, then elimination checked const player = createMockPlayer('Player1', 0); // Piece at spawn // Receive 1 garbage row - player NOT eliminated addGarbageToPlayer(player, 'Enemy'); expect(player.eliminated).toBe(false); // Fill top row to simulate overflow player.board[0].fill(GARBAGE_COLOR); player.board[0][5] = 0; // Now lock piece - player eliminated lockPiece(player); expect(player.eliminated).toBe(true); }); test('player with piece above board survives garbage', () => { // Edge case: piece is already above visible area (negative y) // Should still not be eliminated by garbage alone const player = createMockPlayer('Player1', 2); // Push piece 5 rows up (to y=-3) for (let i = 0; i < 5; i++) { addGarbageToPlayer(player, 'Enemy'); } // Piece is at y=-3, but player NOT eliminated expect(player.eliminated).toBe(false); expect(player.currentPiece.y).toBe(-3); }); }); describe('edge cases', () => { test('player with piece at y=0 receives garbage', () => { const player = createMockPlayer('Player1', 0); addGarbageToPlayer(player, 'Enemy'); expect(player.currentPiece.y).toBe(-1); expect(player.eliminated).toBe(false); }); test('18 garbage rows pushes piece well above board', () => { const player = createMockPlayer('Player1', 10); for (let i = 0; i < 18; i++) { addGarbageToPlayer(player, 'Enemy'); } expect(player.currentPiece.y).toBe(-8); // Board overflowed but player not eliminated yet (only on lock) expect(player.eliminated).toBe(false); }); test('garbage with no opponents does not crash', () => { const sender = createMockPlayer('Sender', 5); const lobby = createMockLobby([sender]); const garbageLog = sendGarbage(lobby, sender, 4); expect(garbageLog.length).toBe(0); }); test('zero rows cleared sends no garbage', () => { const sender = createMockPlayer('Sender', 5); const target = createMockPlayer('Target', 10); const lobby = createMockLobby([sender, target]); const garbageLog = sendGarbage(lobby, sender, 0); expect(garbageLog.length).toBe(0); expect(target.garbageReceived.length).toBe(0); }); });