User prompt
If you come across broken glass, go back to the previous point.
User prompt
horizontally expanding everything except weapons and players
User prompt
hide top text
User prompt
Place a gun image in the middle left of the screen. The gun barrel will show the game order.
User prompt
adjust the grid to the new dimensions and center the screen vertically
User prompt
center everything on the screen vertically
User prompt
creating a starting point for players before the platform
User prompt
Swap player and AI
Code edit (1 edits merged)
Please save this source code
User prompt
Glass Bridge Crossing
Initial prompt
Bridge Crossing Game: AI Briefing This is a turn-based game on a 3x15 grid representing a glass bridge. Your objective is to reach the bottom of the grid, while a human player simultaneously tries to reach the top. The player who makes more vertical progress wins. The Bridge The bridge has 3 columns and 15 rows. Each vertical column contains 0, 1, or 2 broken glass panels. You can't see them initially; they only reveal as broken when a player steps on them. All intact panels look identical. Your Goal Move from the top (row 0) to the bottom (row 14) of the grid. Your score is the number of rows you've advanced vertically. For example, reaching row 5 gives you 5 points. Win by achieving a higher vertical score than the human player. How to Play (Your Turn) You can move to an adjacent square: Down: To any of the three squares in the next row below your current position. Sideways: To an adjacent column within your current row. You cannot move backward (up). Broken Glass Impact: If you step on a broken panel, your current turn ends immediately. You remain on that broken panel until your next turn. Key Information You start above the bridge. The human starts below it. You observe all the human player's moves and any broken panels they reveal. The game ends when one player reaches their target end or neither can move. AI Challenge Focus on strategies for: Pathfinding: Identifying safe routes. Risk vs. Reward: Deciding when to test unknown panels. Information Gathering: Using revealed broken panels (yours and the human's) to inform future moves.
/**** * 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