/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Tetromino class var Tetromino = Container.expand(function () { var self = Container.call(this); // Properties self.type = null; self.shape = null; // array of [x,y] self.rotation = 0; self.blocks = []; self.x = 0; // board col self.y = 0; // board row self.isPowerup = false; self.powerupType = null; // Initialize tetromino self.init = function (type, isPowerup) { self.type = type; self.isPowerup = !!isPowerup; self.rotation = 0; self.shape = []; // Copy shape var base = TETROMINO_SHAPES[type][0]; for (var i = 0; i < base.length; i++) { self.shape.push([base[i][0], base[i][1]]); } self.blocks = []; self.removeChildren(); for (var i = 0; i < self.shape.length; i++) { var block; if (self.isPowerup && i === 0) { block = self.attachAsset('powerup', { anchorX: 0.5, anchorY: 0.5, width: CELL_SIZE, height: CELL_SIZE }); self.powerupType = 'clearLine'; // Only one powerup for MVP } else { block = self.attachAsset(self.type, { anchorX: 0.5, anchorY: 0.5, width: CELL_SIZE, height: CELL_SIZE }); } self.blocks.push(block); // Overlay grid lines for each block var gridOverlay = new Container(); var vlineL = LK.getAsset('O', { anchorX: 0.5, anchorY: 0, width: 4, height: CELL_SIZE, color: 0x222222 // match board grid color }); vlineL.x = -CELL_SIZE / 2; vlineL.y = -CELL_SIZE / 2; vlineL.alpha = 0.25; gridOverlay.addChild(vlineL); var vlineR = LK.getAsset('O', { anchorX: 0.5, anchorY: 0, width: 4, height: CELL_SIZE, color: 0x222222 // match board grid color }); vlineR.x = CELL_SIZE / 2; vlineR.y = -CELL_SIZE / 2; vlineR.alpha = 0.25; gridOverlay.addChild(vlineR); var hlineT = LK.getAsset('O', { anchorX: 0, anchorY: 0.5, width: CELL_SIZE, height: 4, color: 0x222222 // match board grid color }); hlineT.x = -CELL_SIZE / 2; hlineT.y = -CELL_SIZE / 2; hlineT.alpha = 0.25; gridOverlay.addChild(hlineT); var hlineB = LK.getAsset('O', { anchorX: 0, anchorY: 0.5, width: CELL_SIZE, height: 4, color: 0x222222 // match board grid color }); hlineB.x = -CELL_SIZE / 2; hlineB.y = CELL_SIZE / 2; hlineB.alpha = 0.25; gridOverlay.addChild(hlineB); block.addChild(gridOverlay); } self.x = Math.floor(BOARD_COLS / 2) - 2; self.y = 0; self.updateBlockPositions(); }; // Update block positions self.updateBlockPositions = function () { for (var i = 0; i < self.shape.length; i++) { var pos = self.shape[i]; self.blocks[i].x = (self.x + pos[0]) * CELL_SIZE + CELL_SIZE / 2; self.blocks[i].y = (self.y + pos[1]) * CELL_SIZE + CELL_SIZE / 2; } }; // Try move self.tryMove = function (dx, dy, board) { if (self.canMove(dx, dy, self.shape, board)) { self.x += dx; self.y += dy; self.updateBlockPositions(); return true; } return false; }; // Try rotate self.tryRotate = function (board) { var rotated = rotateShape(self.shape); if (self.canMove(0, 0, rotated, board)) { self.shape = rotated; self.updateBlockPositions(); return true; } return false; }; // Can move/rotate self.canMove = function (dx, dy, shape, board) { for (var i = 0; i < shape.length; i++) { var nx = self.x + shape[i][0] + dx; var ny = self.y + shape[i][1] + dy; if (nx < 0 || nx >= BOARD_COLS || ny < 0 || ny >= BOARD_ROWS) { return false; } if (board[ny][nx]) { return false; } } return true; }; // Lock tetromino into board self.lockToBoard = function (board, blockRefs) { for (var i = 0; i < self.shape.length; i++) { var nx = self.x + self.shape[i][0]; var ny = self.y + self.shape[i][1]; board[ny][nx] = self.isPowerup && i === 0 ? 'powerup' : self.type; blockRefs[ny][nx] = self.blocks[i]; } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ // No title, no description // Always backgroundColor is black backgroundColor: 0x000000 }); /**** * Game Code ****/ // Board state: 2D array [row][col], 0 = empty, else type string // Tetromino shapes (classic + new) // Colors for each tetromino type // New shapes // Power-up block // Power-up effect // Tetromino definitions (relative coordinates for each block in the tetromino) var TETROMINO_SHAPES = { O: [[[1, 0], [2, 0], [1, 1], [2, 1]]], T: [[[1, 0], [0, 1], [1, 1], [2, 1]]], S: [[[1, 0], [2, 0], [0, 1], [1, 1]]], Z: [[[0, 0], [1, 0], [1, 1], [2, 1]]], J: [[[0, 0], [0, 1], [1, 1], [2, 1]]], L: [[[2, 0], [0, 1], [1, 1], [2, 1]]], U: [[[0, 0], [2, 0], [0, 1], [1, 1], [2, 1]]], P: [[[0, 0], [1, 0], [0, 1], [1, 1], [0, 2]]], Dot: [[[1, 1]]] }; var TETROMINO_TYPES = ['O', 'T', 'S', 'Z', 'J', 'L', 'U', 'P']; var POWERUP_CHANCE = 0.08; // 8% chance for a powerup block // Board settings var BOARD_COLS = 8; // Reduced width for fewer columns, block size unchanged var BOARD_ROWS = 20; var CELL_SIZE = 90; // px, fits well on 2048x2732 var BOARD_OFFSET_X = Math.floor((2048 - BOARD_COLS * CELL_SIZE) / 2) - 80; // Shift left to make space for preview var BOARD_OFFSET_Y = 300; // Helper: rotate a shape (array of [x,y]) 90deg clockwise function rotateShape(shape) { var maxX = 0, maxY = 0; for (var i = 0; i < shape.length; i++) { if (shape[i][0] > maxX) { maxX = shape[i][0]; } if (shape[i][1] > maxY) { maxY = shape[i][1]; } } var rotated = []; for (var i = 0; i < shape.length; i++) { var x = shape[i][0], y = shape[i][1]; rotated.push([maxY - y, x]); } return rotated; } var board = []; var blockRefs = []; // 2D array of block display objects for (var r = 0; r < BOARD_ROWS; r++) { board[r] = []; blockRefs[r] = []; for (var c = 0; c < BOARD_COLS; c++) { board[r][c] = 0; blockRefs[r][c] = null; } } // Score and level var score = 0; var level = 1; var linesCleared = 0; var fallInterval = 1000; // ms, decreases with level // GUI var scoreTxt = new Text2('0', { size: 100, fill: "#fff" }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); var levelTxt = new Text2('Lv.1', { size: 60, fill: "#fff" }); levelTxt.anchor.set(0.5, 0); LK.gui.top.addChild(levelTxt); levelTxt.y = 110; // Next tetromino preview var nextTetrominoType = null; var nextTetrominoPreview = new Container(); game.addChild(nextTetrominoPreview); // Current falling tetromino var currentTetromino = null; // Game state var isGameOver = false; var isDropping = false; var dropTimer = null; var moveTimer = null; var moveDir = 0; // -1 left, 1 right, 0 none // Helper: spawn new tetromino function spawnTetromino() { var type = nextTetrominoType; if (!type) { type = TETROMINO_TYPES[Math.floor(Math.random() * TETROMINO_TYPES.length)]; } var isPowerup = Math.random() < POWERUP_CHANCE; var tetro = new Tetromino(); tetro.init(type, isPowerup); game.addChild(tetro); currentTetromino = tetro; // Next nextTetrominoType = TETROMINO_TYPES[Math.floor(Math.random() * TETROMINO_TYPES.length)]; updateNextPreview(); // Flash effect on next preview if (nextTetrominoPreview.children && nextTetrominoPreview.children.length > 0) { for (var i = 0; i < nextTetrominoPreview.children.length; i++) { LK.effects.flashObject(nextTetrominoPreview.children[i], 0xffffff, 180); } } // If cannot place, game over if (!tetro.canMove(0, 0, tetro.shape, board)) { isGameOver = true; LK.effects.flashScreen(0xff0000, 1000); LK.showGameOver(); } } // Helper: update next preview function updateNextPreview() { nextTetrominoPreview.removeChildren(); var type = nextTetrominoType; if (!type) { return; } var shape = TETROMINO_SHAPES[type][0]; // Find min/max for centering var minX = 99, maxX = -99, minY = 99, maxY = -99; for (var i = 0; i < shape.length; i++) { if (shape[i][0] < minX) { minX = shape[i][0]; } if (shape[i][0] > maxX) { maxX = shape[i][0]; } if (shape[i][1] < minY) { minY = shape[i][1]; } if (shape[i][1] > maxY) { maxY = shape[i][1]; } } var previewWidth = (maxX - minX + 1) * CELL_SIZE; var previewHeight = (maxY - minY + 1) * CELL_SIZE; for (var i = 0; i < shape.length; i++) { var block = LK.getAsset(type, { anchorX: 0.5, anchorY: 0.5, width: CELL_SIZE, height: CELL_SIZE }); block.x = (shape[i][0] - minX) * CELL_SIZE + CELL_SIZE / 2; block.y = (shape[i][1] - minY) * CELL_SIZE + CELL_SIZE / 2; nextTetrominoPreview.addChild(block); } // Place preview to the right of the board, fully visible and inside the screen nextTetrominoPreview.x = BOARD_OFFSET_X + BOARD_COLS * CELL_SIZE + 120; nextTetrominoPreview.y = BOARD_OFFSET_Y + 200; } // Helper: clear full lines function clearLines() { var lines = []; 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.push(r); } } if (lines.length === 0) { return 0; } // Animate and remove lines for (var i = 0; i < lines.length; i++) { var row = lines[i]; for (var c = 0; c < BOARD_COLS; c++) { if (blockRefs[row][c]) { tween(blockRefs[row][c], { alpha: 0 }, { duration: 200, onFinish: function (obj) { if (obj.parent) { obj.parent.removeChild(obj); } }.bind(null, blockRefs[row][c]) }); } } } // Remove from board for (var i = 0; i < lines.length; i++) { var row = lines[i]; for (var c = 0; c < BOARD_COLS; c++) { board[row][c] = 0; blockRefs[row][c] = null; } } // Drop above lines for (var i = lines.length - 1; i >= 0; i--) { var row = lines[i]; for (var r = row - 1; r >= 0; r--) { for (var c = 0; c < BOARD_COLS; c++) { board[r + 1][c] = board[r][c]; blockRefs[r + 1][c] = blockRefs[r][c]; if (blockRefs[r + 1][c]) { tween(blockRefs[r + 1][c], { y: blockRefs[r + 1][c].y + CELL_SIZE }, { duration: 100 }); } } } // Clear top row for (var c = 0; c < BOARD_COLS; c++) { board[0][c] = 0; blockRefs[0][c] = null; } } return lines.length; } // Helper: activate powerup function activatePowerup(row) { // For MVP: clear the row where powerup landed for (var c = 0; c < BOARD_COLS; c++) { if (blockRefs[row][c]) { tween(blockRefs[row][c], { alpha: 0 }, { duration: 200, onFinish: function (obj) { if (obj.parent) { obj.parent.removeChild(obj); } }.bind(null, blockRefs[row][c]) }); board[row][c] = 0; blockRefs[row][c] = null; } } // Drop above lines for (var r = row - 1; r >= 0; r--) { for (var c = 0; c < BOARD_COLS; c++) { board[r + 1][c] = board[r][c]; blockRefs[r + 1][c] = blockRefs[r][c]; if (blockRefs[r + 1][c]) { tween(blockRefs[r + 1][c], { y: blockRefs[r + 1][c].y + CELL_SIZE }, { duration: 100 }); } } } // Clear top row for (var c = 0; c < BOARD_COLS; c++) { board[0][c] = 0; blockRefs[0][c] = null; } } // Helper: update score/level function updateScore(lines) { var points = [0, 100, 300, 500, 800]; score += points[lines] || 0; linesCleared += lines; scoreTxt.setText(score); var newLevel = 1 + Math.floor(linesCleared / 10); if (newLevel !== level) { level = newLevel; levelTxt.setText('Lv.' + level); fallInterval = Math.max(150, 1000 - (level - 1) * 100); } } // Helper: draw board grid (for visual reference) var gridLines = new Container(); game.addChild(gridLines); // Draw faint cell backgrounds for subtle grid for (var r = 0; r < BOARD_ROWS; r++) { for (var c = 0; c < BOARD_COLS; c++) { var cell = LK.getAsset('O', { anchorX: 0.5, anchorY: 0.5, width: CELL_SIZE - 4, height: CELL_SIZE - 4, color: 0x222222 }); cell.x = c * CELL_SIZE + CELL_SIZE / 2; cell.y = r * CELL_SIZE + CELL_SIZE / 2; cell.alpha = 0.15; gridLines.addChild(cell); } } // Draw vertical grid lines for (var c = 0; c <= BOARD_COLS; c++) { var vline = LK.getAsset('O', { anchorX: 0.5, anchorY: 0, width: 4, height: CELL_SIZE * BOARD_ROWS, color: 0x222222 // darker gray for better contrast }); vline.x = c * CELL_SIZE; vline.y = 0; vline.alpha = 0.35; gridLines.addChild(vline); } // Draw horizontal grid lines for (var r = 0; r <= BOARD_ROWS; r++) { var hline = LK.getAsset('O', { anchorX: 0, anchorY: 0.5, width: CELL_SIZE * BOARD_COLS, height: 4, color: 0x222222 // darker gray for better contrast }); hline.x = 0; hline.y = r * CELL_SIZE; hline.alpha = 0.35; gridLines.addChild(hline); } gridLines.x = BOARD_OFFSET_X; gridLines.y = BOARD_OFFSET_Y; // Move all blocks/containers to board offset function updateBoardDisplayOffsets() { gridLines.x = BOARD_OFFSET_X; gridLines.y = BOARD_OFFSET_Y; for (var r = 0; r < BOARD_ROWS; r++) { for (var c = 0; c < BOARD_COLS; c++) { if (blockRefs[r][c]) { blockRefs[r][c].x = c * CELL_SIZE + CELL_SIZE / 2 + BOARD_OFFSET_X; blockRefs[r][c].y = r * CELL_SIZE + CELL_SIZE / 2 + BOARD_OFFSET_Y; } } } if (currentTetromino) { for (var i = 0; i < currentTetromino.blocks.length; i++) { currentTetromino.blocks[i].x += BOARD_OFFSET_X; currentTetromino.blocks[i].y += BOARD_OFFSET_Y; } } } // Remove board offset from tetromino before logic function removeTetrominoOffset() { if (currentTetromino) { for (var i = 0; i < currentTetromino.blocks.length; i++) { currentTetromino.blocks[i].x -= BOARD_OFFSET_X; currentTetromino.blocks[i].y -= BOARD_OFFSET_Y; } } } // Touch controls var dragStartX = null; var dragStartY = null; var dragTetrominoX = null; var dragTetrominoY = null; var lastTouchMove = 0; var touchDown = false; var hardDrop = false; // Convert screen x,y to board col,row function screenToBoard(x, y) { var bx = Math.floor((x - BOARD_OFFSET_X) / CELL_SIZE); var by = Math.floor((y - BOARD_OFFSET_Y) / CELL_SIZE); return { col: bx, row: by }; } // Touch/mouse events game.down = function (x, y, obj) { if (isGameOver || !currentTetromino) { return; } dragStartX = x; dragStartY = y; dragTetrominoX = currentTetromino.x; dragTetrominoY = currentTetromino.y; touchDown = true; hardDrop = false; lastTouchMove = Date.now(); }; game.move = function (x, y, obj) { if (isGameOver || !currentTetromino || !touchDown) { return; } var dx = x - dragStartX; var dy = y - dragStartY; var moved = false; // Horizontal drag: move left/right if (Math.abs(dx) > CELL_SIZE / 2) { var dir = dx > 0 ? 1 : -1; if (currentTetromino.tryMove(dir, 0, board)) { dragStartX = x; dragTetrominoX = currentTetromino.x; moved = true; } } // Vertical drag: hard drop if (dy > CELL_SIZE * 2 && !hardDrop) { // Hard drop while (currentTetromino.tryMove(0, 1, board)) {} hardDrop = true; moved = true; } // Tap: rotate (handled in up) if (moved) { currentTetromino.updateBlockPositions(); updateBoardDisplayOffsets(); } }; game.up = function (x, y, obj) { if (isGameOver || !currentTetromino) { return; } touchDown = false; var dt = Date.now() - lastTouchMove; var dx = x - dragStartX; var dy = y - dragStartY; // Tap: rotate if (Math.abs(dx) < 30 && Math.abs(dy) < 30 && dt < 400) { removeTetrominoOffset(); currentTetromino.tryRotate(board); currentTetromino.updateBlockPositions(); updateBoardDisplayOffsets(); } }; // Auto fall function scheduleDrop() { if (dropTimer) { LK.clearTimeout(dropTimer); } if (isGameOver) { return; } dropTimer = LK.setTimeout(function () { if (isGameOver) { return; } removeTetrominoOffset(); var moved = currentTetromino.tryMove(0, 1, board); currentTetromino.updateBlockPositions(); updateBoardDisplayOffsets(); if (!moved) { // Lock to board currentTetromino.lockToBoard(board, blockRefs); // Flash effect on lock for (var i = 0; i < currentTetromino.blocks.length; i++) { LK.effects.flashObject(currentTetromino.blocks[i], 0xffffff, 180); } // Powerup check var landedPowerup = false; for (var i = 0; i < currentTetromino.shape.length; i++) { var nx = currentTetromino.x + currentTetromino.shape[i][0]; var ny = currentTetromino.y + currentTetromino.shape[i][1]; if (board[ny][nx] === 'powerup') { activatePowerup(ny); landedPowerup = true; } } // Clear lines var lines = clearLines(); updateScore(lines); // Do not remove tetromino blocks here; they are now part of the board and will be removed when lines are cleared currentTetromino = null; // Spawn next spawnTetromino(); updateBoardDisplayOffsets(); } scheduleDrop(); }, fallInterval); } // Game update game.update = function () { // No per-frame logic needed for MVP }; // Start game function startGame() { // Reset board for (var r = 0; r < BOARD_ROWS; r++) { for (var c = 0; c < BOARD_COLS; c++) { board[r][c] = 0; if (blockRefs[r][c]) { if (blockRefs[r][c].parent) { blockRefs[r][c].parent.removeChild(blockRefs[r][c]); } blockRefs[r][c] = null; } } } score = 0; level = 1; linesCleared = 0; fallInterval = 1000; scoreTxt.setText(score); levelTxt.setText('Lv.1'); isGameOver = false; nextTetrominoType = TETROMINO_TYPES[Math.floor(Math.random() * TETROMINO_TYPES.length)]; updateNextPreview(); if (currentTetromino) { for (var i = 0; i < currentTetromino.blocks.length; i++) { if (currentTetromino.blocks[i].parent) { currentTetromino.blocks[i].parent.removeChild(currentTetromino.blocks[i]); } } currentTetromino = null; } spawnTetromino(); updateBoardDisplayOffsets(); scheduleDrop(); } // On game over, restart on new game LK.on('gameStart', function () { startGame(); }); // Initial start LK.playMusic('tetris_bgm'); startGame();
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Tetromino class
var Tetromino = Container.expand(function () {
var self = Container.call(this);
// Properties
self.type = null;
self.shape = null; // array of [x,y]
self.rotation = 0;
self.blocks = [];
self.x = 0; // board col
self.y = 0; // board row
self.isPowerup = false;
self.powerupType = null;
// Initialize tetromino
self.init = function (type, isPowerup) {
self.type = type;
self.isPowerup = !!isPowerup;
self.rotation = 0;
self.shape = [];
// Copy shape
var base = TETROMINO_SHAPES[type][0];
for (var i = 0; i < base.length; i++) {
self.shape.push([base[i][0], base[i][1]]);
}
self.blocks = [];
self.removeChildren();
for (var i = 0; i < self.shape.length; i++) {
var block;
if (self.isPowerup && i === 0) {
block = self.attachAsset('powerup', {
anchorX: 0.5,
anchorY: 0.5,
width: CELL_SIZE,
height: CELL_SIZE
});
self.powerupType = 'clearLine'; // Only one powerup for MVP
} else {
block = self.attachAsset(self.type, {
anchorX: 0.5,
anchorY: 0.5,
width: CELL_SIZE,
height: CELL_SIZE
});
}
self.blocks.push(block);
// Overlay grid lines for each block
var gridOverlay = new Container();
var vlineL = LK.getAsset('O', {
anchorX: 0.5,
anchorY: 0,
width: 4,
height: CELL_SIZE,
color: 0x222222 // match board grid color
});
vlineL.x = -CELL_SIZE / 2;
vlineL.y = -CELL_SIZE / 2;
vlineL.alpha = 0.25;
gridOverlay.addChild(vlineL);
var vlineR = LK.getAsset('O', {
anchorX: 0.5,
anchorY: 0,
width: 4,
height: CELL_SIZE,
color: 0x222222 // match board grid color
});
vlineR.x = CELL_SIZE / 2;
vlineR.y = -CELL_SIZE / 2;
vlineR.alpha = 0.25;
gridOverlay.addChild(vlineR);
var hlineT = LK.getAsset('O', {
anchorX: 0,
anchorY: 0.5,
width: CELL_SIZE,
height: 4,
color: 0x222222 // match board grid color
});
hlineT.x = -CELL_SIZE / 2;
hlineT.y = -CELL_SIZE / 2;
hlineT.alpha = 0.25;
gridOverlay.addChild(hlineT);
var hlineB = LK.getAsset('O', {
anchorX: 0,
anchorY: 0.5,
width: CELL_SIZE,
height: 4,
color: 0x222222 // match board grid color
});
hlineB.x = -CELL_SIZE / 2;
hlineB.y = CELL_SIZE / 2;
hlineB.alpha = 0.25;
gridOverlay.addChild(hlineB);
block.addChild(gridOverlay);
}
self.x = Math.floor(BOARD_COLS / 2) - 2;
self.y = 0;
self.updateBlockPositions();
};
// Update block positions
self.updateBlockPositions = function () {
for (var i = 0; i < self.shape.length; i++) {
var pos = self.shape[i];
self.blocks[i].x = (self.x + pos[0]) * CELL_SIZE + CELL_SIZE / 2;
self.blocks[i].y = (self.y + pos[1]) * CELL_SIZE + CELL_SIZE / 2;
}
};
// Try move
self.tryMove = function (dx, dy, board) {
if (self.canMove(dx, dy, self.shape, board)) {
self.x += dx;
self.y += dy;
self.updateBlockPositions();
return true;
}
return false;
};
// Try rotate
self.tryRotate = function (board) {
var rotated = rotateShape(self.shape);
if (self.canMove(0, 0, rotated, board)) {
self.shape = rotated;
self.updateBlockPositions();
return true;
}
return false;
};
// Can move/rotate
self.canMove = function (dx, dy, shape, board) {
for (var i = 0; i < shape.length; i++) {
var nx = self.x + shape[i][0] + dx;
var ny = self.y + shape[i][1] + dy;
if (nx < 0 || nx >= BOARD_COLS || ny < 0 || ny >= BOARD_ROWS) {
return false;
}
if (board[ny][nx]) {
return false;
}
}
return true;
};
// Lock tetromino into board
self.lockToBoard = function (board, blockRefs) {
for (var i = 0; i < self.shape.length; i++) {
var nx = self.x + self.shape[i][0];
var ny = self.y + self.shape[i][1];
board[ny][nx] = self.isPowerup && i === 0 ? 'powerup' : self.type;
blockRefs[ny][nx] = self.blocks[i];
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
// No title, no description
// Always backgroundColor is black
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Board state: 2D array [row][col], 0 = empty, else type string
// Tetromino shapes (classic + new)
// Colors for each tetromino type
// New shapes
// Power-up block
// Power-up effect
// Tetromino definitions (relative coordinates for each block in the tetromino)
var TETROMINO_SHAPES = {
O: [[[1, 0], [2, 0], [1, 1], [2, 1]]],
T: [[[1, 0], [0, 1], [1, 1], [2, 1]]],
S: [[[1, 0], [2, 0], [0, 1], [1, 1]]],
Z: [[[0, 0], [1, 0], [1, 1], [2, 1]]],
J: [[[0, 0], [0, 1], [1, 1], [2, 1]]],
L: [[[2, 0], [0, 1], [1, 1], [2, 1]]],
U: [[[0, 0], [2, 0], [0, 1], [1, 1], [2, 1]]],
P: [[[0, 0], [1, 0], [0, 1], [1, 1], [0, 2]]],
Dot: [[[1, 1]]]
};
var TETROMINO_TYPES = ['O', 'T', 'S', 'Z', 'J', 'L', 'U', 'P'];
var POWERUP_CHANCE = 0.08; // 8% chance for a powerup block
// Board settings
var BOARD_COLS = 8; // Reduced width for fewer columns, block size unchanged
var BOARD_ROWS = 20;
var CELL_SIZE = 90; // px, fits well on 2048x2732
var BOARD_OFFSET_X = Math.floor((2048 - BOARD_COLS * CELL_SIZE) / 2) - 80; // Shift left to make space for preview
var BOARD_OFFSET_Y = 300;
// Helper: rotate a shape (array of [x,y]) 90deg clockwise
function rotateShape(shape) {
var maxX = 0,
maxY = 0;
for (var i = 0; i < shape.length; i++) {
if (shape[i][0] > maxX) {
maxX = shape[i][0];
}
if (shape[i][1] > maxY) {
maxY = shape[i][1];
}
}
var rotated = [];
for (var i = 0; i < shape.length; i++) {
var x = shape[i][0],
y = shape[i][1];
rotated.push([maxY - y, x]);
}
return rotated;
}
var board = [];
var blockRefs = []; // 2D array of block display objects
for (var r = 0; r < BOARD_ROWS; r++) {
board[r] = [];
blockRefs[r] = [];
for (var c = 0; c < BOARD_COLS; c++) {
board[r][c] = 0;
blockRefs[r][c] = null;
}
}
// Score and level
var score = 0;
var level = 1;
var linesCleared = 0;
var fallInterval = 1000; // ms, decreases with level
// GUI
var scoreTxt = new Text2('0', {
size: 100,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
var levelTxt = new Text2('Lv.1', {
size: 60,
fill: "#fff"
});
levelTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(levelTxt);
levelTxt.y = 110;
// Next tetromino preview
var nextTetrominoType = null;
var nextTetrominoPreview = new Container();
game.addChild(nextTetrominoPreview);
// Current falling tetromino
var currentTetromino = null;
// Game state
var isGameOver = false;
var isDropping = false;
var dropTimer = null;
var moveTimer = null;
var moveDir = 0; // -1 left, 1 right, 0 none
// Helper: spawn new tetromino
function spawnTetromino() {
var type = nextTetrominoType;
if (!type) {
type = TETROMINO_TYPES[Math.floor(Math.random() * TETROMINO_TYPES.length)];
}
var isPowerup = Math.random() < POWERUP_CHANCE;
var tetro = new Tetromino();
tetro.init(type, isPowerup);
game.addChild(tetro);
currentTetromino = tetro;
// Next
nextTetrominoType = TETROMINO_TYPES[Math.floor(Math.random() * TETROMINO_TYPES.length)];
updateNextPreview();
// Flash effect on next preview
if (nextTetrominoPreview.children && nextTetrominoPreview.children.length > 0) {
for (var i = 0; i < nextTetrominoPreview.children.length; i++) {
LK.effects.flashObject(nextTetrominoPreview.children[i], 0xffffff, 180);
}
}
// If cannot place, game over
if (!tetro.canMove(0, 0, tetro.shape, board)) {
isGameOver = true;
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
}
}
// Helper: update next preview
function updateNextPreview() {
nextTetrominoPreview.removeChildren();
var type = nextTetrominoType;
if (!type) {
return;
}
var shape = TETROMINO_SHAPES[type][0];
// Find min/max for centering
var minX = 99,
maxX = -99,
minY = 99,
maxY = -99;
for (var i = 0; i < shape.length; i++) {
if (shape[i][0] < minX) {
minX = shape[i][0];
}
if (shape[i][0] > maxX) {
maxX = shape[i][0];
}
if (shape[i][1] < minY) {
minY = shape[i][1];
}
if (shape[i][1] > maxY) {
maxY = shape[i][1];
}
}
var previewWidth = (maxX - minX + 1) * CELL_SIZE;
var previewHeight = (maxY - minY + 1) * CELL_SIZE;
for (var i = 0; i < shape.length; i++) {
var block = LK.getAsset(type, {
anchorX: 0.5,
anchorY: 0.5,
width: CELL_SIZE,
height: CELL_SIZE
});
block.x = (shape[i][0] - minX) * CELL_SIZE + CELL_SIZE / 2;
block.y = (shape[i][1] - minY) * CELL_SIZE + CELL_SIZE / 2;
nextTetrominoPreview.addChild(block);
}
// Place preview to the right of the board, fully visible and inside the screen
nextTetrominoPreview.x = BOARD_OFFSET_X + BOARD_COLS * CELL_SIZE + 120;
nextTetrominoPreview.y = BOARD_OFFSET_Y + 200;
}
// Helper: clear full lines
function clearLines() {
var lines = [];
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.push(r);
}
}
if (lines.length === 0) {
return 0;
}
// Animate and remove lines
for (var i = 0; i < lines.length; i++) {
var row = lines[i];
for (var c = 0; c < BOARD_COLS; c++) {
if (blockRefs[row][c]) {
tween(blockRefs[row][c], {
alpha: 0
}, {
duration: 200,
onFinish: function (obj) {
if (obj.parent) {
obj.parent.removeChild(obj);
}
}.bind(null, blockRefs[row][c])
});
}
}
}
// Remove from board
for (var i = 0; i < lines.length; i++) {
var row = lines[i];
for (var c = 0; c < BOARD_COLS; c++) {
board[row][c] = 0;
blockRefs[row][c] = null;
}
}
// Drop above lines
for (var i = lines.length - 1; i >= 0; i--) {
var row = lines[i];
for (var r = row - 1; r >= 0; r--) {
for (var c = 0; c < BOARD_COLS; c++) {
board[r + 1][c] = board[r][c];
blockRefs[r + 1][c] = blockRefs[r][c];
if (blockRefs[r + 1][c]) {
tween(blockRefs[r + 1][c], {
y: blockRefs[r + 1][c].y + CELL_SIZE
}, {
duration: 100
});
}
}
}
// Clear top row
for (var c = 0; c < BOARD_COLS; c++) {
board[0][c] = 0;
blockRefs[0][c] = null;
}
}
return lines.length;
}
// Helper: activate powerup
function activatePowerup(row) {
// For MVP: clear the row where powerup landed
for (var c = 0; c < BOARD_COLS; c++) {
if (blockRefs[row][c]) {
tween(blockRefs[row][c], {
alpha: 0
}, {
duration: 200,
onFinish: function (obj) {
if (obj.parent) {
obj.parent.removeChild(obj);
}
}.bind(null, blockRefs[row][c])
});
board[row][c] = 0;
blockRefs[row][c] = null;
}
}
// Drop above lines
for (var r = row - 1; r >= 0; r--) {
for (var c = 0; c < BOARD_COLS; c++) {
board[r + 1][c] = board[r][c];
blockRefs[r + 1][c] = blockRefs[r][c];
if (blockRefs[r + 1][c]) {
tween(blockRefs[r + 1][c], {
y: blockRefs[r + 1][c].y + CELL_SIZE
}, {
duration: 100
});
}
}
}
// Clear top row
for (var c = 0; c < BOARD_COLS; c++) {
board[0][c] = 0;
blockRefs[0][c] = null;
}
}
// Helper: update score/level
function updateScore(lines) {
var points = [0, 100, 300, 500, 800];
score += points[lines] || 0;
linesCleared += lines;
scoreTxt.setText(score);
var newLevel = 1 + Math.floor(linesCleared / 10);
if (newLevel !== level) {
level = newLevel;
levelTxt.setText('Lv.' + level);
fallInterval = Math.max(150, 1000 - (level - 1) * 100);
}
}
// Helper: draw board grid (for visual reference)
var gridLines = new Container();
game.addChild(gridLines);
// Draw faint cell backgrounds for subtle grid
for (var r = 0; r < BOARD_ROWS; r++) {
for (var c = 0; c < BOARD_COLS; c++) {
var cell = LK.getAsset('O', {
anchorX: 0.5,
anchorY: 0.5,
width: CELL_SIZE - 4,
height: CELL_SIZE - 4,
color: 0x222222
});
cell.x = c * CELL_SIZE + CELL_SIZE / 2;
cell.y = r * CELL_SIZE + CELL_SIZE / 2;
cell.alpha = 0.15;
gridLines.addChild(cell);
}
}
// Draw vertical grid lines
for (var c = 0; c <= BOARD_COLS; c++) {
var vline = LK.getAsset('O', {
anchorX: 0.5,
anchorY: 0,
width: 4,
height: CELL_SIZE * BOARD_ROWS,
color: 0x222222 // darker gray for better contrast
});
vline.x = c * CELL_SIZE;
vline.y = 0;
vline.alpha = 0.35;
gridLines.addChild(vline);
}
// Draw horizontal grid lines
for (var r = 0; r <= BOARD_ROWS; r++) {
var hline = LK.getAsset('O', {
anchorX: 0,
anchorY: 0.5,
width: CELL_SIZE * BOARD_COLS,
height: 4,
color: 0x222222 // darker gray for better contrast
});
hline.x = 0;
hline.y = r * CELL_SIZE;
hline.alpha = 0.35;
gridLines.addChild(hline);
}
gridLines.x = BOARD_OFFSET_X;
gridLines.y = BOARD_OFFSET_Y;
// Move all blocks/containers to board offset
function updateBoardDisplayOffsets() {
gridLines.x = BOARD_OFFSET_X;
gridLines.y = BOARD_OFFSET_Y;
for (var r = 0; r < BOARD_ROWS; r++) {
for (var c = 0; c < BOARD_COLS; c++) {
if (blockRefs[r][c]) {
blockRefs[r][c].x = c * CELL_SIZE + CELL_SIZE / 2 + BOARD_OFFSET_X;
blockRefs[r][c].y = r * CELL_SIZE + CELL_SIZE / 2 + BOARD_OFFSET_Y;
}
}
}
if (currentTetromino) {
for (var i = 0; i < currentTetromino.blocks.length; i++) {
currentTetromino.blocks[i].x += BOARD_OFFSET_X;
currentTetromino.blocks[i].y += BOARD_OFFSET_Y;
}
}
}
// Remove board offset from tetromino before logic
function removeTetrominoOffset() {
if (currentTetromino) {
for (var i = 0; i < currentTetromino.blocks.length; i++) {
currentTetromino.blocks[i].x -= BOARD_OFFSET_X;
currentTetromino.blocks[i].y -= BOARD_OFFSET_Y;
}
}
}
// Touch controls
var dragStartX = null;
var dragStartY = null;
var dragTetrominoX = null;
var dragTetrominoY = null;
var lastTouchMove = 0;
var touchDown = false;
var hardDrop = false;
// Convert screen x,y to board col,row
function screenToBoard(x, y) {
var bx = Math.floor((x - BOARD_OFFSET_X) / CELL_SIZE);
var by = Math.floor((y - BOARD_OFFSET_Y) / CELL_SIZE);
return {
col: bx,
row: by
};
}
// Touch/mouse events
game.down = function (x, y, obj) {
if (isGameOver || !currentTetromino) {
return;
}
dragStartX = x;
dragStartY = y;
dragTetrominoX = currentTetromino.x;
dragTetrominoY = currentTetromino.y;
touchDown = true;
hardDrop = false;
lastTouchMove = Date.now();
};
game.move = function (x, y, obj) {
if (isGameOver || !currentTetromino || !touchDown) {
return;
}
var dx = x - dragStartX;
var dy = y - dragStartY;
var moved = false;
// Horizontal drag: move left/right
if (Math.abs(dx) > CELL_SIZE / 2) {
var dir = dx > 0 ? 1 : -1;
if (currentTetromino.tryMove(dir, 0, board)) {
dragStartX = x;
dragTetrominoX = currentTetromino.x;
moved = true;
}
}
// Vertical drag: hard drop
if (dy > CELL_SIZE * 2 && !hardDrop) {
// Hard drop
while (currentTetromino.tryMove(0, 1, board)) {}
hardDrop = true;
moved = true;
}
// Tap: rotate (handled in up)
if (moved) {
currentTetromino.updateBlockPositions();
updateBoardDisplayOffsets();
}
};
game.up = function (x, y, obj) {
if (isGameOver || !currentTetromino) {
return;
}
touchDown = false;
var dt = Date.now() - lastTouchMove;
var dx = x - dragStartX;
var dy = y - dragStartY;
// Tap: rotate
if (Math.abs(dx) < 30 && Math.abs(dy) < 30 && dt < 400) {
removeTetrominoOffset();
currentTetromino.tryRotate(board);
currentTetromino.updateBlockPositions();
updateBoardDisplayOffsets();
}
};
// Auto fall
function scheduleDrop() {
if (dropTimer) {
LK.clearTimeout(dropTimer);
}
if (isGameOver) {
return;
}
dropTimer = LK.setTimeout(function () {
if (isGameOver) {
return;
}
removeTetrominoOffset();
var moved = currentTetromino.tryMove(0, 1, board);
currentTetromino.updateBlockPositions();
updateBoardDisplayOffsets();
if (!moved) {
// Lock to board
currentTetromino.lockToBoard(board, blockRefs);
// Flash effect on lock
for (var i = 0; i < currentTetromino.blocks.length; i++) {
LK.effects.flashObject(currentTetromino.blocks[i], 0xffffff, 180);
}
// Powerup check
var landedPowerup = false;
for (var i = 0; i < currentTetromino.shape.length; i++) {
var nx = currentTetromino.x + currentTetromino.shape[i][0];
var ny = currentTetromino.y + currentTetromino.shape[i][1];
if (board[ny][nx] === 'powerup') {
activatePowerup(ny);
landedPowerup = true;
}
}
// Clear lines
var lines = clearLines();
updateScore(lines);
// Do not remove tetromino blocks here; they are now part of the board and will be removed when lines are cleared
currentTetromino = null;
// Spawn next
spawnTetromino();
updateBoardDisplayOffsets();
}
scheduleDrop();
}, fallInterval);
}
// Game update
game.update = function () {
// No per-frame logic needed for MVP
};
// Start game
function startGame() {
// Reset board
for (var r = 0; r < BOARD_ROWS; r++) {
for (var c = 0; c < BOARD_COLS; c++) {
board[r][c] = 0;
if (blockRefs[r][c]) {
if (blockRefs[r][c].parent) {
blockRefs[r][c].parent.removeChild(blockRefs[r][c]);
}
blockRefs[r][c] = null;
}
}
}
score = 0;
level = 1;
linesCleared = 0;
fallInterval = 1000;
scoreTxt.setText(score);
levelTxt.setText('Lv.1');
isGameOver = false;
nextTetrominoType = TETROMINO_TYPES[Math.floor(Math.random() * TETROMINO_TYPES.length)];
updateNextPreview();
if (currentTetromino) {
for (var i = 0; i < currentTetromino.blocks.length; i++) {
if (currentTetromino.blocks[i].parent) {
currentTetromino.blocks[i].parent.removeChild(currentTetromino.blocks[i]);
}
}
currentTetromino = null;
}
spawnTetromino();
updateBoardDisplayOffsets();
scheduleDrop();
}
// On game over, restart on new game
LK.on('gameStart', function () {
startGame();
});
// Initial start
LK.playMusic('tetris_bgm');
startGame();