Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (22 edits merged)
Please save this source code
User prompt
Add a full width full height background on the back of everything
User prompt
Remove the Player: ... AI: ... score text elements. Instead, use the PLAYER and AI which are at -160 to also show the score like PLAYER(10) AI(2)
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
User prompt
No sorry, I want two background assets - one for the AI background and one for the player background
Code edit (7 edits merged)
Please save this source code
User prompt
Add a background asset that is full screen covering the back of my screen
Code edit (1 edits merged)
Please save this source code
Code edit (3 edits merged)
Please save this source code
User prompt
Please fix: gray lines disappear. They should never nor for the player nor for the ai
User prompt
No, remove the controls of the pieces for the player (rotation, moving down, moving left, moving right). Instead, on the bottom of the game screen, place 4 widgets, 1 per rotate clockwise, one to move left, one to move right, one to move down. Full width, like on the bottom on the screen after the AI and PLayer screens.
User prompt
Change a little the controls for the player: - You rotate a piece if you click on it. - You move left a piece if you click on the left of a piece or left above the piece, but never left below. - You move right a piece if you click on the right of a piece or right above the piece, but never right below.
User prompt
There is a bug: if I have lines that I got from my competitor, when a new piece lands they are translated to my competitor screen. That is not right. Lines that appeared in my screen from my competitor stay forever in my screen.
User prompt
Only move pieces right or left on down, if the y position of the tap is more or less the same as the y of the currently moving piece. Don't move left or right a piece if it's above or below the piece.
User prompt
Can you make it so that every line achieved by the player appears on the bottom of the AI pushing all pieces up, and the same from the player if the AI achieves a line? Kind of a "competitive" mode.
User prompt
For the fast drop mode - can It be only while I'm holding the mouse button / tapping the screen? If I release it it goes out from the fast mode. Thank you.
User prompt
For the player: if I click below the piece, the piece goes quicker down.
User prompt
For the player, If I click on the right of the current moving piece, the piece moves right. If I click on the left, the piece moves left. If I click exactly on the piece, the piece rotates.
Code edit (1 edits merged)
Please save this source code
User prompt
Blank Canvas
Initial prompt
An empty game
/**** * 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: 35, 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: 35,
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;
}
}
}