Files
battle-royal-tetris/public/js/game.js
T
jozamudi 634c6e8eab Implement shared 7-bag piece sequence
- Add generatePieceBag() and createPieceQueue() for 7-bag system
- Add pieceQueue and playerSequenceIndex to lobby state
- Modify spawnPiece() to consume from shared queue per player
- Update player-hold handler to manage sequence index on hold
- Add sequenceIndex to state updates for tracking
- Update client loadState() to receive sequenceIndex

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 01:37:55 +00:00

381 lines
10 KiB
JavaScript

// Core Tetris Game Logic
const BOARD_WIDTH = 10;
const BOARD_HEIGHT = 20;
const CELL_SIZE = 24;
// Tetromino definitions with colors
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 GARbage_COLOR = '#666666';
class TetrisGame {
constructor(playerId) {
this.playerId = playerId;
this.board = this.createEmptyBoard();
this.currentPiece = null;
this.nextPiece = null;
this.holdPiece = null;
this.canHold = true;
this.score = 0;
this.lines = 0;
this.level = 1;
this.gameOver = false;
this.eliminated = false;
// Game speed (milliseconds per drop)
this.dropInterval = 1000;
this.lastDrop = 0;
// Input lock for delay hits
this.inputLocked = false;
this.lockUntil = 0;
}
createEmptyBoard() {
return Array(BOARD_HEIGHT).fill(null).map(() => Array(BOARD_WIDTH).fill(0));
}
initialize() {
this.board = this.createEmptyBoard();
this.score = 0;
this.lines = 0;
this.level = 1;
this.gameOver = false;
this.eliminated = false;
this.dropInterval = 1000;
this.holdPiece = null;
this.canHold = true;
this.currentPiece = this.spawnPiece();
this.nextPiece = this.getRandomPiece();
if (!this.currentPiece) {
this.gameOver = true;
}
}
getRandomPiece() {
const key = TETROMINO_KEYS[Math.floor(Math.random() * TETROMINO_KEYS.length)];
const tetromino = TETROMINOS[key];
return {
type: key,
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
};
}
spawnPiece() {
const piece = this.nextPiece;
this.nextPiece = this.getRandomPiece();
// Check if spawn position is valid
if (!this.isValidPosition(piece.x, piece.y, piece.shape)) {
return null;
}
return piece;
}
isValidPosition(x, y, shape) {
for (let row = 0; row < shape.length; row++) {
for (let col = 0; col < shape[row].length; col++) {
if (shape[row][col]) {
const newX = x + col;
const newY = y + row;
// Check bounds
if (newX < 0 || newX >= BOARD_WIDTH || newY >= BOARD_HEIGHT) {
return false;
}
// Check collision with locked pieces (only if on board)
if (newY >= 0 && this.board[newY][newX]) {
return false;
}
}
}
}
return true;
}
move(direction) {
if (this.gameOver || this.inputLocked) return false;
const newX = this.currentPiece.x + (direction === 'left' ? -1 : 1);
if (this.isValidPosition(newX, this.currentPiece.y, this.currentPiece.shape)) {
this.currentPiece.x = newX;
return true;
}
return false;
}
rotate() {
if (this.gameOver || this.inputLocked) return false;
const originalShape = this.currentPiece.shape;
const rows = originalShape.length;
const cols = originalShape[0].length;
// Rotate 90 degrees clockwise
const rotated = Array(cols).fill(null).map(() => Array(rows).fill(0));
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
rotated[col][rows - 1 - row] = originalShape[row][col];
}
}
// Try rotation with wall kicks
const kicks = [0, -1, 1, -2, 2];
for (const kick of kicks) {
if (this.isValidPosition(this.currentPiece.x + kick, this.currentPiece.y, rotated)) {
this.currentPiece.shape = rotated;
this.currentPiece.x += kick;
return true;
}
}
return false;
}
drop() {
if (this.gameOver || this.inputLocked) return false;
const newY = this.currentPiece.y + 1;
if (this.isValidPosition(this.currentPiece.x, newY, this.currentPiece.shape)) {
this.currentPiece.y = newY;
return true;
}
// Lock the piece
return this.lockPiece();
}
hardDrop() {
if (this.gameOver || this.inputLocked) return 0;
let dropped = 0;
while (this.isValidPosition(this.currentPiece.x, this.currentPiece.y + 1, this.currentPiece.shape)) {
this.currentPiece.y++;
dropped++;
}
this.lockPiece();
return dropped;
}
hold() {
if (this.gameOver || this.inputLocked || !this.canHold) return false;
if (this.holdPiece === null) {
// First hold - store current piece and spawn next
this.holdPiece = {
type: this.currentPiece.type,
shape: JSON.parse(JSON.stringify(this.currentPiece.shape)),
color: this.currentPiece.color
};
this.currentPiece = this.nextPiece;
this.nextPiece = this.getRandomPiece();
} else {
// Swap with held piece
const temp = {
type: this.currentPiece.type,
shape: JSON.parse(JSON.stringify(this.currentPiece.shape)),
color: this.currentPiece.color
};
this.currentPiece = {
...this.holdPiece,
shape: JSON.parse(JSON.stringify(this.holdPiece.shape)),
x: Math.floor(BOARD_WIDTH / 2) - Math.floor(this.holdPiece.shape[0].length / 2),
y: 0
};
this.holdPiece = temp;
}
this.canHold = false;
return true;
}
lockPiece() {
if (!this.currentPiece) return false;
// Lock piece into board
for (let row = 0; row < this.currentPiece.shape.length; row++) {
for (let col = 0; col < this.currentPiece.shape[row].length; col++) {
if (this.currentPiece.shape[row][col]) {
const boardY = this.currentPiece.y + row;
const boardX = this.currentPiece.x + col;
if (boardY >= 0 && boardY < BOARD_HEIGHT && boardX >= 0 && boardX < BOARD_WIDTH) {
this.board[boardY][boardX] = this.currentPiece.color;
}
}
}
}
// Check for game over (piece locked above visible area)
if (this.currentPiece.y <= 0) {
this.gameOver = true;
this.eliminated = true;
}
// Clear completed rows
const cleared = this.clearRows();
// Spawn new piece
this.currentPiece = this.spawnPiece();
if (!this.currentPiece) {
this.gameOver = true;
this.eliminated = true;
}
// Reset canHold for the new piece
this.canHold = true;
return cleared;
}
clearRows() {
let rowsCleared = 0;
for (let row = BOARD_HEIGHT - 1; row >= 0; row--) {
if (this.board[row].every(cell => cell !== 0)) {
// Remove the row
this.board.splice(row, 1);
// Add empty row at top
this.board.unshift(Array(BOARD_WIDTH).fill(0));
rowsCleared++;
row++; // Check same row again
}
}
if (rowsCleared > 0) {
this.lines += rowsCleared;
this.updateScore(rowsCleared);
this.updateLevel();
}
return rowsCleared;
}
updateScore(rowsCleared) {
const points = [0, 100, 300, 500, 800];
this.score += points[rowsCleared] * this.level;
}
updateLevel() {
this.level = Math.floor(this.lines / 10) + 1;
this.dropInterval = Math.max(100, 1000 - (this.level - 1) * 100);
}
getGhostY() {
if (!this.currentPiece) return null;
let ghostY = this.currentPiece.y;
while (this.isValidPosition(this.currentPiece.x, ghostY + 1, this.currentPiece.shape)) {
ghostY++;
}
return ghostY;
}
receiveGarbage(rows) {
if (this.gameOver) return;
// Add garbage rows at bottom
for (let i = 0; i < rows; i++) {
// Remove top row (simulating overflow)
this.board.pop();
// Add garbage row with random gap
const garbageRow = Array(BOARD_WIDTH).fill(GARbage_COLOR);
const gap = Math.floor(Math.random() * BOARD_WIDTH);
garbageRow[gap] = 0;
this.board.unshift(garbageRow);
}
// Check if garbage caused game over
for (let row = 0; row < 2; row++) {
for (let col = 0; col < BOARD_WIDTH; col++) {
if (this.board[row][col] !== 0) {
this.gameOver = true;
this.eliminated = true;
return;
}
}
}
}
applyDelayHit(duration) {
this.inputLocked = true;
this.lockUntil = Date.now() + duration;
}
update(deltaTime) {
if (this.gameOver) return;
// Check if input lock has expired
if (this.inputLocked && Date.now() >= this.lockUntil) {
this.inputLocked = false;
}
// Auto drop
if (!this.inputLocked && Date.now() - this.lastDrop >= this.dropInterval) {
this.drop();
this.lastDrop = Date.now();
}
}
getState() {
const ghostY = this.currentPiece ? this.getGhostY() : null;
return {
playerId: this.playerId,
board: JSON.parse(JSON.stringify(this.board)),
currentPiece: this.currentPiece ? JSON.parse(JSON.stringify(this.currentPiece)) : null,
ghostY: ghostY,
nextPiece: this.nextPiece ? JSON.parse(JSON.stringify(this.nextPiece)) : null,
holdPiece: this.holdPiece ? JSON.parse(JSON.stringify(this.holdPiece)) : null,
canHold: this.canHold,
score: this.score,
lines: this.lines,
level: this.level,
gameOver: this.gameOver,
eliminated: this.eliminated
};
}
loadState(state) {
this.board = JSON.parse(JSON.stringify(state.board));
this.currentPiece = state.currentPiece ? JSON.parse(JSON.stringify(state.currentPiece)) : null;
this.nextPiece = state.nextPiece ? JSON.parse(JSON.stringify(state.nextPiece)) : null;
this.holdPiece = state.holdPiece ? JSON.parse(JSON.stringify(state.holdPiece)) : null;
this.canHold = state.canHold !== undefined ? state.canHold : true;
this.score = state.score;
this.lines = state.lines;
this.level = state.level;
this.gameOver = state.gameOver;
this.eliminated = state.eliminated;
this.sequenceIndex = state.sequenceIndex || 0;
this.dropInterval = Math.max(100, 1000 - (this.level - 1) * 100);
}
}
// Export for use in other modules
window.TetrisGame = TetrisGame;
window.TETROMINOS = TETROMINOS;
window.BOARD_WIDTH = BOARD_WIDTH;
window.BOARD_HEIGHT = BOARD_HEIGHT;
window.CELL_SIZE = CELL_SIZE;
window.GARBAGE_COLOR = GARbage_COLOR;