/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var AIPlayer = Container.expand(function () { var self = Container.call(this); var graphics = self.attachAsset('ai', { anchorX: 0.5, anchorY: 0.5 }); self.row = -1; // AI starts above the bridge self.col = 3; // Center column of 7-column grid self.isAI = true; self.moveTo = function (newRow, newCol) { self.row = newRow; self.col = newCol; var targetX = gameStartX + newCol * CELL_SIZE; var targetY = gameStartY + newRow * CELL_SIZE; tween(self, { x: targetX, y: targetY }, { duration: 300 }); LK.getSound('move').play(); }; return self; }); var CharacterSelectionButton = Container.expand(function (characterType, x, y) { var self = Container.call(this); var button = self.attachAsset('selectionButton', { anchorX: 0.5, anchorY: 0.5 }); var character = self.attachAsset(characterType, { anchorX: 0.5, anchorY: 0.5 }); self.x = x; self.y = y; self.characterType = characterType; self.selected = false; self.highlight = function () { tween(button, { tint: 0xFFFFAA }, { duration: 200 }); }; self.unhighlight = function () { tween(button, { tint: 0xFFFFFF }, { duration: 200 }); }; self.select = function () { self.selected = true; tween(button, { tint: 0x00FF00 }, { duration: 200 }); }; self.down = function (x, y, obj) { selectCharacter(self.characterType); }; return self; }); var GridCell = Container.expand(function () { var self = Container.call(this); var border = self.attachAsset('gridBorder', { anchorX: 0.5, anchorY: 0.5 }); var cell = self.attachAsset('gridCell', { anchorX: 0.5, anchorY: 0.5 }); self.isBroken = false; self.isRevealed = false; self.row = 0; self.col = 0; self.revealBroken = function () { if (self.isBroken && !self.isRevealed) { cell.visible = false; var brokenGraphics = self.attachAsset('brokenGlass', { anchorX: 0.5, anchorY: 0.5 }); self.isRevealed = true; LK.getSound('break').play(); tween(brokenGraphics, { alpha: 0.8 }, { duration: 300 }); } }; self.highlight = function () { tween(cell, { tint: 0xFFFFAA }, { duration: 200 }); }; self.unhighlight = function () { tween(cell, { tint: 0xFFFFFF }, { duration: 200 }); }; return self; }); var Player = Container.expand(function (characterType) { var self = Container.call(this); var assetName = characterType || 'player'; var graphics = self.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); self.row = 15; // Player starts below the bridge self.col = 3; // Center column of 7-column grid self.isAI = false; self.characterType = assetName; self.moveTo = function (newRow, newCol) { self.row = newRow; self.col = newCol; var targetX = gameStartX + newCol * CELL_SIZE; var targetY = gameStartY + newRow * CELL_SIZE; tween(self, { x: targetX, y: targetY }, { duration: 300 }); LK.getSound('move').play(); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x001122 }); /**** * Game Code ****/ var GRID_ROWS = 15; var GRID_COLS = 7; // Expanded from 5 to 7 columns for wider gameplay area var CELL_SIZE = 120; // Increased cell size for better mobile experience var gameStartX = (2048 - GRID_COLS * CELL_SIZE) / 2 + CELL_SIZE / 2; var totalGameHeight = CELL_SIZE + GRID_ROWS * CELL_SIZE + CELL_SIZE + 100; // AI start area + bridge + player start area + padding var gameStartY = (2732 - totalGameHeight) / 2 + CELL_SIZE; // Center vertically and account for AI start area var startingAreaY = gameStartY - CELL_SIZE - 50; // Starting area above bridge for AI var endingAreaY = gameStartY + GRID_ROWS * CELL_SIZE + 50; // Starting area below bridge for player var grid = []; var brokenPanels = []; var player; var aiPlayer; var currentTurn = 'player'; var gameOver = false; var turnInProgress = false; var gunDisplay; // Gun display for showing turn order // Previous position tracking for broken glass restoration var playerPreviousPosition = { row: 15, col: 3 }; // Player's previous position var aiPreviousPosition = { row: -1, col: 3 }; // AI's previous position // Character selection variables var gameState = 'characterSelection'; // 'characterSelection', 'weaponRotation', or 'playing' var selectedCharacter = null; var characterOptions = []; var characterSelectionUI = []; // Weapon rotation variables var weaponRotationGun; var rotationTurns = 0; var maxRotationTurns = 0; var rotationSpeed = 100; // Initial rotation speed in ms var rotationTimer = null; var whoStarts = 'player'; // Will be determined by weapon rotation // Lighting variables var leftLight; var rightLight; var lightingInitialized = false; // Street light variables var streetLights = []; var streetLightBulbs = []; // Initialize character selection screen function initCharacterSelection() { var titleText = new Text2('Choose Your Character', { size: 120, fill: 0xFFFFFF }); titleText.anchor.set(0.5, 0.5); titleText.x = 2048 / 2; titleText.y = 400; game.addChild(titleText); characterSelectionUI.push(titleText); // Add shine effect to title text tween(titleText, { tint: 0xFFDD00 }, { duration: 1500, easing: tween.easeInOut, onFinish: function onFinish() { tween(titleText, { tint: 0xFFFFFF }, { duration: 1500, easing: tween.easeInOut }); } }); var characters = [{ type: 'player', name: 'Frog' }, { type: 'playerBlue', name: 'Potato' }, { type: 'playerRed', name: 'Cat' }, { type: 'playerPurple', name: 'Mushroom' }]; var startX = 2048 / 2 - (characters.length - 1) * 150; for (var i = 0; i < characters.length; i++) { var button = new CharacterSelectionButton(characters[i].type, startX + i * 300, 2732 / 2); game.addChild(button); characterOptions.push(button); characterSelectionUI.push(button); var nameText = new Text2(characters[i].name, { size: 60, fill: 0xFFFFFF }); nameText.anchor.set(0.5, 0.5); nameText.x = button.x; nameText.y = button.y + 150; game.addChild(nameText); characterSelectionUI.push(nameText); // Add shine effect to character selection buttons var delay = i * 200; LK.setTimeout(function (btn, txt) { return function () { tween(btn, { tint: 0xFFDD88 }, { duration: 1200, easing: tween.easeInOut, onFinish: function onFinish() { tween(btn, { tint: 0xFFFFFF }, { duration: 1200, easing: tween.easeInOut }); } }); tween(txt, { tint: 0xFFDD88 }, { duration: 1200, easing: tween.easeInOut, onFinish: function onFinish() { tween(txt, { tint: 0xFFFFFF }, { duration: 1200, easing: tween.easeInOut }); } }); }; }(button, nameText), delay); } } function selectCharacter(characterType) { selectedCharacter = characterType; // Clear character selection UI for (var i = 0; i < characterSelectionUI.length; i++) { characterSelectionUI[i].destroy(); } characterSelectionUI = []; characterOptions = []; // Start weapon rotation to determine who goes first startWeaponRotation(); } function startWeaponRotation() { gameState = 'weaponRotation'; // Create title text for weapon rotation var rotationTitle = new Text2('Spinning to decide who starts...', { size: 100, fill: 0xFFFFFF }); rotationTitle.anchor.set(0.5, 0.5); rotationTitle.x = 2048 / 2; rotationTitle.y = 500; game.addChild(rotationTitle); // Create weapon display for rotation weaponRotationGun = LK.getAsset('gun', { anchorX: 0.5, anchorY: 0.5 }); weaponRotationGun.x = 2048 / 2; weaponRotationGun.y = 2732 / 2; weaponRotationGun.scaleX = 1.5; weaponRotationGun.scaleY = 1.5; game.addChild(weaponRotationGun); // Create player indicators var playerIndicator = new Text2('YOU', { size: 80, fill: 0x00FF00 }); playerIndicator.anchor.set(0.5, 0.5); playerIndicator.x = 2048 / 2; playerIndicator.y = 2732 / 2 - 300; game.addChild(playerIndicator); var aiIndicator = new Text2('AI', { size: 80, fill: 0xFF0000 }); aiIndicator.anchor.set(0.5, 0.5); aiIndicator.x = 2048 / 2; aiIndicator.y = 2732 / 2 + 300; game.addChild(aiIndicator); // Set random number of turns (5-10) maxRotationTurns = Math.floor(Math.random() * 6) + 5; // 5 to 10 turns rotationTurns = 0; rotationSpeed = 100; // Start rotation rotateWeapon(); } function rotateWeapon() { // Rotate gun 180 degrees tween(weaponRotationGun, { rotation: weaponRotationGun.rotation + Math.PI }, { duration: rotationSpeed, easing: tween.easeInOut, onFinish: function onFinish() { rotationTurns++; // Determine who the gun is pointing at var normalizedRotation = weaponRotationGun.rotation % (2 * Math.PI); if (normalizedRotation < 0) normalizedRotation += 2 * Math.PI; // Gun points up (to player) when rotation is around -π/2 or 3π/2 // Gun points down (to AI) when rotation is around π/2 var pointingUp = normalizedRotation > Math.PI * 1.25 && normalizedRotation < Math.PI * 1.75 || normalizedRotation > Math.PI * 0.25 && normalizedRotation < Math.PI * 0.75; whoStarts = pointingUp ? 'ai' : 'player'; if (rotationTurns >= maxRotationTurns) { // Stop rotation and show result finishWeaponRotation(); } else { // Continue rotation but slow down gradually rotationSpeed += 50; // Increase duration to slow down rotateWeapon(); } } }); } function finishWeaponRotation() { // Show who starts var resultText = new Text2(whoStarts === 'player' ? 'You start!' : 'AI starts!', { size: 120, fill: whoStarts === 'player' ? 0x00FF00 : 0xFF0000 }); resultText.anchor.set(0.5, 0.5); resultText.x = 2048 / 2; resultText.y = 800; game.addChild(resultText); // Flash the result tween(resultText, { tint: 0xFFFFAA }, { duration: 500, easing: tween.easeInOut, onFinish: function onFinish() { tween(resultText, { tint: 0xFFFFFF }, { duration: 500, easing: tween.easeInOut }); } }); // Wait 2 seconds then start the main game LK.setTimeout(function () { // Clear weapon rotation UI game.removeChild(weaponRotationGun); // Clear all children to remove rotation UI while (game.children.length > 0) { game.children[0].destroy(); } // Set initial turn based on weapon rotation result currentTurn = whoStarts; // Initialize the main game initMainGame(); gameState = 'playing'; }, 2000); } function initMainGame() { // Initialize grid for (var row = 0; row < GRID_ROWS; row++) { grid[row] = []; for (var col = 0; col < GRID_COLS; col++) { var cell = new GridCell(); cell.row = row; cell.col = col; cell.x = gameStartX + col * CELL_SIZE; cell.y = gameStartY + row * CELL_SIZE; // Randomly assign broken panels (about 20% chance) if (Math.random() < 0.2) { cell.isBroken = true; brokenPanels.push({ row: row, col: col }); } grid[row][col] = cell; game.addChild(cell); // Broken windows will only be revealed when stepped on } } // Create starting areas var aiStartArea = new GridCell(); aiStartArea.x = gameStartX + 3 * CELL_SIZE; // Center column of 7-column grid aiStartArea.y = startingAreaY; game.addChild(aiStartArea); var playerStartArea = new GridCell(); playerStartArea.x = gameStartX + 3 * CELL_SIZE; // Center column of 7-column grid playerStartArea.y = endingAreaY; game.addChild(playerStartArea); // Create players player = new Player(selectedCharacter); player.row = 15; // Start below the bridge player.x = gameStartX + player.col * CELL_SIZE; player.y = endingAreaY; game.addChild(player); aiPlayer = new AIPlayer(); aiPlayer.row = -1; // Start above the bridge aiPlayer.x = gameStartX + aiPlayer.col * CELL_SIZE; aiPlayer.y = startingAreaY; game.addChild(aiPlayer); // Initialize previous positions playerPreviousPosition = { row: player.row, col: player.col }; aiPreviousPosition = { row: aiPlayer.row, col: aiPlayer.col }; // Initialize lighting system if (!lightingInitialized) { leftLight = LK.getAsset('leftLight', { anchorX: 1.0, anchorY: 0.5, alpha: 0.15 }); leftLight.x = 0; leftLight.y = 2732 / 2; game.addChild(leftLight); rightLight = LK.getAsset('rightLight', { anchorX: 0.0, anchorY: 0.5, alpha: 0.15 }); rightLight.x = 2048; rightLight.y = 2732 / 2; game.addChild(rightLight); // Start lighting animation animateLighting(); lightingInitialized = true; // Add street lights on 4 corners var cornerPositions = [{ x: 200, y: 300 }, // Top left corner { x: 2048 - 200, y: 300 }, // Top right corner { x: 200, y: 2732 - 300 }, // Bottom left corner { x: 2048 - 200, y: 2732 - 300 } // Bottom right corner ]; for (var i = 0; i < cornerPositions.length; i++) { // Create street light pole var streetLight = LK.getAsset('streetLight', { anchorX: 0.5, anchorY: 1.0 }); streetLight.x = cornerPositions[i].x; streetLight.y = cornerPositions[i].y; game.addChild(streetLight); streetLights.push(streetLight); // Create street light bulb var streetLightBulb = LK.getAsset('streetLightBulb', { anchorX: 0.5, anchorY: 0.5 }); streetLightBulb.x = cornerPositions[i].x; streetLightBulb.y = cornerPositions[i].y - 180; // Position bulb at top of pole game.addChild(streetLightBulb); streetLightBulbs.push(streetLightBulb); } } // Gun display for game order gunDisplay = LK.getAsset('gun', { anchorX: 0.5, anchorY: 0.5 }); gunDisplay.x = 200; // Middle left position gunDisplay.y = 2732 / 2; // Vertically centered game.addChild(gunDisplay); // Start playing theme music LK.playMusic('themeMusic'); // Add shine effect to all elements LK.setTimeout(function () { addShineToAllElements(); }, 1000); // Delay to ensure all elements are properly initialized // Automatically reduce obstacles after initialization LK.setTimeout(function () { reduceBrokenWindows(20); // Remove 20% of broken windows at start }, 1500); } // Start with character selection initCharacterSelection(); // Gun barrel rotation to show turn order function updateGunDirection() { if (!gunDisplay) return; // Don't update if gun not initialized yet if (currentTurn === 'player') { // Point gun up towards AI gunDisplay.rotation = -Math.PI / 2; } else { // Point gun down towards player gunDisplay.rotation = Math.PI / 2; } } // Initialize gun direction based on weapon rotation result updateGunDirection(); // UI Elements var turnText = new Text2('Your Turn', { size: 80, fill: 0xFFFFFF }); turnText.anchor.set(0.5, 0); turnText.visible = false; LK.gui.top.addChild(turnText); var instructionText = new Text2('Tap adjacent cell to move', { size: 60, fill: 0xCCCCCC }); instructionText.anchor.set(0.5, 0); instructionText.y = 100; instructionText.visible = false; LK.gui.top.addChild(instructionText); var progressText = new Text2('Progress: You 0/15 - AI 0/15', { size: 50, fill: 0xFFFFFF }); progressText.anchor.set(0.5, 1); LK.gui.bottom.addChild(progressText); function updateProgressText() { var playerProgress = Math.max(0, 15 - player.row); // Player progress from starting position var aiProgress = Math.max(0, aiPlayer.row + 1); // AI progress from starting position progressText.setText('Progress: You ' + playerProgress + '/15 - AI ' + aiProgress + '/15'); } function isValidMove(fromRow, fromCol, toRow, toCol) { // Special case: moving from starting areas onto bridge if (fromRow === -1 && toRow === 0) { // AI entering bridge from above return toCol >= 0 && toCol < GRID_COLS && Math.abs(toCol - fromCol) <= 1; } if (fromRow === 15 && toRow === 14) { // Player entering bridge from below return toCol >= 0 && toCol < GRID_COLS && Math.abs(toCol - fromCol) <= 1; } // Check bounds for normal bridge movement if (toRow < 0 || toRow >= GRID_ROWS || toCol < 0 || toCol >= GRID_COLS) { return false; } // Check if adjacent (including diagonal) var rowDiff = Math.abs(toRow - fromRow); var colDiff = Math.abs(toCol - fromCol); if (rowDiff > 1 || colDiff > 1 || rowDiff === 0 && colDiff === 0) { return false; } return true; } // Calculate optimal path for player (moving up to reach top) function calculatePlayerPath(startRow, startCol, targetRow) { var openSet = [{ row: startRow, col: startCol, gScore: 0, fScore: Math.abs(startRow - targetRow), parent: null }]; var closedSet = []; var visited = {}; while (openSet.length > 0) { // Find node with lowest fScore var current = openSet[0]; var currentIndex = 0; for (var i = 1; i < openSet.length; i++) { if (openSet[i].fScore < current.fScore) { current = openSet[i]; currentIndex = i; } } // Remove current from openSet openSet.splice(currentIndex, 1); // Add to closed set var key = current.row + ',' + current.col; closedSet.push(current); visited[key] = true; // Check if we reached target row if (current.row <= targetRow) { // Reconstruct path var path = []; var node = current; while (node !== null) { path.unshift({ row: node.row, col: node.col }); node = node.parent; } return path; } // Explore neighbors for (var dRow = -1; dRow <= 1; dRow++) { for (var dCol = -1; dCol <= 1; dCol++) { if (dRow === 0 && dCol === 0) continue; var newRow = current.row + dRow; var newCol = current.col + dCol; var neighborKey = newRow + ',' + newCol; // Skip if already visited or invalid move if (visited[neighborKey] || !isValidMove(current.row, current.col, newRow, newCol)) { continue; } // Skip if we know it's broken glass if (newRow >= 0 && newRow < GRID_ROWS && grid[newRow] && grid[newRow][newCol] && grid[newRow][newCol].isRevealed && grid[newRow][newCol].isBroken) { continue; } // Calculate scores var gScore = current.gScore + 1; // Add penalty for potentially broken glass (unexplored cells) var heuristic = Math.abs(newRow - targetRow) + Math.abs(3 - newCol); // Prefer center column if (newRow >= 0 && newRow < GRID_ROWS && grid[newRow] && grid[newRow][newCol] && !grid[newRow][newCol].isRevealed) { heuristic += 1; // Small penalty for unknown cells } var fScore = gScore + heuristic; // Check if this path to neighbor is better var existingNode = null; for (var j = 0; j < openSet.length; j++) { if (openSet[j].row === newRow && openSet[j].col === newCol) { existingNode = openSet[j]; break; } } if (existingNode === null || gScore < existingNode.gScore) { var neighbor = { row: newRow, col: newCol, gScore: gScore, fScore: fScore, parent: current }; if (existingNode === null) { openSet.push(neighbor); } else { existingNode.gScore = gScore; existingNode.fScore = fScore; existingNode.parent = current; } } } } } return null; // No path found } function highlightValidMoves(currentPlayer) { // Check if grid is initialized if (!grid || grid.length === 0) return; // Unhighlight all cells first for (var row = 0; row < GRID_ROWS; row++) { for (var col = 0; col < GRID_COLS; col++) { if (grid[row] && grid[row][col]) { grid[row][col].unhighlight(); } } } // Calculate optimal path for player guidance var optimalPath = null; if (!currentPlayer.isAI) { optimalPath = calculatePlayerPath(currentPlayer.row, currentPlayer.col, 0); } // Highlight valid moves with path guidance for (var dRow = -1; dRow <= 1; dRow++) { for (var dCol = -1; dCol <= 1; dCol++) { if (dRow === 0 && dCol === 0) continue; var newRow = currentPlayer.row + dRow; var newCol = currentPlayer.col + dCol; if (isValidMove(currentPlayer.row, currentPlayer.col, newRow, newCol)) { if (grid[newRow] && grid[newRow][newCol]) { // Check if this move is part of optimal path var isOptimalMove = false; if (optimalPath && optimalPath.length > 1) { for (var p = 1; p < optimalPath.length; p++) { if (optimalPath[p].row === newRow && optimalPath[p].col === newCol) { isOptimalMove = true; break; } } } if (isOptimalMove) { // Highlight optimal moves more prominently tween(grid[newRow][newCol], { tint: 0x00FF88 // Green for optimal path }, { duration: 200 }); } else { grid[newRow][newCol].highlight(); } } } } } } // AI pathfinding algorithm using A* search function calculateAIPath(startRow, startCol, targetRow) { var openSet = [{ row: startRow, col: startCol, gScore: 0, fScore: Math.abs(targetRow - startRow), parent: null }]; var closedSet = []; var visited = {}; while (openSet.length > 0) { // Find node with lowest fScore var current = openSet[0]; var currentIndex = 0; for (var i = 1; i < openSet.length; i++) { if (openSet[i].fScore < current.fScore) { current = openSet[i]; currentIndex = i; } } // Remove current from openSet openSet.splice(currentIndex, 1); // Add to closed set var key = current.row + ',' + current.col; closedSet.push(current); visited[key] = true; // Check if we reached target row if (current.row >= targetRow) { // Reconstruct path var path = []; var node = current; while (node !== null) { path.unshift({ row: node.row, col: node.col }); node = node.parent; } return path; } // Explore neighbors for (var dRow = -1; dRow <= 1; dRow++) { for (var dCol = -1; dCol <= 1; dCol++) { if (dRow === 0 && dCol === 0) continue; var newRow = current.row + dRow; var newCol = current.col + dCol; var neighborKey = newRow + ',' + newCol; // Skip if already visited or invalid move if (visited[neighborKey] || !isValidMove(current.row, current.col, newRow, newCol)) { continue; } // Skip if we know it's broken glass if (newRow >= 0 && newRow < GRID_ROWS && grid[newRow] && grid[newRow][newCol] && grid[newRow][newCol].isRevealed && grid[newRow][newCol].isBroken) { continue; } // Calculate scores var gScore = current.gScore + 1; // Add penalty for potentially broken glass (unexplored cells) var heuristic = Math.abs(targetRow - newRow) + Math.abs(3 - newCol); // Prefer center column if (newRow >= 0 && newRow < GRID_ROWS && grid[newRow] && grid[newRow][newCol] && !grid[newRow][newCol].isRevealed) { heuristic += 2; // Small penalty for unknown cells } var fScore = gScore + heuristic; // Check if this path to neighbor is better var existingNode = null; for (var j = 0; j < openSet.length; j++) { if (openSet[j].row === newRow && openSet[j].col === newCol) { existingNode = openSet[j]; break; } } if (existingNode === null || gScore < existingNode.gScore) { var neighbor = { row: newRow, col: newCol, gScore: gScore, fScore: fScore, parent: current }; if (existingNode === null) { openSet.push(neighbor); } else { existingNode.gScore = gScore; existingNode.fScore = fScore; existingNode.parent = current; } } } } } return null; // No path found } function makeAIMove() { if (gameOver || turnInProgress) return; var validMoves = []; // Find all valid moves for AI for (var dRow = -1; dRow <= 1; dRow++) { for (var dCol = -1; dCol <= 1; dCol++) { if (dRow === 0 && dCol === 0) continue; var newRow = aiPlayer.row + dRow; var newCol = aiPlayer.col + dCol; if (isValidMove(aiPlayer.row, aiPlayer.col, newRow, newCol)) { validMoves.push({ row: newRow, col: newCol }); } } } if (validMoves.length === 0) { endGame(); return; } // Use AI pathfinding to determine best move var bestPath = calculateAIPath(aiPlayer.row, aiPlayer.col, 14); // Target: reach bottom of bridge var chosenMove; if (bestPath && bestPath.length > 1) { // Follow the calculated path chosenMove = bestPath[1]; // Next step in optimal path } else { // Fallback to heuristic-based strategy if no path found validMoves.sort(function (a, b) { // Prioritize avoiding known broken panels var aIsBroken = a.row >= 0 && a.row < GRID_ROWS && grid[a.row][a.col].isRevealed && grid[a.row][a.col].isBroken; var bIsBroken = b.row >= 0 && b.row < GRID_ROWS && grid[b.row][b.col].isRevealed && grid[b.row][b.col].isBroken; if (aIsBroken && !bIsBroken) return 1; if (!aIsBroken && bIsBroken) return -1; // Prefer moves toward center columns (safer) var aCenterDist = Math.abs(a.col - 3); var bCenterDist = Math.abs(b.col - 3); if (aCenterDist !== bCenterDist) return aCenterDist - bCenterDist; // Prefer moving down (higher row numbers) return b.row - a.row; }); chosenMove = validMoves[0]; } executeMove(aiPlayer, chosenMove.row, chosenMove.col); } function executeMove(currentPlayer, newRow, newCol) { turnInProgress = true; // Store current position as previous position before moving if (currentPlayer.isAI) { aiPreviousPosition = { row: currentPlayer.row, col: currentPlayer.col }; } else { playerPreviousPosition = { row: currentPlayer.row, col: currentPlayer.col }; } currentPlayer.moveTo(newRow, newCol); LK.setTimeout(function () { var targetCell = grid[newRow][newCol]; if (targetCell.isBroken) { targetCell.revealBroken(); // Return to previous position when stepping on broken glass LK.setTimeout(function () { if (currentPlayer.isAI) { currentPlayer.moveTo(aiPreviousPosition.row, aiPreviousPosition.col); } else { currentPlayer.moveTo(playerPreviousPosition.row, playerPreviousPosition.col); } }, 500); // Wait for broken glass animation to complete } updateProgressText(); checkGameEnd(); if (!gameOver) { switchTurn(); } turnInProgress = false; }, 400); } function switchTurn() { currentTurn = currentTurn === 'player' ? 'ai' : 'player'; updateGunDirection(); // Update gun barrel direction // Check if opponent's path is blocked and reduce broken windows if needed var opponent = currentTurn === 'player' ? aiPlayer : player; if (isPathBlocked(opponent)) { // Reduce broken windows by 50% when opponent's path is blocked reduceBrokenWindows(50); } if (currentTurn === 'player') { turnText.setText('Your Turn'); turnText.visible = true; instructionText.visible = true; highlightValidMoves(player); } else { turnText.setText('AI Turn'); turnText.visible = true; instructionText.visible = false; // Unhighlight all cells for (var row = 0; row < GRID_ROWS; row++) { for (var col = 0; col < GRID_COLS; col++) { grid[row][col].unhighlight(); } } LK.setTimeout(function () { makeAIMove(); }, 1000); } } function checkGameEnd() { var playerProgress = Math.max(0, 15 - player.row); // Player progress from starting position var aiProgress = Math.max(0, aiPlayer.row + 1); // AI progress from starting position // Check if player reached the end (top of bridge) if (player.row <= 0) { LK.showYouWin(); gameOver = true; return; } // Check if AI reached the end (bottom of bridge) if (aiPlayer.row >= 14) { LK.showGameOver(); gameOver = true; return; } // Check if no valid moves available var playerHasMoves = false; var aiHasMoves = false; for (var dRow = -1; dRow <= 1; dRow++) { for (var dCol = -1; dCol <= 1; dCol++) { if (dRow === 0 && dCol === 0) continue; var playerNewRow = player.row + dRow; var playerNewCol = player.col + dCol; if (isValidMove(player.row, player.col, playerNewRow, playerNewCol)) { playerHasMoves = true; } var aiNewRow = aiPlayer.row + dRow; var aiNewCol = aiPlayer.col + dCol; if (isValidMove(aiPlayer.row, aiPlayer.col, aiNewRow, aiNewCol)) { aiHasMoves = true; } } } if (!playerHasMoves && !aiHasMoves) { endGame(); } } function animateLighting() { if (!leftLight || !rightLight) return; // Animate left light tween(leftLight, { alpha: 0.25 }, { duration: 2000, yoyo: true, repeat: -1, ease: 'sine' }); // Animate right light with offset timing LK.setTimeout(function () { tween(rightLight, { alpha: 0.25 }, { duration: 2000, yoyo: true, repeat: -1, ease: 'sine' }); }, 500); } function addShineToAllElements() { // Add shine effect to all grid cells for (var row = 0; row < GRID_ROWS; row++) { for (var col = 0; col < GRID_COLS; col++) { if (grid[row] && grid[row][col]) { var delay = (row * GRID_COLS + col) * 50; // Stagger the animations LK.setTimeout(function (cell) { return function () { tween(cell, { tint: 0xFFFFAA }, { duration: 1500, easing: tween.easeInOut, onFinish: function onFinish() { tween(cell, { tint: 0xFFFFFF }, { duration: 1500, easing: tween.easeInOut }); } }); }; }(grid[row][col]), delay); } } } // Add shine effect to players if (player) { tween(player, { tint: 0xFFDD88 }, { duration: 2000, easing: tween.easeInOut, onFinish: function onFinish() { tween(player, { tint: 0xFFFFFF }, { duration: 2000, easing: tween.easeInOut }); } }); } if (aiPlayer) { tween(aiPlayer, { tint: 0x88DDFF }, { duration: 2000, easing: tween.easeInOut, onFinish: function onFinish() { tween(aiPlayer, { tint: 0xFFFFFF }, { duration: 2000, easing: tween.easeInOut }); } }); } // Add shine effect to gun display if (gunDisplay) { tween(gunDisplay, { tint: 0xFFCC00 }, { duration: 1800, easing: tween.easeInOut, onFinish: function onFinish() { tween(gunDisplay, { tint: 0xFFFFFF }, { duration: 1800, easing: tween.easeInOut }); } }); } // Add shine effect to lighting if (leftLight) { tween(leftLight, { tint: 0xFFFFCC }, { duration: 2500, easing: tween.easeInOut, onFinish: function onFinish() { tween(leftLight, { tint: 0xFFFFFF }, { duration: 2500, easing: tween.easeInOut }); } }); } if (rightLight) { tween(rightLight, { tint: 0xFFFFCC }, { duration: 2500, easing: tween.easeInOut, onFinish: function onFinish() { tween(rightLight, { tint: 0xFFFFFF }, { duration: 2500, easing: tween.easeInOut }); } }); } // Add shine effect to street lights for (var i = 0; i < streetLights.length; i++) { var delay = i * 300; LK.setTimeout(function (light, bulb) { return function () { tween(light, { tint: 0xAAAADD }, { duration: 2000, easing: tween.easeInOut, onFinish: function onFinish() { tween(light, { tint: 0xFFFFFF }, { duration: 2000, easing: tween.easeInOut }); } }); tween(bulb, { tint: 0xFFFFCC, alpha: 0.9 }, { duration: 1800, yoyo: true, repeat: -1, easing: tween.easeInOut }); }; }(streetLights[i], streetLightBulbs[i]), delay); } } function isPathBlocked(currentPlayer) { var targetRow = currentPlayer.isAI ? 14 : 0; var path = currentPlayer.isAI ? calculateAIPath(currentPlayer.row, currentPlayer.col, targetRow) : calculatePlayerPath(currentPlayer.row, currentPlayer.col, targetRow); return path === null || path.length <= 1; } function reduceBrokenWindows(percentage) { var brokenCells = []; // Collect all currently broken and revealed cells for (var row = 0; row < GRID_ROWS; row++) { for (var col = 0; col < GRID_COLS; col++) { if (grid[row] && grid[row][col] && grid[row][col].isBroken && grid[row][col].isRevealed) { brokenCells.push(grid[row][col]); } } } // Calculate how many to repair var numberToRepair = Math.floor(brokenCells.length * (percentage / 100)); // Randomly select cells to repair for (var i = 0; i < numberToRepair && brokenCells.length > 0; i++) { var randomIndex = Math.floor(Math.random() * brokenCells.length); var cellToRepair = brokenCells[randomIndex]; brokenCells.splice(randomIndex, 1); // Repair the cell - hide broken glass and show normal cell cellToRepair.isBroken = false; cellToRepair.isRevealed = false; // Remove all children (broken glass graphics) while (cellToRepair.children.length > 2) { cellToRepair.children[cellToRepair.children.length - 1].destroy(); } // Make normal cell visible again cellToRepair.children[1].visible = true; } } function endGame() { gameOver = true; var playerProgress = Math.max(0, 15 - player.row); // Player progress from starting position var aiProgress = Math.max(0, aiPlayer.row + 1); // AI progress from starting position if (playerProgress > aiProgress) { LK.showYouWin(); } else if (aiProgress > playerProgress) { LK.showGameOver(); } else { LK.showGameOver(); // Tie goes to AI } } // Initialize first turn - only after main game is initialized if (gameState === 'playing') { if (currentTurn === 'player') { turnText.setText('Your Turn'); turnText.visible = true; instructionText.visible = true; highlightValidMoves(player); } else { turnText.setText('AI Turn'); turnText.visible = true; instructionText.visible = false; LK.setTimeout(function () { makeAIMove(); }, 1000); } updateProgressText(); } game.down = function (x, y, obj) { if (gameState !== 'playing') return; if (gameOver || turnInProgress || currentTurn !== 'player') return; // Find which cell was clicked var clickedRow = Math.floor((y - gameStartY + CELL_SIZE / 2) / CELL_SIZE); var clickedCol = Math.floor((x - gameStartX + CELL_SIZE / 2) / CELL_SIZE); if (clickedRow < 0 || clickedRow >= GRID_ROWS || clickedCol < 0 || clickedCol >= GRID_COLS) { return; } if (isValidMove(player.row, player.col, clickedRow, clickedCol)) { executeMove(player, clickedRow, clickedCol); } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var AIPlayer = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('ai', {
anchorX: 0.5,
anchorY: 0.5
});
self.row = -1; // AI starts above the bridge
self.col = 3; // Center column of 7-column grid
self.isAI = true;
self.moveTo = function (newRow, newCol) {
self.row = newRow;
self.col = newCol;
var targetX = gameStartX + newCol * CELL_SIZE;
var targetY = gameStartY + newRow * CELL_SIZE;
tween(self, {
x: targetX,
y: targetY
}, {
duration: 300
});
LK.getSound('move').play();
};
return self;
});
var CharacterSelectionButton = Container.expand(function (characterType, x, y) {
var self = Container.call(this);
var button = self.attachAsset('selectionButton', {
anchorX: 0.5,
anchorY: 0.5
});
var character = self.attachAsset(characterType, {
anchorX: 0.5,
anchorY: 0.5
});
self.x = x;
self.y = y;
self.characterType = characterType;
self.selected = false;
self.highlight = function () {
tween(button, {
tint: 0xFFFFAA
}, {
duration: 200
});
};
self.unhighlight = function () {
tween(button, {
tint: 0xFFFFFF
}, {
duration: 200
});
};
self.select = function () {
self.selected = true;
tween(button, {
tint: 0x00FF00
}, {
duration: 200
});
};
self.down = function (x, y, obj) {
selectCharacter(self.characterType);
};
return self;
});
var GridCell = Container.expand(function () {
var self = Container.call(this);
var border = self.attachAsset('gridBorder', {
anchorX: 0.5,
anchorY: 0.5
});
var cell = self.attachAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5
});
self.isBroken = false;
self.isRevealed = false;
self.row = 0;
self.col = 0;
self.revealBroken = function () {
if (self.isBroken && !self.isRevealed) {
cell.visible = false;
var brokenGraphics = self.attachAsset('brokenGlass', {
anchorX: 0.5,
anchorY: 0.5
});
self.isRevealed = true;
LK.getSound('break').play();
tween(brokenGraphics, {
alpha: 0.8
}, {
duration: 300
});
}
};
self.highlight = function () {
tween(cell, {
tint: 0xFFFFAA
}, {
duration: 200
});
};
self.unhighlight = function () {
tween(cell, {
tint: 0xFFFFFF
}, {
duration: 200
});
};
return self;
});
var Player = Container.expand(function (characterType) {
var self = Container.call(this);
var assetName = characterType || 'player';
var graphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
self.row = 15; // Player starts below the bridge
self.col = 3; // Center column of 7-column grid
self.isAI = false;
self.characterType = assetName;
self.moveTo = function (newRow, newCol) {
self.row = newRow;
self.col = newCol;
var targetX = gameStartX + newCol * CELL_SIZE;
var targetY = gameStartY + newRow * CELL_SIZE;
tween(self, {
x: targetX,
y: targetY
}, {
duration: 300
});
LK.getSound('move').play();
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x001122
});
/****
* Game Code
****/
var GRID_ROWS = 15;
var GRID_COLS = 7; // Expanded from 5 to 7 columns for wider gameplay area
var CELL_SIZE = 120; // Increased cell size for better mobile experience
var gameStartX = (2048 - GRID_COLS * CELL_SIZE) / 2 + CELL_SIZE / 2;
var totalGameHeight = CELL_SIZE + GRID_ROWS * CELL_SIZE + CELL_SIZE + 100; // AI start area + bridge + player start area + padding
var gameStartY = (2732 - totalGameHeight) / 2 + CELL_SIZE; // Center vertically and account for AI start area
var startingAreaY = gameStartY - CELL_SIZE - 50; // Starting area above bridge for AI
var endingAreaY = gameStartY + GRID_ROWS * CELL_SIZE + 50; // Starting area below bridge for player
var grid = [];
var brokenPanels = [];
var player;
var aiPlayer;
var currentTurn = 'player';
var gameOver = false;
var turnInProgress = false;
var gunDisplay; // Gun display for showing turn order
// Previous position tracking for broken glass restoration
var playerPreviousPosition = {
row: 15,
col: 3
}; // Player's previous position
var aiPreviousPosition = {
row: -1,
col: 3
}; // AI's previous position
// Character selection variables
var gameState = 'characterSelection'; // 'characterSelection', 'weaponRotation', or 'playing'
var selectedCharacter = null;
var characterOptions = [];
var characterSelectionUI = [];
// Weapon rotation variables
var weaponRotationGun;
var rotationTurns = 0;
var maxRotationTurns = 0;
var rotationSpeed = 100; // Initial rotation speed in ms
var rotationTimer = null;
var whoStarts = 'player'; // Will be determined by weapon rotation
// Lighting variables
var leftLight;
var rightLight;
var lightingInitialized = false;
// Street light variables
var streetLights = [];
var streetLightBulbs = [];
// Initialize character selection screen
function initCharacterSelection() {
var titleText = new Text2('Choose Your Character', {
size: 120,
fill: 0xFFFFFF
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 2048 / 2;
titleText.y = 400;
game.addChild(titleText);
characterSelectionUI.push(titleText);
// Add shine effect to title text
tween(titleText, {
tint: 0xFFDD00
}, {
duration: 1500,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(titleText, {
tint: 0xFFFFFF
}, {
duration: 1500,
easing: tween.easeInOut
});
}
});
var characters = [{
type: 'player',
name: 'Frog'
}, {
type: 'playerBlue',
name: 'Potato'
}, {
type: 'playerRed',
name: 'Cat'
}, {
type: 'playerPurple',
name: 'Mushroom'
}];
var startX = 2048 / 2 - (characters.length - 1) * 150;
for (var i = 0; i < characters.length; i++) {
var button = new CharacterSelectionButton(characters[i].type, startX + i * 300, 2732 / 2);
game.addChild(button);
characterOptions.push(button);
characterSelectionUI.push(button);
var nameText = new Text2(characters[i].name, {
size: 60,
fill: 0xFFFFFF
});
nameText.anchor.set(0.5, 0.5);
nameText.x = button.x;
nameText.y = button.y + 150;
game.addChild(nameText);
characterSelectionUI.push(nameText);
// Add shine effect to character selection buttons
var delay = i * 200;
LK.setTimeout(function (btn, txt) {
return function () {
tween(btn, {
tint: 0xFFDD88
}, {
duration: 1200,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(btn, {
tint: 0xFFFFFF
}, {
duration: 1200,
easing: tween.easeInOut
});
}
});
tween(txt, {
tint: 0xFFDD88
}, {
duration: 1200,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(txt, {
tint: 0xFFFFFF
}, {
duration: 1200,
easing: tween.easeInOut
});
}
});
};
}(button, nameText), delay);
}
}
function selectCharacter(characterType) {
selectedCharacter = characterType;
// Clear character selection UI
for (var i = 0; i < characterSelectionUI.length; i++) {
characterSelectionUI[i].destroy();
}
characterSelectionUI = [];
characterOptions = [];
// Start weapon rotation to determine who goes first
startWeaponRotation();
}
function startWeaponRotation() {
gameState = 'weaponRotation';
// Create title text for weapon rotation
var rotationTitle = new Text2('Spinning to decide who starts...', {
size: 100,
fill: 0xFFFFFF
});
rotationTitle.anchor.set(0.5, 0.5);
rotationTitle.x = 2048 / 2;
rotationTitle.y = 500;
game.addChild(rotationTitle);
// Create weapon display for rotation
weaponRotationGun = LK.getAsset('gun', {
anchorX: 0.5,
anchorY: 0.5
});
weaponRotationGun.x = 2048 / 2;
weaponRotationGun.y = 2732 / 2;
weaponRotationGun.scaleX = 1.5;
weaponRotationGun.scaleY = 1.5;
game.addChild(weaponRotationGun);
// Create player indicators
var playerIndicator = new Text2('YOU', {
size: 80,
fill: 0x00FF00
});
playerIndicator.anchor.set(0.5, 0.5);
playerIndicator.x = 2048 / 2;
playerIndicator.y = 2732 / 2 - 300;
game.addChild(playerIndicator);
var aiIndicator = new Text2('AI', {
size: 80,
fill: 0xFF0000
});
aiIndicator.anchor.set(0.5, 0.5);
aiIndicator.x = 2048 / 2;
aiIndicator.y = 2732 / 2 + 300;
game.addChild(aiIndicator);
// Set random number of turns (5-10)
maxRotationTurns = Math.floor(Math.random() * 6) + 5; // 5 to 10 turns
rotationTurns = 0;
rotationSpeed = 100;
// Start rotation
rotateWeapon();
}
function rotateWeapon() {
// Rotate gun 180 degrees
tween(weaponRotationGun, {
rotation: weaponRotationGun.rotation + Math.PI
}, {
duration: rotationSpeed,
easing: tween.easeInOut,
onFinish: function onFinish() {
rotationTurns++;
// Determine who the gun is pointing at
var normalizedRotation = weaponRotationGun.rotation % (2 * Math.PI);
if (normalizedRotation < 0) normalizedRotation += 2 * Math.PI;
// Gun points up (to player) when rotation is around -π/2 or 3π/2
// Gun points down (to AI) when rotation is around π/2
var pointingUp = normalizedRotation > Math.PI * 1.25 && normalizedRotation < Math.PI * 1.75 || normalizedRotation > Math.PI * 0.25 && normalizedRotation < Math.PI * 0.75;
whoStarts = pointingUp ? 'ai' : 'player';
if (rotationTurns >= maxRotationTurns) {
// Stop rotation and show result
finishWeaponRotation();
} else {
// Continue rotation but slow down gradually
rotationSpeed += 50; // Increase duration to slow down
rotateWeapon();
}
}
});
}
function finishWeaponRotation() {
// Show who starts
var resultText = new Text2(whoStarts === 'player' ? 'You start!' : 'AI starts!', {
size: 120,
fill: whoStarts === 'player' ? 0x00FF00 : 0xFF0000
});
resultText.anchor.set(0.5, 0.5);
resultText.x = 2048 / 2;
resultText.y = 800;
game.addChild(resultText);
// Flash the result
tween(resultText, {
tint: 0xFFFFAA
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(resultText, {
tint: 0xFFFFFF
}, {
duration: 500,
easing: tween.easeInOut
});
}
});
// Wait 2 seconds then start the main game
LK.setTimeout(function () {
// Clear weapon rotation UI
game.removeChild(weaponRotationGun);
// Clear all children to remove rotation UI
while (game.children.length > 0) {
game.children[0].destroy();
}
// Set initial turn based on weapon rotation result
currentTurn = whoStarts;
// Initialize the main game
initMainGame();
gameState = 'playing';
}, 2000);
}
function initMainGame() {
// Initialize grid
for (var row = 0; row < GRID_ROWS; row++) {
grid[row] = [];
for (var col = 0; col < GRID_COLS; col++) {
var cell = new GridCell();
cell.row = row;
cell.col = col;
cell.x = gameStartX + col * CELL_SIZE;
cell.y = gameStartY + row * CELL_SIZE;
// Randomly assign broken panels (about 20% chance)
if (Math.random() < 0.2) {
cell.isBroken = true;
brokenPanels.push({
row: row,
col: col
});
}
grid[row][col] = cell;
game.addChild(cell);
// Broken windows will only be revealed when stepped on
}
}
// Create starting areas
var aiStartArea = new GridCell();
aiStartArea.x = gameStartX + 3 * CELL_SIZE; // Center column of 7-column grid
aiStartArea.y = startingAreaY;
game.addChild(aiStartArea);
var playerStartArea = new GridCell();
playerStartArea.x = gameStartX + 3 * CELL_SIZE; // Center column of 7-column grid
playerStartArea.y = endingAreaY;
game.addChild(playerStartArea);
// Create players
player = new Player(selectedCharacter);
player.row = 15; // Start below the bridge
player.x = gameStartX + player.col * CELL_SIZE;
player.y = endingAreaY;
game.addChild(player);
aiPlayer = new AIPlayer();
aiPlayer.row = -1; // Start above the bridge
aiPlayer.x = gameStartX + aiPlayer.col * CELL_SIZE;
aiPlayer.y = startingAreaY;
game.addChild(aiPlayer);
// Initialize previous positions
playerPreviousPosition = {
row: player.row,
col: player.col
};
aiPreviousPosition = {
row: aiPlayer.row,
col: aiPlayer.col
};
// Initialize lighting system
if (!lightingInitialized) {
leftLight = LK.getAsset('leftLight', {
anchorX: 1.0,
anchorY: 0.5,
alpha: 0.15
});
leftLight.x = 0;
leftLight.y = 2732 / 2;
game.addChild(leftLight);
rightLight = LK.getAsset('rightLight', {
anchorX: 0.0,
anchorY: 0.5,
alpha: 0.15
});
rightLight.x = 2048;
rightLight.y = 2732 / 2;
game.addChild(rightLight);
// Start lighting animation
animateLighting();
lightingInitialized = true;
// Add street lights on 4 corners
var cornerPositions = [{
x: 200,
y: 300
},
// Top left corner
{
x: 2048 - 200,
y: 300
},
// Top right corner
{
x: 200,
y: 2732 - 300
},
// Bottom left corner
{
x: 2048 - 200,
y: 2732 - 300
} // Bottom right corner
];
for (var i = 0; i < cornerPositions.length; i++) {
// Create street light pole
var streetLight = LK.getAsset('streetLight', {
anchorX: 0.5,
anchorY: 1.0
});
streetLight.x = cornerPositions[i].x;
streetLight.y = cornerPositions[i].y;
game.addChild(streetLight);
streetLights.push(streetLight);
// Create street light bulb
var streetLightBulb = LK.getAsset('streetLightBulb', {
anchorX: 0.5,
anchorY: 0.5
});
streetLightBulb.x = cornerPositions[i].x;
streetLightBulb.y = cornerPositions[i].y - 180; // Position bulb at top of pole
game.addChild(streetLightBulb);
streetLightBulbs.push(streetLightBulb);
}
}
// Gun display for game order
gunDisplay = LK.getAsset('gun', {
anchorX: 0.5,
anchorY: 0.5
});
gunDisplay.x = 200; // Middle left position
gunDisplay.y = 2732 / 2; // Vertically centered
game.addChild(gunDisplay);
// Start playing theme music
LK.playMusic('themeMusic');
// Add shine effect to all elements
LK.setTimeout(function () {
addShineToAllElements();
}, 1000); // Delay to ensure all elements are properly initialized
// Automatically reduce obstacles after initialization
LK.setTimeout(function () {
reduceBrokenWindows(20); // Remove 20% of broken windows at start
}, 1500);
}
// Start with character selection
initCharacterSelection();
// Gun barrel rotation to show turn order
function updateGunDirection() {
if (!gunDisplay) return; // Don't update if gun not initialized yet
if (currentTurn === 'player') {
// Point gun up towards AI
gunDisplay.rotation = -Math.PI / 2;
} else {
// Point gun down towards player
gunDisplay.rotation = Math.PI / 2;
}
}
// Initialize gun direction based on weapon rotation result
updateGunDirection();
// UI Elements
var turnText = new Text2('Your Turn', {
size: 80,
fill: 0xFFFFFF
});
turnText.anchor.set(0.5, 0);
turnText.visible = false;
LK.gui.top.addChild(turnText);
var instructionText = new Text2('Tap adjacent cell to move', {
size: 60,
fill: 0xCCCCCC
});
instructionText.anchor.set(0.5, 0);
instructionText.y = 100;
instructionText.visible = false;
LK.gui.top.addChild(instructionText);
var progressText = new Text2('Progress: You 0/15 - AI 0/15', {
size: 50,
fill: 0xFFFFFF
});
progressText.anchor.set(0.5, 1);
LK.gui.bottom.addChild(progressText);
function updateProgressText() {
var playerProgress = Math.max(0, 15 - player.row); // Player progress from starting position
var aiProgress = Math.max(0, aiPlayer.row + 1); // AI progress from starting position
progressText.setText('Progress: You ' + playerProgress + '/15 - AI ' + aiProgress + '/15');
}
function isValidMove(fromRow, fromCol, toRow, toCol) {
// Special case: moving from starting areas onto bridge
if (fromRow === -1 && toRow === 0) {
// AI entering bridge from above
return toCol >= 0 && toCol < GRID_COLS && Math.abs(toCol - fromCol) <= 1;
}
if (fromRow === 15 && toRow === 14) {
// Player entering bridge from below
return toCol >= 0 && toCol < GRID_COLS && Math.abs(toCol - fromCol) <= 1;
}
// Check bounds for normal bridge movement
if (toRow < 0 || toRow >= GRID_ROWS || toCol < 0 || toCol >= GRID_COLS) {
return false;
}
// Check if adjacent (including diagonal)
var rowDiff = Math.abs(toRow - fromRow);
var colDiff = Math.abs(toCol - fromCol);
if (rowDiff > 1 || colDiff > 1 || rowDiff === 0 && colDiff === 0) {
return false;
}
return true;
}
// Calculate optimal path for player (moving up to reach top)
function calculatePlayerPath(startRow, startCol, targetRow) {
var openSet = [{
row: startRow,
col: startCol,
gScore: 0,
fScore: Math.abs(startRow - targetRow),
parent: null
}];
var closedSet = [];
var visited = {};
while (openSet.length > 0) {
// Find node with lowest fScore
var current = openSet[0];
var currentIndex = 0;
for (var i = 1; i < openSet.length; i++) {
if (openSet[i].fScore < current.fScore) {
current = openSet[i];
currentIndex = i;
}
}
// Remove current from openSet
openSet.splice(currentIndex, 1);
// Add to closed set
var key = current.row + ',' + current.col;
closedSet.push(current);
visited[key] = true;
// Check if we reached target row
if (current.row <= targetRow) {
// Reconstruct path
var path = [];
var node = current;
while (node !== null) {
path.unshift({
row: node.row,
col: node.col
});
node = node.parent;
}
return path;
}
// Explore neighbors
for (var dRow = -1; dRow <= 1; dRow++) {
for (var dCol = -1; dCol <= 1; dCol++) {
if (dRow === 0 && dCol === 0) continue;
var newRow = current.row + dRow;
var newCol = current.col + dCol;
var neighborKey = newRow + ',' + newCol;
// Skip if already visited or invalid move
if (visited[neighborKey] || !isValidMove(current.row, current.col, newRow, newCol)) {
continue;
}
// Skip if we know it's broken glass
if (newRow >= 0 && newRow < GRID_ROWS && grid[newRow] && grid[newRow][newCol] && grid[newRow][newCol].isRevealed && grid[newRow][newCol].isBroken) {
continue;
}
// Calculate scores
var gScore = current.gScore + 1;
// Add penalty for potentially broken glass (unexplored cells)
var heuristic = Math.abs(newRow - targetRow) + Math.abs(3 - newCol); // Prefer center column
if (newRow >= 0 && newRow < GRID_ROWS && grid[newRow] && grid[newRow][newCol] && !grid[newRow][newCol].isRevealed) {
heuristic += 1; // Small penalty for unknown cells
}
var fScore = gScore + heuristic;
// Check if this path to neighbor is better
var existingNode = null;
for (var j = 0; j < openSet.length; j++) {
if (openSet[j].row === newRow && openSet[j].col === newCol) {
existingNode = openSet[j];
break;
}
}
if (existingNode === null || gScore < existingNode.gScore) {
var neighbor = {
row: newRow,
col: newCol,
gScore: gScore,
fScore: fScore,
parent: current
};
if (existingNode === null) {
openSet.push(neighbor);
} else {
existingNode.gScore = gScore;
existingNode.fScore = fScore;
existingNode.parent = current;
}
}
}
}
}
return null; // No path found
}
function highlightValidMoves(currentPlayer) {
// Check if grid is initialized
if (!grid || grid.length === 0) return;
// Unhighlight all cells first
for (var row = 0; row < GRID_ROWS; row++) {
for (var col = 0; col < GRID_COLS; col++) {
if (grid[row] && grid[row][col]) {
grid[row][col].unhighlight();
}
}
}
// Calculate optimal path for player guidance
var optimalPath = null;
if (!currentPlayer.isAI) {
optimalPath = calculatePlayerPath(currentPlayer.row, currentPlayer.col, 0);
}
// Highlight valid moves with path guidance
for (var dRow = -1; dRow <= 1; dRow++) {
for (var dCol = -1; dCol <= 1; dCol++) {
if (dRow === 0 && dCol === 0) continue;
var newRow = currentPlayer.row + dRow;
var newCol = currentPlayer.col + dCol;
if (isValidMove(currentPlayer.row, currentPlayer.col, newRow, newCol)) {
if (grid[newRow] && grid[newRow][newCol]) {
// Check if this move is part of optimal path
var isOptimalMove = false;
if (optimalPath && optimalPath.length > 1) {
for (var p = 1; p < optimalPath.length; p++) {
if (optimalPath[p].row === newRow && optimalPath[p].col === newCol) {
isOptimalMove = true;
break;
}
}
}
if (isOptimalMove) {
// Highlight optimal moves more prominently
tween(grid[newRow][newCol], {
tint: 0x00FF88 // Green for optimal path
}, {
duration: 200
});
} else {
grid[newRow][newCol].highlight();
}
}
}
}
}
}
// AI pathfinding algorithm using A* search
function calculateAIPath(startRow, startCol, targetRow) {
var openSet = [{
row: startRow,
col: startCol,
gScore: 0,
fScore: Math.abs(targetRow - startRow),
parent: null
}];
var closedSet = [];
var visited = {};
while (openSet.length > 0) {
// Find node with lowest fScore
var current = openSet[0];
var currentIndex = 0;
for (var i = 1; i < openSet.length; i++) {
if (openSet[i].fScore < current.fScore) {
current = openSet[i];
currentIndex = i;
}
}
// Remove current from openSet
openSet.splice(currentIndex, 1);
// Add to closed set
var key = current.row + ',' + current.col;
closedSet.push(current);
visited[key] = true;
// Check if we reached target row
if (current.row >= targetRow) {
// Reconstruct path
var path = [];
var node = current;
while (node !== null) {
path.unshift({
row: node.row,
col: node.col
});
node = node.parent;
}
return path;
}
// Explore neighbors
for (var dRow = -1; dRow <= 1; dRow++) {
for (var dCol = -1; dCol <= 1; dCol++) {
if (dRow === 0 && dCol === 0) continue;
var newRow = current.row + dRow;
var newCol = current.col + dCol;
var neighborKey = newRow + ',' + newCol;
// Skip if already visited or invalid move
if (visited[neighborKey] || !isValidMove(current.row, current.col, newRow, newCol)) {
continue;
}
// Skip if we know it's broken glass
if (newRow >= 0 && newRow < GRID_ROWS && grid[newRow] && grid[newRow][newCol] && grid[newRow][newCol].isRevealed && grid[newRow][newCol].isBroken) {
continue;
}
// Calculate scores
var gScore = current.gScore + 1;
// Add penalty for potentially broken glass (unexplored cells)
var heuristic = Math.abs(targetRow - newRow) + Math.abs(3 - newCol); // Prefer center column
if (newRow >= 0 && newRow < GRID_ROWS && grid[newRow] && grid[newRow][newCol] && !grid[newRow][newCol].isRevealed) {
heuristic += 2; // Small penalty for unknown cells
}
var fScore = gScore + heuristic;
// Check if this path to neighbor is better
var existingNode = null;
for (var j = 0; j < openSet.length; j++) {
if (openSet[j].row === newRow && openSet[j].col === newCol) {
existingNode = openSet[j];
break;
}
}
if (existingNode === null || gScore < existingNode.gScore) {
var neighbor = {
row: newRow,
col: newCol,
gScore: gScore,
fScore: fScore,
parent: current
};
if (existingNode === null) {
openSet.push(neighbor);
} else {
existingNode.gScore = gScore;
existingNode.fScore = fScore;
existingNode.parent = current;
}
}
}
}
}
return null; // No path found
}
function makeAIMove() {
if (gameOver || turnInProgress) return;
var validMoves = [];
// Find all valid moves for AI
for (var dRow = -1; dRow <= 1; dRow++) {
for (var dCol = -1; dCol <= 1; dCol++) {
if (dRow === 0 && dCol === 0) continue;
var newRow = aiPlayer.row + dRow;
var newCol = aiPlayer.col + dCol;
if (isValidMove(aiPlayer.row, aiPlayer.col, newRow, newCol)) {
validMoves.push({
row: newRow,
col: newCol
});
}
}
}
if (validMoves.length === 0) {
endGame();
return;
}
// Use AI pathfinding to determine best move
var bestPath = calculateAIPath(aiPlayer.row, aiPlayer.col, 14); // Target: reach bottom of bridge
var chosenMove;
if (bestPath && bestPath.length > 1) {
// Follow the calculated path
chosenMove = bestPath[1]; // Next step in optimal path
} else {
// Fallback to heuristic-based strategy if no path found
validMoves.sort(function (a, b) {
// Prioritize avoiding known broken panels
var aIsBroken = a.row >= 0 && a.row < GRID_ROWS && grid[a.row][a.col].isRevealed && grid[a.row][a.col].isBroken;
var bIsBroken = b.row >= 0 && b.row < GRID_ROWS && grid[b.row][b.col].isRevealed && grid[b.row][b.col].isBroken;
if (aIsBroken && !bIsBroken) return 1;
if (!aIsBroken && bIsBroken) return -1;
// Prefer moves toward center columns (safer)
var aCenterDist = Math.abs(a.col - 3);
var bCenterDist = Math.abs(b.col - 3);
if (aCenterDist !== bCenterDist) return aCenterDist - bCenterDist;
// Prefer moving down (higher row numbers)
return b.row - a.row;
});
chosenMove = validMoves[0];
}
executeMove(aiPlayer, chosenMove.row, chosenMove.col);
}
function executeMove(currentPlayer, newRow, newCol) {
turnInProgress = true;
// Store current position as previous position before moving
if (currentPlayer.isAI) {
aiPreviousPosition = {
row: currentPlayer.row,
col: currentPlayer.col
};
} else {
playerPreviousPosition = {
row: currentPlayer.row,
col: currentPlayer.col
};
}
currentPlayer.moveTo(newRow, newCol);
LK.setTimeout(function () {
var targetCell = grid[newRow][newCol];
if (targetCell.isBroken) {
targetCell.revealBroken();
// Return to previous position when stepping on broken glass
LK.setTimeout(function () {
if (currentPlayer.isAI) {
currentPlayer.moveTo(aiPreviousPosition.row, aiPreviousPosition.col);
} else {
currentPlayer.moveTo(playerPreviousPosition.row, playerPreviousPosition.col);
}
}, 500); // Wait for broken glass animation to complete
}
updateProgressText();
checkGameEnd();
if (!gameOver) {
switchTurn();
}
turnInProgress = false;
}, 400);
}
function switchTurn() {
currentTurn = currentTurn === 'player' ? 'ai' : 'player';
updateGunDirection(); // Update gun barrel direction
// Check if opponent's path is blocked and reduce broken windows if needed
var opponent = currentTurn === 'player' ? aiPlayer : player;
if (isPathBlocked(opponent)) {
// Reduce broken windows by 50% when opponent's path is blocked
reduceBrokenWindows(50);
}
if (currentTurn === 'player') {
turnText.setText('Your Turn');
turnText.visible = true;
instructionText.visible = true;
highlightValidMoves(player);
} else {
turnText.setText('AI Turn');
turnText.visible = true;
instructionText.visible = false;
// Unhighlight all cells
for (var row = 0; row < GRID_ROWS; row++) {
for (var col = 0; col < GRID_COLS; col++) {
grid[row][col].unhighlight();
}
}
LK.setTimeout(function () {
makeAIMove();
}, 1000);
}
}
function checkGameEnd() {
var playerProgress = Math.max(0, 15 - player.row); // Player progress from starting position
var aiProgress = Math.max(0, aiPlayer.row + 1); // AI progress from starting position
// Check if player reached the end (top of bridge)
if (player.row <= 0) {
LK.showYouWin();
gameOver = true;
return;
}
// Check if AI reached the end (bottom of bridge)
if (aiPlayer.row >= 14) {
LK.showGameOver();
gameOver = true;
return;
}
// Check if no valid moves available
var playerHasMoves = false;
var aiHasMoves = false;
for (var dRow = -1; dRow <= 1; dRow++) {
for (var dCol = -1; dCol <= 1; dCol++) {
if (dRow === 0 && dCol === 0) continue;
var playerNewRow = player.row + dRow;
var playerNewCol = player.col + dCol;
if (isValidMove(player.row, player.col, playerNewRow, playerNewCol)) {
playerHasMoves = true;
}
var aiNewRow = aiPlayer.row + dRow;
var aiNewCol = aiPlayer.col + dCol;
if (isValidMove(aiPlayer.row, aiPlayer.col, aiNewRow, aiNewCol)) {
aiHasMoves = true;
}
}
}
if (!playerHasMoves && !aiHasMoves) {
endGame();
}
}
function animateLighting() {
if (!leftLight || !rightLight) return;
// Animate left light
tween(leftLight, {
alpha: 0.25
}, {
duration: 2000,
yoyo: true,
repeat: -1,
ease: 'sine'
});
// Animate right light with offset timing
LK.setTimeout(function () {
tween(rightLight, {
alpha: 0.25
}, {
duration: 2000,
yoyo: true,
repeat: -1,
ease: 'sine'
});
}, 500);
}
function addShineToAllElements() {
// Add shine effect to all grid cells
for (var row = 0; row < GRID_ROWS; row++) {
for (var col = 0; col < GRID_COLS; col++) {
if (grid[row] && grid[row][col]) {
var delay = (row * GRID_COLS + col) * 50; // Stagger the animations
LK.setTimeout(function (cell) {
return function () {
tween(cell, {
tint: 0xFFFFAA
}, {
duration: 1500,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(cell, {
tint: 0xFFFFFF
}, {
duration: 1500,
easing: tween.easeInOut
});
}
});
};
}(grid[row][col]), delay);
}
}
}
// Add shine effect to players
if (player) {
tween(player, {
tint: 0xFFDD88
}, {
duration: 2000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(player, {
tint: 0xFFFFFF
}, {
duration: 2000,
easing: tween.easeInOut
});
}
});
}
if (aiPlayer) {
tween(aiPlayer, {
tint: 0x88DDFF
}, {
duration: 2000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(aiPlayer, {
tint: 0xFFFFFF
}, {
duration: 2000,
easing: tween.easeInOut
});
}
});
}
// Add shine effect to gun display
if (gunDisplay) {
tween(gunDisplay, {
tint: 0xFFCC00
}, {
duration: 1800,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(gunDisplay, {
tint: 0xFFFFFF
}, {
duration: 1800,
easing: tween.easeInOut
});
}
});
}
// Add shine effect to lighting
if (leftLight) {
tween(leftLight, {
tint: 0xFFFFCC
}, {
duration: 2500,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(leftLight, {
tint: 0xFFFFFF
}, {
duration: 2500,
easing: tween.easeInOut
});
}
});
}
if (rightLight) {
tween(rightLight, {
tint: 0xFFFFCC
}, {
duration: 2500,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(rightLight, {
tint: 0xFFFFFF
}, {
duration: 2500,
easing: tween.easeInOut
});
}
});
}
// Add shine effect to street lights
for (var i = 0; i < streetLights.length; i++) {
var delay = i * 300;
LK.setTimeout(function (light, bulb) {
return function () {
tween(light, {
tint: 0xAAAADD
}, {
duration: 2000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(light, {
tint: 0xFFFFFF
}, {
duration: 2000,
easing: tween.easeInOut
});
}
});
tween(bulb, {
tint: 0xFFFFCC,
alpha: 0.9
}, {
duration: 1800,
yoyo: true,
repeat: -1,
easing: tween.easeInOut
});
};
}(streetLights[i], streetLightBulbs[i]), delay);
}
}
function isPathBlocked(currentPlayer) {
var targetRow = currentPlayer.isAI ? 14 : 0;
var path = currentPlayer.isAI ? calculateAIPath(currentPlayer.row, currentPlayer.col, targetRow) : calculatePlayerPath(currentPlayer.row, currentPlayer.col, targetRow);
return path === null || path.length <= 1;
}
function reduceBrokenWindows(percentage) {
var brokenCells = [];
// Collect all currently broken and revealed cells
for (var row = 0; row < GRID_ROWS; row++) {
for (var col = 0; col < GRID_COLS; col++) {
if (grid[row] && grid[row][col] && grid[row][col].isBroken && grid[row][col].isRevealed) {
brokenCells.push(grid[row][col]);
}
}
}
// Calculate how many to repair
var numberToRepair = Math.floor(brokenCells.length * (percentage / 100));
// Randomly select cells to repair
for (var i = 0; i < numberToRepair && brokenCells.length > 0; i++) {
var randomIndex = Math.floor(Math.random() * brokenCells.length);
var cellToRepair = brokenCells[randomIndex];
brokenCells.splice(randomIndex, 1);
// Repair the cell - hide broken glass and show normal cell
cellToRepair.isBroken = false;
cellToRepair.isRevealed = false;
// Remove all children (broken glass graphics)
while (cellToRepair.children.length > 2) {
cellToRepair.children[cellToRepair.children.length - 1].destroy();
}
// Make normal cell visible again
cellToRepair.children[1].visible = true;
}
}
function endGame() {
gameOver = true;
var playerProgress = Math.max(0, 15 - player.row); // Player progress from starting position
var aiProgress = Math.max(0, aiPlayer.row + 1); // AI progress from starting position
if (playerProgress > aiProgress) {
LK.showYouWin();
} else if (aiProgress > playerProgress) {
LK.showGameOver();
} else {
LK.showGameOver(); // Tie goes to AI
}
}
// Initialize first turn - only after main game is initialized
if (gameState === 'playing') {
if (currentTurn === 'player') {
turnText.setText('Your Turn');
turnText.visible = true;
instructionText.visible = true;
highlightValidMoves(player);
} else {
turnText.setText('AI Turn');
turnText.visible = true;
instructionText.visible = false;
LK.setTimeout(function () {
makeAIMove();
}, 1000);
}
updateProgressText();
}
game.down = function (x, y, obj) {
if (gameState !== 'playing') return;
if (gameOver || turnInProgress || currentTurn !== 'player') return;
// Find which cell was clicked
var clickedRow = Math.floor((y - gameStartY + CELL_SIZE / 2) / CELL_SIZE);
var clickedCol = Math.floor((x - gameStartX + CELL_SIZE / 2) / CELL_SIZE);
if (clickedRow < 0 || clickedRow >= GRID_ROWS || clickedCol < 0 || clickedCol >= GRID_COLS) {
return;
}
if (isValidMove(player.row, player.col, clickedRow, clickedCol)) {
executeMove(player, clickedRow, clickedCol);
}
};
old 6-shooter pistol. In-Game asset
square glass top view. In-Game asset
broken glass
frog top view. In-Game asset
potato top view. In-Game asset
mushroom top view. In-Game asset
cat top view. In-Game asset
Robot Potato. In-Game asset
street light. In-Game asset
street light. In-Game asset
burst of light in all directions. In-Game asset