User prompt
When you finish part 1, move on to part 2.
User prompt
If the target score is reached in the first section, move on to the second section and have 10 moves and the target score is 2500.
User prompt
Let's give 10 moves for the first section. Let's give the target score as 1800.
User prompt
Please fix the bug: 'TypeError: Cannot read properties of null (reading 'row')' in or related to this line: 'var r1 = c1.row,' Line Number: 152
User prompt
fnt music starts when game starts
User prompt
eliminate the spaces between the candies
User prompt
finish the sugars
User prompt
If you make a wrong move, your right to move will be reduced.
User prompt
assign candies to assets
Code edit (1 edits merged)
Please save this source code
User prompt
Candy Match Mania
Initial prompt
There will be 7 different candies and we will need to bring at least 3 of these candies together and explode them, and we will have a certain number of moves.
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ // Candy class: represents a single candy on the board var Candy = Container.expand(function () { var self = Container.call(this); // Properties self.type = 0; // 0-6, for 7 types self.row = 0; self.col = 0; self.isMatched = false; // Attach asset for candy, anchor center self.candyAsset = null; // Set candy type and update asset self.setType = function (type) { self.type = type; // Remove previous asset if exists if (self.candyAsset) { self.removeChild(self.candyAsset); } // Assign each candy type to a unique asset for visual distinction var assetIds = ['Sg1', // type 0 'Sg2', // type 1 'Sg3', // type 2 'Sg4', // type 3 'Sg5', // type 4 'Sg6', // type 5 'Sg7' // type 6 ]; self.candyAsset = self.attachAsset(assetIds[type], { anchorX: 0.5, anchorY: 0.5 }); }; // Animate pop (match) self.pop = function (_onFinish) { tween(self, { scaleX: 1.3, scaleY: 1.3, alpha: 0 }, { duration: 250, easing: tween.easeIn, onFinish: function onFinish() { self.alpha = 1; self.scaleX = 1; self.scaleY = 1; if (_onFinish) _onFinish(); } }); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x222244 }); /**** * Game Code ****/ // --- Game Constants --- var boardCols = 7; var boardRows = 9; var candyTypes = 7; var candySize = 200; // px, will be scaled to fit var boardPadding = 0; // No padding between candies var moveLimit = 20; var targetScore = 1800; // --- Game State --- var board = []; // 2D array [row][col] of Candy var candies = []; // Flat array of all Candy objects var selectedCandy = null; var swappingCandy = null; var isSwapping = false; var isAnimating = false; var movesLeft = moveLimit; var score = 0; var level = storage.level || 1; // --- UI Elements --- var scoreTxt = new Text2('0', { size: 90, fill: "#fff" }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); var movesTxt = new Text2('Moves: ' + movesLeft, { size: 70, fill: "#fff" }); movesTxt.anchor.set(0.5, 0); LK.gui.top.addChild(movesTxt); movesTxt.y = 110; // Best score UI var bestScore = storage.bestScore || 0; var bestScoreTxt = new Text2('Best: ' + bestScore, { size: 60, fill: "#fff" }); bestScoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(bestScoreTxt); bestScoreTxt.y = 190; // Current score UI (label) var currentScoreTxt = new Text2('Score', { size: 60, fill: "#fff" }); currentScoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(currentScoreTxt); currentScoreTxt.y = 40; // --- Board Positioning --- // Add Sg_bg asset as a background layer, scaled to cover the game area var sgBgAsset = LK.getAsset('Sg_bg', { anchorX: 0, anchorY: 0, x: 0, y: 0, width: 2048, height: 2732 }); game.addChild(sgBgAsset); // Add Bg asset as background, scaled to cover the game area var bgAsset = LK.getAsset('Bg', { anchorX: 0, anchorY: 0, x: 0, y: 0, width: 2048, height: 2732 }); game.addChild(bgAsset); var boardWidth = boardCols * candySize; var boardHeight = boardRows * candySize; var boardOffsetX = Math.floor((2048 - boardWidth) / 2); var boardOffsetY = Math.floor((2732 - boardHeight) / 2) + 60; // --- Helper Functions --- // Get board position (x, y) for a given row, col function getBoardPos(row, col) { return { x: boardOffsetX + col * candySize + candySize / 2, y: boardOffsetY + row * candySize + candySize / 2 }; } // Get candy at (row, col) function getCandy(row, col) { if (row < 0 || row >= boardRows || col < 0 || col >= boardCols) return null; return board[row][col]; } // Swap two candies in board and update their row/col function swapCandies(c1, c2) { if (!c1 || !c2) { // Defensive: do nothing if either candy is null or undefined return; } var r1 = c1.row, c1c = c1.col, r2 = c2.row, c2c = c2.col; board[r1][c1c] = c2; board[r2][c2c] = c1; c1.row = r2; c1.col = c2c; c2.row = r1; c2.col = c1c; } // Animate swap between two candies function animateSwap(c1, c2, _onFinish2) { if (!c1 || !c2) { // Defensive: do nothing if either candy is null or undefined isAnimating = false; if (_onFinish2) _onFinish2(); return; } isAnimating = true; var pos1 = getBoardPos(c1.row, c1.col); var pos2 = getBoardPos(c2.row, c2.col); tween(c1, { x: pos2.x, y: pos2.y }, { duration: 180, easing: tween.cubicInOut }); tween(c2, { x: pos1.x, y: pos1.y }, { duration: 180, easing: tween.cubicInOut, onFinish: function onFinish() { isAnimating = false; if (_onFinish2) _onFinish2(); } }); } // Animate candy falling to new position function animateMoveTo(candy, newRow, newCol, onFinish) { var pos = getBoardPos(newRow, newCol); tween(candy, { x: pos.x, y: pos.y }, { duration: 180, easing: tween.cubicInOut, onFinish: onFinish }); } // Deselect all candies function deselectAll() { if (selectedCandy) { tween(selectedCandy, { scaleX: 1, scaleY: 1 }, { duration: 100 }); selectedCandy = null; } } // Check if two candies are adjacent function areAdjacent(c1, c2) { return Math.abs(c1.row - c2.row) + Math.abs(c1.col - c2.col) === 1; } // Find all matches on the board, mark candies as isMatched function findMatches() { var found = false; // Clear previous matches for (var r = 0; r < boardRows; r++) { for (var c = 0; c < boardCols; c++) { if (board[r][c]) board[r][c].isMatched = false; } } // Horizontal matches for (var r = 0; r < boardRows; r++) { var matchLen = 1; for (var c = 1; c < boardCols; c++) { if (board[r][c].type === board[r][c - 1].type) { matchLen++; } else { if (matchLen >= 3) { for (var k = 0; k < matchLen; k++) { board[r][c - 1 - k].isMatched = true; } found = true; } matchLen = 1; } } if (matchLen >= 3) { for (var k = 0; k < matchLen; k++) { board[r][boardCols - 1 - k].isMatched = true; } found = true; } } // Vertical matches for (var c = 0; c < boardCols; c++) { var matchLen = 1; for (var r = 1; r < boardRows; r++) { if (board[r][c].type === board[r - 1][c].type) { matchLen++; } else { if (matchLen >= 3) { for (var k = 0; k < matchLen; k++) { board[r - 1 - k][c].isMatched = true; } found = true; } matchLen = 1; } } if (matchLen >= 3) { for (var k = 0; k < matchLen; k++) { board[boardRows - 1 - k][c].isMatched = true; } found = true; } } return found; } // Remove matched candies, return number removed function removeMatches(onFinish) { var removed = 0; var toPop = []; for (var r = 0; r < boardRows; r++) { for (var c = 0; c < boardCols; c++) { var candy = board[r][c]; if (candy.isMatched) { toPop.push(candy); removed++; } } } if (removed === 0) { if (onFinish) onFinish(); return; } var popped = 0; for (var i = 0; i < toPop.length; i++) { // Play sg_crash sound for each exploding candy LK.getSound('Sg_crash').play(); toPop[i].pop(function () { popped++; if (popped === toPop.length) { for (var j = 0; j < toPop.length; j++) { var c = toPop[j]; game.removeChild(c); candies.splice(candies.indexOf(c), 1); board[c.row][c.col] = null; } if (onFinish) onFinish(removed); } }); } } // Drop candies down to fill empty spaces, return true if any moved function dropCandies(onFinish) { var moved = false; var moves = []; for (var c = 0; c < boardCols; c++) { for (var r = boardRows - 1; r >= 0; r--) { if (board[r][c] === null) { // Find first non-null above for (var k = r - 1; k >= 0; k--) { if (board[k][c]) { var candy = board[k][c]; board[r][c] = candy; board[k][c] = null; var oldRow = candy.row; candy.row = r; moves.push({ candy: candy, from: oldRow, to: r, col: c }); break; } } } } } if (moves.length === 0) { if (onFinish) onFinish(false); return; } var finished = 0; for (var i = 0; i < moves.length; i++) { var move = moves[i]; animateMoveTo(move.candy, move.to, move.col, function () { finished++; if (finished === moves.length) { if (onFinish) onFinish(true); } }); } } // Fill empty spaces at the top with new candies function fillBoard(onFinish) { var filled = 0; var toFill = []; for (var c = 0; c < boardCols; c++) { for (var r = 0; r < boardRows; r++) { if (board[r][c] === null) { var candy = new Candy(); var type = Math.floor(Math.random() * candyTypes); candy.setType(type); candy.row = r; candy.col = c; var pos = getBoardPos(r, c); candy.x = pos.x; candy.y = pos.y - 400; // Drop from above game.addChild(candy); candies.push(candy); board[r][c] = candy; toFill.push(candy); } } } if (toFill.length === 0) { if (onFinish) onFinish(); return; } var finished = 0; for (var i = 0; i < toFill.length; i++) { var candy = toFill[i]; var pos = getBoardPos(candy.row, candy.col); animateMoveTo(candy, candy.row, candy.col, function () { finished++; if (finished === toFill.length) { if (onFinish) onFinish(); } }); } } // Check if the board has any possible moves (for future: shuffle if not) function hasPossibleMoves() { // For MVP, skip shuffle, always assume possible moves return true; } // Update UI function updateUI() { scoreTxt.setText(score); movesTxt.setText('Moves: ' + movesLeft); if (score > bestScore) { bestScore = score; storage.bestScore = bestScore; } bestScoreTxt.setText('Best: ' + bestScore); } // Handle end of turn: check for matches, drop, fill, repeat as needed function resolveBoard(afterResolve) { if (findMatches()) { removeMatches(function (removed) { // Award more points for more candies exploded at once: 3=100, 4=150, 5=200, 6+=4x points var points = 0; if (removed >= 3) { if (removed === 3) { points = 100; } else if (removed === 4) { points = Math.floor(100 * 1.5); // 150 } else if (removed === 5) { points = 200; } else if (removed >= 6) { // 6-pack or more: 4x the base points for that many candies // Base: 3=100, 4=150, 5=200, 6=250, 7=300, 8=350, etc. var base = 100 + Math.max(0, removed - 3) * 50; points = base * 4; } } score += points; updateUI(); dropCandies(function () { fillBoard(function () { resolveBoard(afterResolve); }); }); }); } else { if (afterResolve) afterResolve(); } } // Handle win/lose function checkEnd() { if (movesLeft <= 0) { // Only end the game if there are no matches left to resolve if (!findMatches()) { // Show game over when moves are finished and board is resolved LK.showGameOver(); return true; } // Otherwise, let the board resolve naturally before ending return false; } return false; } // --- Board Initialization --- function createBoard() { // Clear previous for (var i = 0; i < candies.length; i++) { game.removeChild(candies[i]); } candies = []; board = []; for (var r = 0; r < boardRows; r++) { board[r] = []; for (var c = 0; c < boardCols; c++) { var candy = new Candy(); var type = Math.floor(Math.random() * candyTypes); candy.setType(type); candy.row = r; candy.col = c; var pos = getBoardPos(r, c); candy.x = pos.x; candy.y = pos.y; game.addChild(candy); candies.push(candy); board[r][c] = candy; } } // Remove any initial matches while (findMatches()) { for (var r = 0; r < boardRows; r++) { for (var c = 0; c < boardCols; c++) { if (board[r][c] && board[r][c].isMatched) { var newType; do { newType = Math.floor(Math.random() * candyTypes); } while (r > 0 && board[r - 1][c] && board[r - 1][c].type === newType || c > 0 && board[r][c - 1] && board[r][c - 1].type === newType); board[r][c].setType(newType); board[r][c].isMatched = false; } } } } } // --- Input Handling --- // Find candy at (x, y) in game coordinates function findCandyAt(x, y) { for (var i = 0; i < candies.length; i++) { var c = candies[i]; var dx = x - c.x; var dy = y - c.y; if (Math.abs(dx) < candySize / 2 && Math.abs(dy) < candySize / 2) { return c; } } return null; } // Handle tap or drag game.down = function (x, y, obj) { if (isAnimating) return; var c = findCandyAt(x, y); if (!c) { deselectAll(); return; } if (!selectedCandy) { selectedCandy = c; tween(selectedCandy, { scaleX: 1.15, scaleY: 1.15 }, { duration: 100 }); } else if (selectedCandy === c) { deselectAll(); } else if (areAdjacent(selectedCandy, c)) { // Swap attempt isSwapping = true; swappingCandy = c; // Animate swap animateSwap(selectedCandy, swappingCandy, function () { swapCandies(selectedCandy, swappingCandy); // Check if swap creates a match if (findMatches()) { movesLeft--; updateUI(); resolveBoard(function () { deselectAll(); isSwapping = false; swappingCandy = null; checkEnd(); }); } else { // No match, swap back and decrement moves (wrong move) // Play Wrong sound LK.getSound('Wrong').play(); movesLeft--; updateUI(); animateSwap(selectedCandy, swappingCandy, function () { swapCandies(selectedCandy, swappingCandy); deselectAll(); isSwapping = false; swappingCandy = null; checkEnd(); }); } }); } else { // Select new candy tween(selectedCandy, { scaleX: 1, scaleY: 1 }, { duration: 100 }); selectedCandy = c; tween(selectedCandy, { scaleX: 1.15, scaleY: 1.15 }, { duration: 100 }); } }; // Drag to swap game.move = function (x, y, obj) { if (isAnimating || !selectedCandy) return; var c = findCandyAt(x, y); if (c && c !== selectedCandy && areAdjacent(selectedCandy, c)) { game.down(x, y, obj); } }; game.up = function (x, y, obj) { // No-op for now }; // --- Game Start --- function startGame() { level = storage.level || 1; score = 0; if (level === 2) { movesLeft = 10; targetScore = 2500; } else { movesLeft = moveLimit; targetScore = 1800; } updateUI(); createBoard(); deselectAll(); isAnimating = false; isSwapping = false; swappingCandy = null; selectedCandy = null; // Start Fnt music LK.playMusic('Fnt'); // Remove any matches at start resolveBoard(); } startGame(); // --- Game Tick --- game.update = function () { // No per-frame logic needed for MVP };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Candy class: represents a single candy on the board
var Candy = Container.expand(function () {
var self = Container.call(this);
// Properties
self.type = 0; // 0-6, for 7 types
self.row = 0;
self.col = 0;
self.isMatched = false;
// Attach asset for candy, anchor center
self.candyAsset = null;
// Set candy type and update asset
self.setType = function (type) {
self.type = type;
// Remove previous asset if exists
if (self.candyAsset) {
self.removeChild(self.candyAsset);
}
// Assign each candy type to a unique asset for visual distinction
var assetIds = ['Sg1',
// type 0
'Sg2',
// type 1
'Sg3',
// type 2
'Sg4',
// type 3
'Sg5',
// type 4
'Sg6',
// type 5
'Sg7' // type 6
];
self.candyAsset = self.attachAsset(assetIds[type], {
anchorX: 0.5,
anchorY: 0.5
});
};
// Animate pop (match)
self.pop = function (_onFinish) {
tween(self, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0
}, {
duration: 250,
easing: tween.easeIn,
onFinish: function onFinish() {
self.alpha = 1;
self.scaleX = 1;
self.scaleY = 1;
if (_onFinish) _onFinish();
}
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222244
});
/****
* Game Code
****/
// --- Game Constants ---
var boardCols = 7;
var boardRows = 9;
var candyTypes = 7;
var candySize = 200; // px, will be scaled to fit
var boardPadding = 0; // No padding between candies
var moveLimit = 20;
var targetScore = 1800;
// --- Game State ---
var board = []; // 2D array [row][col] of Candy
var candies = []; // Flat array of all Candy objects
var selectedCandy = null;
var swappingCandy = null;
var isSwapping = false;
var isAnimating = false;
var movesLeft = moveLimit;
var score = 0;
var level = storage.level || 1;
// --- UI Elements ---
var scoreTxt = new Text2('0', {
size: 90,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
var movesTxt = new Text2('Moves: ' + movesLeft, {
size: 70,
fill: "#fff"
});
movesTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(movesTxt);
movesTxt.y = 110;
// Best score UI
var bestScore = storage.bestScore || 0;
var bestScoreTxt = new Text2('Best: ' + bestScore, {
size: 60,
fill: "#fff"
});
bestScoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(bestScoreTxt);
bestScoreTxt.y = 190;
// Current score UI (label)
var currentScoreTxt = new Text2('Score', {
size: 60,
fill: "#fff"
});
currentScoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(currentScoreTxt);
currentScoreTxt.y = 40;
// --- Board Positioning ---
// Add Sg_bg asset as a background layer, scaled to cover the game area
var sgBgAsset = LK.getAsset('Sg_bg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 2732
});
game.addChild(sgBgAsset);
// Add Bg asset as background, scaled to cover the game area
var bgAsset = LK.getAsset('Bg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 2732
});
game.addChild(bgAsset);
var boardWidth = boardCols * candySize;
var boardHeight = boardRows * candySize;
var boardOffsetX = Math.floor((2048 - boardWidth) / 2);
var boardOffsetY = Math.floor((2732 - boardHeight) / 2) + 60;
// --- Helper Functions ---
// Get board position (x, y) for a given row, col
function getBoardPos(row, col) {
return {
x: boardOffsetX + col * candySize + candySize / 2,
y: boardOffsetY + row * candySize + candySize / 2
};
}
// Get candy at (row, col)
function getCandy(row, col) {
if (row < 0 || row >= boardRows || col < 0 || col >= boardCols) return null;
return board[row][col];
}
// Swap two candies in board and update their row/col
function swapCandies(c1, c2) {
if (!c1 || !c2) {
// Defensive: do nothing if either candy is null or undefined
return;
}
var r1 = c1.row,
c1c = c1.col,
r2 = c2.row,
c2c = c2.col;
board[r1][c1c] = c2;
board[r2][c2c] = c1;
c1.row = r2;
c1.col = c2c;
c2.row = r1;
c2.col = c1c;
}
// Animate swap between two candies
function animateSwap(c1, c2, _onFinish2) {
if (!c1 || !c2) {
// Defensive: do nothing if either candy is null or undefined
isAnimating = false;
if (_onFinish2) _onFinish2();
return;
}
isAnimating = true;
var pos1 = getBoardPos(c1.row, c1.col);
var pos2 = getBoardPos(c2.row, c2.col);
tween(c1, {
x: pos2.x,
y: pos2.y
}, {
duration: 180,
easing: tween.cubicInOut
});
tween(c2, {
x: pos1.x,
y: pos1.y
}, {
duration: 180,
easing: tween.cubicInOut,
onFinish: function onFinish() {
isAnimating = false;
if (_onFinish2) _onFinish2();
}
});
}
// Animate candy falling to new position
function animateMoveTo(candy, newRow, newCol, onFinish) {
var pos = getBoardPos(newRow, newCol);
tween(candy, {
x: pos.x,
y: pos.y
}, {
duration: 180,
easing: tween.cubicInOut,
onFinish: onFinish
});
}
// Deselect all candies
function deselectAll() {
if (selectedCandy) {
tween(selectedCandy, {
scaleX: 1,
scaleY: 1
}, {
duration: 100
});
selectedCandy = null;
}
}
// Check if two candies are adjacent
function areAdjacent(c1, c2) {
return Math.abs(c1.row - c2.row) + Math.abs(c1.col - c2.col) === 1;
}
// Find all matches on the board, mark candies as isMatched
function findMatches() {
var found = false;
// Clear previous matches
for (var r = 0; r < boardRows; r++) {
for (var c = 0; c < boardCols; c++) {
if (board[r][c]) board[r][c].isMatched = false;
}
}
// Horizontal matches
for (var r = 0; r < boardRows; r++) {
var matchLen = 1;
for (var c = 1; c < boardCols; c++) {
if (board[r][c].type === board[r][c - 1].type) {
matchLen++;
} else {
if (matchLen >= 3) {
for (var k = 0; k < matchLen; k++) {
board[r][c - 1 - k].isMatched = true;
}
found = true;
}
matchLen = 1;
}
}
if (matchLen >= 3) {
for (var k = 0; k < matchLen; k++) {
board[r][boardCols - 1 - k].isMatched = true;
}
found = true;
}
}
// Vertical matches
for (var c = 0; c < boardCols; c++) {
var matchLen = 1;
for (var r = 1; r < boardRows; r++) {
if (board[r][c].type === board[r - 1][c].type) {
matchLen++;
} else {
if (matchLen >= 3) {
for (var k = 0; k < matchLen; k++) {
board[r - 1 - k][c].isMatched = true;
}
found = true;
}
matchLen = 1;
}
}
if (matchLen >= 3) {
for (var k = 0; k < matchLen; k++) {
board[boardRows - 1 - k][c].isMatched = true;
}
found = true;
}
}
return found;
}
// Remove matched candies, return number removed
function removeMatches(onFinish) {
var removed = 0;
var toPop = [];
for (var r = 0; r < boardRows; r++) {
for (var c = 0; c < boardCols; c++) {
var candy = board[r][c];
if (candy.isMatched) {
toPop.push(candy);
removed++;
}
}
}
if (removed === 0) {
if (onFinish) onFinish();
return;
}
var popped = 0;
for (var i = 0; i < toPop.length; i++) {
// Play sg_crash sound for each exploding candy
LK.getSound('Sg_crash').play();
toPop[i].pop(function () {
popped++;
if (popped === toPop.length) {
for (var j = 0; j < toPop.length; j++) {
var c = toPop[j];
game.removeChild(c);
candies.splice(candies.indexOf(c), 1);
board[c.row][c.col] = null;
}
if (onFinish) onFinish(removed);
}
});
}
}
// Drop candies down to fill empty spaces, return true if any moved
function dropCandies(onFinish) {
var moved = false;
var moves = [];
for (var c = 0; c < boardCols; c++) {
for (var r = boardRows - 1; r >= 0; r--) {
if (board[r][c] === null) {
// Find first non-null above
for (var k = r - 1; k >= 0; k--) {
if (board[k][c]) {
var candy = board[k][c];
board[r][c] = candy;
board[k][c] = null;
var oldRow = candy.row;
candy.row = r;
moves.push({
candy: candy,
from: oldRow,
to: r,
col: c
});
break;
}
}
}
}
}
if (moves.length === 0) {
if (onFinish) onFinish(false);
return;
}
var finished = 0;
for (var i = 0; i < moves.length; i++) {
var move = moves[i];
animateMoveTo(move.candy, move.to, move.col, function () {
finished++;
if (finished === moves.length) {
if (onFinish) onFinish(true);
}
});
}
}
// Fill empty spaces at the top with new candies
function fillBoard(onFinish) {
var filled = 0;
var toFill = [];
for (var c = 0; c < boardCols; c++) {
for (var r = 0; r < boardRows; r++) {
if (board[r][c] === null) {
var candy = new Candy();
var type = Math.floor(Math.random() * candyTypes);
candy.setType(type);
candy.row = r;
candy.col = c;
var pos = getBoardPos(r, c);
candy.x = pos.x;
candy.y = pos.y - 400; // Drop from above
game.addChild(candy);
candies.push(candy);
board[r][c] = candy;
toFill.push(candy);
}
}
}
if (toFill.length === 0) {
if (onFinish) onFinish();
return;
}
var finished = 0;
for (var i = 0; i < toFill.length; i++) {
var candy = toFill[i];
var pos = getBoardPos(candy.row, candy.col);
animateMoveTo(candy, candy.row, candy.col, function () {
finished++;
if (finished === toFill.length) {
if (onFinish) onFinish();
}
});
}
}
// Check if the board has any possible moves (for future: shuffle if not)
function hasPossibleMoves() {
// For MVP, skip shuffle, always assume possible moves
return true;
}
// Update UI
function updateUI() {
scoreTxt.setText(score);
movesTxt.setText('Moves: ' + movesLeft);
if (score > bestScore) {
bestScore = score;
storage.bestScore = bestScore;
}
bestScoreTxt.setText('Best: ' + bestScore);
}
// Handle end of turn: check for matches, drop, fill, repeat as needed
function resolveBoard(afterResolve) {
if (findMatches()) {
removeMatches(function (removed) {
// Award more points for more candies exploded at once: 3=100, 4=150, 5=200, 6+=4x points
var points = 0;
if (removed >= 3) {
if (removed === 3) {
points = 100;
} else if (removed === 4) {
points = Math.floor(100 * 1.5); // 150
} else if (removed === 5) {
points = 200;
} else if (removed >= 6) {
// 6-pack or more: 4x the base points for that many candies
// Base: 3=100, 4=150, 5=200, 6=250, 7=300, 8=350, etc.
var base = 100 + Math.max(0, removed - 3) * 50;
points = base * 4;
}
}
score += points;
updateUI();
dropCandies(function () {
fillBoard(function () {
resolveBoard(afterResolve);
});
});
});
} else {
if (afterResolve) afterResolve();
}
}
// Handle win/lose
function checkEnd() {
if (movesLeft <= 0) {
// Only end the game if there are no matches left to resolve
if (!findMatches()) {
// Show game over when moves are finished and board is resolved
LK.showGameOver();
return true;
}
// Otherwise, let the board resolve naturally before ending
return false;
}
return false;
}
// --- Board Initialization ---
function createBoard() {
// Clear previous
for (var i = 0; i < candies.length; i++) {
game.removeChild(candies[i]);
}
candies = [];
board = [];
for (var r = 0; r < boardRows; r++) {
board[r] = [];
for (var c = 0; c < boardCols; c++) {
var candy = new Candy();
var type = Math.floor(Math.random() * candyTypes);
candy.setType(type);
candy.row = r;
candy.col = c;
var pos = getBoardPos(r, c);
candy.x = pos.x;
candy.y = pos.y;
game.addChild(candy);
candies.push(candy);
board[r][c] = candy;
}
}
// Remove any initial matches
while (findMatches()) {
for (var r = 0; r < boardRows; r++) {
for (var c = 0; c < boardCols; c++) {
if (board[r][c] && board[r][c].isMatched) {
var newType;
do {
newType = Math.floor(Math.random() * candyTypes);
} while (r > 0 && board[r - 1][c] && board[r - 1][c].type === newType || c > 0 && board[r][c - 1] && board[r][c - 1].type === newType);
board[r][c].setType(newType);
board[r][c].isMatched = false;
}
}
}
}
}
// --- Input Handling ---
// Find candy at (x, y) in game coordinates
function findCandyAt(x, y) {
for (var i = 0; i < candies.length; i++) {
var c = candies[i];
var dx = x - c.x;
var dy = y - c.y;
if (Math.abs(dx) < candySize / 2 && Math.abs(dy) < candySize / 2) {
return c;
}
}
return null;
}
// Handle tap or drag
game.down = function (x, y, obj) {
if (isAnimating) return;
var c = findCandyAt(x, y);
if (!c) {
deselectAll();
return;
}
if (!selectedCandy) {
selectedCandy = c;
tween(selectedCandy, {
scaleX: 1.15,
scaleY: 1.15
}, {
duration: 100
});
} else if (selectedCandy === c) {
deselectAll();
} else if (areAdjacent(selectedCandy, c)) {
// Swap attempt
isSwapping = true;
swappingCandy = c;
// Animate swap
animateSwap(selectedCandy, swappingCandy, function () {
swapCandies(selectedCandy, swappingCandy);
// Check if swap creates a match
if (findMatches()) {
movesLeft--;
updateUI();
resolveBoard(function () {
deselectAll();
isSwapping = false;
swappingCandy = null;
checkEnd();
});
} else {
// No match, swap back and decrement moves (wrong move)
// Play Wrong sound
LK.getSound('Wrong').play();
movesLeft--;
updateUI();
animateSwap(selectedCandy, swappingCandy, function () {
swapCandies(selectedCandy, swappingCandy);
deselectAll();
isSwapping = false;
swappingCandy = null;
checkEnd();
});
}
});
} else {
// Select new candy
tween(selectedCandy, {
scaleX: 1,
scaleY: 1
}, {
duration: 100
});
selectedCandy = c;
tween(selectedCandy, {
scaleX: 1.15,
scaleY: 1.15
}, {
duration: 100
});
}
};
// Drag to swap
game.move = function (x, y, obj) {
if (isAnimating || !selectedCandy) return;
var c = findCandyAt(x, y);
if (c && c !== selectedCandy && areAdjacent(selectedCandy, c)) {
game.down(x, y, obj);
}
};
game.up = function (x, y, obj) {
// No-op for now
};
// --- Game Start ---
function startGame() {
level = storage.level || 1;
score = 0;
if (level === 2) {
movesLeft = 10;
targetScore = 2500;
} else {
movesLeft = moveLimit;
targetScore = 1800;
}
updateUI();
createBoard();
deselectAll();
isAnimating = false;
isSwapping = false;
swappingCandy = null;
selectedCandy = null;
// Start Fnt music
LK.playMusic('Fnt');
// Remove any matches at start
resolveBoard();
}
startGame();
// --- Game Tick ---
game.update = function () {
// No per-frame logic needed for MVP
};