/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Ball: The player-controlled ball
var Ball = Container.expand(function () {
var self = Container.call(this);
var ballSize = 0;
self.setSize = function (size) {
ballSize = size;
if (self.ballAsset) {
self.removeChild(self.ballAsset);
}
self.ballAsset = self.attachAsset('ball', {
width: ballSize,
height: ballSize,
color: 0x3a8cff,
shape: 'ellipse',
anchorX: 0.5,
anchorY: 0.5
});
};
self.radius = function () {
return ballSize / 2;
};
return self;
});
// Finish: The goal area
var Finish = Container.expand(function () {
var self = Container.call(this);
var finishSize = 0;
self.setSize = function (size) {
finishSize = size;
if (self.finishAsset) {
self.removeChild(self.finishAsset);
}
self.finishAsset = self.attachAsset('finish', {
width: finishSize,
height: finishSize,
color: 0x44de83,
shape: 'ellipse',
anchorX: 0.5,
anchorY: 0.5
});
};
return self;
});
// MazeCell: Represents a single cell in the maze grid
var MazeCell = Container.expand(function () {
var self = Container.call(this);
// Walls: {top, right, bottom, left}
self.walls = [true, true, true, true];
self.visited = false;
self.xIndex = 0;
self.yIndex = 0;
self.size = 0;
self.wallGraphics = [];
// Draws the cell walls
self.draw = function () {
// Remove old wall graphics
for (var i = 0; i < self.wallGraphics.length; i++) {
self.removeChild(self.wallGraphics[i]);
}
self.wallGraphics = [];
var s = self.size;
var wallColor = 0x222222;
var wallThickness = Math.max(8, Math.floor(s * 0.12));
// Top wall
if (self.walls[0]) {
var topWall = LK.getAsset('wall', {
width: s,
height: wallThickness,
color: wallColor,
shape: 'box',
anchorX: 0,
anchorY: 0
});
topWall.x = 0;
topWall.y = 0;
self.addChild(topWall);
self.wallGraphics.push(topWall);
}
// Right wall
if (self.walls[1]) {
var rightWall = LK.getAsset('wall', {
width: wallThickness,
height: s,
color: wallColor,
shape: 'box',
anchorX: 0,
anchorY: 0
});
rightWall.x = s - wallThickness;
rightWall.y = 0;
self.addChild(rightWall);
self.wallGraphics.push(rightWall);
}
// Bottom wall
if (self.walls[2]) {
var bottomWall = LK.getAsset('wall', {
width: s,
height: wallThickness,
color: wallColor,
shape: 'box',
anchorX: 0,
anchorY: 0
});
bottomWall.x = 0;
bottomWall.y = s - wallThickness;
self.addChild(bottomWall);
self.wallGraphics.push(bottomWall);
}
// Left wall
if (self.walls[3]) {
var leftWall = LK.getAsset('wall', {
width: wallThickness,
height: s,
color: wallColor,
shape: 'box',
anchorX: 0,
anchorY: 0
});
leftWall.x = 0;
leftWall.y = 0;
self.addChild(leftWall);
self.wallGraphics.push(leftWall);
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xf7f7f7
});
/****
* Game Code
****/
// Maze parameters
var MAZE_COLS = 18;
var MAZE_ROWS = 24;
var MAZE_MARGIN = 40; // px margin around maze (reduce margin to fit bigger maze)
var mazeCellSize = 0;
var mazeOriginX = 0;
var mazeOriginY = 0;
var maze = [];
var mazeContainer = new Container();
game.addChild(mazeContainer);
// Ball and Finish
var ball = null;
var finish = null;
// Ball physics
var ballPos = {
x: 0,
y: 0
};
var ballVel = {
x: 0,
y: 0
};
var ballMaxSpeed = 32;
var ballAccel = 2.2;
var ballFriction = 0.96;
// Gyro state
var gyro = {
x: 0,
y: 0
};
// Timer
var startTime = 0;
var elapsedTime = 0;
// Final score text removed as per requirements
// Helper: Clamp
function clamp(val, min, max) {
return Math.max(min, Math.min(max, val));
}
// Helper: Shuffle array
function shuffle(arr) {
for (var i = arr.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
return arr;
}
// Maze generation: Recursive backtracker with bias for normal difficulty route
function generateMaze(cols, rows) {
var grid = [];
for (var y = 0; y < rows; y++) {
var row = [];
for (var x = 0; x < cols; x++) {
var cell = new MazeCell();
cell.xIndex = x;
cell.yIndex = y;
cell.size = mazeCellSize;
cell.visited = false;
row.push(cell);
}
grid.push(row);
}
function cellAt(x, y) {
if (x < 0 || y < 0 || x >= cols || y >= rows) return null;
return grid[y][x];
}
var stack = [];
var current = grid[0][0];
current.visited = true;
var visitedCount = 1;
var totalCells = cols * rows;
// Bias: prefer to move generally toward the finish, but not always
var finishX = cols - 1;
var finishY = rows - 1;
while (visitedCount < totalCells) {
// Find unvisited neighbors
var neighbors = [];
var dirs = [[0, -1, 0],
// top
[1, 0, 1],
// right
[0, 1, 2],
// bottom
[-1, 0, 3] // left
];
for (var d = 0; d < 4; d++) {
var nx = current.xIndex + dirs[d][0];
var ny = current.yIndex + dirs[d][1];
var neighbor = cellAt(nx, ny);
if (neighbor && !neighbor.visited) {
neighbors.push({
cell: neighbor,
dir: d
});
}
}
if (neighbors.length > 0) {
// Bias: 60% chance to pick neighbor that is closer to finish, else random
var pick;
if (Math.random() < 0.6) {
// Sort neighbors by distance to finish, pick the closest
neighbors.sort(function (a, b) {
var da = Math.abs(a.cell.xIndex - finishX) + Math.abs(a.cell.yIndex - finishY);
var db = Math.abs(b.cell.xIndex - finishX) + Math.abs(b.cell.yIndex - finishY);
return da - db;
});
// 50% chance to pick the closest, 50% chance to pick second closest (if exists)
if (neighbors.length > 1 && Math.random() < 0.5) {
pick = neighbors[1];
} else {
pick = neighbors[0];
}
} else {
pick = neighbors[Math.floor(Math.random() * neighbors.length)];
}
// Remove wall between current and neighbor
current.walls[pick.dir] = false;
var opp = (pick.dir + 2) % 4;
pick.cell.walls[opp] = false;
stack.push(current);
current = pick.cell;
current.visited = true;
visitedCount++;
} else if (stack.length > 0) {
current = stack.pop();
}
}
// Draw all cells
for (var y = 0; y < rows; y++) {
for (var x = 0; x < cols; x++) {
grid[y][x].draw();
}
}
return grid;
}
// Place maze in center of screen, calculate cell size
function layoutMaze() {
var availW = 2048 - MAZE_MARGIN * 2;
var availH = 2732 - MAZE_MARGIN * 2;
mazeCellSize = Math.floor(Math.min(availW / MAZE_COLS, availH / MAZE_ROWS));
var mazeW = mazeCellSize * MAZE_COLS;
var mazeH = mazeCellSize * MAZE_ROWS;
mazeOriginX = Math.floor((2048 - mazeW) / 2);
mazeOriginY = Math.floor((2732 - mazeH) / 2);
mazeContainer.x = mazeOriginX;
mazeContainer.y = mazeOriginY;
}
// Remove all children from mazeContainer
function clearMaze() {
while (mazeContainer.children.length > 0) {
mazeContainer.removeChild(mazeContainer.children[0]);
}
}
// Place ball at start
function placeBall() {
if (ball) {
ball.destroy();
}
ball = new Ball();
ball.setSize(Math.floor(mazeCellSize * 0.55));
ballPos.x = mazeCellSize / 2;
ballPos.y = mazeCellSize / 2;
ball.x = ballPos.x;
ball.y = ballPos.y;
ballVel.x = 0;
ballVel.y = 0;
mazeContainer.addChild(ball);
}
// Place finish at bottom-right cell
function placeFinish() {
if (finish) {
finish.destroy();
}
finish = new Finish();
finish.setSize(Math.floor(mazeCellSize * 0.55));
// Pick a random cell in the maze that is not the start cell (0,0)
var finishCellX = 0;
var finishCellY = 0;
while (finishCellX === 0 && finishCellY === 0) {
finishCellX = Math.floor(Math.random() * MAZE_COLS);
finishCellY = Math.floor(Math.random() * MAZE_ROWS);
}
var fx = finishCellX * mazeCellSize + mazeCellSize / 2;
var fy = finishCellY * mazeCellSize + mazeCellSize / 2;
finish.x = fx;
finish.y = fy;
mazeContainer.addChild(finish);
// Store finish cell for win check
finish.finishCellX = finishCellX;
finish.finishCellY = finishCellY;
}
// Check collision between ball and maze walls
function resolveBallMazeCollision() {
var r = ball.radius();
var cx = Math.floor(ballPos.x / mazeCellSize);
var cy = Math.floor(ballPos.y / mazeCellSize);
// Clamp to maze bounds
cx = clamp(cx, 0, MAZE_COLS - 1);
cy = clamp(cy, 0, MAZE_ROWS - 1);
var cell = maze[cy][cx];
var px = ballPos.x - cx * mazeCellSize;
var py = ballPos.y - cy * mazeCellSize;
var s = mazeCellSize;
var wallThickness = Math.max(8, Math.floor(s * 0.12));
// Top wall
if (cell.walls[0] && py - r < wallThickness) {
ballPos.y = cy * mazeCellSize + wallThickness + r;
ballVel.y = Math.max(0, ballVel.y);
}
// Right wall
if (cell.walls[1] && px + r > s - wallThickness) {
ballPos.x = cx * mazeCellSize + s - wallThickness - r;
ballVel.x = Math.min(0, ballVel.x);
}
// Bottom wall
if (cell.walls[2] && py + r > s - wallThickness) {
ballPos.y = cy * mazeCellSize + s - wallThickness - r;
ballVel.y = Math.min(0, ballVel.y);
}
// Left wall
if (cell.walls[3] && px - r < wallThickness) {
ballPos.x = cx * mazeCellSize + wallThickness + r;
ballVel.x = Math.max(0, ballVel.x);
}
// Clamp to maze area
ballPos.x = clamp(ballPos.x, r, MAZE_COLS * mazeCellSize - r);
ballPos.y = clamp(ballPos.y, r, MAZE_ROWS * mazeCellSize - r);
}
// Check if ball reached finish
function checkBallFinish() {
// Use the actual finish cell position
var fx = finish.finishCellX * mazeCellSize + mazeCellSize / 2;
var fy = finish.finishCellY * mazeCellSize + mazeCellSize / 2;
var dx = ballPos.x - fx;
var dy = ballPos.y - fy;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < ball.radius() + finish.finishAsset.width / 2 - 6) {
return true;
}
return false;
}
// Start new game
function startGame() {
clearMaze();
layoutMaze();
maze = generateMaze(MAZE_COLS, MAZE_ROWS);
// Add all cells to container
for (var y = 0; y < MAZE_ROWS; y++) {
for (var x = 0; x < MAZE_COLS; x++) {
var cell = maze[y][x];
cell.x = x * mazeCellSize;
cell.y = y * mazeCellSize;
mazeContainer.addChild(cell);
}
}
placeFinish();
placeBall();
startTime = Date.now();
elapsedTime = 0;
}
// Gyroscope/motion support
function handleDeviceMotion(event) {
// event.accelerationIncludingGravity.{x,y,z}
// On iOS, x is left/right, y is up/down, z is toward/away
// On Android, axes may be swapped, so we allow both
var ax = 0,
ay = 0;
if (event.accelerationIncludingGravity) {
ax = event.accelerationIncludingGravity.x || 0;
ay = event.accelerationIncludingGravity.y || 0;
}
// Heuristic: On portrait, invert y for natural tilt
gyro.x = clamp(ax, -6, 6);
gyro.y = clamp(-ay, -6, 6);
}
if (typeof window !== "undefined" && window.addEventListener) {
window.addEventListener('devicemotion', handleDeviceMotion, true);
}
// Fallback: Touch drag to control ball if no gyro
var dragging = false;
var lastTouch = {
x: 0,
y: 0
};
game.down = function (x, y, obj) {
// Only start drag if inside maze area
var gx = x - mazeOriginX;
var gy = y - mazeOriginY;
if (gx >= 0 && gx < mazeCellSize * MAZE_COLS && gy >= 0 && gy < mazeCellSize * MAZE_ROWS) {
dragging = true;
lastTouch.x = x;
lastTouch.y = y;
}
};
game.up = function (x, y, obj) {
dragging = false;
};
game.move = function (x, y, obj) {
if (dragging) {
var dx = x - lastTouch.x;
var dy = y - lastTouch.y;
// Simulate tilt by touch drag
gyro.x = clamp(dx * 0.2, -6, 6);
gyro.y = clamp(dy * 0.2, -6, 6);
lastTouch.x = x;
lastTouch.y = y;
}
};
// Main update loop
game.update = function () {
// Ball physics
// Use gyro.x/gyro.y as acceleration
ballVel.x += ballAccel * gyro.x * 0.18;
ballVel.y += ballAccel * gyro.y * 0.18;
// Friction
ballVel.x *= ballFriction;
ballVel.y *= ballFriction;
// Clamp speed
ballVel.x = clamp(ballVel.x, -ballMaxSpeed, ballMaxSpeed);
ballVel.y = clamp(ballVel.y, -ballMaxSpeed, ballMaxSpeed);
// Move ball
ballPos.x += ballVel.x;
ballPos.y += ballVel.y;
resolveBallMazeCollision();
ball.x = ballPos.x;
ball.y = ballPos.y;
// Timer
elapsedTime = (Date.now() - startTime) / 1000;
// Win condition
if (checkBallFinish()) {
LK.getSound('finish_reached').play();
LK.effects.flashScreen(0x44de83, 800);
LK.showYouWin();
}
};
// Start game on load
startGame();
// Reset game on game over or win
LK.on('gameover', function () {
startGame();
});
LK.on('youwin', function () {
startGame();
}); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Ball: The player-controlled ball
var Ball = Container.expand(function () {
var self = Container.call(this);
var ballSize = 0;
self.setSize = function (size) {
ballSize = size;
if (self.ballAsset) {
self.removeChild(self.ballAsset);
}
self.ballAsset = self.attachAsset('ball', {
width: ballSize,
height: ballSize,
color: 0x3a8cff,
shape: 'ellipse',
anchorX: 0.5,
anchorY: 0.5
});
};
self.radius = function () {
return ballSize / 2;
};
return self;
});
// Finish: The goal area
var Finish = Container.expand(function () {
var self = Container.call(this);
var finishSize = 0;
self.setSize = function (size) {
finishSize = size;
if (self.finishAsset) {
self.removeChild(self.finishAsset);
}
self.finishAsset = self.attachAsset('finish', {
width: finishSize,
height: finishSize,
color: 0x44de83,
shape: 'ellipse',
anchorX: 0.5,
anchorY: 0.5
});
};
return self;
});
// MazeCell: Represents a single cell in the maze grid
var MazeCell = Container.expand(function () {
var self = Container.call(this);
// Walls: {top, right, bottom, left}
self.walls = [true, true, true, true];
self.visited = false;
self.xIndex = 0;
self.yIndex = 0;
self.size = 0;
self.wallGraphics = [];
// Draws the cell walls
self.draw = function () {
// Remove old wall graphics
for (var i = 0; i < self.wallGraphics.length; i++) {
self.removeChild(self.wallGraphics[i]);
}
self.wallGraphics = [];
var s = self.size;
var wallColor = 0x222222;
var wallThickness = Math.max(8, Math.floor(s * 0.12));
// Top wall
if (self.walls[0]) {
var topWall = LK.getAsset('wall', {
width: s,
height: wallThickness,
color: wallColor,
shape: 'box',
anchorX: 0,
anchorY: 0
});
topWall.x = 0;
topWall.y = 0;
self.addChild(topWall);
self.wallGraphics.push(topWall);
}
// Right wall
if (self.walls[1]) {
var rightWall = LK.getAsset('wall', {
width: wallThickness,
height: s,
color: wallColor,
shape: 'box',
anchorX: 0,
anchorY: 0
});
rightWall.x = s - wallThickness;
rightWall.y = 0;
self.addChild(rightWall);
self.wallGraphics.push(rightWall);
}
// Bottom wall
if (self.walls[2]) {
var bottomWall = LK.getAsset('wall', {
width: s,
height: wallThickness,
color: wallColor,
shape: 'box',
anchorX: 0,
anchorY: 0
});
bottomWall.x = 0;
bottomWall.y = s - wallThickness;
self.addChild(bottomWall);
self.wallGraphics.push(bottomWall);
}
// Left wall
if (self.walls[3]) {
var leftWall = LK.getAsset('wall', {
width: wallThickness,
height: s,
color: wallColor,
shape: 'box',
anchorX: 0,
anchorY: 0
});
leftWall.x = 0;
leftWall.y = 0;
self.addChild(leftWall);
self.wallGraphics.push(leftWall);
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xf7f7f7
});
/****
* Game Code
****/
// Maze parameters
var MAZE_COLS = 18;
var MAZE_ROWS = 24;
var MAZE_MARGIN = 40; // px margin around maze (reduce margin to fit bigger maze)
var mazeCellSize = 0;
var mazeOriginX = 0;
var mazeOriginY = 0;
var maze = [];
var mazeContainer = new Container();
game.addChild(mazeContainer);
// Ball and Finish
var ball = null;
var finish = null;
// Ball physics
var ballPos = {
x: 0,
y: 0
};
var ballVel = {
x: 0,
y: 0
};
var ballMaxSpeed = 32;
var ballAccel = 2.2;
var ballFriction = 0.96;
// Gyro state
var gyro = {
x: 0,
y: 0
};
// Timer
var startTime = 0;
var elapsedTime = 0;
// Final score text removed as per requirements
// Helper: Clamp
function clamp(val, min, max) {
return Math.max(min, Math.min(max, val));
}
// Helper: Shuffle array
function shuffle(arr) {
for (var i = arr.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
return arr;
}
// Maze generation: Recursive backtracker with bias for normal difficulty route
function generateMaze(cols, rows) {
var grid = [];
for (var y = 0; y < rows; y++) {
var row = [];
for (var x = 0; x < cols; x++) {
var cell = new MazeCell();
cell.xIndex = x;
cell.yIndex = y;
cell.size = mazeCellSize;
cell.visited = false;
row.push(cell);
}
grid.push(row);
}
function cellAt(x, y) {
if (x < 0 || y < 0 || x >= cols || y >= rows) return null;
return grid[y][x];
}
var stack = [];
var current = grid[0][0];
current.visited = true;
var visitedCount = 1;
var totalCells = cols * rows;
// Bias: prefer to move generally toward the finish, but not always
var finishX = cols - 1;
var finishY = rows - 1;
while (visitedCount < totalCells) {
// Find unvisited neighbors
var neighbors = [];
var dirs = [[0, -1, 0],
// top
[1, 0, 1],
// right
[0, 1, 2],
// bottom
[-1, 0, 3] // left
];
for (var d = 0; d < 4; d++) {
var nx = current.xIndex + dirs[d][0];
var ny = current.yIndex + dirs[d][1];
var neighbor = cellAt(nx, ny);
if (neighbor && !neighbor.visited) {
neighbors.push({
cell: neighbor,
dir: d
});
}
}
if (neighbors.length > 0) {
// Bias: 60% chance to pick neighbor that is closer to finish, else random
var pick;
if (Math.random() < 0.6) {
// Sort neighbors by distance to finish, pick the closest
neighbors.sort(function (a, b) {
var da = Math.abs(a.cell.xIndex - finishX) + Math.abs(a.cell.yIndex - finishY);
var db = Math.abs(b.cell.xIndex - finishX) + Math.abs(b.cell.yIndex - finishY);
return da - db;
});
// 50% chance to pick the closest, 50% chance to pick second closest (if exists)
if (neighbors.length > 1 && Math.random() < 0.5) {
pick = neighbors[1];
} else {
pick = neighbors[0];
}
} else {
pick = neighbors[Math.floor(Math.random() * neighbors.length)];
}
// Remove wall between current and neighbor
current.walls[pick.dir] = false;
var opp = (pick.dir + 2) % 4;
pick.cell.walls[opp] = false;
stack.push(current);
current = pick.cell;
current.visited = true;
visitedCount++;
} else if (stack.length > 0) {
current = stack.pop();
}
}
// Draw all cells
for (var y = 0; y < rows; y++) {
for (var x = 0; x < cols; x++) {
grid[y][x].draw();
}
}
return grid;
}
// Place maze in center of screen, calculate cell size
function layoutMaze() {
var availW = 2048 - MAZE_MARGIN * 2;
var availH = 2732 - MAZE_MARGIN * 2;
mazeCellSize = Math.floor(Math.min(availW / MAZE_COLS, availH / MAZE_ROWS));
var mazeW = mazeCellSize * MAZE_COLS;
var mazeH = mazeCellSize * MAZE_ROWS;
mazeOriginX = Math.floor((2048 - mazeW) / 2);
mazeOriginY = Math.floor((2732 - mazeH) / 2);
mazeContainer.x = mazeOriginX;
mazeContainer.y = mazeOriginY;
}
// Remove all children from mazeContainer
function clearMaze() {
while (mazeContainer.children.length > 0) {
mazeContainer.removeChild(mazeContainer.children[0]);
}
}
// Place ball at start
function placeBall() {
if (ball) {
ball.destroy();
}
ball = new Ball();
ball.setSize(Math.floor(mazeCellSize * 0.55));
ballPos.x = mazeCellSize / 2;
ballPos.y = mazeCellSize / 2;
ball.x = ballPos.x;
ball.y = ballPos.y;
ballVel.x = 0;
ballVel.y = 0;
mazeContainer.addChild(ball);
}
// Place finish at bottom-right cell
function placeFinish() {
if (finish) {
finish.destroy();
}
finish = new Finish();
finish.setSize(Math.floor(mazeCellSize * 0.55));
// Pick a random cell in the maze that is not the start cell (0,0)
var finishCellX = 0;
var finishCellY = 0;
while (finishCellX === 0 && finishCellY === 0) {
finishCellX = Math.floor(Math.random() * MAZE_COLS);
finishCellY = Math.floor(Math.random() * MAZE_ROWS);
}
var fx = finishCellX * mazeCellSize + mazeCellSize / 2;
var fy = finishCellY * mazeCellSize + mazeCellSize / 2;
finish.x = fx;
finish.y = fy;
mazeContainer.addChild(finish);
// Store finish cell for win check
finish.finishCellX = finishCellX;
finish.finishCellY = finishCellY;
}
// Check collision between ball and maze walls
function resolveBallMazeCollision() {
var r = ball.radius();
var cx = Math.floor(ballPos.x / mazeCellSize);
var cy = Math.floor(ballPos.y / mazeCellSize);
// Clamp to maze bounds
cx = clamp(cx, 0, MAZE_COLS - 1);
cy = clamp(cy, 0, MAZE_ROWS - 1);
var cell = maze[cy][cx];
var px = ballPos.x - cx * mazeCellSize;
var py = ballPos.y - cy * mazeCellSize;
var s = mazeCellSize;
var wallThickness = Math.max(8, Math.floor(s * 0.12));
// Top wall
if (cell.walls[0] && py - r < wallThickness) {
ballPos.y = cy * mazeCellSize + wallThickness + r;
ballVel.y = Math.max(0, ballVel.y);
}
// Right wall
if (cell.walls[1] && px + r > s - wallThickness) {
ballPos.x = cx * mazeCellSize + s - wallThickness - r;
ballVel.x = Math.min(0, ballVel.x);
}
// Bottom wall
if (cell.walls[2] && py + r > s - wallThickness) {
ballPos.y = cy * mazeCellSize + s - wallThickness - r;
ballVel.y = Math.min(0, ballVel.y);
}
// Left wall
if (cell.walls[3] && px - r < wallThickness) {
ballPos.x = cx * mazeCellSize + wallThickness + r;
ballVel.x = Math.max(0, ballVel.x);
}
// Clamp to maze area
ballPos.x = clamp(ballPos.x, r, MAZE_COLS * mazeCellSize - r);
ballPos.y = clamp(ballPos.y, r, MAZE_ROWS * mazeCellSize - r);
}
// Check if ball reached finish
function checkBallFinish() {
// Use the actual finish cell position
var fx = finish.finishCellX * mazeCellSize + mazeCellSize / 2;
var fy = finish.finishCellY * mazeCellSize + mazeCellSize / 2;
var dx = ballPos.x - fx;
var dy = ballPos.y - fy;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < ball.radius() + finish.finishAsset.width / 2 - 6) {
return true;
}
return false;
}
// Start new game
function startGame() {
clearMaze();
layoutMaze();
maze = generateMaze(MAZE_COLS, MAZE_ROWS);
// Add all cells to container
for (var y = 0; y < MAZE_ROWS; y++) {
for (var x = 0; x < MAZE_COLS; x++) {
var cell = maze[y][x];
cell.x = x * mazeCellSize;
cell.y = y * mazeCellSize;
mazeContainer.addChild(cell);
}
}
placeFinish();
placeBall();
startTime = Date.now();
elapsedTime = 0;
}
// Gyroscope/motion support
function handleDeviceMotion(event) {
// event.accelerationIncludingGravity.{x,y,z}
// On iOS, x is left/right, y is up/down, z is toward/away
// On Android, axes may be swapped, so we allow both
var ax = 0,
ay = 0;
if (event.accelerationIncludingGravity) {
ax = event.accelerationIncludingGravity.x || 0;
ay = event.accelerationIncludingGravity.y || 0;
}
// Heuristic: On portrait, invert y for natural tilt
gyro.x = clamp(ax, -6, 6);
gyro.y = clamp(-ay, -6, 6);
}
if (typeof window !== "undefined" && window.addEventListener) {
window.addEventListener('devicemotion', handleDeviceMotion, true);
}
// Fallback: Touch drag to control ball if no gyro
var dragging = false;
var lastTouch = {
x: 0,
y: 0
};
game.down = function (x, y, obj) {
// Only start drag if inside maze area
var gx = x - mazeOriginX;
var gy = y - mazeOriginY;
if (gx >= 0 && gx < mazeCellSize * MAZE_COLS && gy >= 0 && gy < mazeCellSize * MAZE_ROWS) {
dragging = true;
lastTouch.x = x;
lastTouch.y = y;
}
};
game.up = function (x, y, obj) {
dragging = false;
};
game.move = function (x, y, obj) {
if (dragging) {
var dx = x - lastTouch.x;
var dy = y - lastTouch.y;
// Simulate tilt by touch drag
gyro.x = clamp(dx * 0.2, -6, 6);
gyro.y = clamp(dy * 0.2, -6, 6);
lastTouch.x = x;
lastTouch.y = y;
}
};
// Main update loop
game.update = function () {
// Ball physics
// Use gyro.x/gyro.y as acceleration
ballVel.x += ballAccel * gyro.x * 0.18;
ballVel.y += ballAccel * gyro.y * 0.18;
// Friction
ballVel.x *= ballFriction;
ballVel.y *= ballFriction;
// Clamp speed
ballVel.x = clamp(ballVel.x, -ballMaxSpeed, ballMaxSpeed);
ballVel.y = clamp(ballVel.y, -ballMaxSpeed, ballMaxSpeed);
// Move ball
ballPos.x += ballVel.x;
ballPos.y += ballVel.y;
resolveBallMazeCollision();
ball.x = ballPos.x;
ball.y = ballPos.y;
// Timer
elapsedTime = (Date.now() - startTime) / 1000;
// Win condition
if (checkBallFinish()) {
LK.getSound('finish_reached').play();
LK.effects.flashScreen(0x44de83, 800);
LK.showYouWin();
}
};
// Start game on load
startGame();
// Reset game on game over or win
LK.on('gameover', function () {
startGame();
});
LK.on('youwin', function () {
startGame();
});