/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// PuzzleTile: Represents a single tile in the puzzle grid.
var PuzzleTile = Container.expand(function () {
var self = Container.call(this);
// Properties
self.gridX = 0; // grid position x
self.gridY = 0; // grid position y
self.correctX = 0; // correct grid position x
self.correctY = 0; // correct grid position y
self.tileIndex = 0; // index in the tile array
self.isEmpty = false; // is this the empty tile?
// The tile's visual
var tileAsset = self.attachAsset('tile', {
anchorX: 0,
anchorY: 0
});
// For MVP, we color each tile differently for visual distinction
self.setColor = function (color) {
tileAsset.color = color;
// Use tween to smoothly animate the tint change for more vibrant effect
tween(tileAsset, {
tint: color
}, {
duration: 300,
easing: tween.easeOut
});
};
// Set the label (number) for the tile
self.setLabel = function (label) {
if (self.labelText) {
self.labelText.setText(label);
} else {
var txt = new Text2(label + '', {
size: 120,
fill: 0x222222
});
txt.anchor.set(0.5, 0.5);
txt.x = tileAsset.width / 2;
txt.y = tileAsset.height / 2;
self.addChild(txt);
self.labelText = txt;
}
};
// Hide label for empty tile
self.hideLabel = function () {
if (self.labelText) {
self.labelText.visible = false;
}
};
// Show label
self.showLabel = function () {
if (self.labelText) {
self.labelText.visible = true;
}
};
// Animate to a new position
self.moveTo = function (x, y, duration) {
tween(self, {
x: x,
y: y
}, {
duration: duration || 200,
easing: tween.cubicOut
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222222
});
/****
* Game Code
****/
// In the future, this can be replaced with a real image or a procedurally generated pattern.
// For MVP, we use a colored box as a placeholder for each tile, with a random color per game.
// We'll use a single image asset for the puzzle, which will be randomly generated each game.
// --- Puzzle Settings ---
var gridSize = 4; // 4x4 grid (15-puzzle style)
var tileSize = 400; // px, will be scaled to fit screen
var tileSpacing = 12; // px gap between tiles
var puzzleOffsetX = 0;
var puzzleOffsetY = 0;
var puzzleWidth = gridSize * tileSize + (gridSize - 1) * tileSpacing;
var puzzleHeight = gridSize * tileSize + (gridSize - 1) * tileSpacing;
// --- State ---
var tiles = []; // 2D array [y][x] of PuzzleTile
var tileList = []; // flat array of all PuzzleTile
var emptyTile = null; // reference to the empty tile
var moveCount = 0;
var startTime = 0;
var timerInterval = null;
var isSolved = false;
// --- UI ---
var moveText = new Text2('Moves: 0', {
size: 90,
fill: 0xFFFFFF
});
moveText.anchor.set(0, 0); // Anchor to top-left for easier positioning
LK.gui.topLeft.addChild(moveText); // Add to topLeft GUI container
var timeText = new Text2('Time: 0s', {
size: 90,
fill: 0xFFFFFF
});
timeText.anchor.set(0.5, 0);
LK.gui.top.addChild(timeText); // Keep timeText in the top-center
// Position UI
moveText.x = 20; // Small margin from the left edge
moveText.y = 120; // Position below the platform menu icon area
timeText.x = LK.gui.top.width / 2; // Center timeText
timeText.y = 20;
// --- Remix Button ---
var remixBtn = new Text2('Remix', {
size: 90,
fill: 0xFFD700
});
remixBtn.anchor.set(0.5, 0);
remixBtn.x = LK.gui.top.width / 2;
remixBtn.y = 120;
remixBtn.interactive = true;
remixBtn.buttonMode = true;
remixBtn.down = function (x, y, obj) {
if (isSolved) return;
shufflePuzzle();
moveCount = 0;
startTime = Date.now();
updateUI();
};
LK.gui.top.addChild(remixBtn);
// --- New Round Button ---
var newRoundBtn = new Text2('New Round', {
size: 90,
fill: 0x4CAF50 // A nice green color for the new round button
});
newRoundBtn.anchor.set(0.5, 0);
newRoundBtn.x = LK.gui.top.width / 2;
newRoundBtn.y = remixBtn.y + remixBtn.height + 30; // Position below the Remix button with some padding
newRoundBtn.interactive = true;
newRoundBtn.buttonMode = true;
newRoundBtn.visible = false; // Initially hidden
newRoundBtn.down = function (x, y, obj) {
createPuzzle(); // This will re-initialize the game, hide this button, and reset isSolved
};
LK.gui.top.addChild(newRoundBtn);
// --- Background Menu Button ---
var bgMenuBtn = new Text2('BG', {
size: 60,
fill: 0xFFFFFF
});
bgMenuBtn.anchor.set(1, 0);
bgMenuBtn.x = LK.gui.topRight.width - 20;
bgMenuBtn.y = 20;
bgMenuBtn.interactive = true;
bgMenuBtn.buttonMode = true;
bgMenuBtn.down = function (x, y, obj) {
showBackgroundMenu();
};
LK.gui.topRight.addChild(bgMenuBtn);
// --- Background Selection Variables ---
var backgroundMenu = null;
var backgroundColors = [0x222222,
// Dark gray (default)
0x00FF00,
// Bright green
0x0080FF,
// Bright blue
0xFF00FF,
// Bright magenta
0xFF0040,
// Bright red
0xFF8000,
// Bright orange
0x8000FF,
// Bright purple
'rainbow' // Rainbow option
];
var currentBgIndex = 0;
// --- Rainbow Background Variables ---
var rainbowInterval = null;
var rainbowHue = 0;
// --- Helper Functions ---
// Generate a random color palette for the puzzle
function generateColorPalette(n) {
var colors = [];
var baseHue = Math.floor(Math.random() * 360);
for (var i = 0; i < n; i++) {
// HSL to RGB conversion with more vibrant saturation and lightness
var hue = (baseHue + i * (360 / n)) % 360;
var rgb = hslToRgb(hue / 360, 0.9, 0.7); // Increased saturation to 0.9 and lightness to 0.7
var color = rgb[0] << 16 | rgb[1] << 8 | rgb[2];
colors.push(color);
}
return colors;
}
// HSL to RGB helper
function hslToRgb(h, s, l) {
var r, g, b;
if (s == 0) {
r = g = b = l;
} else {
var hue2rgb = function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
// Shuffle an array in-place
function shuffleArray(arr) {
for (var i = arr.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
// Check if the puzzle is solved
function checkSolved() {
for (var y = 0; y < gridSize; y++) {
for (var x = 0; x < gridSize; x++) {
var tile = tiles[y][x];
if (tile.gridX !== tile.correctX || tile.gridY !== tile.correctY) {
return false;
}
}
}
return true;
}
// Update move and time UI
function updateUI() {
moveText.setText('Moves: ' + moveCount);
var elapsed = Math.floor((Date.now() - startTime) / 1000);
timeText.setText('Time: ' + elapsed + 's');
}
// --- Puzzle Generation ---
function createPuzzle() {
if (typeof newRoundBtn !== 'undefined' && newRoundBtn) {
newRoundBtn.visible = false;
}
if (typeof hideTutorial === 'function') {
hideTutorial();
}
// Remove old tiles
for (var i = 0; i < tileList.length; i++) {
tileList[i].destroy();
}
tiles = [];
tileList = [];
emptyTile = null;
isSolved = false;
moveCount = 0;
updateUI();
// Generate color palette
var numTiles = gridSize * gridSize;
var colors = generateColorPalette(numTiles - 1);
// Calculate tile size to fit screen
var maxWidth = 2048 - 200;
var maxHeight = 2732 - 400;
tileSize = Math.floor(Math.min((maxWidth - (gridSize - 1) * tileSpacing) / gridSize, (maxHeight - (gridSize - 1) * tileSpacing) / gridSize));
puzzleWidth = gridSize * tileSize + (gridSize - 1) * tileSpacing;
puzzleHeight = gridSize * tileSize + (gridSize - 1) * tileSpacing;
puzzleOffsetX = Math.floor((2048 - puzzleWidth) / 2);
puzzleOffsetY = Math.floor((2732 - puzzleHeight) / 2) + 80;
// Create tiles in solved order
var idx = 0;
for (var y = 0; y < gridSize; y++) {
tiles[y] = [];
for (var x = 0; x < gridSize; x++) {
var tile = new PuzzleTile();
tile.gridX = x;
tile.gridY = y;
tile.correctX = x;
tile.correctY = y;
tile.tileIndex = idx;
tile.x = puzzleOffsetX + x * (tileSize + tileSpacing);
tile.y = puzzleOffsetY + y * (tileSize + tileSpacing);
tile.width = tileSize;
tile.height = tileSize;
tile.setColor(colors[idx] || 0x222222);
tile.setLabel(idx + 1);
tile.isEmpty = false;
tileList.push(tile);
tiles[y][x] = tile;
game.addChild(tile);
idx++;
}
}
// Make last tile the empty tile
var lastTile = tiles[gridSize - 1][gridSize - 1];
lastTile.isEmpty = true;
lastTile.setColor(0x222222);
lastTile.hideLabel();
emptyTile = lastTile;
// Shuffle tiles
shufflePuzzle();
// Start timer
startTime = Date.now();
if (timerInterval) LK.clearInterval(timerInterval);
timerInterval = LK.setInterval(updateUI, 500);
}
// Shuffle the puzzle by simulating random valid moves
function shufflePuzzle() {
var moves = [[0, 1], [1, 0], [0, -1], [-1, 0]];
var prevX = emptyTile.gridX,
prevY = emptyTile.gridY;
var shuffleCount = 200 + Math.floor(Math.random() * 100);
for (var i = 0; i < shuffleCount; i++) {
var possible = [];
for (var d = 0; d < moves.length; d++) {
var nx = emptyTile.gridX + moves[d][0];
var ny = emptyTile.gridY + moves[d][1];
if (nx >= 0 && nx < gridSize && ny >= 0 && ny < gridSize) {
if (!(nx === prevX && ny === prevY)) {
possible.push([nx, ny]);
}
}
}
if (possible.length === 0) continue;
var pick = possible[Math.floor(Math.random() * possible.length)];
var tile = tiles[pick[1]][pick[0]];
swapWithEmpty(tile, false);
prevX = emptyTile.gridX;
prevY = emptyTile.gridY;
}
// After shuffling, update all tile positions visually
for (var y = 0; y < gridSize; y++) {
for (var x = 0; x < gridSize; x++) {
var tile = tiles[y][x];
tile.x = puzzleOffsetX + x * (tileSize + tileSpacing);
tile.y = puzzleOffsetY + y * (tileSize + tileSpacing);
tile.gridX = x;
tile.gridY = y;
}
}
}
// Swap a tile with the empty tile (if adjacent)
function swapWithEmpty(tile, animate) {
if (tile.isEmpty) return false;
var dx = Math.abs(tile.gridX - emptyTile.gridX);
var dy = Math.abs(tile.gridY - emptyTile.gridY);
if (dx === 1 && dy === 0 || dx === 0 && dy === 1) {
// Swap in tiles array
var tx = tile.gridX,
ty = tile.gridY;
var ex = emptyTile.gridX,
ey = emptyTile.gridY;
tiles[ty][tx] = emptyTile;
tiles[ey][ex] = tile;
// Swap grid positions
var tmpX = tile.gridX,
tmpY = tile.gridY;
tile.gridX = emptyTile.gridX;
tile.gridY = emptyTile.gridY;
emptyTile.gridX = tmpX;
emptyTile.gridY = tmpY;
// Animate movement
var tileTargetX = puzzleOffsetX + tile.gridX * (tileSize + tileSpacing);
var tileTargetY = puzzleOffsetY + tile.gridY * (tileSize + tileSpacing);
var emptyTargetX = puzzleOffsetX + emptyTile.gridX * (tileSize + tileSpacing);
var emptyTargetY = puzzleOffsetY + emptyTile.gridY * (tileSize + tileSpacing);
if (animate !== false) {
tile.moveTo(tileTargetX, tileTargetY, 120);
emptyTile.moveTo(emptyTargetX, emptyTargetY, 120);
} else {
tile.x = tileTargetX;
tile.y = tileTargetY;
emptyTile.x = emptyTargetX;
emptyTile.y = emptyTargetY;
}
return true;
}
return false;
}
// --- Input Handling ---
// Find which tile was pressed
function getTileAtPos(x, y) {
for (var i = 0; i < tileList.length; i++) {
var tile = tileList[i];
if (tile.isEmpty) continue;
var tx = tile.x,
ty = tile.y;
var tw = tile.width,
th = tile.height;
if (x >= tx && x <= tx + tw && y >= ty && y <= ty + th) {
return tile;
}
}
return null;
}
// Handle tap/click on the puzzle
game.down = function (x, y, obj) {
if (isSolved) return;
var tile = getTileAtPos(x, y);
if (!tile) return;
var moved = swapWithEmpty(tile, true);
if (moved) {
moveCount++;
updateUI();
if (checkSolved()) {
isSolved = true;
updateUI();
LK.effects.flashScreen(0x44ff44, 800);
// Instead of showing YouWin screen, show the New Round button
if (typeof newRoundBtn !== 'undefined' && newRoundBtn) {
newRoundBtn.visible = true;
}
// LK.setTimeout(function () { // Original YouWin call removed
// LK.showYouWin();
// }, 900);
}
}
};
// --- Game Update ---
game.update = function () {
// No per-frame logic needed for MVP
};
// --- Tutorial Overlay ---
var tutorialOverlay = null;
var tutorialStep = 0;
var tutorialActive = false;
// Show tutorial overlay for the first level
function showTutorial() {
tutorialActive = true;
tutorialStep = 0;
if (tutorialOverlay) {
tutorialOverlay.destroy();
tutorialOverlay = null;
}
tutorialOverlay = new Container();
var bg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 20,
scaleY: 20,
x: 2048 / 2,
y: 2732 / 2
});
bg.alpha = 0.7;
tutorialOverlay.addChild(bg);
var tutText = new Text2("Welcome to Puzzle It!\n\nWatch how to move the tiles...", {
size: 100,
fill: 0xffffff
});
tutText.anchor.set(0.5, 0.5);
tutText.x = 2048 / 2;
tutText.y = 2732 / 2 - 200;
tutorialOverlay.addChild(tutText);
var tutBtn = new Text2("Got it!", {
size: 80,
fill: 0x4CAF50
});
tutBtn.anchor.set(0.5, 0.5);
tutBtn.x = 2048 / 2;
tutBtn.y = 2732 / 2 + 300;
tutBtn.interactive = true;
tutBtn.buttonMode = true;
tutBtn.visible = false; // Hide button initially
tutorialOverlay.addChild(tutBtn);
// Add the tutorial overlay to the game directly instead of LK.gui.center
game.addChild(tutorialOverlay);
// Set up the button's click handler after it's added to the display tree
tutBtn.down = function (x, y, obj) {
hideTutorial();
};
// Start the step-by-step tutorial
startTutorialSteps(tutText, tutBtn);
}
// Start step-by-step tutorial movements
function startTutorialSteps(tutText, tutBtn) {
var stepDelay = 2000; // 2 seconds between steps
var moveDelay = 1000; // 1 second to show movement
var steps = [{
text: "Step 1: Move tile 15 up...",
tileToMove: function tileToMove() {
// Find tile with number 15
for (var i = 0; i < tileList.length; i++) {
if (tileList[i].labelText && tileList[i].labelText.text === "15") {
return tileList[i];
}
}
return null;
}
}, {
text: "Step 2: Move tile 14 right...",
tileToMove: function tileToMove() {
// Find tile with number 14
for (var i = 0; i < tileList.length; i++) {
if (tileList[i].labelText && tileList[i].labelText.text === "14") {
return tileList[i];
}
}
return null;
}
}, {
text: "Step 3: Move tile 11 down...",
tileToMove: function tileToMove() {
// Find tile with number 11
for (var i = 0; i < tileList.length; i++) {
if (tileList[i].labelText && tileList[i].labelText.text === "11") {
return tileList[i];
}
}
return null;
}
}];
var currentStep = 0;
function performNextStep() {
if (currentStep >= steps.length) {
// Tutorial complete
tutText.setText("Great! Now you try.\n\nTap a tile next to the empty space to move it.\n\nPut the numbers in order to win.");
tutBtn.visible = true;
return;
}
var step = steps[currentStep];
tutText.setText(step.text);
// Wait a moment, then perform the move
LK.setTimeout(function () {
var tile = step.tileToMove();
if (tile && swapWithEmpty(tile, true)) {
// Move was successful, wait for animation then continue
LK.setTimeout(function () {
currentStep++;
performNextStep();
}, moveDelay);
} else {
// Move failed, skip to next step
currentStep++;
performNextStep();
}
}, stepDelay);
}
// Start the tutorial steps
performNextStep();
}
// Hide tutorial overlay
function hideTutorial() {
tutorialActive = false;
if (tutorialOverlay) {
game.removeChild(tutorialOverlay);
tutorialOverlay.destroy();
tutorialOverlay = null;
}
}
// Show background color selection menu
function showBackgroundMenu() {
if (backgroundMenu) {
hideBackgroundMenu();
return;
}
backgroundMenu = new Container();
// Semi-transparent background
var menuBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 12,
scaleY: 8,
x: 2048 / 2,
y: 2732 / 2
});
menuBg.alpha = 0.9;
menuBg.tint = 0x000000;
backgroundMenu.addChild(menuBg);
// Title
var titleText = new Text2("Choose Background", {
size: 80,
fill: 0xFFFFFF
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 2048 / 2;
titleText.y = 2732 / 2 - 300;
backgroundMenu.addChild(titleText);
// Color options
var colorsPerRow = 4;
var colorSize = 120;
var colorSpacing = 40;
var startX = 2048 / 2 - (colorsPerRow * colorSize + (colorsPerRow - 1) * colorSpacing) / 2 + colorSize / 2;
var startY = 2732 / 2 - 100;
for (var i = 0; i < backgroundColors.length; i++) {
var row = Math.floor(i / colorsPerRow);
var col = i % colorsPerRow;
var colorBtn = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: colorSize / 100,
scaleY: colorSize / 100,
x: startX + col * (colorSize + colorSpacing),
y: startY + row * (colorSize + colorSpacing)
});
if (backgroundColors[i] === 'rainbow') {
// Create a rainbow gradient effect for the rainbow button
colorBtn.tint = 0xFF0000; // Start with red, will be animated
// Add a visual indicator that this is the rainbow option
var rainbowText = new Text2("๐", {
size: 60,
fill: 0xFFFFFF
});
rainbowText.anchor.set(0.5, 0.5);
rainbowText.x = 0;
rainbowText.y = 0;
colorBtn.addChild(rainbowText);
} else {
colorBtn.tint = backgroundColors[i];
}
colorBtn.interactive = true;
colorBtn.buttonMode = true;
colorBtn.colorIndex = i;
// Add selection indicator for current background
if (i === currentBgIndex) {
var indicator = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: (colorSize + 20) / 100,
scaleY: (colorSize + 20) / 100,
x: 0,
y: 0
});
indicator.tint = 0xFFD700;
indicator.alpha = 0.8;
colorBtn.addChild(indicator);
}
(function (colorIndex) {
colorBtn.down = function (x, y, obj) {
currentBgIndex = colorIndex;
if (backgroundColors[colorIndex] === 'rainbow') {
startRainbowBackground();
} else {
stopRainbowBackground();
game.setBackgroundColor(backgroundColors[colorIndex]);
}
hideBackgroundMenu();
};
})(i);
backgroundMenu.addChild(colorBtn);
}
// Close button
var closeBtn = new Text2("Close", {
size: 70,
fill: 0xFF4444
});
closeBtn.anchor.set(0.5, 0.5);
closeBtn.x = 2048 / 2;
closeBtn.y = 2732 / 2 + 250;
closeBtn.interactive = true;
closeBtn.buttonMode = true;
closeBtn.down = function (x, y, obj) {
hideBackgroundMenu();
};
backgroundMenu.addChild(closeBtn);
game.addChild(backgroundMenu);
}
// Hide background selection menu
function hideBackgroundMenu() {
if (backgroundMenu) {
game.removeChild(backgroundMenu);
backgroundMenu.destroy();
backgroundMenu = null;
}
}
// Start rainbow background animation
function startRainbowBackground() {
stopRainbowBackground(); // Stop any existing rainbow
rainbowInterval = LK.setInterval(function () {
rainbowHue = (rainbowHue + 2) % 360; // Increment hue
var rgb = hslToRgb(rainbowHue / 360, 0.8, 0.5); // High saturation, medium lightness
var color = rgb[0] << 16 | rgb[1] << 8 | rgb[2];
game.setBackgroundColor(color);
}, 50); // Update every 50ms for smooth animation
}
// Stop rainbow background animation
function stopRainbowBackground() {
if (rainbowInterval) {
LK.clearInterval(rainbowInterval);
rainbowInterval = null;
}
}
// --- Start Game ---
createPuzzle();
;
// Show tutorial only for the first level (first time game is loaded)
showTutorial();
// Block input while tutorial is active
var origGameDown = game.down;
game.down = function (x, y, obj) {
if (tutorialActive) return;
origGameDown(x, y, obj);
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// PuzzleTile: Represents a single tile in the puzzle grid.
var PuzzleTile = Container.expand(function () {
var self = Container.call(this);
// Properties
self.gridX = 0; // grid position x
self.gridY = 0; // grid position y
self.correctX = 0; // correct grid position x
self.correctY = 0; // correct grid position y
self.tileIndex = 0; // index in the tile array
self.isEmpty = false; // is this the empty tile?
// The tile's visual
var tileAsset = self.attachAsset('tile', {
anchorX: 0,
anchorY: 0
});
// For MVP, we color each tile differently for visual distinction
self.setColor = function (color) {
tileAsset.color = color;
// Use tween to smoothly animate the tint change for more vibrant effect
tween(tileAsset, {
tint: color
}, {
duration: 300,
easing: tween.easeOut
});
};
// Set the label (number) for the tile
self.setLabel = function (label) {
if (self.labelText) {
self.labelText.setText(label);
} else {
var txt = new Text2(label + '', {
size: 120,
fill: 0x222222
});
txt.anchor.set(0.5, 0.5);
txt.x = tileAsset.width / 2;
txt.y = tileAsset.height / 2;
self.addChild(txt);
self.labelText = txt;
}
};
// Hide label for empty tile
self.hideLabel = function () {
if (self.labelText) {
self.labelText.visible = false;
}
};
// Show label
self.showLabel = function () {
if (self.labelText) {
self.labelText.visible = true;
}
};
// Animate to a new position
self.moveTo = function (x, y, duration) {
tween(self, {
x: x,
y: y
}, {
duration: duration || 200,
easing: tween.cubicOut
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222222
});
/****
* Game Code
****/
// In the future, this can be replaced with a real image or a procedurally generated pattern.
// For MVP, we use a colored box as a placeholder for each tile, with a random color per game.
// We'll use a single image asset for the puzzle, which will be randomly generated each game.
// --- Puzzle Settings ---
var gridSize = 4; // 4x4 grid (15-puzzle style)
var tileSize = 400; // px, will be scaled to fit screen
var tileSpacing = 12; // px gap between tiles
var puzzleOffsetX = 0;
var puzzleOffsetY = 0;
var puzzleWidth = gridSize * tileSize + (gridSize - 1) * tileSpacing;
var puzzleHeight = gridSize * tileSize + (gridSize - 1) * tileSpacing;
// --- State ---
var tiles = []; // 2D array [y][x] of PuzzleTile
var tileList = []; // flat array of all PuzzleTile
var emptyTile = null; // reference to the empty tile
var moveCount = 0;
var startTime = 0;
var timerInterval = null;
var isSolved = false;
// --- UI ---
var moveText = new Text2('Moves: 0', {
size: 90,
fill: 0xFFFFFF
});
moveText.anchor.set(0, 0); // Anchor to top-left for easier positioning
LK.gui.topLeft.addChild(moveText); // Add to topLeft GUI container
var timeText = new Text2('Time: 0s', {
size: 90,
fill: 0xFFFFFF
});
timeText.anchor.set(0.5, 0);
LK.gui.top.addChild(timeText); // Keep timeText in the top-center
// Position UI
moveText.x = 20; // Small margin from the left edge
moveText.y = 120; // Position below the platform menu icon area
timeText.x = LK.gui.top.width / 2; // Center timeText
timeText.y = 20;
// --- Remix Button ---
var remixBtn = new Text2('Remix', {
size: 90,
fill: 0xFFD700
});
remixBtn.anchor.set(0.5, 0);
remixBtn.x = LK.gui.top.width / 2;
remixBtn.y = 120;
remixBtn.interactive = true;
remixBtn.buttonMode = true;
remixBtn.down = function (x, y, obj) {
if (isSolved) return;
shufflePuzzle();
moveCount = 0;
startTime = Date.now();
updateUI();
};
LK.gui.top.addChild(remixBtn);
// --- New Round Button ---
var newRoundBtn = new Text2('New Round', {
size: 90,
fill: 0x4CAF50 // A nice green color for the new round button
});
newRoundBtn.anchor.set(0.5, 0);
newRoundBtn.x = LK.gui.top.width / 2;
newRoundBtn.y = remixBtn.y + remixBtn.height + 30; // Position below the Remix button with some padding
newRoundBtn.interactive = true;
newRoundBtn.buttonMode = true;
newRoundBtn.visible = false; // Initially hidden
newRoundBtn.down = function (x, y, obj) {
createPuzzle(); // This will re-initialize the game, hide this button, and reset isSolved
};
LK.gui.top.addChild(newRoundBtn);
// --- Background Menu Button ---
var bgMenuBtn = new Text2('BG', {
size: 60,
fill: 0xFFFFFF
});
bgMenuBtn.anchor.set(1, 0);
bgMenuBtn.x = LK.gui.topRight.width - 20;
bgMenuBtn.y = 20;
bgMenuBtn.interactive = true;
bgMenuBtn.buttonMode = true;
bgMenuBtn.down = function (x, y, obj) {
showBackgroundMenu();
};
LK.gui.topRight.addChild(bgMenuBtn);
// --- Background Selection Variables ---
var backgroundMenu = null;
var backgroundColors = [0x222222,
// Dark gray (default)
0x00FF00,
// Bright green
0x0080FF,
// Bright blue
0xFF00FF,
// Bright magenta
0xFF0040,
// Bright red
0xFF8000,
// Bright orange
0x8000FF,
// Bright purple
'rainbow' // Rainbow option
];
var currentBgIndex = 0;
// --- Rainbow Background Variables ---
var rainbowInterval = null;
var rainbowHue = 0;
// --- Helper Functions ---
// Generate a random color palette for the puzzle
function generateColorPalette(n) {
var colors = [];
var baseHue = Math.floor(Math.random() * 360);
for (var i = 0; i < n; i++) {
// HSL to RGB conversion with more vibrant saturation and lightness
var hue = (baseHue + i * (360 / n)) % 360;
var rgb = hslToRgb(hue / 360, 0.9, 0.7); // Increased saturation to 0.9 and lightness to 0.7
var color = rgb[0] << 16 | rgb[1] << 8 | rgb[2];
colors.push(color);
}
return colors;
}
// HSL to RGB helper
function hslToRgb(h, s, l) {
var r, g, b;
if (s == 0) {
r = g = b = l;
} else {
var hue2rgb = function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
// Shuffle an array in-place
function shuffleArray(arr) {
for (var i = arr.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
// Check if the puzzle is solved
function checkSolved() {
for (var y = 0; y < gridSize; y++) {
for (var x = 0; x < gridSize; x++) {
var tile = tiles[y][x];
if (tile.gridX !== tile.correctX || tile.gridY !== tile.correctY) {
return false;
}
}
}
return true;
}
// Update move and time UI
function updateUI() {
moveText.setText('Moves: ' + moveCount);
var elapsed = Math.floor((Date.now() - startTime) / 1000);
timeText.setText('Time: ' + elapsed + 's');
}
// --- Puzzle Generation ---
function createPuzzle() {
if (typeof newRoundBtn !== 'undefined' && newRoundBtn) {
newRoundBtn.visible = false;
}
if (typeof hideTutorial === 'function') {
hideTutorial();
}
// Remove old tiles
for (var i = 0; i < tileList.length; i++) {
tileList[i].destroy();
}
tiles = [];
tileList = [];
emptyTile = null;
isSolved = false;
moveCount = 0;
updateUI();
// Generate color palette
var numTiles = gridSize * gridSize;
var colors = generateColorPalette(numTiles - 1);
// Calculate tile size to fit screen
var maxWidth = 2048 - 200;
var maxHeight = 2732 - 400;
tileSize = Math.floor(Math.min((maxWidth - (gridSize - 1) * tileSpacing) / gridSize, (maxHeight - (gridSize - 1) * tileSpacing) / gridSize));
puzzleWidth = gridSize * tileSize + (gridSize - 1) * tileSpacing;
puzzleHeight = gridSize * tileSize + (gridSize - 1) * tileSpacing;
puzzleOffsetX = Math.floor((2048 - puzzleWidth) / 2);
puzzleOffsetY = Math.floor((2732 - puzzleHeight) / 2) + 80;
// Create tiles in solved order
var idx = 0;
for (var y = 0; y < gridSize; y++) {
tiles[y] = [];
for (var x = 0; x < gridSize; x++) {
var tile = new PuzzleTile();
tile.gridX = x;
tile.gridY = y;
tile.correctX = x;
tile.correctY = y;
tile.tileIndex = idx;
tile.x = puzzleOffsetX + x * (tileSize + tileSpacing);
tile.y = puzzleOffsetY + y * (tileSize + tileSpacing);
tile.width = tileSize;
tile.height = tileSize;
tile.setColor(colors[idx] || 0x222222);
tile.setLabel(idx + 1);
tile.isEmpty = false;
tileList.push(tile);
tiles[y][x] = tile;
game.addChild(tile);
idx++;
}
}
// Make last tile the empty tile
var lastTile = tiles[gridSize - 1][gridSize - 1];
lastTile.isEmpty = true;
lastTile.setColor(0x222222);
lastTile.hideLabel();
emptyTile = lastTile;
// Shuffle tiles
shufflePuzzle();
// Start timer
startTime = Date.now();
if (timerInterval) LK.clearInterval(timerInterval);
timerInterval = LK.setInterval(updateUI, 500);
}
// Shuffle the puzzle by simulating random valid moves
function shufflePuzzle() {
var moves = [[0, 1], [1, 0], [0, -1], [-1, 0]];
var prevX = emptyTile.gridX,
prevY = emptyTile.gridY;
var shuffleCount = 200 + Math.floor(Math.random() * 100);
for (var i = 0; i < shuffleCount; i++) {
var possible = [];
for (var d = 0; d < moves.length; d++) {
var nx = emptyTile.gridX + moves[d][0];
var ny = emptyTile.gridY + moves[d][1];
if (nx >= 0 && nx < gridSize && ny >= 0 && ny < gridSize) {
if (!(nx === prevX && ny === prevY)) {
possible.push([nx, ny]);
}
}
}
if (possible.length === 0) continue;
var pick = possible[Math.floor(Math.random() * possible.length)];
var tile = tiles[pick[1]][pick[0]];
swapWithEmpty(tile, false);
prevX = emptyTile.gridX;
prevY = emptyTile.gridY;
}
// After shuffling, update all tile positions visually
for (var y = 0; y < gridSize; y++) {
for (var x = 0; x < gridSize; x++) {
var tile = tiles[y][x];
tile.x = puzzleOffsetX + x * (tileSize + tileSpacing);
tile.y = puzzleOffsetY + y * (tileSize + tileSpacing);
tile.gridX = x;
tile.gridY = y;
}
}
}
// Swap a tile with the empty tile (if adjacent)
function swapWithEmpty(tile, animate) {
if (tile.isEmpty) return false;
var dx = Math.abs(tile.gridX - emptyTile.gridX);
var dy = Math.abs(tile.gridY - emptyTile.gridY);
if (dx === 1 && dy === 0 || dx === 0 && dy === 1) {
// Swap in tiles array
var tx = tile.gridX,
ty = tile.gridY;
var ex = emptyTile.gridX,
ey = emptyTile.gridY;
tiles[ty][tx] = emptyTile;
tiles[ey][ex] = tile;
// Swap grid positions
var tmpX = tile.gridX,
tmpY = tile.gridY;
tile.gridX = emptyTile.gridX;
tile.gridY = emptyTile.gridY;
emptyTile.gridX = tmpX;
emptyTile.gridY = tmpY;
// Animate movement
var tileTargetX = puzzleOffsetX + tile.gridX * (tileSize + tileSpacing);
var tileTargetY = puzzleOffsetY + tile.gridY * (tileSize + tileSpacing);
var emptyTargetX = puzzleOffsetX + emptyTile.gridX * (tileSize + tileSpacing);
var emptyTargetY = puzzleOffsetY + emptyTile.gridY * (tileSize + tileSpacing);
if (animate !== false) {
tile.moveTo(tileTargetX, tileTargetY, 120);
emptyTile.moveTo(emptyTargetX, emptyTargetY, 120);
} else {
tile.x = tileTargetX;
tile.y = tileTargetY;
emptyTile.x = emptyTargetX;
emptyTile.y = emptyTargetY;
}
return true;
}
return false;
}
// --- Input Handling ---
// Find which tile was pressed
function getTileAtPos(x, y) {
for (var i = 0; i < tileList.length; i++) {
var tile = tileList[i];
if (tile.isEmpty) continue;
var tx = tile.x,
ty = tile.y;
var tw = tile.width,
th = tile.height;
if (x >= tx && x <= tx + tw && y >= ty && y <= ty + th) {
return tile;
}
}
return null;
}
// Handle tap/click on the puzzle
game.down = function (x, y, obj) {
if (isSolved) return;
var tile = getTileAtPos(x, y);
if (!tile) return;
var moved = swapWithEmpty(tile, true);
if (moved) {
moveCount++;
updateUI();
if (checkSolved()) {
isSolved = true;
updateUI();
LK.effects.flashScreen(0x44ff44, 800);
// Instead of showing YouWin screen, show the New Round button
if (typeof newRoundBtn !== 'undefined' && newRoundBtn) {
newRoundBtn.visible = true;
}
// LK.setTimeout(function () { // Original YouWin call removed
// LK.showYouWin();
// }, 900);
}
}
};
// --- Game Update ---
game.update = function () {
// No per-frame logic needed for MVP
};
// --- Tutorial Overlay ---
var tutorialOverlay = null;
var tutorialStep = 0;
var tutorialActive = false;
// Show tutorial overlay for the first level
function showTutorial() {
tutorialActive = true;
tutorialStep = 0;
if (tutorialOverlay) {
tutorialOverlay.destroy();
tutorialOverlay = null;
}
tutorialOverlay = new Container();
var bg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 20,
scaleY: 20,
x: 2048 / 2,
y: 2732 / 2
});
bg.alpha = 0.7;
tutorialOverlay.addChild(bg);
var tutText = new Text2("Welcome to Puzzle It!\n\nWatch how to move the tiles...", {
size: 100,
fill: 0xffffff
});
tutText.anchor.set(0.5, 0.5);
tutText.x = 2048 / 2;
tutText.y = 2732 / 2 - 200;
tutorialOverlay.addChild(tutText);
var tutBtn = new Text2("Got it!", {
size: 80,
fill: 0x4CAF50
});
tutBtn.anchor.set(0.5, 0.5);
tutBtn.x = 2048 / 2;
tutBtn.y = 2732 / 2 + 300;
tutBtn.interactive = true;
tutBtn.buttonMode = true;
tutBtn.visible = false; // Hide button initially
tutorialOverlay.addChild(tutBtn);
// Add the tutorial overlay to the game directly instead of LK.gui.center
game.addChild(tutorialOverlay);
// Set up the button's click handler after it's added to the display tree
tutBtn.down = function (x, y, obj) {
hideTutorial();
};
// Start the step-by-step tutorial
startTutorialSteps(tutText, tutBtn);
}
// Start step-by-step tutorial movements
function startTutorialSteps(tutText, tutBtn) {
var stepDelay = 2000; // 2 seconds between steps
var moveDelay = 1000; // 1 second to show movement
var steps = [{
text: "Step 1: Move tile 15 up...",
tileToMove: function tileToMove() {
// Find tile with number 15
for (var i = 0; i < tileList.length; i++) {
if (tileList[i].labelText && tileList[i].labelText.text === "15") {
return tileList[i];
}
}
return null;
}
}, {
text: "Step 2: Move tile 14 right...",
tileToMove: function tileToMove() {
// Find tile with number 14
for (var i = 0; i < tileList.length; i++) {
if (tileList[i].labelText && tileList[i].labelText.text === "14") {
return tileList[i];
}
}
return null;
}
}, {
text: "Step 3: Move tile 11 down...",
tileToMove: function tileToMove() {
// Find tile with number 11
for (var i = 0; i < tileList.length; i++) {
if (tileList[i].labelText && tileList[i].labelText.text === "11") {
return tileList[i];
}
}
return null;
}
}];
var currentStep = 0;
function performNextStep() {
if (currentStep >= steps.length) {
// Tutorial complete
tutText.setText("Great! Now you try.\n\nTap a tile next to the empty space to move it.\n\nPut the numbers in order to win.");
tutBtn.visible = true;
return;
}
var step = steps[currentStep];
tutText.setText(step.text);
// Wait a moment, then perform the move
LK.setTimeout(function () {
var tile = step.tileToMove();
if (tile && swapWithEmpty(tile, true)) {
// Move was successful, wait for animation then continue
LK.setTimeout(function () {
currentStep++;
performNextStep();
}, moveDelay);
} else {
// Move failed, skip to next step
currentStep++;
performNextStep();
}
}, stepDelay);
}
// Start the tutorial steps
performNextStep();
}
// Hide tutorial overlay
function hideTutorial() {
tutorialActive = false;
if (tutorialOverlay) {
game.removeChild(tutorialOverlay);
tutorialOverlay.destroy();
tutorialOverlay = null;
}
}
// Show background color selection menu
function showBackgroundMenu() {
if (backgroundMenu) {
hideBackgroundMenu();
return;
}
backgroundMenu = new Container();
// Semi-transparent background
var menuBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 12,
scaleY: 8,
x: 2048 / 2,
y: 2732 / 2
});
menuBg.alpha = 0.9;
menuBg.tint = 0x000000;
backgroundMenu.addChild(menuBg);
// Title
var titleText = new Text2("Choose Background", {
size: 80,
fill: 0xFFFFFF
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 2048 / 2;
titleText.y = 2732 / 2 - 300;
backgroundMenu.addChild(titleText);
// Color options
var colorsPerRow = 4;
var colorSize = 120;
var colorSpacing = 40;
var startX = 2048 / 2 - (colorsPerRow * colorSize + (colorsPerRow - 1) * colorSpacing) / 2 + colorSize / 2;
var startY = 2732 / 2 - 100;
for (var i = 0; i < backgroundColors.length; i++) {
var row = Math.floor(i / colorsPerRow);
var col = i % colorsPerRow;
var colorBtn = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: colorSize / 100,
scaleY: colorSize / 100,
x: startX + col * (colorSize + colorSpacing),
y: startY + row * (colorSize + colorSpacing)
});
if (backgroundColors[i] === 'rainbow') {
// Create a rainbow gradient effect for the rainbow button
colorBtn.tint = 0xFF0000; // Start with red, will be animated
// Add a visual indicator that this is the rainbow option
var rainbowText = new Text2("๐", {
size: 60,
fill: 0xFFFFFF
});
rainbowText.anchor.set(0.5, 0.5);
rainbowText.x = 0;
rainbowText.y = 0;
colorBtn.addChild(rainbowText);
} else {
colorBtn.tint = backgroundColors[i];
}
colorBtn.interactive = true;
colorBtn.buttonMode = true;
colorBtn.colorIndex = i;
// Add selection indicator for current background
if (i === currentBgIndex) {
var indicator = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: (colorSize + 20) / 100,
scaleY: (colorSize + 20) / 100,
x: 0,
y: 0
});
indicator.tint = 0xFFD700;
indicator.alpha = 0.8;
colorBtn.addChild(indicator);
}
(function (colorIndex) {
colorBtn.down = function (x, y, obj) {
currentBgIndex = colorIndex;
if (backgroundColors[colorIndex] === 'rainbow') {
startRainbowBackground();
} else {
stopRainbowBackground();
game.setBackgroundColor(backgroundColors[colorIndex]);
}
hideBackgroundMenu();
};
})(i);
backgroundMenu.addChild(colorBtn);
}
// Close button
var closeBtn = new Text2("Close", {
size: 70,
fill: 0xFF4444
});
closeBtn.anchor.set(0.5, 0.5);
closeBtn.x = 2048 / 2;
closeBtn.y = 2732 / 2 + 250;
closeBtn.interactive = true;
closeBtn.buttonMode = true;
closeBtn.down = function (x, y, obj) {
hideBackgroundMenu();
};
backgroundMenu.addChild(closeBtn);
game.addChild(backgroundMenu);
}
// Hide background selection menu
function hideBackgroundMenu() {
if (backgroundMenu) {
game.removeChild(backgroundMenu);
backgroundMenu.destroy();
backgroundMenu = null;
}
}
// Start rainbow background animation
function startRainbowBackground() {
stopRainbowBackground(); // Stop any existing rainbow
rainbowInterval = LK.setInterval(function () {
rainbowHue = (rainbowHue + 2) % 360; // Increment hue
var rgb = hslToRgb(rainbowHue / 360, 0.8, 0.5); // High saturation, medium lightness
var color = rgb[0] << 16 | rgb[1] << 8 | rgb[2];
game.setBackgroundColor(color);
}, 50); // Update every 50ms for smooth animation
}
// Stop rainbow background animation
function stopRainbowBackground() {
if (rainbowInterval) {
LK.clearInterval(rainbowInterval);
rainbowInterval = null;
}
}
// --- Start Game ---
createPuzzle();
;
// Show tutorial only for the first level (first time game is loaded)
showTutorial();
// Block input while tutorial is active
var origGameDown = game.down;
game.down = function (x, y, obj) {
if (tutorialActive) return;
origGameDown(x, y, obj);
};