Add hold piece feature

- Added holdPiece and canHold state to TetrisGame class
- Implemented hold() method to swap current piece with held piece
- Added player-hold socket event on server
- Added HOLD preview canvas showing held piece (grayed when unavailable)
- Added C key keyboard shortcut and touch button for hold
- Fixed canHold reset on piece spawn for proper swap functionality

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-20 08:50:52 -07:00
parent cde1643606
commit 4a49c76cdc
8 changed files with 410 additions and 8 deletions
+69 -2
View File
@@ -43,15 +43,30 @@ class TetrisRenderer {
nextDiv.appendChild(nextCanvas);
boardDiv.appendChild(nextDiv);
// Hold piece preview
const holdDiv = document.createElement('div');
holdDiv.className = 'board-info';
const holdCanvas = document.createElement('canvas');
holdCanvas.id = `hold-${playerId}`;
holdCanvas.width = 80;
holdCanvas.height = 80;
holdCanvas.style.width = '80px';
holdCanvas.style.height = '80px';
holdDiv.innerHTML = '<span>HOLD:</span>';
holdDiv.appendChild(holdCanvas);
boardDiv.appendChild(holdDiv);
this.container.appendChild(boardDiv);
this.boards.set(playerId, {
element: boardDiv,
canvas: canvas,
nextCanvas: nextCanvas,
holdCanvas: holdCanvas,
info: infoDiv,
ctx: canvas.getContext('2d'),
nextCtx: nextCanvas.getContext('2d')
nextCtx: nextCanvas.getContext('2d'),
holdCtx: holdCanvas.getContext('2d')
});
return boardDiv;
@@ -69,7 +84,7 @@ class TetrisRenderer {
const boardData = this.boards.get(playerId);
if (!boardData) return;
const { ctx, nextCtx, element, info } = boardData;
const { ctx, nextCtx, holdCtx, element, info } = boardData;
// Clear canvas
ctx.fillStyle = '#000';
@@ -93,6 +108,15 @@ class TetrisRenderer {
this.drawNextPiece(nextCtx, gameState.nextPiece);
}
// Draw hold piece
if (gameState && gameState.holdPiece) {
this.drawHoldPiece(holdCtx, gameState.holdPiece, gameState.canHold);
} else if (holdCtx) {
// Clear hold canvas if no hold piece
holdCtx.fillStyle = '#000';
holdCtx.fillRect(0, 0, holdCtx.canvas.width, holdCtx.canvas.height);
}
// Update stats
if (gameState) {
const linesSpan = info.querySelector('.lines');
@@ -222,6 +246,49 @@ class TetrisRenderer {
}
}
drawHoldPiece(ctx, piece, canHold) {
// Clear
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
if (!piece) return;
// Gray out if cannot hold again this turn
const alpha = canHold ? 1.0 : 0.4;
// Center the piece
const pieceWidth = piece.shape[0].length * 20;
const pieceHeight = piece.shape.length * 20;
const offsetX = (ctx.canvas.width - pieceWidth) / 2;
const offsetY = (ctx.canvas.height - pieceHeight) / 2;
for (let row = 0; row < piece.shape.length; row++) {
for (let col = 0; col < piece.shape[row].length; col++) {
if (piece.shape[row][col]) {
const x = offsetX + col * 20;
const y = offsetY + row * 20;
ctx.fillStyle = piece.color + Math.floor(alpha * 255).toString(16).padStart(2, '0');
ctx.fillRect(x, y, 20, 20);
if (canHold) {
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fillRect(x, y, 20, 3);
ctx.fillRect(x, y, 3, 20);
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.fillRect(x, y + 17, 20, 3);
ctx.fillRect(x + 17, y, 3, 20);
ctx.strokeStyle = 'rgba(0,0,0,0.5)';
ctx.lineWidth = 1;
ctx.strokeRect(x, y, 20, 20);
}
}
}
}
}
setActivePlayer(playerId) {
// Remove main class from all boards
this.boards.forEach((boardData, id) => {