/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var GridBlock = Container.expand(function (shapeType) { var self = Container.call(this); self.shapeType = shapeType; self.color = shapeColors[shapeType] || 0xffffff; self.gridX = 0; self.gridY = 0; var shapeGraphic = self.attachAsset(shapeType, { anchorX: 0.5, anchorY: 0.5 }); // Add bomb fuse for bomb blocks if (shapeType === 'bomb') { var fuseGraphic = self.attachAsset('bombFuse', { anchorX: 0.5, anchorY: 1, x: 0, y: -25 }); // Animate fuse flickering var _fuseFlicker = function fuseFlicker() { tween(fuseGraphic, { alpha: 0.3 }, { duration: 200, onFinish: function onFinish() { tween(fuseGraphic, { alpha: 1 }, { duration: 200, onFinish: _fuseFlicker }); } }); }; _fuseFlicker(); } return self; }); var PlacedShape = Container.expand(function (shapeType) { var self = Container.call(this); self.shapeType = shapeType; self.isStable = true; self.isDragging = false; var shapeGraphic = self.attachAsset(shapeType, { anchorX: 0.5, anchorY: 0.5 }); self.rotate = function () { // Smooth rotation animation tween(shapeGraphic, { rotation: shapeGraphic.rotation + Math.PI / 2 }, { duration: 300, easing: tween.easeInOut, onFinish: function onFinish() { calculateStability(); updateStability(); } }); }; self.down = function (x, y, obj) { // Only set as selected if no other shape is already being dragged if (!selectedShape || !selectedShape.isDragging) { self.isDragging = true; selectedShape = self; } }; self.up = function (x, y, obj) { self.isDragging = false; if (selectedShape === self) { selectedShape = null; } calculateStability(); updateStability(); }; return self; }); var TetrisBlock = Container.expand(function (shapeType) { var self = Container.call(this); self.shapeType = shapeType; self.fallSpeed = Math.random() < 0.3 ? 3.5 : 2.5; // Increased by ~66% for more dynamic gameplay self.isSpecial = specialBlocks.indexOf(shapeType) !== -1; self.gridX = Math.floor(Math.random() * (gridWidth - 3)); // Ensure space for shape self.gridY = 0; self.lastMoveTime = 0; self.targetX = 0; // For smooth movement self.isMoving = false; // Track if block is currently moving self.rotation = 0; // Track current rotation state (0, 1, 2, 3) self.blocks = []; // Array to hold individual block graphics self.hasLanded = false; // Track if shape has touched ground or another block // Rotation configurations for each shape self.rotationConfigs = { 'iblock': [[[-1, 0], [0, 0], [1, 0], [2, 0]], // horizontal [[0, -1], [0, 0], [0, 1], [0, 2]], // vertical [[-1, 0], [0, 0], [1, 0], [2, 0]], // horizontal [[0, -1], [0, 0], [0, 1], [0, 2]] // vertical ], 'oblock': [[[0, 0], [1, 0], [0, 1], [1, 1]], // same for all rotations [[0, 0], [1, 0], [0, 1], [1, 1]], [[0, 0], [1, 0], [0, 1], [1, 1]], [[0, 0], [1, 0], [0, 1], [1, 1]]], 'tblock': [[[-1, 0], [0, 0], [1, 0], [0, 1]], // T pointing down [[0, -1], [0, 0], [1, 0], [0, 1]], // T pointing left [[-1, 0], [0, 0], [1, 0], [0, -1]], // T pointing up [[0, -1], [-1, 0], [0, 0], [0, 1]] // T pointing right ], 'lblock': [[[-1, 0], [0, 0], [1, 0], [1, 1]], // L standard [[0, -1], [0, 0], [0, 1], [-1, 1]], // L rotated 90 [[-1, -1], [-1, 0], [0, 0], [1, 0]], // L rotated 180 [[0, -1], [0, 0], [0, 1], [1, -1]] // L rotated 270 ], 'jblock': [[[-1, 0], [0, 0], [1, 0], [-1, 1]], // J standard [[0, -1], [0, 0], [0, 1], [1, 1]], // J rotated 90 [[-1, 0], [0, 0], [1, 0], [1, -1]], // J rotated 180 [[0, -1], [-1, -1], [0, 0], [0, 1]] // J rotated 270 ], 'sblock': [[[-1, 1], [0, 1], [0, 0], [1, 0]], // S horizontal [[0, -1], [0, 0], [1, 0], [1, 1]], // S vertical [[-1, 1], [0, 1], [0, 0], [1, 0]], // S horizontal [[0, -1], [0, 0], [1, 0], [1, 1]] // S vertical ], 'zblock': [[[-1, 0], [0, 0], [0, 1], [1, 1]], // Z horizontal [[1, -1], [1, 0], [0, 0], [0, 1]], // Z vertical [[-1, 0], [0, 0], [0, 1], [1, 1]], // Z horizontal [[1, -1], [1, 0], [0, 0], [0, 1]] // Z vertical ] }; // Define classic Tetris shape configurations with proper proportions var shapeConfigs = { 'iblock': [[-1, 0], [0, 0], [1, 0], [2, 0] // I-piece: centered horizontal line ], 'oblock': [[0, 0], [1, 0], [0, 1], [1, 1] // O-piece: 2x2 square ], 'tblock': [[-1, 0], [0, 0], [1, 0], [0, 1] // T-piece: centered with stem down ], 'lblock': [[-1, 0], [0, 0], [1, 0], [1, 1] // L-piece: centered with corner ], 'jblock': [[-1, 0], [0, 0], [1, 0], [-1, 1] // J-piece: centered with corner ], 'sblock': [[-1, 1], [0, 1], [0, 0], [1, 0] // S-piece: classic S shape ], 'zblock': [[-1, 0], [0, 0], [0, 1], [1, 1] // Z-piece: classic Z shape ] }; // Get shape configuration or default to single block var config = shapeConfigs[shapeType] || [[0, 0]]; // Store current shape configuration after config is defined self.currentConfig = self.rotationConfigs[shapeType] && self.rotationConfigs[shapeType][0] ? self.rotationConfigs[shapeType][0] : config; var shapeColor = shapeColors[shapeType] || 0x4a90e2; // Create a single group container for all blocks self.blockGroup = new Container(); self.addChild(self.blockGroup); // Create individual blocks within the group for (var i = 0; i < config.length; i++) { var blockGraphic = self.blockGroup.attachAsset(shapeType, { anchorX: 0.5, anchorY: 0.5 }); // Position block relative to group center blockGraphic.x = config[i][0] * blockSize; blockGraphic.y = config[i][1] * blockSize; // Add subtle outline for better visibility blockGraphic.alpha = 0.9; // Completely disable interactivity on individual blocks to prevent structural changes blockGraphic.interactive = false; blockGraphic.buttonMode = false; // Prevent any touch events from reaching individual blocks blockGraphic.down = null; blockGraphic.up = null; blockGraphic.move = null; self.blocks.push({ graphic: blockGraphic, offsetX: config[i][0], offsetY: config[i][1] }); } // Calculate proper pivot point based on shape bounds for centered rotation var minX = 0, maxX = 0, minY = 0, maxY = 0; for (var i = 0; i < config.length; i++) { minX = Math.min(minX, config[i][0]); maxX = Math.max(maxX, config[i][0]); minY = Math.min(minY, config[i][1]); maxY = Math.max(maxY, config[i][1]); } var centerX = (minX + maxX) * blockSize / 2; var centerY = (minY + maxY) * blockSize / 2; self.blockGroup.pivot.set(centerX, centerY); // Add bomb fuse for bomb blocks if (shapeType === 'bomb') { var fuseGraphic = self.attachAsset('bombFuse', { anchorX: 0.5, anchorY: 1, x: 0, y: -25 }); // Animate fuse flickering var _fuseFlicker2 = function fuseFlicker() { tween(fuseGraphic, { alpha: 0.3 }, { duration: 200, onFinish: function onFinish() { tween(fuseGraphic, { alpha: 1 }, { duration: 200, onFinish: _fuseFlicker2 }); } }); }; _fuseFlicker2(); } // Position on grid self.x = self.gridX * blockSize + blockSize / 2 + (2048 - gridWidth * blockSize) / 2; self.y = self.gridY * blockSize + blockSize / 2 + 200; self.targetX = self.x; // Initialize target position self.checkCollision = function (newGridX, newGridY, configToCheck) { // Use provided configuration or current configuration var config = configToCheck || self.currentConfig; // Check collision for each block in the configuration for (var i = 0; i < config.length; i++) { var blockX = newGridX + config[i][0]; var blockY = newGridY + config[i][1]; // Check bounds if (blockX < 0 || blockX >= gridWidth || blockY < 0 || blockY >= gridHeight) { return true; } // Check if position is occupied in grid if (blockY >= 0 && blockY < gridHeight && grid[blockY] && grid[blockY][blockX]) { return true; } // Check collision with other falling shapes for (var j = 0; j < fallingShapes.length; j++) { var otherShape = fallingShapes[j]; if (otherShape !== self) { for (var k = 0; k < otherShape.blocks.length; k++) { var otherBlock = otherShape.blocks[k]; var otherBlockX = otherShape.gridX + otherBlock.offsetX; var otherBlockY = otherShape.gridY + otherBlock.offsetY; if (otherBlockX === blockX && otherBlockY === blockY) { return true; } } } } } return false; }; self.moveToPosition = function (newGridX) { // Track that the shape has moved (for distinguishing tap from drag) if (Math.abs(newGridX - self.gridX) > 0) { self.hasMoved = true; } // Clamp to grid boundaries based on current shape configuration var minX = 0; var maxX = 0; for (var i = 0; i < self.currentConfig.length; i++) { var blockX = self.currentConfig[i][0]; minX = Math.min(minX, blockX); maxX = Math.max(maxX, blockX); } // Clamp position to keep all blocks within grid bounds newGridX = Math.max(-minX, Math.min(gridWidth - 1 - maxX, newGridX)); // Allow movement even if there might be collision - let player move freely while dragging self.gridX = newGridX; self.targetX = self.gridX * blockSize + blockSize / 2 + (2048 - gridWidth * blockSize) / 2; self.isMoving = true; // Stop any existing movement tween tween.stop(self, { x: true }); // Instant movement for maximum responsiveness when dragging if (self.isDragging) { self.x = self.targetX; self.isMoving = false; } else { // Ultra-fast responsive movement for non-dragging cases tween(self, { x: self.targetX }, { duration: 50, easing: tween.easeOut, onFinish: function onFinish() { self.isMoving = false; } }); } }; self.update = function () { var currentTime = LK.ticks; if (currentTime - self.lastMoveTime > 80 / self.fallSpeed) { self.lastMoveTime = currentTime; // Check if can move down using collision detection var canMoveDown = !self.checkCollision(self.gridX, self.gridY + 1); if (canMoveDown) { // Play light drop sound for step-down movement LK.getSound('drop').play(); self.gridY++; self.y = self.gridY * blockSize + blockSize / 2 + 200; } else { // Check if touching another shape (not ground) for contact sound var touchingShape = false; for (var j = 0; j < fallingShapes.length; j++) { var otherShape = fallingShapes[j]; if (otherShape !== self && self.checkCollision(self.gridX, self.gridY + 1)) { touchingShape = true; break; } } // Check if there are blocks below in grid var touchingGrid = false; for (var i = 0; i < self.currentConfig.length; i++) { var blockX = self.gridX + self.currentConfig[i][0]; var blockY = self.gridY + self.currentConfig[i][1] + 1; if (blockY < gridHeight && blockY >= 0 && grid[blockY] && grid[blockY][blockX]) { touchingGrid = true; break; } } // Play appropriate sound if (touchingShape || touchingGrid) { if (self.gridY + 1 >= gridHeight) { // Landing on ground LK.getSound('land').play(); } else { // Contact with another shape LK.getSound('contact').play(); } } // Mark as landed when it can't move down self.hasLanded = true; // Only place if we're in a valid position and can't move down if (self.gridY >= 0) { self.placeInGrid(); } else { // If stuck above grid, try to find a valid position var foundValidPosition = false; for (var testY = 0; testY < gridHeight; testY++) { if (!self.checkCollision(self.gridX, testY)) { self.gridY = testY; self.y = self.gridY * blockSize + blockSize / 2 + 200; foundValidPosition = true; break; } } if (!foundValidPosition) { // Stop any existing tweens before destroying tween.stop(self); tween.stop(self.blockGroup); for (var i = 0; i < self.blocks.length; i++) { tween.stop(self.blocks[i].graphic); } // Remove stuck shape var index = fallingShapes.indexOf(self); if (index > -1) { fallingShapes.splice(index, 1); } self.destroy(); } } } } }; self.placeInGrid = function () { var allBlocksValid = true; // Check if all blocks can be placed for (var i = 0; i < self.currentConfig.length; i++) { var blockX = self.gridX + self.currentConfig[i][0]; var blockY = self.gridY + self.currentConfig[i][1]; if (blockY < 0 || blockY >= gridHeight || blockX < 0 || blockX >= gridWidth) { allBlocksValid = false; break; } } if (allBlocksValid) { // Freeze the group before disassembly to maintain structural stability self.blockGroup.rotation = Math.round(self.blockGroup.rotation / (Math.PI / 2)) * (Math.PI / 2); // Place all blocks in grid - disassemble the group into static tiles for (var i = 0; i < self.currentConfig.length; i++) { var blockX = self.gridX + self.currentConfig[i][0]; var blockY = self.gridY + self.currentConfig[i][1]; grid[blockY][blockX] = self.shapeType; // Create visual block in grid (static tile) var placedBlock = new GridBlock(self.shapeType); placedBlock.gridX = blockX; placedBlock.gridY = blockY; placedBlock.x = blockX * blockSize + blockSize / 2 + (2048 - gridWidth * blockSize) / 2; placedBlock.y = blockY * blockSize + blockSize / 2 + 200; // Apply the same rotation as the group had placedBlock.rotation = self.blockGroup.rotation; placedShapes.push(placedBlock); game.addChild(placedBlock); } LK.getSound('place').play(); score += 10 * self.blocks.length; updateScore(); checkForLines(); // Handle special blocks if (self.isSpecial) { handleSpecialBlock(self.shapeType, self.gridX, self.gridY); } } // Stop any existing tweens before destroying tween.stop(self); tween.stop(self.blockGroup); for (var i = 0; i < self.blocks.length; i++) { tween.stop(self.blocks[i].graphic); } // Remove from falling shapes var index = fallingShapes.indexOf(self); if (index > -1) { fallingShapes.splice(index, 1); } self.destroy(); // Check game over if (self.gridY <= 1) { LK.showGameOver(); } }; self.rotate = function () { // Only allow rotation if shape hasn't landed to maintain structural stability if (self.hasLanded) return; // Get rotation configurations for this shape type if (!self.rotationConfigs[self.shapeType]) return; // Calculate next rotation state var nextRotation = (self.rotation + 1) % 4; var nextConfig = self.rotationConfigs[self.shapeType][nextRotation]; // Check if rotation is valid (no collision) if (!self.checkCollision(self.gridX, self.gridY, nextConfig)) { // Update rotation state and configuration self.rotation = nextRotation; self.currentConfig = nextConfig; // Update pivot point for proper centered rotation var minX = 0, maxX = 0, minY = 0, maxY = 0; for (var i = 0; i < nextConfig.length; i++) { minX = Math.min(minX, nextConfig[i][0]); maxX = Math.max(maxX, nextConfig[i][0]); minY = Math.min(minY, nextConfig[i][1]); maxY = Math.max(maxY, nextConfig[i][1]); } var centerX = (minX + maxX) * blockSize / 2; var centerY = (minY + maxY) * blockSize / 2; self.blockGroup.pivot.set(centerX, centerY); // Update block positions based on new configuration for (var i = 0; i < self.blocks.length; i++) { if (nextConfig[i]) { self.blocks[i].graphic.x = nextConfig[i][0] * blockSize; self.blocks[i].graphic.y = nextConfig[i][1] * blockSize; self.blocks[i].offsetX = nextConfig[i][0]; self.blocks[i].offsetY = nextConfig[i][1]; } } // Play rotation sound effect LK.getSound('rotate').play(); // Animate rotation on the block group with responsive timing tween(self.blockGroup, { rotation: self.blockGroup.rotation + Math.PI / 2 }, { duration: 150, easing: tween.easeOut }); } }; self.rotateLeft = function () { // Only allow rotation if shape hasn't landed to maintain structural stability if (self.hasLanded) return; // Get rotation configurations for this shape type if (!self.rotationConfigs[self.shapeType]) return; // Calculate previous rotation state (counter-clockwise) var nextRotation = (self.rotation + 3) % 4; // +3 is same as -1 in mod 4 var nextConfig = self.rotationConfigs[self.shapeType][nextRotation]; // Check if rotation is valid (no collision) if (!self.checkCollision(self.gridX, self.gridY, nextConfig)) { // Update rotation state and configuration self.rotation = nextRotation; self.currentConfig = nextConfig; // Update pivot point for proper centered rotation var minX = 0, maxX = 0, minY = 0, maxY = 0; for (var i = 0; i < nextConfig.length; i++) { minX = Math.min(minX, nextConfig[i][0]); maxX = Math.max(maxX, nextConfig[i][0]); minY = Math.min(minY, nextConfig[i][1]); maxY = Math.max(maxY, nextConfig[i][1]); } var centerX = (minX + maxX) * blockSize / 2; var centerY = (minY + maxY) * blockSize / 2; self.blockGroup.pivot.set(centerX, centerY); // Update block positions based on new configuration for (var i = 0; i < self.blocks.length; i++) { if (nextConfig[i]) { self.blocks[i].graphic.x = nextConfig[i][0] * blockSize; self.blocks[i].graphic.y = nextConfig[i][1] * blockSize; self.blocks[i].offsetX = nextConfig[i][0]; self.blocks[i].offsetY = nextConfig[i][1]; } } // Play rotation sound effect LK.getSound('rotate').play(); // Animate rotation on the block group with responsive timing (counter-clockwise) tween(self.blockGroup, { rotation: self.blockGroup.rotation - Math.PI / 2 }, { duration: 150, easing: tween.easeOut }); } }; self.down = function (x, y, obj) { // Ignore touches on landed shapes to maintain structural stability until row completion if (self.hasLanded) return; // Store touch start time for tap detection self.touchStartTime = LK.ticks; // Clear any existing selection first if (selectedShape && selectedShape !== self) { // Reset previous shape tween(selectedShape, { scaleX: 1, scaleY: 1, alpha: 1 }, { duration: 100 }); selectedShape.isDragging = false; } // Immediately select this shape when touched selectedShape = self; selectedShape.isDragging = true; // No touch offset - use direct positioning for 1:1 movement // Immediate visual feedback on the group tween(self.blockGroup, { scaleX: 1.1, scaleY: 1.1, alpha: 0.8 }, { duration: 100, easing: tween.easeOut }); }; self.up = function (x, y, obj) { // Handle tap-to-rotate for falling shapes (maintain structural stability after landing) if (selectedShape === self && !self.hasLanded) { // Check if this was a tap (not a drag) and shape hasn't moved var touchDuration = LK.ticks - (self.touchStartTime || 0); if (!self.hasMoved && touchDuration < 30) { // Quick tap under 0.5 seconds self.rotate(); } selectedShape.isDragging = false; selectedShape = null; self.hasMoved = false; // Reset movement flag // Reset visual feedback on the group tween(self.blockGroup, { scaleX: 1, scaleY: 1, alpha: 1 }, { duration: 150, easing: tween.easeOut }); } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x87CEEB }); /**** * Game Code ****/ var fallingShapes = []; var placedShapes = []; var score = 0; var linesCleared = 0; var level = 1; var groundY = 2400; var shapeTypes = ['lblock', 'tblock', 'zblock', 'iblock', 'jblock', 'sblock', 'oblock']; var specialBlocks = ['gift', 'bomb', 'wildcard']; var shapeColors = { 'lblock': 0xFF6600, // Orange - L piece 'tblock': 0x9900FF, // Purple - T piece 'zblock': 0xFF0000, // Red - Z piece 'iblock': 0x00FFFF, // Cyan - I piece 'jblock': 0x0000FF, // Blue - J piece 'sblock': 0x00FF00, // Green - S piece 'oblock': 0xFFFF00, // Yellow - O piece 'cube': 0x4a90e2, 'pyramid': 0xf5a623, 'triangle': 0x00b894, 'diamond': 0xfd79a8, 'star': 0xff6b35, 'cross': 0x1b998b, 'hexagon': 0x6c5ce7 }; var gridWidth = 24; var gridHeight = 24; var blockSize = 80; var grid = []; var stability = 100; var maxHeight = 0; var selectedShape = null; var inventory = []; // Initialize empty grid for (var row = 0; row < gridHeight; row++) { grid[row] = []; for (var col = 0; col < gridWidth; col++) { grid[row][col] = null; } } var nextShapeTimer = 0; var shapeSpawnDelay = 180; // 3 seconds at 60fps - slower initial spawn var maxFallingShapes = 1; // Start with only 1 falling shape // Create ground var ground = game.addChild(LK.getAsset('ground', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: groundY })); // Create faint grid lines for visual alignment var gridContainer = new Container(); game.addChild(gridContainer); var gameAreaLeft = (2048 - gridWidth * blockSize) / 2; var gameAreaTop = 200; // Create vertical grid lines for (var col = 0; col <= gridWidth; col++) { var vLine = LK.getAsset('gridLine', { anchorX: 0, anchorY: 0 }); vLine.x = gameAreaLeft + col * blockSize; vLine.y = gameAreaTop; vLine.width = 2; vLine.height = gridHeight * blockSize; vLine.tint = 0x444444; vLine.alpha = 0.3; gridContainer.addChild(vLine); } // Create horizontal grid lines for (var row = 0; row <= gridHeight; row++) { var hLine = LK.getAsset('gridLine', { anchorX: 0, anchorY: 0 }); hLine.x = gameAreaLeft; hLine.y = gameAreaTop + row * blockSize; hLine.width = gridWidth * blockSize; hLine.height = 2; hLine.tint = 0x444444; hLine.alpha = 0.3; gridContainer.addChild(hLine); } // Create UI elements var scoreText = new Text2('Score: 0', { size: 40, fill: 0xFFFFFF }); scoreText.anchor.set(0, 0); scoreText.x = 120; scoreText.y = 120; LK.gui.topLeft.addChild(scoreText); var stabilityText = new Text2('Stability: 100%', { size: 40, fill: 0xFFFFFF }); stabilityText.anchor.set(0, 0); stabilityText.x = 120; stabilityText.y = 180; LK.gui.topLeft.addChild(stabilityText); // Stability bar background var stabilityBarBg = LK.getAsset('stabilityBarBg', { anchorX: 0, anchorY: 0, x: 120, y: 220 }); LK.gui.topLeft.addChild(stabilityBarBg); // Stability bar var stabilityBar = LK.getAsset('stabilityBar', { anchorX: 0, anchorY: 0, x: 120, y: 220 }); LK.gui.topLeft.addChild(stabilityBar); var inventoryText = new Text2('Inventory: 0', { size: 40, fill: 0xFFFFFF }); inventoryText.anchor.set(1, 0); LK.gui.topRight.addChild(inventoryText); // Create rotation buttons var rotateLeftButton = new Text2('⟲', { size: 80, fill: 0xFFFFFF }); rotateLeftButton.anchor.set(0.5, 1); rotateLeftButton.x = -150; rotateLeftButton.y = -50; LK.gui.bottom.addChild(rotateLeftButton); var rotateRightButton = new Text2('⟳', { size: 80, fill: 0xFFFFFF }); rotateRightButton.anchor.set(0.5, 1); rotateRightButton.x = 150; rotateRightButton.y = -50; LK.gui.bottom.addChild(rotateRightButton); // Enable rotation for falling shapes while maintaining structural integrity after landing rotateLeftButton.down = function (x, y, obj) { // Find the first falling shape that can rotate for (var i = 0; i < fallingShapes.length; i++) { var shape = fallingShapes[i]; if (!shape.hasLanded) { shape.rotateLeft(); break; } } }; rotateRightButton.down = function (x, y, obj) { // Find the first falling shape that can rotate for (var i = 0; i < fallingShapes.length; i++) { var shape = fallingShapes[i]; if (!shape.hasLanded) { shape.rotate(); break; } } }; var heightText = new Text2('Height: 0', { size: 40, fill: 0xFFFFFF }); heightText.anchor.set(0, 0); heightText.x = 120; heightText.y = 260; LK.gui.topLeft.addChild(heightText); function spawnShape() { var shapeType; // 10% chance for special blocks if (Math.random() < 0.1) { shapeType = specialBlocks[Math.floor(Math.random() * specialBlocks.length)]; } else { shapeType = shapeTypes[Math.floor(Math.random() * shapeTypes.length)]; } var shape = new TetrisBlock(shapeType); fallingShapes.push(shape); game.addChild(shape); // Play soft spawn sound when shape enters from top LK.getSound('spawn').play(); } function updateInventoryDisplay() { inventoryText.setText('Inventory: ' + inventory.length); } function updateScore() { scoreText.setText('Score: ' + score); } function calculateStability() { // Calculate stability based on grid density and structure var totalBlocks = 0; var connectedBlocks = 0; var baseSupport = 0; // Count total blocks and base support for (var row = 0; row < gridHeight; row++) { for (var col = 0; col < gridWidth; col++) { if (grid[row][col]) { totalBlocks++; // Check if block has support below or is on ground if (row === gridHeight - 1 || grid[row + 1][col]) { baseSupport++; } // Check connections (adjacent blocks) var connections = 0; if (row > 0 && grid[row - 1][col]) connections++; if (row < gridHeight - 1 && grid[row + 1][col]) connections++; if (col > 0 && grid[row][col - 1]) connections++; if (col < gridWidth - 1 && grid[row][col + 1]) connections++; if (connections > 0) connectedBlocks++; } } } if (totalBlocks === 0) { stability = 100; } else { // Calculate stability percentage var supportRatio = baseSupport / totalBlocks; var connectionRatio = connectedBlocks / totalBlocks; stability = Math.max(10, Math.min(100, (supportRatio * 60 + connectionRatio * 40) * 100)); } } function updateStability() { stabilityText.setText('Stability: ' + Math.round(stability) + '%'); var stabilityPercent = Math.max(0, Math.min(1, stability / 100)); stabilityBar.width = 300 * stabilityPercent; // Color based on stability if (stability > 70) { stabilityBar.tint = 0x00FF00; // Green } else if (stability > 30) { stabilityBar.tint = 0xFFFF00; // Yellow } else { stabilityBar.tint = 0xFF0000; // Red } } function updateHeight() { var currentHeight = 0; for (var i = 0; i < placedShapes.length; i++) { var shapeBottom = placedShapes[i].y + 40; var heightFromGround = groundY - shapeBottom; if (heightFromGround > currentHeight) { currentHeight = heightFromGround; } } maxHeight = Math.max(maxHeight, currentHeight); heightText.setText('Height: ' + Math.round(maxHeight / 80)); } function applyGravity() { var hasFloatingBlocks = true; while (hasFloatingBlocks) { hasFloatingBlocks = false; // Check each grid position from bottom to top for (var row = gridHeight - 2; row >= 0; row--) { for (var col = 0; col < gridWidth; col++) { if (grid[row][col] && !grid[row + 1][col]) { // Block is floating - make it fall grid[row + 1][col] = grid[row][col]; grid[row][col] = null; hasFloatingBlocks = true; // Find and move corresponding visual block for (var i = 0; i < placedShapes.length; i++) { var block = placedShapes[i]; if (block.gridX === col && block.gridY === row) { // Stop any existing tween on this block first tween.stop(block); block.gridY = row + 1; tween(block, { y: block.gridY * blockSize + blockSize / 2 + 200 }, { duration: 150, easing: tween.easeOut }); break; } } } } } } } function checkForLines() { var linesToClear = []; // Check each row for full row completion (24 consecutive blocks from multiple shapes) for (var row = 0; row < gridHeight; row++) { var blocksInRow = 0; var shapesInRow = {}; // Count blocks and track unique shapes in this row for (var col = 0; col < gridWidth; col++) { if (grid[row][col]) { blocksInRow++; // Track which shapes contributed to this row for (var i = 0; i < placedShapes.length; i++) { var shape = placedShapes[i]; if (shape.gridY === row && shape.gridX === col) { // Use shape object reference as unique identifier var shapeId = shape.shapeType + '_' + shape.x + '_' + shape.y; shapesInRow[shapeId] = true; break; } } } } // Count unique shapes in this row var uniqueShapeCount = 0; for (var shapeId in shapesInRow) { uniqueShapeCount++; } // Only clear if entire row is filled (24 blocks) AND comes from multiple whole shapes if (blocksInRow === gridWidth && uniqueShapeCount >= 2) { linesToClear.push({ row: row, type: 'full_row', points: 100 }); } } // Clear matched lines if (linesToClear.length > 0) { clearLines(linesToClear); } } function clearLines(lines) { LK.getSound('lineClear').play(); var totalPoints = 0; // Calculate bonus for multiple rows cleared simultaneously var linesCount = lines.length; for (var i = 0; i < lines.length; i++) { totalPoints += lines[i].points || 100; } // Bonus scoring for multiple lines if (linesCount === 2) { totalPoints = totalPoints * 3; // Triple score for 2 lines } else if (linesCount === 3) { totalPoints = totalPoints * 5; // 5x score for 3 lines } else if (linesCount >= 4) { totalPoints = totalPoints * 8; // 8x score for 4+ lines (Tetris!) } score += totalPoints; linesCleared += linesCount; updateScore(); // Remove visual blocks and clear grid for (var i = 0; i < lines.length; i++) { var row = lines[i].row; // Remove visual blocks for (var j = placedShapes.length - 1; j >= 0; j--) { var block = placedShapes[j]; if (block.gridY === row) { tween(block, { alpha: 0, scaleX: 0, scaleY: 0 }, { duration: 300, onFinish: function onFinish() { block.destroy(); } }); placedShapes.splice(j, 1); } } // Clear grid row for (var col = 0; col < gridWidth; col++) { grid[row][col] = null; } } // Drop remaining blocks down LK.setTimeout(function () { dropBlocks(lines); }, 400); } function dropBlocks(clearedLines) { // Sort cleared lines from top to bottom to handle dropping correctly var sortedLines = []; for (var i = 0; i < clearedLines.length; i++) { sortedLines.push(clearedLines[i].row); } sortedLines.sort(function (a, b) { return a - b; }); // Process from bottom to top for (var row = gridHeight - 1; row >= 0; row--) { var dropDistance = 0; // Count how many cleared lines are below this row for (var i = 0; i < sortedLines.length; i++) { if (sortedLines[i] > row) { dropDistance++; } } if (dropDistance > 0) { // Move grid data for (var col = 0; col < gridWidth; col++) { if (grid[row][col]) { grid[row + dropDistance][col] = grid[row][col]; grid[row][col] = null; } } // Move visual blocks for (var j = 0; j < placedShapes.length; j++) { var block = placedShapes[j]; if (block.gridY === row) { block.gridY += dropDistance; tween(block, { y: block.gridY * blockSize + blockSize / 2 + 200 }, { duration: 200, easing: tween.easeOut }); } } } } } function handleSpecialBlock(type, gridX, gridY) { if (type === 'gift') { score += 50; updateScore(); LK.effects.flashObject(game, 0xffd700, 500); } else if (type === 'bomb') { LK.getSound('bomb').play(); // Destroy blocks in 3x3 area for (var row = Math.max(0, gridY - 1); row <= Math.min(gridHeight - 1, gridY + 1); row++) { for (var col = Math.max(0, gridX - 1); col <= Math.min(gridWidth - 1, gridX + 1); col++) { if (grid[row][col]) { grid[row][col] = null; // Remove visual block for (var i = placedShapes.length - 1; i >= 0; i--) { var block = placedShapes[i]; if (block.gridX === col && block.gridY === row) { tween(block, { alpha: 0, scaleX: 2, scaleY: 2 }, { duration: 300, onFinish: function onFinish() { block.destroy(); } }); placedShapes.splice(i, 1); break; } } } } } } else if (type === 'wildcard') { // Fill nearest gap var nearestGap = findNearestGap(gridX, gridY); if (nearestGap) { grid[nearestGap.row][nearestGap.col] = 'cube'; var wildBlock = new GridBlock('cube'); wildBlock.gridX = nearestGap.col; wildBlock.gridY = nearestGap.row; wildBlock.x = nearestGap.col * blockSize + blockSize / 2 + (2048 - gridWidth * blockSize) / 2; wildBlock.y = nearestGap.row * blockSize + blockSize / 2 + 200; placedShapes.push(wildBlock); game.addChild(wildBlock); tween(wildBlock, { scaleX: 1.5, scaleY: 1.5 }, { duration: 200, easing: tween.bounceOut, onFinish: function onFinish() { tween(wildBlock, { scaleX: 1, scaleY: 1 }, { duration: 100 }); } }); } } // Trigger stability and height recalculation after special block effects calculateStability(); updateStability(); applyGravity(); checkForLines(); } function findNearestGap(startX, startY) { var searchRadius = 1; while (searchRadius < Math.max(gridWidth, gridHeight)) { for (var row = Math.max(0, startY - searchRadius); row <= Math.min(gridHeight - 1, startY + searchRadius); row++) { for (var col = Math.max(0, startX - searchRadius); col <= Math.min(gridWidth - 1, startX + searchRadius); col++) { if (!grid[row][col]) { return { row: row, col: col }; } } } searchRadius++; } return null; } function triggerCollapse() { LK.getSound('collapse').play(); // Remove unstable shapes for (var i = placedShapes.length - 1; i >= 0; i--) { if (Math.random() < 0.3) { // 30% chance each shape collapses var shape = placedShapes[i]; tween(shape, { alpha: 0, y: shape.y + 100 }, { duration: 500, onFinish: function onFinish() { shape.destroy(); } }); placedShapes.splice(i, 1); } } score = Math.max(0, score - 50); updateScore(); } function placeShape(shape, x, y) { if (inventory.length === 0) return; var inventoryShape = inventory.shift(); var placedShape = new PlacedShape(inventoryShape.shapeType); placedShape.x = x; placedShape.y = y; // Start with smaller scale for placement animation placedShape.scaleX = 0.5; placedShape.scaleY = 0.5; placedShape.alpha = 0.7; placedShapes.push(placedShape); game.addChild(placedShape); // Animate placement with smooth scaling tween(placedShape, { scaleX: 1, scaleY: 1, alpha: 1 }, { duration: 300, easing: tween.elasticOut }); LK.getSound('place').play(); score += 10; // Bonus for good placement var centerX = 1024; var distanceFromCenter = Math.abs(x - centerX); if (distanceFromCenter < 100) { score += 5; // Bonus for center placement // Add bonus glow effect for center placement tween(placedShape, { tint: 0xffff00 }, { duration: 500, onFinish: function onFinish() { tween(placedShape, { tint: 0xffffff }, { duration: 500 }); } }); } updateScore(); updateInventoryDisplay(); calculateStability(); updateHeight(); inventoryShape.destroy(); } // Event handlers game.down = function (x, y, obj) { // Touch handling is now done directly by TetrisBlock shapes // This ensures only the touched shape responds }; game.move = function (x, y, obj) { // Handle dragging of selected shape with precise 1:1 movement mapping // Prevent movement of landed shapes to maintain structural stability until row completion if (selectedShape && selectedShape.isDragging && !selectedShape.hasLanded) { var gameAreaLeft = (2048 - gridWidth * blockSize) / 2; var gameAreaRight = gameAreaLeft + gridWidth * blockSize; // Use direct finger position for movement - no offset calculation // This creates 1:1 mapping between finger movement and shape movement var clampedX = Math.max(gameAreaLeft + blockSize / 2, Math.min(gameAreaRight - blockSize / 2, x)); var targetGridX = Math.floor((clampedX - gameAreaLeft) / blockSize); targetGridX = Math.max(0, Math.min(gridWidth - 1, targetGridX)); // Move to new position only if it's actually different if (targetGridX !== selectedShape.gridX) { selectedShape.moveToPosition(targetGridX); } } }; game.up = function (x, y, obj) { // Touch release is now handled directly by TetrisBlock shapes // This ensures proper cleanup when touch ends }; game.update = function () { // One-at-a-time spawn rule: only spawn when no shapes are falling nextShapeTimer++; if (nextShapeTimer >= shapeSpawnDelay && fallingShapes.length === 0) { spawnShape(); nextShapeTimer = 0; } // Apply gravity to prevent floating blocks if (LK.ticks % 20 === 0) { applyGravity(); } // Update stability periodically if (LK.ticks % 30 === 0) { calculateStability(); updateStability(); } // Check win condition if (maxHeight > 800) { // About 10 blocks high LK.showYouWin(); } // Check game over condition - stack overflow (blocked spawn area) var spawnAreaBlocked = false; // Check if spawn area (top 3 rows) has any blocks that would prevent new shapes for (var row = 0; row < 3; row++) { for (var col = 0; col < gridWidth; col++) { if (grid[row][col]) { spawnAreaBlocked = true; break; } } if (spawnAreaBlocked) break; } // Game over if spawn area is blocked or stability is too low if (spawnAreaBlocked || stability < 10 && placedShapes.length > 0) { LK.showGameOver(); } }; // Initialize display updateScore(); updateStability(); updateInventoryDisplay(); updateHeight();
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var GridBlock = Container.expand(function (shapeType) {
var self = Container.call(this);
self.shapeType = shapeType;
self.color = shapeColors[shapeType] || 0xffffff;
self.gridX = 0;
self.gridY = 0;
var shapeGraphic = self.attachAsset(shapeType, {
anchorX: 0.5,
anchorY: 0.5
});
// Add bomb fuse for bomb blocks
if (shapeType === 'bomb') {
var fuseGraphic = self.attachAsset('bombFuse', {
anchorX: 0.5,
anchorY: 1,
x: 0,
y: -25
});
// Animate fuse flickering
var _fuseFlicker = function fuseFlicker() {
tween(fuseGraphic, {
alpha: 0.3
}, {
duration: 200,
onFinish: function onFinish() {
tween(fuseGraphic, {
alpha: 1
}, {
duration: 200,
onFinish: _fuseFlicker
});
}
});
};
_fuseFlicker();
}
return self;
});
var PlacedShape = Container.expand(function (shapeType) {
var self = Container.call(this);
self.shapeType = shapeType;
self.isStable = true;
self.isDragging = false;
var shapeGraphic = self.attachAsset(shapeType, {
anchorX: 0.5,
anchorY: 0.5
});
self.rotate = function () {
// Smooth rotation animation
tween(shapeGraphic, {
rotation: shapeGraphic.rotation + Math.PI / 2
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
calculateStability();
updateStability();
}
});
};
self.down = function (x, y, obj) {
// Only set as selected if no other shape is already being dragged
if (!selectedShape || !selectedShape.isDragging) {
self.isDragging = true;
selectedShape = self;
}
};
self.up = function (x, y, obj) {
self.isDragging = false;
if (selectedShape === self) {
selectedShape = null;
}
calculateStability();
updateStability();
};
return self;
});
var TetrisBlock = Container.expand(function (shapeType) {
var self = Container.call(this);
self.shapeType = shapeType;
self.fallSpeed = Math.random() < 0.3 ? 3.5 : 2.5; // Increased by ~66% for more dynamic gameplay
self.isSpecial = specialBlocks.indexOf(shapeType) !== -1;
self.gridX = Math.floor(Math.random() * (gridWidth - 3)); // Ensure space for shape
self.gridY = 0;
self.lastMoveTime = 0;
self.targetX = 0; // For smooth movement
self.isMoving = false; // Track if block is currently moving
self.rotation = 0; // Track current rotation state (0, 1, 2, 3)
self.blocks = []; // Array to hold individual block graphics
self.hasLanded = false; // Track if shape has touched ground or another block
// Rotation configurations for each shape
self.rotationConfigs = {
'iblock': [[[-1, 0], [0, 0], [1, 0], [2, 0]],
// horizontal
[[0, -1], [0, 0], [0, 1], [0, 2]],
// vertical
[[-1, 0], [0, 0], [1, 0], [2, 0]],
// horizontal
[[0, -1], [0, 0], [0, 1], [0, 2]] // vertical
],
'oblock': [[[0, 0], [1, 0], [0, 1], [1, 1]],
// same for all rotations
[[0, 0], [1, 0], [0, 1], [1, 1]], [[0, 0], [1, 0], [0, 1], [1, 1]], [[0, 0], [1, 0], [0, 1], [1, 1]]],
'tblock': [[[-1, 0], [0, 0], [1, 0], [0, 1]],
// T pointing down
[[0, -1], [0, 0], [1, 0], [0, 1]],
// T pointing left
[[-1, 0], [0, 0], [1, 0], [0, -1]],
// T pointing up
[[0, -1], [-1, 0], [0, 0], [0, 1]] // T pointing right
],
'lblock': [[[-1, 0], [0, 0], [1, 0], [1, 1]],
// L standard
[[0, -1], [0, 0], [0, 1], [-1, 1]],
// L rotated 90
[[-1, -1], [-1, 0], [0, 0], [1, 0]],
// L rotated 180
[[0, -1], [0, 0], [0, 1], [1, -1]] // L rotated 270
],
'jblock': [[[-1, 0], [0, 0], [1, 0], [-1, 1]],
// J standard
[[0, -1], [0, 0], [0, 1], [1, 1]],
// J rotated 90
[[-1, 0], [0, 0], [1, 0], [1, -1]],
// J rotated 180
[[0, -1], [-1, -1], [0, 0], [0, 1]] // J rotated 270
],
'sblock': [[[-1, 1], [0, 1], [0, 0], [1, 0]],
// S horizontal
[[0, -1], [0, 0], [1, 0], [1, 1]],
// S vertical
[[-1, 1], [0, 1], [0, 0], [1, 0]],
// S horizontal
[[0, -1], [0, 0], [1, 0], [1, 1]] // S vertical
],
'zblock': [[[-1, 0], [0, 0], [0, 1], [1, 1]],
// Z horizontal
[[1, -1], [1, 0], [0, 0], [0, 1]],
// Z vertical
[[-1, 0], [0, 0], [0, 1], [1, 1]],
// Z horizontal
[[1, -1], [1, 0], [0, 0], [0, 1]] // Z vertical
]
};
// Define classic Tetris shape configurations with proper proportions
var shapeConfigs = {
'iblock': [[-1, 0], [0, 0], [1, 0], [2, 0] // I-piece: centered horizontal line
],
'oblock': [[0, 0], [1, 0], [0, 1], [1, 1] // O-piece: 2x2 square
],
'tblock': [[-1, 0], [0, 0], [1, 0], [0, 1] // T-piece: centered with stem down
],
'lblock': [[-1, 0], [0, 0], [1, 0], [1, 1] // L-piece: centered with corner
],
'jblock': [[-1, 0], [0, 0], [1, 0], [-1, 1] // J-piece: centered with corner
],
'sblock': [[-1, 1], [0, 1], [0, 0], [1, 0] // S-piece: classic S shape
],
'zblock': [[-1, 0], [0, 0], [0, 1], [1, 1] // Z-piece: classic Z shape
]
};
// Get shape configuration or default to single block
var config = shapeConfigs[shapeType] || [[0, 0]];
// Store current shape configuration after config is defined
self.currentConfig = self.rotationConfigs[shapeType] && self.rotationConfigs[shapeType][0] ? self.rotationConfigs[shapeType][0] : config;
var shapeColor = shapeColors[shapeType] || 0x4a90e2;
// Create a single group container for all blocks
self.blockGroup = new Container();
self.addChild(self.blockGroup);
// Create individual blocks within the group
for (var i = 0; i < config.length; i++) {
var blockGraphic = self.blockGroup.attachAsset(shapeType, {
anchorX: 0.5,
anchorY: 0.5
});
// Position block relative to group center
blockGraphic.x = config[i][0] * blockSize;
blockGraphic.y = config[i][1] * blockSize;
// Add subtle outline for better visibility
blockGraphic.alpha = 0.9;
// Completely disable interactivity on individual blocks to prevent structural changes
blockGraphic.interactive = false;
blockGraphic.buttonMode = false;
// Prevent any touch events from reaching individual blocks
blockGraphic.down = null;
blockGraphic.up = null;
blockGraphic.move = null;
self.blocks.push({
graphic: blockGraphic,
offsetX: config[i][0],
offsetY: config[i][1]
});
}
// Calculate proper pivot point based on shape bounds for centered rotation
var minX = 0,
maxX = 0,
minY = 0,
maxY = 0;
for (var i = 0; i < config.length; i++) {
minX = Math.min(minX, config[i][0]);
maxX = Math.max(maxX, config[i][0]);
minY = Math.min(minY, config[i][1]);
maxY = Math.max(maxY, config[i][1]);
}
var centerX = (minX + maxX) * blockSize / 2;
var centerY = (minY + maxY) * blockSize / 2;
self.blockGroup.pivot.set(centerX, centerY);
// Add bomb fuse for bomb blocks
if (shapeType === 'bomb') {
var fuseGraphic = self.attachAsset('bombFuse', {
anchorX: 0.5,
anchorY: 1,
x: 0,
y: -25
});
// Animate fuse flickering
var _fuseFlicker2 = function fuseFlicker() {
tween(fuseGraphic, {
alpha: 0.3
}, {
duration: 200,
onFinish: function onFinish() {
tween(fuseGraphic, {
alpha: 1
}, {
duration: 200,
onFinish: _fuseFlicker2
});
}
});
};
_fuseFlicker2();
}
// Position on grid
self.x = self.gridX * blockSize + blockSize / 2 + (2048 - gridWidth * blockSize) / 2;
self.y = self.gridY * blockSize + blockSize / 2 + 200;
self.targetX = self.x; // Initialize target position
self.checkCollision = function (newGridX, newGridY, configToCheck) {
// Use provided configuration or current configuration
var config = configToCheck || self.currentConfig;
// Check collision for each block in the configuration
for (var i = 0; i < config.length; i++) {
var blockX = newGridX + config[i][0];
var blockY = newGridY + config[i][1];
// Check bounds
if (blockX < 0 || blockX >= gridWidth || blockY < 0 || blockY >= gridHeight) {
return true;
}
// Check if position is occupied in grid
if (blockY >= 0 && blockY < gridHeight && grid[blockY] && grid[blockY][blockX]) {
return true;
}
// Check collision with other falling shapes
for (var j = 0; j < fallingShapes.length; j++) {
var otherShape = fallingShapes[j];
if (otherShape !== self) {
for (var k = 0; k < otherShape.blocks.length; k++) {
var otherBlock = otherShape.blocks[k];
var otherBlockX = otherShape.gridX + otherBlock.offsetX;
var otherBlockY = otherShape.gridY + otherBlock.offsetY;
if (otherBlockX === blockX && otherBlockY === blockY) {
return true;
}
}
}
}
}
return false;
};
self.moveToPosition = function (newGridX) {
// Track that the shape has moved (for distinguishing tap from drag)
if (Math.abs(newGridX - self.gridX) > 0) {
self.hasMoved = true;
}
// Clamp to grid boundaries based on current shape configuration
var minX = 0;
var maxX = 0;
for (var i = 0; i < self.currentConfig.length; i++) {
var blockX = self.currentConfig[i][0];
minX = Math.min(minX, blockX);
maxX = Math.max(maxX, blockX);
}
// Clamp position to keep all blocks within grid bounds
newGridX = Math.max(-minX, Math.min(gridWidth - 1 - maxX, newGridX));
// Allow movement even if there might be collision - let player move freely while dragging
self.gridX = newGridX;
self.targetX = self.gridX * blockSize + blockSize / 2 + (2048 - gridWidth * blockSize) / 2;
self.isMoving = true;
// Stop any existing movement tween
tween.stop(self, {
x: true
});
// Instant movement for maximum responsiveness when dragging
if (self.isDragging) {
self.x = self.targetX;
self.isMoving = false;
} else {
// Ultra-fast responsive movement for non-dragging cases
tween(self, {
x: self.targetX
}, {
duration: 50,
easing: tween.easeOut,
onFinish: function onFinish() {
self.isMoving = false;
}
});
}
};
self.update = function () {
var currentTime = LK.ticks;
if (currentTime - self.lastMoveTime > 80 / self.fallSpeed) {
self.lastMoveTime = currentTime;
// Check if can move down using collision detection
var canMoveDown = !self.checkCollision(self.gridX, self.gridY + 1);
if (canMoveDown) {
// Play light drop sound for step-down movement
LK.getSound('drop').play();
self.gridY++;
self.y = self.gridY * blockSize + blockSize / 2 + 200;
} else {
// Check if touching another shape (not ground) for contact sound
var touchingShape = false;
for (var j = 0; j < fallingShapes.length; j++) {
var otherShape = fallingShapes[j];
if (otherShape !== self && self.checkCollision(self.gridX, self.gridY + 1)) {
touchingShape = true;
break;
}
}
// Check if there are blocks below in grid
var touchingGrid = false;
for (var i = 0; i < self.currentConfig.length; i++) {
var blockX = self.gridX + self.currentConfig[i][0];
var blockY = self.gridY + self.currentConfig[i][1] + 1;
if (blockY < gridHeight && blockY >= 0 && grid[blockY] && grid[blockY][blockX]) {
touchingGrid = true;
break;
}
}
// Play appropriate sound
if (touchingShape || touchingGrid) {
if (self.gridY + 1 >= gridHeight) {
// Landing on ground
LK.getSound('land').play();
} else {
// Contact with another shape
LK.getSound('contact').play();
}
}
// Mark as landed when it can't move down
self.hasLanded = true;
// Only place if we're in a valid position and can't move down
if (self.gridY >= 0) {
self.placeInGrid();
} else {
// If stuck above grid, try to find a valid position
var foundValidPosition = false;
for (var testY = 0; testY < gridHeight; testY++) {
if (!self.checkCollision(self.gridX, testY)) {
self.gridY = testY;
self.y = self.gridY * blockSize + blockSize / 2 + 200;
foundValidPosition = true;
break;
}
}
if (!foundValidPosition) {
// Stop any existing tweens before destroying
tween.stop(self);
tween.stop(self.blockGroup);
for (var i = 0; i < self.blocks.length; i++) {
tween.stop(self.blocks[i].graphic);
}
// Remove stuck shape
var index = fallingShapes.indexOf(self);
if (index > -1) {
fallingShapes.splice(index, 1);
}
self.destroy();
}
}
}
}
};
self.placeInGrid = function () {
var allBlocksValid = true;
// Check if all blocks can be placed
for (var i = 0; i < self.currentConfig.length; i++) {
var blockX = self.gridX + self.currentConfig[i][0];
var blockY = self.gridY + self.currentConfig[i][1];
if (blockY < 0 || blockY >= gridHeight || blockX < 0 || blockX >= gridWidth) {
allBlocksValid = false;
break;
}
}
if (allBlocksValid) {
// Freeze the group before disassembly to maintain structural stability
self.blockGroup.rotation = Math.round(self.blockGroup.rotation / (Math.PI / 2)) * (Math.PI / 2);
// Place all blocks in grid - disassemble the group into static tiles
for (var i = 0; i < self.currentConfig.length; i++) {
var blockX = self.gridX + self.currentConfig[i][0];
var blockY = self.gridY + self.currentConfig[i][1];
grid[blockY][blockX] = self.shapeType;
// Create visual block in grid (static tile)
var placedBlock = new GridBlock(self.shapeType);
placedBlock.gridX = blockX;
placedBlock.gridY = blockY;
placedBlock.x = blockX * blockSize + blockSize / 2 + (2048 - gridWidth * blockSize) / 2;
placedBlock.y = blockY * blockSize + blockSize / 2 + 200;
// Apply the same rotation as the group had
placedBlock.rotation = self.blockGroup.rotation;
placedShapes.push(placedBlock);
game.addChild(placedBlock);
}
LK.getSound('place').play();
score += 10 * self.blocks.length;
updateScore();
checkForLines();
// Handle special blocks
if (self.isSpecial) {
handleSpecialBlock(self.shapeType, self.gridX, self.gridY);
}
}
// Stop any existing tweens before destroying
tween.stop(self);
tween.stop(self.blockGroup);
for (var i = 0; i < self.blocks.length; i++) {
tween.stop(self.blocks[i].graphic);
}
// Remove from falling shapes
var index = fallingShapes.indexOf(self);
if (index > -1) {
fallingShapes.splice(index, 1);
}
self.destroy();
// Check game over
if (self.gridY <= 1) {
LK.showGameOver();
}
};
self.rotate = function () {
// Only allow rotation if shape hasn't landed to maintain structural stability
if (self.hasLanded) return;
// Get rotation configurations for this shape type
if (!self.rotationConfigs[self.shapeType]) return;
// Calculate next rotation state
var nextRotation = (self.rotation + 1) % 4;
var nextConfig = self.rotationConfigs[self.shapeType][nextRotation];
// Check if rotation is valid (no collision)
if (!self.checkCollision(self.gridX, self.gridY, nextConfig)) {
// Update rotation state and configuration
self.rotation = nextRotation;
self.currentConfig = nextConfig;
// Update pivot point for proper centered rotation
var minX = 0,
maxX = 0,
minY = 0,
maxY = 0;
for (var i = 0; i < nextConfig.length; i++) {
minX = Math.min(minX, nextConfig[i][0]);
maxX = Math.max(maxX, nextConfig[i][0]);
minY = Math.min(minY, nextConfig[i][1]);
maxY = Math.max(maxY, nextConfig[i][1]);
}
var centerX = (minX + maxX) * blockSize / 2;
var centerY = (minY + maxY) * blockSize / 2;
self.blockGroup.pivot.set(centerX, centerY);
// Update block positions based on new configuration
for (var i = 0; i < self.blocks.length; i++) {
if (nextConfig[i]) {
self.blocks[i].graphic.x = nextConfig[i][0] * blockSize;
self.blocks[i].graphic.y = nextConfig[i][1] * blockSize;
self.blocks[i].offsetX = nextConfig[i][0];
self.blocks[i].offsetY = nextConfig[i][1];
}
}
// Play rotation sound effect
LK.getSound('rotate').play();
// Animate rotation on the block group with responsive timing
tween(self.blockGroup, {
rotation: self.blockGroup.rotation + Math.PI / 2
}, {
duration: 150,
easing: tween.easeOut
});
}
};
self.rotateLeft = function () {
// Only allow rotation if shape hasn't landed to maintain structural stability
if (self.hasLanded) return;
// Get rotation configurations for this shape type
if (!self.rotationConfigs[self.shapeType]) return;
// Calculate previous rotation state (counter-clockwise)
var nextRotation = (self.rotation + 3) % 4; // +3 is same as -1 in mod 4
var nextConfig = self.rotationConfigs[self.shapeType][nextRotation];
// Check if rotation is valid (no collision)
if (!self.checkCollision(self.gridX, self.gridY, nextConfig)) {
// Update rotation state and configuration
self.rotation = nextRotation;
self.currentConfig = nextConfig;
// Update pivot point for proper centered rotation
var minX = 0,
maxX = 0,
minY = 0,
maxY = 0;
for (var i = 0; i < nextConfig.length; i++) {
minX = Math.min(minX, nextConfig[i][0]);
maxX = Math.max(maxX, nextConfig[i][0]);
minY = Math.min(minY, nextConfig[i][1]);
maxY = Math.max(maxY, nextConfig[i][1]);
}
var centerX = (minX + maxX) * blockSize / 2;
var centerY = (minY + maxY) * blockSize / 2;
self.blockGroup.pivot.set(centerX, centerY);
// Update block positions based on new configuration
for (var i = 0; i < self.blocks.length; i++) {
if (nextConfig[i]) {
self.blocks[i].graphic.x = nextConfig[i][0] * blockSize;
self.blocks[i].graphic.y = nextConfig[i][1] * blockSize;
self.blocks[i].offsetX = nextConfig[i][0];
self.blocks[i].offsetY = nextConfig[i][1];
}
}
// Play rotation sound effect
LK.getSound('rotate').play();
// Animate rotation on the block group with responsive timing (counter-clockwise)
tween(self.blockGroup, {
rotation: self.blockGroup.rotation - Math.PI / 2
}, {
duration: 150,
easing: tween.easeOut
});
}
};
self.down = function (x, y, obj) {
// Ignore touches on landed shapes to maintain structural stability until row completion
if (self.hasLanded) return;
// Store touch start time for tap detection
self.touchStartTime = LK.ticks;
// Clear any existing selection first
if (selectedShape && selectedShape !== self) {
// Reset previous shape
tween(selectedShape, {
scaleX: 1,
scaleY: 1,
alpha: 1
}, {
duration: 100
});
selectedShape.isDragging = false;
}
// Immediately select this shape when touched
selectedShape = self;
selectedShape.isDragging = true;
// No touch offset - use direct positioning for 1:1 movement
// Immediate visual feedback on the group
tween(self.blockGroup, {
scaleX: 1.1,
scaleY: 1.1,
alpha: 0.8
}, {
duration: 100,
easing: tween.easeOut
});
};
self.up = function (x, y, obj) {
// Handle tap-to-rotate for falling shapes (maintain structural stability after landing)
if (selectedShape === self && !self.hasLanded) {
// Check if this was a tap (not a drag) and shape hasn't moved
var touchDuration = LK.ticks - (self.touchStartTime || 0);
if (!self.hasMoved && touchDuration < 30) {
// Quick tap under 0.5 seconds
self.rotate();
}
selectedShape.isDragging = false;
selectedShape = null;
self.hasMoved = false; // Reset movement flag
// Reset visual feedback on the group
tween(self.blockGroup, {
scaleX: 1,
scaleY: 1,
alpha: 1
}, {
duration: 150,
easing: tween.easeOut
});
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x87CEEB
});
/****
* Game Code
****/
var fallingShapes = [];
var placedShapes = [];
var score = 0;
var linesCleared = 0;
var level = 1;
var groundY = 2400;
var shapeTypes = ['lblock', 'tblock', 'zblock', 'iblock', 'jblock', 'sblock', 'oblock'];
var specialBlocks = ['gift', 'bomb', 'wildcard'];
var shapeColors = {
'lblock': 0xFF6600,
// Orange - L piece
'tblock': 0x9900FF,
// Purple - T piece
'zblock': 0xFF0000,
// Red - Z piece
'iblock': 0x00FFFF,
// Cyan - I piece
'jblock': 0x0000FF,
// Blue - J piece
'sblock': 0x00FF00,
// Green - S piece
'oblock': 0xFFFF00,
// Yellow - O piece
'cube': 0x4a90e2,
'pyramid': 0xf5a623,
'triangle': 0x00b894,
'diamond': 0xfd79a8,
'star': 0xff6b35,
'cross': 0x1b998b,
'hexagon': 0x6c5ce7
};
var gridWidth = 24;
var gridHeight = 24;
var blockSize = 80;
var grid = [];
var stability = 100;
var maxHeight = 0;
var selectedShape = null;
var inventory = [];
// Initialize empty grid
for (var row = 0; row < gridHeight; row++) {
grid[row] = [];
for (var col = 0; col < gridWidth; col++) {
grid[row][col] = null;
}
}
var nextShapeTimer = 0;
var shapeSpawnDelay = 180; // 3 seconds at 60fps - slower initial spawn
var maxFallingShapes = 1; // Start with only 1 falling shape
// Create ground
var ground = game.addChild(LK.getAsset('ground', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: groundY
}));
// Create faint grid lines for visual alignment
var gridContainer = new Container();
game.addChild(gridContainer);
var gameAreaLeft = (2048 - gridWidth * blockSize) / 2;
var gameAreaTop = 200;
// Create vertical grid lines
for (var col = 0; col <= gridWidth; col++) {
var vLine = LK.getAsset('gridLine', {
anchorX: 0,
anchorY: 0
});
vLine.x = gameAreaLeft + col * blockSize;
vLine.y = gameAreaTop;
vLine.width = 2;
vLine.height = gridHeight * blockSize;
vLine.tint = 0x444444;
vLine.alpha = 0.3;
gridContainer.addChild(vLine);
}
// Create horizontal grid lines
for (var row = 0; row <= gridHeight; row++) {
var hLine = LK.getAsset('gridLine', {
anchorX: 0,
anchorY: 0
});
hLine.x = gameAreaLeft;
hLine.y = gameAreaTop + row * blockSize;
hLine.width = gridWidth * blockSize;
hLine.height = 2;
hLine.tint = 0x444444;
hLine.alpha = 0.3;
gridContainer.addChild(hLine);
}
// Create UI elements
var scoreText = new Text2('Score: 0', {
size: 40,
fill: 0xFFFFFF
});
scoreText.anchor.set(0, 0);
scoreText.x = 120;
scoreText.y = 120;
LK.gui.topLeft.addChild(scoreText);
var stabilityText = new Text2('Stability: 100%', {
size: 40,
fill: 0xFFFFFF
});
stabilityText.anchor.set(0, 0);
stabilityText.x = 120;
stabilityText.y = 180;
LK.gui.topLeft.addChild(stabilityText);
// Stability bar background
var stabilityBarBg = LK.getAsset('stabilityBarBg', {
anchorX: 0,
anchorY: 0,
x: 120,
y: 220
});
LK.gui.topLeft.addChild(stabilityBarBg);
// Stability bar
var stabilityBar = LK.getAsset('stabilityBar', {
anchorX: 0,
anchorY: 0,
x: 120,
y: 220
});
LK.gui.topLeft.addChild(stabilityBar);
var inventoryText = new Text2('Inventory: 0', {
size: 40,
fill: 0xFFFFFF
});
inventoryText.anchor.set(1, 0);
LK.gui.topRight.addChild(inventoryText);
// Create rotation buttons
var rotateLeftButton = new Text2('⟲', {
size: 80,
fill: 0xFFFFFF
});
rotateLeftButton.anchor.set(0.5, 1);
rotateLeftButton.x = -150;
rotateLeftButton.y = -50;
LK.gui.bottom.addChild(rotateLeftButton);
var rotateRightButton = new Text2('⟳', {
size: 80,
fill: 0xFFFFFF
});
rotateRightButton.anchor.set(0.5, 1);
rotateRightButton.x = 150;
rotateRightButton.y = -50;
LK.gui.bottom.addChild(rotateRightButton);
// Enable rotation for falling shapes while maintaining structural integrity after landing
rotateLeftButton.down = function (x, y, obj) {
// Find the first falling shape that can rotate
for (var i = 0; i < fallingShapes.length; i++) {
var shape = fallingShapes[i];
if (!shape.hasLanded) {
shape.rotateLeft();
break;
}
}
};
rotateRightButton.down = function (x, y, obj) {
// Find the first falling shape that can rotate
for (var i = 0; i < fallingShapes.length; i++) {
var shape = fallingShapes[i];
if (!shape.hasLanded) {
shape.rotate();
break;
}
}
};
var heightText = new Text2('Height: 0', {
size: 40,
fill: 0xFFFFFF
});
heightText.anchor.set(0, 0);
heightText.x = 120;
heightText.y = 260;
LK.gui.topLeft.addChild(heightText);
function spawnShape() {
var shapeType;
// 10% chance for special blocks
if (Math.random() < 0.1) {
shapeType = specialBlocks[Math.floor(Math.random() * specialBlocks.length)];
} else {
shapeType = shapeTypes[Math.floor(Math.random() * shapeTypes.length)];
}
var shape = new TetrisBlock(shapeType);
fallingShapes.push(shape);
game.addChild(shape);
// Play soft spawn sound when shape enters from top
LK.getSound('spawn').play();
}
function updateInventoryDisplay() {
inventoryText.setText('Inventory: ' + inventory.length);
}
function updateScore() {
scoreText.setText('Score: ' + score);
}
function calculateStability() {
// Calculate stability based on grid density and structure
var totalBlocks = 0;
var connectedBlocks = 0;
var baseSupport = 0;
// Count total blocks and base support
for (var row = 0; row < gridHeight; row++) {
for (var col = 0; col < gridWidth; col++) {
if (grid[row][col]) {
totalBlocks++;
// Check if block has support below or is on ground
if (row === gridHeight - 1 || grid[row + 1][col]) {
baseSupport++;
}
// Check connections (adjacent blocks)
var connections = 0;
if (row > 0 && grid[row - 1][col]) connections++;
if (row < gridHeight - 1 && grid[row + 1][col]) connections++;
if (col > 0 && grid[row][col - 1]) connections++;
if (col < gridWidth - 1 && grid[row][col + 1]) connections++;
if (connections > 0) connectedBlocks++;
}
}
}
if (totalBlocks === 0) {
stability = 100;
} else {
// Calculate stability percentage
var supportRatio = baseSupport / totalBlocks;
var connectionRatio = connectedBlocks / totalBlocks;
stability = Math.max(10, Math.min(100, (supportRatio * 60 + connectionRatio * 40) * 100));
}
}
function updateStability() {
stabilityText.setText('Stability: ' + Math.round(stability) + '%');
var stabilityPercent = Math.max(0, Math.min(1, stability / 100));
stabilityBar.width = 300 * stabilityPercent;
// Color based on stability
if (stability > 70) {
stabilityBar.tint = 0x00FF00; // Green
} else if (stability > 30) {
stabilityBar.tint = 0xFFFF00; // Yellow
} else {
stabilityBar.tint = 0xFF0000; // Red
}
}
function updateHeight() {
var currentHeight = 0;
for (var i = 0; i < placedShapes.length; i++) {
var shapeBottom = placedShapes[i].y + 40;
var heightFromGround = groundY - shapeBottom;
if (heightFromGround > currentHeight) {
currentHeight = heightFromGround;
}
}
maxHeight = Math.max(maxHeight, currentHeight);
heightText.setText('Height: ' + Math.round(maxHeight / 80));
}
function applyGravity() {
var hasFloatingBlocks = true;
while (hasFloatingBlocks) {
hasFloatingBlocks = false;
// Check each grid position from bottom to top
for (var row = gridHeight - 2; row >= 0; row--) {
for (var col = 0; col < gridWidth; col++) {
if (grid[row][col] && !grid[row + 1][col]) {
// Block is floating - make it fall
grid[row + 1][col] = grid[row][col];
grid[row][col] = null;
hasFloatingBlocks = true;
// Find and move corresponding visual block
for (var i = 0; i < placedShapes.length; i++) {
var block = placedShapes[i];
if (block.gridX === col && block.gridY === row) {
// Stop any existing tween on this block first
tween.stop(block);
block.gridY = row + 1;
tween(block, {
y: block.gridY * blockSize + blockSize / 2 + 200
}, {
duration: 150,
easing: tween.easeOut
});
break;
}
}
}
}
}
}
}
function checkForLines() {
var linesToClear = [];
// Check each row for full row completion (24 consecutive blocks from multiple shapes)
for (var row = 0; row < gridHeight; row++) {
var blocksInRow = 0;
var shapesInRow = {};
// Count blocks and track unique shapes in this row
for (var col = 0; col < gridWidth; col++) {
if (grid[row][col]) {
blocksInRow++;
// Track which shapes contributed to this row
for (var i = 0; i < placedShapes.length; i++) {
var shape = placedShapes[i];
if (shape.gridY === row && shape.gridX === col) {
// Use shape object reference as unique identifier
var shapeId = shape.shapeType + '_' + shape.x + '_' + shape.y;
shapesInRow[shapeId] = true;
break;
}
}
}
}
// Count unique shapes in this row
var uniqueShapeCount = 0;
for (var shapeId in shapesInRow) {
uniqueShapeCount++;
}
// Only clear if entire row is filled (24 blocks) AND comes from multiple whole shapes
if (blocksInRow === gridWidth && uniqueShapeCount >= 2) {
linesToClear.push({
row: row,
type: 'full_row',
points: 100
});
}
}
// Clear matched lines
if (linesToClear.length > 0) {
clearLines(linesToClear);
}
}
function clearLines(lines) {
LK.getSound('lineClear').play();
var totalPoints = 0;
// Calculate bonus for multiple rows cleared simultaneously
var linesCount = lines.length;
for (var i = 0; i < lines.length; i++) {
totalPoints += lines[i].points || 100;
}
// Bonus scoring for multiple lines
if (linesCount === 2) {
totalPoints = totalPoints * 3; // Triple score for 2 lines
} else if (linesCount === 3) {
totalPoints = totalPoints * 5; // 5x score for 3 lines
} else if (linesCount >= 4) {
totalPoints = totalPoints * 8; // 8x score for 4+ lines (Tetris!)
}
score += totalPoints;
linesCleared += linesCount;
updateScore();
// Remove visual blocks and clear grid
for (var i = 0; i < lines.length; i++) {
var row = lines[i].row;
// Remove visual blocks
for (var j = placedShapes.length - 1; j >= 0; j--) {
var block = placedShapes[j];
if (block.gridY === row) {
tween(block, {
alpha: 0,
scaleX: 0,
scaleY: 0
}, {
duration: 300,
onFinish: function onFinish() {
block.destroy();
}
});
placedShapes.splice(j, 1);
}
}
// Clear grid row
for (var col = 0; col < gridWidth; col++) {
grid[row][col] = null;
}
}
// Drop remaining blocks down
LK.setTimeout(function () {
dropBlocks(lines);
}, 400);
}
function dropBlocks(clearedLines) {
// Sort cleared lines from top to bottom to handle dropping correctly
var sortedLines = [];
for (var i = 0; i < clearedLines.length; i++) {
sortedLines.push(clearedLines[i].row);
}
sortedLines.sort(function (a, b) {
return a - b;
});
// Process from bottom to top
for (var row = gridHeight - 1; row >= 0; row--) {
var dropDistance = 0;
// Count how many cleared lines are below this row
for (var i = 0; i < sortedLines.length; i++) {
if (sortedLines[i] > row) {
dropDistance++;
}
}
if (dropDistance > 0) {
// Move grid data
for (var col = 0; col < gridWidth; col++) {
if (grid[row][col]) {
grid[row + dropDistance][col] = grid[row][col];
grid[row][col] = null;
}
}
// Move visual blocks
for (var j = 0; j < placedShapes.length; j++) {
var block = placedShapes[j];
if (block.gridY === row) {
block.gridY += dropDistance;
tween(block, {
y: block.gridY * blockSize + blockSize / 2 + 200
}, {
duration: 200,
easing: tween.easeOut
});
}
}
}
}
}
function handleSpecialBlock(type, gridX, gridY) {
if (type === 'gift') {
score += 50;
updateScore();
LK.effects.flashObject(game, 0xffd700, 500);
} else if (type === 'bomb') {
LK.getSound('bomb').play();
// Destroy blocks in 3x3 area
for (var row = Math.max(0, gridY - 1); row <= Math.min(gridHeight - 1, gridY + 1); row++) {
for (var col = Math.max(0, gridX - 1); col <= Math.min(gridWidth - 1, gridX + 1); col++) {
if (grid[row][col]) {
grid[row][col] = null;
// Remove visual block
for (var i = placedShapes.length - 1; i >= 0; i--) {
var block = placedShapes[i];
if (block.gridX === col && block.gridY === row) {
tween(block, {
alpha: 0,
scaleX: 2,
scaleY: 2
}, {
duration: 300,
onFinish: function onFinish() {
block.destroy();
}
});
placedShapes.splice(i, 1);
break;
}
}
}
}
}
} else if (type === 'wildcard') {
// Fill nearest gap
var nearestGap = findNearestGap(gridX, gridY);
if (nearestGap) {
grid[nearestGap.row][nearestGap.col] = 'cube';
var wildBlock = new GridBlock('cube');
wildBlock.gridX = nearestGap.col;
wildBlock.gridY = nearestGap.row;
wildBlock.x = nearestGap.col * blockSize + blockSize / 2 + (2048 - gridWidth * blockSize) / 2;
wildBlock.y = nearestGap.row * blockSize + blockSize / 2 + 200;
placedShapes.push(wildBlock);
game.addChild(wildBlock);
tween(wildBlock, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 200,
easing: tween.bounceOut,
onFinish: function onFinish() {
tween(wildBlock, {
scaleX: 1,
scaleY: 1
}, {
duration: 100
});
}
});
}
}
// Trigger stability and height recalculation after special block effects
calculateStability();
updateStability();
applyGravity();
checkForLines();
}
function findNearestGap(startX, startY) {
var searchRadius = 1;
while (searchRadius < Math.max(gridWidth, gridHeight)) {
for (var row = Math.max(0, startY - searchRadius); row <= Math.min(gridHeight - 1, startY + searchRadius); row++) {
for (var col = Math.max(0, startX - searchRadius); col <= Math.min(gridWidth - 1, startX + searchRadius); col++) {
if (!grid[row][col]) {
return {
row: row,
col: col
};
}
}
}
searchRadius++;
}
return null;
}
function triggerCollapse() {
LK.getSound('collapse').play();
// Remove unstable shapes
for (var i = placedShapes.length - 1; i >= 0; i--) {
if (Math.random() < 0.3) {
// 30% chance each shape collapses
var shape = placedShapes[i];
tween(shape, {
alpha: 0,
y: shape.y + 100
}, {
duration: 500,
onFinish: function onFinish() {
shape.destroy();
}
});
placedShapes.splice(i, 1);
}
}
score = Math.max(0, score - 50);
updateScore();
}
function placeShape(shape, x, y) {
if (inventory.length === 0) return;
var inventoryShape = inventory.shift();
var placedShape = new PlacedShape(inventoryShape.shapeType);
placedShape.x = x;
placedShape.y = y;
// Start with smaller scale for placement animation
placedShape.scaleX = 0.5;
placedShape.scaleY = 0.5;
placedShape.alpha = 0.7;
placedShapes.push(placedShape);
game.addChild(placedShape);
// Animate placement with smooth scaling
tween(placedShape, {
scaleX: 1,
scaleY: 1,
alpha: 1
}, {
duration: 300,
easing: tween.elasticOut
});
LK.getSound('place').play();
score += 10;
// Bonus for good placement
var centerX = 1024;
var distanceFromCenter = Math.abs(x - centerX);
if (distanceFromCenter < 100) {
score += 5; // Bonus for center placement
// Add bonus glow effect for center placement
tween(placedShape, {
tint: 0xffff00
}, {
duration: 500,
onFinish: function onFinish() {
tween(placedShape, {
tint: 0xffffff
}, {
duration: 500
});
}
});
}
updateScore();
updateInventoryDisplay();
calculateStability();
updateHeight();
inventoryShape.destroy();
}
// Event handlers
game.down = function (x, y, obj) {
// Touch handling is now done directly by TetrisBlock shapes
// This ensures only the touched shape responds
};
game.move = function (x, y, obj) {
// Handle dragging of selected shape with precise 1:1 movement mapping
// Prevent movement of landed shapes to maintain structural stability until row completion
if (selectedShape && selectedShape.isDragging && !selectedShape.hasLanded) {
var gameAreaLeft = (2048 - gridWidth * blockSize) / 2;
var gameAreaRight = gameAreaLeft + gridWidth * blockSize;
// Use direct finger position for movement - no offset calculation
// This creates 1:1 mapping between finger movement and shape movement
var clampedX = Math.max(gameAreaLeft + blockSize / 2, Math.min(gameAreaRight - blockSize / 2, x));
var targetGridX = Math.floor((clampedX - gameAreaLeft) / blockSize);
targetGridX = Math.max(0, Math.min(gridWidth - 1, targetGridX));
// Move to new position only if it's actually different
if (targetGridX !== selectedShape.gridX) {
selectedShape.moveToPosition(targetGridX);
}
}
};
game.up = function (x, y, obj) {
// Touch release is now handled directly by TetrisBlock shapes
// This ensures proper cleanup when touch ends
};
game.update = function () {
// One-at-a-time spawn rule: only spawn when no shapes are falling
nextShapeTimer++;
if (nextShapeTimer >= shapeSpawnDelay && fallingShapes.length === 0) {
spawnShape();
nextShapeTimer = 0;
}
// Apply gravity to prevent floating blocks
if (LK.ticks % 20 === 0) {
applyGravity();
}
// Update stability periodically
if (LK.ticks % 30 === 0) {
calculateStability();
updateStability();
}
// Check win condition
if (maxHeight > 800) {
// About 10 blocks high
LK.showYouWin();
}
// Check game over condition - stack overflow (blocked spawn area)
var spawnAreaBlocked = false;
// Check if spawn area (top 3 rows) has any blocks that would prevent new shapes
for (var row = 0; row < 3; row++) {
for (var col = 0; col < gridWidth; col++) {
if (grid[row][col]) {
spawnAreaBlocked = true;
break;
}
}
if (spawnAreaBlocked) break;
}
// Game over if spawn area is blocked or stability is too low
if (spawnAreaBlocked || stability < 10 && placedShapes.length > 0) {
LK.showGameOver();
}
};
// Initialize display
updateScore();
updateStability();
updateInventoryDisplay();
updateHeight();