/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 }); /**** * Game Code ****/ // Add full screen background var backgroundImage = LK.getAsset('background', { anchorX: 0, anchorY: 0, x: 150, y: 0 }); game.addChild(backgroundImage); // --- Tetris Game Implementation --- // Board configuration var BOARD_COLS = 10; var BOARD_ROWS = 20; var CELL_SIZE = 80; // Larger cells for better visibility var BOARD_WIDTH = BOARD_COLS * CELL_SIZE; var BOARD_HEIGHT = BOARD_ROWS * CELL_SIZE; var PLAYER_BOARD_X = 1024 + 100; // Right side with more spacing var AI_BOARD_X = 1024 - BOARD_WIDTH - 100; // Left side with more spacing var BOARD_Y = 900; // Add AI background (left half) var aiBackgroundImage = LK.getAsset('aiBackground', { anchorX: 0, anchorY: 0, x: 6, y: BOARD_Y, alpha: 1, tint: 0xa80000 }); game.addChild(aiBackgroundImage); // Add Player background (right half) var playerBackgroundImage = LK.getAsset('playerBackground', { anchorX: 0, anchorY: 0, x: 1025, y: BOARD_Y, alpha: 1, tint: 0xffdd00 }); game.addChild(playerBackgroundImage); // Tetrimino definitions var TETROMINOS = [ // I - Light Blue { color: 0x9acde3, blocks: [[0, 1], [1, 1], [2, 1], [3, 1]] }, // O - Yellow { color: 0xffe800, blocks: [[1, 0], [2, 0], [1, 1], [2, 1]] }, // T - Red { color: 0xcc0000, blocks: [[1, 0], [0, 1], [1, 1], [2, 1]] }, // S - Dark Green { color: 0x008000, blocks: [[1, 0], [2, 0], [0, 1], [1, 1]] }, // Z - Brown { color: 0x8b4513, blocks: [[0, 0], [1, 0], [1, 1], [2, 1]] }, // J - Blue { color: 0x0000cd, blocks: [[0, 0], [0, 1], [1, 1], [2, 1]] }, // L - Dark Orange { color: 0xff8c00, blocks: [[2, 0], [0, 1], [1, 1], [2, 1]] }]; // Board states for player and AI var playerBoard = []; var aiBoard = []; for (var r = 0; r < BOARD_ROWS; r++) { playerBoard[r] = []; aiBoard[r] = []; for (var c = 0; c < BOARD_COLS; c++) { playerBoard[r][c] = null; aiBoard[r][c] = null; } } // Score var playerScore = 0; var aiScore = 0; // Labels for boards // Next piece preview container and label var nextPieceContainer = new Container(); nextPieceContainer.x = PLAYER_BOARD_X + BOARD_WIDTH / 2 - 120; nextPieceContainer.y = BOARD_Y - 350; game.addChild(nextPieceContainer); var nextPieceLabel = new Text2('NEXT', { size: 50, fill: 0xffd900 }); nextPieceLabel.anchor.set(0.5, 0); nextPieceLabel.x = 120; nextPieceLabel.y = -60; nextPieceContainer.addChild(nextPieceLabel); var playerLabel = new Text2('PLAYER', { size: 70, fill: 0xffd900 }); playerLabel.anchor.set(0.5, 0); playerLabel.x = PLAYER_BOARD_X + BOARD_WIDTH / 2; playerLabel.y = BOARD_Y - 100; game.addChild(playerLabel); var aiLabel = new Text2('AI', { size: 70, fill: 0xFF0000 }); aiLabel.anchor.set(0.5, 0); aiLabel.x = AI_BOARD_X + BOARD_WIDTH / 2; aiLabel.y = 2525; game.addChild(aiLabel); // Player board container and graphics var playerContainer = new Container(); playerContainer.x = PLAYER_BOARD_X; playerContainer.y = BOARD_Y; game.addChild(playerContainer); var playerCellGraphics = []; for (var r = 0; r < BOARD_ROWS; r++) { playerCellGraphics[r] = []; for (var c = 0; c < BOARD_COLS; c++) { var cell = LK.getAsset('cell', { anchorX: 0, anchorY: 0 }); cell.width = CELL_SIZE - 2; cell.height = CELL_SIZE - 2; cell.x = c * CELL_SIZE + 1; cell.y = r * CELL_SIZE + 1; cell.visible = false; playerContainer.addChild(cell); playerCellGraphics[r][c] = cell; } } // AI board container and graphics var aiContainer = new Container(); aiContainer.x = AI_BOARD_X; aiContainer.y = BOARD_Y; game.addChild(aiContainer); var aiCellGraphics = []; for (var r = 0; r < BOARD_ROWS; r++) { aiCellGraphics[r] = []; for (var c = 0; c < BOARD_COLS; c++) { var cell = LK.getAsset('cell', { anchorX: 0, anchorY: 0 }); cell.width = CELL_SIZE - 2; cell.height = CELL_SIZE - 2; cell.x = c * CELL_SIZE + 1; cell.y = r * CELL_SIZE + 1; cell.visible = false; aiContainer.addChild(cell); aiCellGraphics[r][c] = cell; } } // Active piece states var playerPiece = null; var playerX = 0; var playerY = 0; var aiPiece = null; var aiX = 0; var aiY = 0; // Next piece preview for player var nextPlayerPiece = null; var nextPieceBlocks = []; var nextPieceGraphics = []; // Helper: draw next piece preview in nextPieceContainer function drawNextPiecePreview(piece) { // Remove previous preview blocks for (var i = 0; i < nextPieceBlocks.length; i++) { nextPieceContainer.removeChild(nextPieceBlocks[i]); } nextPieceBlocks = []; if (!piece) { return; } // Find minX/minY to center the piece var minX = Math.min(piece.blocks[0][0], piece.blocks[1][0], piece.blocks[2][0], piece.blocks[3][0]); var minY = Math.min(piece.blocks[0][1], piece.blocks[1][1], piece.blocks[2][1], piece.blocks[3][1]); var maxX = Math.max(piece.blocks[0][0], piece.blocks[1][0], piece.blocks[2][0], piece.blocks[3][0]); var maxY = Math.max(piece.blocks[0][1], piece.blocks[1][1], piece.blocks[2][1], piece.blocks[3][1]); var previewCellSize = 60; var offsetX = 120 - (maxX - minX + 1) * previewCellSize / 2; var offsetY = 0; for (var i = 0; i < 4; i++) { var block = piece.blocks[i]; var cell = LK.getAsset('cell', { anchorX: 0, anchorY: 0 }); cell.width = previewCellSize - 2; cell.height = previewCellSize - 2; cell.x = offsetX + (block[0] - minX) * previewCellSize + 1; cell.y = offsetY + (block[1] - minY) * previewCellSize + 1; cell.tint = piece.color; cell.alpha = 1; nextPieceContainer.addChild(cell); nextPieceBlocks.push(cell); } } // Helper: update board graphics function updateBoardGraphics(board, graphics) { for (var r = 0; r < BOARD_ROWS; r++) { for (var c = 0; c < BOARD_COLS; c++) { if (board[r][c]) { graphics[r][c].visible = true; graphics[r][c].tint = board[r][c]; graphics[r][c].alpha = 1; } else { graphics[r][c].visible = false; graphics[r][c].alpha = 1; } } } } // Helper: draw active piece function drawActivePiece(piece, x, y, graphics, show) { if (!piece) { return; } for (var i = 0; i < 4; i++) { var block = piece.blocks[i]; var r = y + block[1]; var c = x + block[0]; if (r >= 0 && r < BOARD_ROWS && c >= 0 && c < BOARD_COLS) { graphics[r][c].visible = show; if (show) { graphics[r][c].tint = piece.color; graphics[r][c].alpha = 1; } else { graphics[r][c].alpha = 1; } } } } // Helper: draw ghost piece for player function drawGhostPiece(piece, x, y, board, graphics, show) { if (!piece) { return; } // Find drop Y var ghostY = y; while (!collides(piece.blocks, x, ghostY + 1, board)) { ghostY++; } // Draw ghost piece (only if not overlapping with current piece) for (var i = 0; i < 4; i++) { var block = piece.blocks[i]; var r = ghostY + block[1]; var c = x + block[0]; if (r >= 0 && r < BOARD_ROWS && c >= 0 && c < BOARD_COLS) { // Only draw ghost if not overlapping with current piece if (!(ghostY === y && block[1] === 0)) { graphics[r][c].visible = show; if (show) { // Use a semi-transparent white tint for ghost graphics[r][c].tint = 0xffffff; graphics[r][c].alpha = 0.3; } else { graphics[r][c].alpha = 1; } } } } } // Helper: rotate blocks function rotateBlocks(blocks) { var rotated = []; for (var i = 0; i < blocks.length; i++) { rotated.push([blocks[i][1], -blocks[i][0]]); } // Normalize to top-left var minX = Math.min(rotated[0][0], rotated[1][0], rotated[2][0], rotated[3][0]); var minY = Math.min(rotated[0][1], rotated[1][1], rotated[2][1], rotated[3][1]); for (var i = 0; i < rotated.length; i++) { rotated[i][0] -= minX; rotated[i][1] -= minY; } return rotated; } // Helper: check collision function collides(blocks, x, y, board) { for (var i = 0; i < 4; i++) { var bx = x + blocks[i][0]; var by = y + blocks[i][1]; if (bx < 0 || bx >= BOARD_COLS || by >= BOARD_ROWS) { return true; } if (by >= 0 && board[by][bx]) { // Only collide with real blocks (not null/undefined, not ghost overlays) return true; } } return false; } // Spawn a new piece function spawnPiece(isAI) { if (isAI) { var shapeIdx = Math.floor(Math.random() * TETROMINOS.length); var shape = TETROMINOS[shapeIdx]; var piece = { color: shape.color, blocks: [[shape.blocks[0][0], shape.blocks[0][1]], [shape.blocks[1][0], shape.blocks[1][1]], [shape.blocks[2][0], shape.blocks[2][1]], [shape.blocks[3][0], shape.blocks[3][1]]] }; aiPiece = piece; aiX = 3; aiY = -2; if (collides(aiPiece.blocks, aiX, aiY + 1, aiBoard)) { // AI lost, player wins! LK.showYouWin(); return; } } else { // Use nextPlayerPiece if available, otherwise random var piece; if (nextPlayerPiece) { piece = { color: nextPlayerPiece.color, blocks: [[nextPlayerPiece.blocks[0][0], nextPlayerPiece.blocks[0][1]], [nextPlayerPiece.blocks[1][0], nextPlayerPiece.blocks[1][1]], [nextPlayerPiece.blocks[2][0], nextPlayerPiece.blocks[2][1]], [nextPlayerPiece.blocks[3][0], nextPlayerPiece.blocks[3][1]]] }; } else { var shapeIdx = Math.floor(Math.random() * TETROMINOS.length); var shape = TETROMINOS[shapeIdx]; piece = { color: shape.color, blocks: [[shape.blocks[0][0], shape.blocks[0][1]], [shape.blocks[1][0], shape.blocks[1][1]], [shape.blocks[2][0], shape.blocks[2][1]], [shape.blocks[3][0], shape.blocks[3][1]]] }; } playerPiece = piece; playerX = 3; playerY = -2; // Generate next piece for preview var nextIdx = Math.floor(Math.random() * TETROMINOS.length); var nextShape = TETROMINOS[nextIdx]; nextPlayerPiece = { color: nextShape.color, blocks: [[nextShape.blocks[0][0], nextShape.blocks[0][1]], [nextShape.blocks[1][0], nextShape.blocks[1][1]], [nextShape.blocks[2][0], nextShape.blocks[2][1]], [nextShape.blocks[3][0], nextShape.blocks[3][1]]] }; drawNextPiecePreview(nextPlayerPiece); if (collides(playerPiece.blocks, playerX, playerY + 1, playerBoard)) { LK.showGameOver(); } } } // Reset AI board when AI loses function resetAIBoard() { for (var r = 0; r < BOARD_ROWS; r++) { for (var c = 0; c < BOARD_COLS; c++) { aiBoard[r][c] = null; } } aiScore = 0; playerLabel.setText('PLAYER(' + playerScore + ')'); aiLabel.setText('AI(' + aiScore + ')'); } // Lock the active piece into the board function lockPiece(piece, x, y, board) { for (var i = 0; i < 4; i++) { var block = piece.blocks[i]; var r = y + block[1]; var c = x + block[0]; if (r >= 0 && r < BOARD_ROWS && c >= 0 && c < BOARD_COLS) { board[r][c] = piece.color; } } } // Clear full lines function clearLines(board, isAI) { var lines = 0; var clearedLineData = []; // Store the cleared lines for transfer for (var r = BOARD_ROWS - 1; r >= 0; r--) { var full = true; for (var c = 0; c < BOARD_COLS; c++) { // Only count as filled if it's a real block (not null/undefined, not a ghost, not alpha < 1) if (!board[r][c]) { full = false; break; } } if (full) { // Check if this line contains any transferred blocks (gray blocks) var isTransferredLine = true; for (var cc = 0; cc < BOARD_COLS; cc++) { if (board[r][cc] !== 0x808080) { isTransferredLine = false; break; } } // Skip clearing if this is a transferred line - gray lines stay forever if (isTransferredLine) { continue; // Don't clear gray lines, move to next row } lines++; // Store line data for transfer (only non-gray lines) var lineData = []; for (var cc = 0; cc < BOARD_COLS; cc++) { lineData[cc] = board[r][cc]; } clearedLineData.push(lineData); // Move all rows above down for (var rr = r; rr > 0; rr--) { for (var cc = 0; cc < BOARD_COLS; cc++) { board[rr][cc] = board[rr - 1][cc]; } } for (var cc = 0; cc < BOARD_COLS; cc++) { board[0][cc] = null; } r++; // Check this row again } } if (lines > 0) { // Play line clear sound, but only if at least 7 seconds have passed since last play if (typeof lastLineSoundTime === "undefined") { lastLineSoundTime = 0; } var now = Date.now(); if (now - lastLineSoundTime > 7000) { LK.getSound('lineclear').play(); lastLineSoundTime = now; } if (isAI) { aiScore += lines * 100; // Only transfer lines that are not from previous transfers if (clearedLineData.length > 0) { transferLinesToBoard(clearedLineData, playerBoard); } } else { playerScore += lines * 100; // Only transfer lines that are not from previous transfers if (clearedLineData.length > 0) { transferLinesToBoard(clearedLineData, aiBoard); } } playerLabel.setText('PLAYER(' + playerScore + ')'); aiLabel.setText('AI(' + aiScore + ')'); } } // Try to move player piece function tryMovePlayer(dx, dy, rotate) { var newBlocks = []; if (rotate) { newBlocks = rotateBlocks(playerPiece.blocks); } else { for (var i = 0; i < 4; i++) { newBlocks[i] = [playerPiece.blocks[i][0], playerPiece.blocks[i][1]]; } } var nx = playerX + dx; var ny = playerY + dy; if (!collides(newBlocks, nx, ny, playerBoard)) { drawActivePiece(playerPiece, playerX, playerY, playerCellGraphics, false); playerX = nx; playerY = ny; if (rotate) { playerPiece.blocks = newBlocks; } drawActivePiece(playerPiece, playerX, playerY, playerCellGraphics, true); return true; } return false; } // AI logic functions function getHeight(board) { for (var r = 0; r < BOARD_ROWS; r++) { for (var c = 0; c < BOARD_COLS; c++) { if (board[r][c]) { return BOARD_ROWS - r; } } } return 0; } function countHoles(board) { var holes = 0; for (var c = 0; c < BOARD_COLS; c++) { var blockFound = false; for (var r = 0; r < BOARD_ROWS; r++) { if (board[r][c]) { blockFound = true; } else if (blockFound) { holes++; } } } return holes; } function evaluateBoard(board) { var height = getHeight(board); var holes = countHoles(board); var lines = 0; for (var r = 0; r < BOARD_ROWS; r++) { var full = true; for (var c = 0; c < BOARD_COLS; c++) { if (!board[r][c]) { full = false; break; } } if (full) { lines++; } } return lines * 1000 - height * 10 - holes * 30; } function simulateMove(piece, x, y, board) { var testBoard = []; for (var r = 0; r < BOARD_ROWS; r++) { testBoard[r] = []; for (var c = 0; c < BOARD_COLS; c++) { testBoard[r][c] = board[r][c]; } } // Drop piece to bottom while (!collides(piece.blocks, x, y + 1, testBoard)) { y++; } // Lock piece for (var i = 0; i < 4; i++) { var block = piece.blocks[i]; var r = y + block[1]; var c = x + block[0]; if (r >= 0 && r < BOARD_ROWS && c >= 0 && c < BOARD_COLS) { testBoard[r][c] = piece.color; } } return evaluateBoard(testBoard); } function findBestMove() { if (!aiPiece) { return null; } var bestScore = -99999; var bestX = aiX; var bestRotation = 0; // Try all rotations var testPiece = { color: aiPiece.color, blocks: [] }; for (var rot = 0; rot < 4; rot++) { // Copy blocks for (var i = 0; i < 4; i++) { testPiece.blocks[i] = [aiPiece.blocks[i][0], aiPiece.blocks[i][1]]; } // Apply rotation for (var r = 0; r < rot; r++) { testPiece.blocks = rotateBlocks(testPiece.blocks); } // Try all positions for (var x = -2; x < BOARD_COLS + 2; x++) { if (!collides(testPiece.blocks, x, aiY, aiBoard)) { var score = simulateMove(testPiece, x, aiY, aiBoard); if (score > bestScore) { bestScore = score; bestX = x; bestRotation = rot; } } } } return { x: bestX, rotation: bestRotation }; } // Add variable to track fast drop mode var fastDropMode = false; // Control widgets at bottom of screen var WIDGET_HEIGHT = 200; var WIDGET_Y = 2500; var WIDGET_WIDTH = 150; var WIDGET_START = 1150; var WIDGET_SEP = 50; // Left button var leftButton = LK.getAsset('left', { anchorX: 0, anchorY: 0, x: WIDGET_START, y: WIDGET_Y, width: WIDGET_WIDTH, height: WIDGET_HEIGHT }); game.addChild(leftButton); // Right button var rightButton = LK.getAsset('right', { anchorX: 0, anchorY: 0, x: WIDGET_START + WIDGET_SEP + WIDGET_WIDTH, y: WIDGET_Y, width: WIDGET_WIDTH, height: WIDGET_HEIGHT }); game.addChild(rightButton); // Rotate button var rotateButton = LK.getAsset('rotate', { anchorX: 0, anchorY: 0, x: WIDGET_START + WIDGET_SEP * 2 + WIDGET_WIDTH * 2, y: WIDGET_Y, width: WIDGET_WIDTH, height: WIDGET_HEIGHT }); game.addChild(rotateButton); // Down button var downButton = LK.getAsset('down', { anchorX: 0, anchorY: 0, x: WIDGET_START + WIDGET_SEP * 3 + WIDGET_WIDTH * 3, y: WIDGET_Y, width: WIDGET_WIDTH, height: WIDGET_HEIGHT }); game.addChild(downButton); // Add click handlers for widgets leftButton.down = function (x, y, obj) { if (playerPiece) { tryMovePlayer(-1, 0, false); } }; rightButton.down = function (x, y, obj) { if (playerPiece) { tryMovePlayer(1, 0, false); } }; rotateButton.down = function (x, y, obj) { if (playerPiece) { tryMovePlayer(0, 0, true); } }; downButton.down = function (x, y, obj) { if (playerPiece) { fastDropMode = true; } }; downButton.up = function (x, y, obj) { fastDropMode = false; }; // Control widgets - no direct piece controls game.down = function (x, y, obj) { // Controls are now handled by widgets }; // Release fast drop mode when mouse/touch is released game.up = function (x, y, obj) { fastDropMode = false; }; // Main game update var dropCounter = 0; var aiMoveCounter = 0; var aiTargetX = null; var aiTargetRotation = 0; game.update = function () { dropCounter++; // Player update - use faster drop speed when fastDropMode is active var dropSpeed = fastDropMode ? 3 : 30; // Drop every 3 frames when fast, every 30 frames normally if (dropCounter >= dropSpeed) { dropCounter = 0; if (!tryMovePlayer(0, 1, false)) { drawActivePiece(playerPiece, playerX, playerY, playerCellGraphics, false); lockPiece(playerPiece, playerX, playerY, playerBoard); clearLines(playerBoard, false); updateBoardGraphics(playerBoard, playerCellGraphics); spawnPiece(false); drawActivePiece(playerPiece, playerX, playerY, playerCellGraphics, true); // Reset fast drop mode when piece locks fastDropMode = false; } } // AI update aiMoveCounter++; if (aiMoveCounter >= 25) { // AI moves faster aiMoveCounter = 0; // Get best move if not already calculated if (aiTargetX === null && aiPiece) { var bestMove = findBestMove(); if (bestMove) { aiTargetX = bestMove.x; aiTargetRotation = bestMove.rotation; } } // Execute AI moves if (aiPiece && aiTargetX !== null) { // Rotate first if (aiTargetRotation > 0) { var rotatedBlocks = rotateBlocks(aiPiece.blocks); if (!collides(rotatedBlocks, aiX, aiY, aiBoard)) { drawActivePiece(aiPiece, aiX, aiY, aiCellGraphics, false); aiPiece.blocks = rotatedBlocks; drawActivePiece(aiPiece, aiX, aiY, aiCellGraphics, true); aiTargetRotation--; } } // Then move horizontally else if (aiX < aiTargetX) { if (!collides(aiPiece.blocks, aiX + 1, aiY, aiBoard)) { drawActivePiece(aiPiece, aiX, aiY, aiCellGraphics, false); aiX++; drawActivePiece(aiPiece, aiX, aiY, aiCellGraphics, true); } } else if (aiX > aiTargetX) { if (!collides(aiPiece.blocks, aiX - 1, aiY, aiBoard)) { drawActivePiece(aiPiece, aiX, aiY, aiCellGraphics, false); aiX--; drawActivePiece(aiPiece, aiX, aiY, aiCellGraphics, true); } } // Drop when in position else { if (!collides(aiPiece.blocks, aiX, aiY + 1, aiBoard)) { drawActivePiece(aiPiece, aiX, aiY, aiCellGraphics, false); aiY++; drawActivePiece(aiPiece, aiX, aiY, aiCellGraphics, true); } else { // Lock piece drawActivePiece(aiPiece, aiX, aiY, aiCellGraphics, false); lockPiece(aiPiece, aiX, aiY, aiBoard); clearLines(aiBoard, true); updateBoardGraphics(aiBoard, aiCellGraphics); spawnPiece(true); aiTargetX = null; aiTargetRotation = 0; } } } } updateBoardGraphics(playerBoard, playerCellGraphics); updateBoardGraphics(aiBoard, aiCellGraphics); // Draw ghost piece for player (clear previous ghost first) drawGhostPiece(playerPiece, playerX, playerY, playerBoard, playerCellGraphics, false); // Draw ghost piece for player (show new ghost) drawGhostPiece(playerPiece, playerX, playerY, playerBoard, playerCellGraphics, true); drawActivePiece(playerPiece, playerX, playerY, playerCellGraphics, true); drawActivePiece(aiPiece, aiX, aiY, aiCellGraphics, true); }; // Start game LK.playMusic('moscow'); // Generate first next piece for player before spawning var firstIdx = Math.floor(Math.random() * TETROMINOS.length); var firstShape = TETROMINOS[firstIdx]; nextPlayerPiece = { color: firstShape.color, blocks: [[firstShape.blocks[0][0], firstShape.blocks[0][1]], [firstShape.blocks[1][0], firstShape.blocks[1][1]], [firstShape.blocks[2][0], firstShape.blocks[2][1]], [firstShape.blocks[3][0], firstShape.blocks[3][1]]] }; drawNextPiecePreview(nextPlayerPiece); // Spawn both player and AI pieces in the same frame to start at the same time spawnPiece(false); // Player piece spawnPiece(true); // AI piece drawActivePiece(playerPiece, playerX, playerY, playerCellGraphics, true); drawActivePiece(aiPiece, aiX, aiY, aiCellGraphics, true); updateBoardGraphics(playerBoard, playerCellGraphics); updateBoardGraphics(aiBoard, aiCellGraphics); // Transfer lines to opponent's board (competitive mode) function transferLinesToBoard(lineData, targetBoard) { // Move all existing rows up by the number of transferred lines var numLines = lineData.length; for (var r = 0; r < BOARD_ROWS - numLines; r++) { for (var c = 0; c < BOARD_COLS; c++) { // Only copy real blocks, not ghost piece overlays (ghosts are not stored in board, but just in case) if (targetBoard[r + numLines][c] !== undefined && targetBoard[r + numLines][c] !== null) { targetBoard[r][c] = targetBoard[r + numLines][c]; } else { targetBoard[r][c] = null; } } } // Add the transferred lines at the bottom for (var i = 0; i < numLines; i++) { var targetRow = BOARD_ROWS - numLines + i; for (var c = 0; c < BOARD_COLS; c++) { // Use a neutral gray color for transferred lines to distinguish them targetBoard[targetRow][c] = 0x808080; } } }
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Add full screen background
var backgroundImage = LK.getAsset('background', {
anchorX: 0,
anchorY: 0,
x: 150,
y: 0
});
game.addChild(backgroundImage);
// --- Tetris Game Implementation ---
// Board configuration
var BOARD_COLS = 10;
var BOARD_ROWS = 20;
var CELL_SIZE = 80; // Larger cells for better visibility
var BOARD_WIDTH = BOARD_COLS * CELL_SIZE;
var BOARD_HEIGHT = BOARD_ROWS * CELL_SIZE;
var PLAYER_BOARD_X = 1024 + 100; // Right side with more spacing
var AI_BOARD_X = 1024 - BOARD_WIDTH - 100; // Left side with more spacing
var BOARD_Y = 900;
// Add AI background (left half)
var aiBackgroundImage = LK.getAsset('aiBackground', {
anchorX: 0,
anchorY: 0,
x: 6,
y: BOARD_Y,
alpha: 1,
tint: 0xa80000
});
game.addChild(aiBackgroundImage);
// Add Player background (right half)
var playerBackgroundImage = LK.getAsset('playerBackground', {
anchorX: 0,
anchorY: 0,
x: 1025,
y: BOARD_Y,
alpha: 1,
tint: 0xffdd00
});
game.addChild(playerBackgroundImage);
// Tetrimino definitions
var TETROMINOS = [
// I - Light Blue
{
color: 0x9acde3,
blocks: [[0, 1], [1, 1], [2, 1], [3, 1]]
},
// O - Yellow
{
color: 0xffe800,
blocks: [[1, 0], [2, 0], [1, 1], [2, 1]]
},
// T - Red
{
color: 0xcc0000,
blocks: [[1, 0], [0, 1], [1, 1], [2, 1]]
},
// S - Dark Green
{
color: 0x008000,
blocks: [[1, 0], [2, 0], [0, 1], [1, 1]]
},
// Z - Brown
{
color: 0x8b4513,
blocks: [[0, 0], [1, 0], [1, 1], [2, 1]]
},
// J - Blue
{
color: 0x0000cd,
blocks: [[0, 0], [0, 1], [1, 1], [2, 1]]
},
// L - Dark Orange
{
color: 0xff8c00,
blocks: [[2, 0], [0, 1], [1, 1], [2, 1]]
}];
// Board states for player and AI
var playerBoard = [];
var aiBoard = [];
for (var r = 0; r < BOARD_ROWS; r++) {
playerBoard[r] = [];
aiBoard[r] = [];
for (var c = 0; c < BOARD_COLS; c++) {
playerBoard[r][c] = null;
aiBoard[r][c] = null;
}
}
// Score
var playerScore = 0;
var aiScore = 0;
// Labels for boards
// Next piece preview container and label
var nextPieceContainer = new Container();
nextPieceContainer.x = PLAYER_BOARD_X + BOARD_WIDTH / 2 - 120;
nextPieceContainer.y = BOARD_Y - 350;
game.addChild(nextPieceContainer);
var nextPieceLabel = new Text2('NEXT', {
size: 50,
fill: 0xffd900
});
nextPieceLabel.anchor.set(0.5, 0);
nextPieceLabel.x = 120;
nextPieceLabel.y = -60;
nextPieceContainer.addChild(nextPieceLabel);
var playerLabel = new Text2('PLAYER', {
size: 70,
fill: 0xffd900
});
playerLabel.anchor.set(0.5, 0);
playerLabel.x = PLAYER_BOARD_X + BOARD_WIDTH / 2;
playerLabel.y = BOARD_Y - 100;
game.addChild(playerLabel);
var aiLabel = new Text2('AI', {
size: 70,
fill: 0xFF0000
});
aiLabel.anchor.set(0.5, 0);
aiLabel.x = AI_BOARD_X + BOARD_WIDTH / 2;
aiLabel.y = 2525;
game.addChild(aiLabel);
// Player board container and graphics
var playerContainer = new Container();
playerContainer.x = PLAYER_BOARD_X;
playerContainer.y = BOARD_Y;
game.addChild(playerContainer);
var playerCellGraphics = [];
for (var r = 0; r < BOARD_ROWS; r++) {
playerCellGraphics[r] = [];
for (var c = 0; c < BOARD_COLS; c++) {
var cell = LK.getAsset('cell', {
anchorX: 0,
anchorY: 0
});
cell.width = CELL_SIZE - 2;
cell.height = CELL_SIZE - 2;
cell.x = c * CELL_SIZE + 1;
cell.y = r * CELL_SIZE + 1;
cell.visible = false;
playerContainer.addChild(cell);
playerCellGraphics[r][c] = cell;
}
}
// AI board container and graphics
var aiContainer = new Container();
aiContainer.x = AI_BOARD_X;
aiContainer.y = BOARD_Y;
game.addChild(aiContainer);
var aiCellGraphics = [];
for (var r = 0; r < BOARD_ROWS; r++) {
aiCellGraphics[r] = [];
for (var c = 0; c < BOARD_COLS; c++) {
var cell = LK.getAsset('cell', {
anchorX: 0,
anchorY: 0
});
cell.width = CELL_SIZE - 2;
cell.height = CELL_SIZE - 2;
cell.x = c * CELL_SIZE + 1;
cell.y = r * CELL_SIZE + 1;
cell.visible = false;
aiContainer.addChild(cell);
aiCellGraphics[r][c] = cell;
}
}
// Active piece states
var playerPiece = null;
var playerX = 0;
var playerY = 0;
var aiPiece = null;
var aiX = 0;
var aiY = 0;
// Next piece preview for player
var nextPlayerPiece = null;
var nextPieceBlocks = [];
var nextPieceGraphics = [];
// Helper: draw next piece preview in nextPieceContainer
function drawNextPiecePreview(piece) {
// Remove previous preview blocks
for (var i = 0; i < nextPieceBlocks.length; i++) {
nextPieceContainer.removeChild(nextPieceBlocks[i]);
}
nextPieceBlocks = [];
if (!piece) {
return;
}
// Find minX/minY to center the piece
var minX = Math.min(piece.blocks[0][0], piece.blocks[1][0], piece.blocks[2][0], piece.blocks[3][0]);
var minY = Math.min(piece.blocks[0][1], piece.blocks[1][1], piece.blocks[2][1], piece.blocks[3][1]);
var maxX = Math.max(piece.blocks[0][0], piece.blocks[1][0], piece.blocks[2][0], piece.blocks[3][0]);
var maxY = Math.max(piece.blocks[0][1], piece.blocks[1][1], piece.blocks[2][1], piece.blocks[3][1]);
var previewCellSize = 60;
var offsetX = 120 - (maxX - minX + 1) * previewCellSize / 2;
var offsetY = 0;
for (var i = 0; i < 4; i++) {
var block = piece.blocks[i];
var cell = LK.getAsset('cell', {
anchorX: 0,
anchorY: 0
});
cell.width = previewCellSize - 2;
cell.height = previewCellSize - 2;
cell.x = offsetX + (block[0] - minX) * previewCellSize + 1;
cell.y = offsetY + (block[1] - minY) * previewCellSize + 1;
cell.tint = piece.color;
cell.alpha = 1;
nextPieceContainer.addChild(cell);
nextPieceBlocks.push(cell);
}
}
// Helper: update board graphics
function updateBoardGraphics(board, graphics) {
for (var r = 0; r < BOARD_ROWS; r++) {
for (var c = 0; c < BOARD_COLS; c++) {
if (board[r][c]) {
graphics[r][c].visible = true;
graphics[r][c].tint = board[r][c];
graphics[r][c].alpha = 1;
} else {
graphics[r][c].visible = false;
graphics[r][c].alpha = 1;
}
}
}
}
// Helper: draw active piece
function drawActivePiece(piece, x, y, graphics, show) {
if (!piece) {
return;
}
for (var i = 0; i < 4; i++) {
var block = piece.blocks[i];
var r = y + block[1];
var c = x + block[0];
if (r >= 0 && r < BOARD_ROWS && c >= 0 && c < BOARD_COLS) {
graphics[r][c].visible = show;
if (show) {
graphics[r][c].tint = piece.color;
graphics[r][c].alpha = 1;
} else {
graphics[r][c].alpha = 1;
}
}
}
}
// Helper: draw ghost piece for player
function drawGhostPiece(piece, x, y, board, graphics, show) {
if (!piece) {
return;
}
// Find drop Y
var ghostY = y;
while (!collides(piece.blocks, x, ghostY + 1, board)) {
ghostY++;
}
// Draw ghost piece (only if not overlapping with current piece)
for (var i = 0; i < 4; i++) {
var block = piece.blocks[i];
var r = ghostY + block[1];
var c = x + block[0];
if (r >= 0 && r < BOARD_ROWS && c >= 0 && c < BOARD_COLS) {
// Only draw ghost if not overlapping with current piece
if (!(ghostY === y && block[1] === 0)) {
graphics[r][c].visible = show;
if (show) {
// Use a semi-transparent white tint for ghost
graphics[r][c].tint = 0xffffff;
graphics[r][c].alpha = 0.3;
} else {
graphics[r][c].alpha = 1;
}
}
}
}
}
// Helper: rotate blocks
function rotateBlocks(blocks) {
var rotated = [];
for (var i = 0; i < blocks.length; i++) {
rotated.push([blocks[i][1], -blocks[i][0]]);
}
// Normalize to top-left
var minX = Math.min(rotated[0][0], rotated[1][0], rotated[2][0], rotated[3][0]);
var minY = Math.min(rotated[0][1], rotated[1][1], rotated[2][1], rotated[3][1]);
for (var i = 0; i < rotated.length; i++) {
rotated[i][0] -= minX;
rotated[i][1] -= minY;
}
return rotated;
}
// Helper: check collision
function collides(blocks, x, y, board) {
for (var i = 0; i < 4; i++) {
var bx = x + blocks[i][0];
var by = y + blocks[i][1];
if (bx < 0 || bx >= BOARD_COLS || by >= BOARD_ROWS) {
return true;
}
if (by >= 0 && board[by][bx]) {
// Only collide with real blocks (not null/undefined, not ghost overlays)
return true;
}
}
return false;
}
// Spawn a new piece
function spawnPiece(isAI) {
if (isAI) {
var shapeIdx = Math.floor(Math.random() * TETROMINOS.length);
var shape = TETROMINOS[shapeIdx];
var piece = {
color: shape.color,
blocks: [[shape.blocks[0][0], shape.blocks[0][1]], [shape.blocks[1][0], shape.blocks[1][1]], [shape.blocks[2][0], shape.blocks[2][1]], [shape.blocks[3][0], shape.blocks[3][1]]]
};
aiPiece = piece;
aiX = 3;
aiY = -2;
if (collides(aiPiece.blocks, aiX, aiY + 1, aiBoard)) {
// AI lost, player wins!
LK.showYouWin();
return;
}
} else {
// Use nextPlayerPiece if available, otherwise random
var piece;
if (nextPlayerPiece) {
piece = {
color: nextPlayerPiece.color,
blocks: [[nextPlayerPiece.blocks[0][0], nextPlayerPiece.blocks[0][1]], [nextPlayerPiece.blocks[1][0], nextPlayerPiece.blocks[1][1]], [nextPlayerPiece.blocks[2][0], nextPlayerPiece.blocks[2][1]], [nextPlayerPiece.blocks[3][0], nextPlayerPiece.blocks[3][1]]]
};
} else {
var shapeIdx = Math.floor(Math.random() * TETROMINOS.length);
var shape = TETROMINOS[shapeIdx];
piece = {
color: shape.color,
blocks: [[shape.blocks[0][0], shape.blocks[0][1]], [shape.blocks[1][0], shape.blocks[1][1]], [shape.blocks[2][0], shape.blocks[2][1]], [shape.blocks[3][0], shape.blocks[3][1]]]
};
}
playerPiece = piece;
playerX = 3;
playerY = -2;
// Generate next piece for preview
var nextIdx = Math.floor(Math.random() * TETROMINOS.length);
var nextShape = TETROMINOS[nextIdx];
nextPlayerPiece = {
color: nextShape.color,
blocks: [[nextShape.blocks[0][0], nextShape.blocks[0][1]], [nextShape.blocks[1][0], nextShape.blocks[1][1]], [nextShape.blocks[2][0], nextShape.blocks[2][1]], [nextShape.blocks[3][0], nextShape.blocks[3][1]]]
};
drawNextPiecePreview(nextPlayerPiece);
if (collides(playerPiece.blocks, playerX, playerY + 1, playerBoard)) {
LK.showGameOver();
}
}
}
// Reset AI board when AI loses
function resetAIBoard() {
for (var r = 0; r < BOARD_ROWS; r++) {
for (var c = 0; c < BOARD_COLS; c++) {
aiBoard[r][c] = null;
}
}
aiScore = 0;
playerLabel.setText('PLAYER(' + playerScore + ')');
aiLabel.setText('AI(' + aiScore + ')');
}
// Lock the active piece into the board
function lockPiece(piece, x, y, board) {
for (var i = 0; i < 4; i++) {
var block = piece.blocks[i];
var r = y + block[1];
var c = x + block[0];
if (r >= 0 && r < BOARD_ROWS && c >= 0 && c < BOARD_COLS) {
board[r][c] = piece.color;
}
}
}
// Clear full lines
function clearLines(board, isAI) {
var lines = 0;
var clearedLineData = []; // Store the cleared lines for transfer
for (var r = BOARD_ROWS - 1; r >= 0; r--) {
var full = true;
for (var c = 0; c < BOARD_COLS; c++) {
// Only count as filled if it's a real block (not null/undefined, not a ghost, not alpha < 1)
if (!board[r][c]) {
full = false;
break;
}
}
if (full) {
// Check if this line contains any transferred blocks (gray blocks)
var isTransferredLine = true;
for (var cc = 0; cc < BOARD_COLS; cc++) {
if (board[r][cc] !== 0x808080) {
isTransferredLine = false;
break;
}
}
// Skip clearing if this is a transferred line - gray lines stay forever
if (isTransferredLine) {
continue; // Don't clear gray lines, move to next row
}
lines++;
// Store line data for transfer (only non-gray lines)
var lineData = [];
for (var cc = 0; cc < BOARD_COLS; cc++) {
lineData[cc] = board[r][cc];
}
clearedLineData.push(lineData);
// Move all rows above down
for (var rr = r; rr > 0; rr--) {
for (var cc = 0; cc < BOARD_COLS; cc++) {
board[rr][cc] = board[rr - 1][cc];
}
}
for (var cc = 0; cc < BOARD_COLS; cc++) {
board[0][cc] = null;
}
r++; // Check this row again
}
}
if (lines > 0) {
// Play line clear sound, but only if at least 7 seconds have passed since last play
if (typeof lastLineSoundTime === "undefined") {
lastLineSoundTime = 0;
}
var now = Date.now();
if (now - lastLineSoundTime > 7000) {
LK.getSound('lineclear').play();
lastLineSoundTime = now;
}
if (isAI) {
aiScore += lines * 100;
// Only transfer lines that are not from previous transfers
if (clearedLineData.length > 0) {
transferLinesToBoard(clearedLineData, playerBoard);
}
} else {
playerScore += lines * 100;
// Only transfer lines that are not from previous transfers
if (clearedLineData.length > 0) {
transferLinesToBoard(clearedLineData, aiBoard);
}
}
playerLabel.setText('PLAYER(' + playerScore + ')');
aiLabel.setText('AI(' + aiScore + ')');
}
}
// Try to move player piece
function tryMovePlayer(dx, dy, rotate) {
var newBlocks = [];
if (rotate) {
newBlocks = rotateBlocks(playerPiece.blocks);
} else {
for (var i = 0; i < 4; i++) {
newBlocks[i] = [playerPiece.blocks[i][0], playerPiece.blocks[i][1]];
}
}
var nx = playerX + dx;
var ny = playerY + dy;
if (!collides(newBlocks, nx, ny, playerBoard)) {
drawActivePiece(playerPiece, playerX, playerY, playerCellGraphics, false);
playerX = nx;
playerY = ny;
if (rotate) {
playerPiece.blocks = newBlocks;
}
drawActivePiece(playerPiece, playerX, playerY, playerCellGraphics, true);
return true;
}
return false;
}
// AI logic functions
function getHeight(board) {
for (var r = 0; r < BOARD_ROWS; r++) {
for (var c = 0; c < BOARD_COLS; c++) {
if (board[r][c]) {
return BOARD_ROWS - r;
}
}
}
return 0;
}
function countHoles(board) {
var holes = 0;
for (var c = 0; c < BOARD_COLS; c++) {
var blockFound = false;
for (var r = 0; r < BOARD_ROWS; r++) {
if (board[r][c]) {
blockFound = true;
} else if (blockFound) {
holes++;
}
}
}
return holes;
}
function evaluateBoard(board) {
var height = getHeight(board);
var holes = countHoles(board);
var lines = 0;
for (var r = 0; r < BOARD_ROWS; r++) {
var full = true;
for (var c = 0; c < BOARD_COLS; c++) {
if (!board[r][c]) {
full = false;
break;
}
}
if (full) {
lines++;
}
}
return lines * 1000 - height * 10 - holes * 30;
}
function simulateMove(piece, x, y, board) {
var testBoard = [];
for (var r = 0; r < BOARD_ROWS; r++) {
testBoard[r] = [];
for (var c = 0; c < BOARD_COLS; c++) {
testBoard[r][c] = board[r][c];
}
}
// Drop piece to bottom
while (!collides(piece.blocks, x, y + 1, testBoard)) {
y++;
}
// Lock piece
for (var i = 0; i < 4; i++) {
var block = piece.blocks[i];
var r = y + block[1];
var c = x + block[0];
if (r >= 0 && r < BOARD_ROWS && c >= 0 && c < BOARD_COLS) {
testBoard[r][c] = piece.color;
}
}
return evaluateBoard(testBoard);
}
function findBestMove() {
if (!aiPiece) {
return null;
}
var bestScore = -99999;
var bestX = aiX;
var bestRotation = 0;
// Try all rotations
var testPiece = {
color: aiPiece.color,
blocks: []
};
for (var rot = 0; rot < 4; rot++) {
// Copy blocks
for (var i = 0; i < 4; i++) {
testPiece.blocks[i] = [aiPiece.blocks[i][0], aiPiece.blocks[i][1]];
}
// Apply rotation
for (var r = 0; r < rot; r++) {
testPiece.blocks = rotateBlocks(testPiece.blocks);
}
// Try all positions
for (var x = -2; x < BOARD_COLS + 2; x++) {
if (!collides(testPiece.blocks, x, aiY, aiBoard)) {
var score = simulateMove(testPiece, x, aiY, aiBoard);
if (score > bestScore) {
bestScore = score;
bestX = x;
bestRotation = rot;
}
}
}
}
return {
x: bestX,
rotation: bestRotation
};
}
// Add variable to track fast drop mode
var fastDropMode = false;
// Control widgets at bottom of screen
var WIDGET_HEIGHT = 200;
var WIDGET_Y = 2500;
var WIDGET_WIDTH = 150;
var WIDGET_START = 1150;
var WIDGET_SEP = 50;
// Left button
var leftButton = LK.getAsset('left', {
anchorX: 0,
anchorY: 0,
x: WIDGET_START,
y: WIDGET_Y,
width: WIDGET_WIDTH,
height: WIDGET_HEIGHT
});
game.addChild(leftButton);
// Right button
var rightButton = LK.getAsset('right', {
anchorX: 0,
anchorY: 0,
x: WIDGET_START + WIDGET_SEP + WIDGET_WIDTH,
y: WIDGET_Y,
width: WIDGET_WIDTH,
height: WIDGET_HEIGHT
});
game.addChild(rightButton);
// Rotate button
var rotateButton = LK.getAsset('rotate', {
anchorX: 0,
anchorY: 0,
x: WIDGET_START + WIDGET_SEP * 2 + WIDGET_WIDTH * 2,
y: WIDGET_Y,
width: WIDGET_WIDTH,
height: WIDGET_HEIGHT
});
game.addChild(rotateButton);
// Down button
var downButton = LK.getAsset('down', {
anchorX: 0,
anchorY: 0,
x: WIDGET_START + WIDGET_SEP * 3 + WIDGET_WIDTH * 3,
y: WIDGET_Y,
width: WIDGET_WIDTH,
height: WIDGET_HEIGHT
});
game.addChild(downButton);
// Add click handlers for widgets
leftButton.down = function (x, y, obj) {
if (playerPiece) {
tryMovePlayer(-1, 0, false);
}
};
rightButton.down = function (x, y, obj) {
if (playerPiece) {
tryMovePlayer(1, 0, false);
}
};
rotateButton.down = function (x, y, obj) {
if (playerPiece) {
tryMovePlayer(0, 0, true);
}
};
downButton.down = function (x, y, obj) {
if (playerPiece) {
fastDropMode = true;
}
};
downButton.up = function (x, y, obj) {
fastDropMode = false;
};
// Control widgets - no direct piece controls
game.down = function (x, y, obj) {
// Controls are now handled by widgets
};
// Release fast drop mode when mouse/touch is released
game.up = function (x, y, obj) {
fastDropMode = false;
};
// Main game update
var dropCounter = 0;
var aiMoveCounter = 0;
var aiTargetX = null;
var aiTargetRotation = 0;
game.update = function () {
dropCounter++;
// Player update - use faster drop speed when fastDropMode is active
var dropSpeed = fastDropMode ? 3 : 30; // Drop every 3 frames when fast, every 30 frames normally
if (dropCounter >= dropSpeed) {
dropCounter = 0;
if (!tryMovePlayer(0, 1, false)) {
drawActivePiece(playerPiece, playerX, playerY, playerCellGraphics, false);
lockPiece(playerPiece, playerX, playerY, playerBoard);
clearLines(playerBoard, false);
updateBoardGraphics(playerBoard, playerCellGraphics);
spawnPiece(false);
drawActivePiece(playerPiece, playerX, playerY, playerCellGraphics, true);
// Reset fast drop mode when piece locks
fastDropMode = false;
}
}
// AI update
aiMoveCounter++;
if (aiMoveCounter >= 25) {
// AI moves faster
aiMoveCounter = 0;
// Get best move if not already calculated
if (aiTargetX === null && aiPiece) {
var bestMove = findBestMove();
if (bestMove) {
aiTargetX = bestMove.x;
aiTargetRotation = bestMove.rotation;
}
}
// Execute AI moves
if (aiPiece && aiTargetX !== null) {
// Rotate first
if (aiTargetRotation > 0) {
var rotatedBlocks = rotateBlocks(aiPiece.blocks);
if (!collides(rotatedBlocks, aiX, aiY, aiBoard)) {
drawActivePiece(aiPiece, aiX, aiY, aiCellGraphics, false);
aiPiece.blocks = rotatedBlocks;
drawActivePiece(aiPiece, aiX, aiY, aiCellGraphics, true);
aiTargetRotation--;
}
}
// Then move horizontally
else if (aiX < aiTargetX) {
if (!collides(aiPiece.blocks, aiX + 1, aiY, aiBoard)) {
drawActivePiece(aiPiece, aiX, aiY, aiCellGraphics, false);
aiX++;
drawActivePiece(aiPiece, aiX, aiY, aiCellGraphics, true);
}
} else if (aiX > aiTargetX) {
if (!collides(aiPiece.blocks, aiX - 1, aiY, aiBoard)) {
drawActivePiece(aiPiece, aiX, aiY, aiCellGraphics, false);
aiX--;
drawActivePiece(aiPiece, aiX, aiY, aiCellGraphics, true);
}
}
// Drop when in position
else {
if (!collides(aiPiece.blocks, aiX, aiY + 1, aiBoard)) {
drawActivePiece(aiPiece, aiX, aiY, aiCellGraphics, false);
aiY++;
drawActivePiece(aiPiece, aiX, aiY, aiCellGraphics, true);
} else {
// Lock piece
drawActivePiece(aiPiece, aiX, aiY, aiCellGraphics, false);
lockPiece(aiPiece, aiX, aiY, aiBoard);
clearLines(aiBoard, true);
updateBoardGraphics(aiBoard, aiCellGraphics);
spawnPiece(true);
aiTargetX = null;
aiTargetRotation = 0;
}
}
}
}
updateBoardGraphics(playerBoard, playerCellGraphics);
updateBoardGraphics(aiBoard, aiCellGraphics);
// Draw ghost piece for player (clear previous ghost first)
drawGhostPiece(playerPiece, playerX, playerY, playerBoard, playerCellGraphics, false);
// Draw ghost piece for player (show new ghost)
drawGhostPiece(playerPiece, playerX, playerY, playerBoard, playerCellGraphics, true);
drawActivePiece(playerPiece, playerX, playerY, playerCellGraphics, true);
drawActivePiece(aiPiece, aiX, aiY, aiCellGraphics, true);
};
// Start game
LK.playMusic('moscow');
// Generate first next piece for player before spawning
var firstIdx = Math.floor(Math.random() * TETROMINOS.length);
var firstShape = TETROMINOS[firstIdx];
nextPlayerPiece = {
color: firstShape.color,
blocks: [[firstShape.blocks[0][0], firstShape.blocks[0][1]], [firstShape.blocks[1][0], firstShape.blocks[1][1]], [firstShape.blocks[2][0], firstShape.blocks[2][1]], [firstShape.blocks[3][0], firstShape.blocks[3][1]]]
};
drawNextPiecePreview(nextPlayerPiece);
// Spawn both player and AI pieces in the same frame to start at the same time
spawnPiece(false); // Player piece
spawnPiece(true); // AI piece
drawActivePiece(playerPiece, playerX, playerY, playerCellGraphics, true);
drawActivePiece(aiPiece, aiX, aiY, aiCellGraphics, true);
updateBoardGraphics(playerBoard, playerCellGraphics);
updateBoardGraphics(aiBoard, aiCellGraphics);
// Transfer lines to opponent's board (competitive mode)
function transferLinesToBoard(lineData, targetBoard) {
// Move all existing rows up by the number of transferred lines
var numLines = lineData.length;
for (var r = 0; r < BOARD_ROWS - numLines; r++) {
for (var c = 0; c < BOARD_COLS; c++) {
// Only copy real blocks, not ghost piece overlays (ghosts are not stored in board, but just in case)
if (targetBoard[r + numLines][c] !== undefined && targetBoard[r + numLines][c] !== null) {
targetBoard[r][c] = targetBoard[r + numLines][c];
} else {
targetBoard[r][c] = null;
}
}
}
// Add the transferred lines at the bottom
for (var i = 0; i < numLines; i++) {
var targetRow = BOARD_ROWS - numLines + i;
for (var c = 0; c < BOARD_COLS; c++) {
// Use a neutral gray color for transferred lines to distinguish them
targetBoard[targetRow][c] = 0x808080;
}
}
}