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