/****
* 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;
}
}
}