/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
level: 1,
bestScore: 0
});
/****
* Classes
****/
// Candy class
var Candy = Container.expand(function () {
var self = Container.call(this);
// Properties
self.type = null; // 0-5 for normal, 6 for striped
self.row = 0;
self.col = 0;
self.isSpecial = false;
// Attach asset
self.setType = function (type, isSpecial) {
self.type = type;
self.isSpecial = !!isSpecial;
self.removeChildren();
var assetId;
if (isSpecial) {
assetId = 'candyStriped';
} else {
assetId = ['candyRed', 'candyGreen', 'candyBlue', 'candyYellow', 'candyPurple', 'candyOrange'][type];
}
var candyGfx = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// For striped, tint to match base color
if (isSpecial) {
var tints = [0xff4b5c, 0x4be04b, 0x4b7bff, 0xffe14b, 0xb44bff, 0xffa14b];
candyGfx.tint = tints[type];
}
};
// Animate pop
self.pop = function (_onFinish) {
tween(self, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0
}, {
duration: 220,
easing: tween.easeIn,
onFinish: function onFinish() {
if (_onFinish) _onFinish();
}
});
};
// Animate drop
self.dropTo = function (newY, onFinish) {
tween(self, {
y: newY
}, {
duration: 220,
easing: tween.bounceOut,
onFinish: onFinish
});
};
// Animate swap
self.swapTo = function (newX, newY, onFinish) {
tween(self, {
x: newX,
y: newY
}, {
duration: 180,
easing: tween.cubicInOut,
onFinish: onFinish
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181828
});
/****
* Game Code
****/
// Music
// Sound effects
// Board background
// Special candy (striped)
// Candy shapes/colors (6 types)
// Board config
var BOARD_SIZE = 8;
var CELL_SIZE = 200;
var BOARD_OFFSET_X = (2048 - BOARD_SIZE * CELL_SIZE) / 2;
var BOARD_OFFSET_Y = 320;
var CANDY_TYPES = 6;
var MOVES_PER_LEVEL = 20;
var TARGET_SCORE_BASE = 1200;
// State
var board = []; // 2D array [row][col] of Candy
var selectedCandy = null;
var swappingCandy = null;
var isAnimating = false;
var movesLeft = MOVES_PER_LEVEL;
var score = 0;
var targetScore = TARGET_SCORE_BASE + (storage.level - 1) * 400;
var level = storage.level || 1;
// GUI
var scoreTxt = new Text2('Score: 0', {
size: 90,
fill: '#fff'
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
scoreTxt.y = 30;
scoreTxt.x = 2048 / 2;
// Total score
var totalScore = storage.totalScore || 0;
var totalScoreTxt = new Text2('Total: ' + totalScore, {
size: 60,
fill: '#fff'
});
totalScoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(totalScoreTxt);
totalScoreTxt.y = 100;
totalScoreTxt.x = 2048 / 2;
var movesTxt = new Text2('Moves: ' + MOVES_PER_LEVEL, {
size: 70,
fill: '#fff'
});
movesTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(movesTxt);
movesTxt.y = 140;
movesTxt.x = 2048 / 2;
var targetTxt = new Text2('Target: ' + targetScore, {
size: 60,
fill: '#fff'
});
targetTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(targetTxt);
targetTxt.y = 220;
targetTxt.x = 2048 / 2;
// Leaderboard button
var leaderboardBtn = new Text2('๐', {
size: 90,
fill: '#fff'
});
leaderboardBtn.anchor.set(1, 0);
LK.gui.top.addChild(leaderboardBtn);
// Place at top right, leaving margin for safe area
leaderboardBtn.x = 2048 - 40;
leaderboardBtn.y = 30;
// Show leaderboard on press
leaderboardBtn.down = function (x, y, obj) {
LK.showLeaderboard();
};
// Board background
var boardBg = LK.getAsset('boardBg', {
anchorX: 0,
anchorY: 0
});
boardBg.x = BOARD_OFFSET_X - 10;
boardBg.y = BOARD_OFFSET_Y - 10;
game.addChild(boardBg);
// Helper: get candy at board position
function getCandy(row, col) {
if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) return null;
return board[row][col];
}
// Helper: get board position from x,y
function getBoardPos(x, y) {
var col = Math.floor((x - BOARD_OFFSET_X) / CELL_SIZE);
var row = Math.floor((y - BOARD_OFFSET_Y) / CELL_SIZE);
if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) return null;
return {
row: row,
col: col
};
}
// Helper: get pixel position from board pos
function getPixelPos(row, col) {
return {
x: BOARD_OFFSET_X + col * CELL_SIZE + CELL_SIZE / 2,
y: BOARD_OFFSET_Y + row * CELL_SIZE + CELL_SIZE / 2
};
}
// Helper: random candy type
function randomCandyType() {
return Math.floor(Math.random() * CANDY_TYPES);
}
// Helper: check if two candies are adjacent
function areAdjacent(c1, c2) {
if (!c1 || !c2) return false;
var dr = Math.abs(c1.row - c2.row);
var dc = Math.abs(c1.col - c2.col);
return dr + dc === 1;
}
// Helper: find all matches on board
function findMatches() {
var matches = [];
// Horizontal
for (var r = 0; r < BOARD_SIZE; r++) {
var runType = null,
runStart = 0,
runLen = 0;
for (var c = 0; c <= BOARD_SIZE; c++) {
var candy = c < BOARD_SIZE ? board[r][c] : null;
var t = candy && !candy.isSpecial ? candy.type : -1;
if (t === runType) {
runLen++;
} else {
if (runLen >= 3 && runType !== null) {
var arr = [];
for (var i = runStart; i < runStart + runLen; i++) arr.push(board[r][i]);
matches.push(arr);
}
runType = t;
runStart = c;
runLen = 1;
}
}
}
// Vertical
for (var c = 0; c < BOARD_SIZE; c++) {
var runType = null,
runStart = 0,
runLen = 0;
for (var r = 0; r <= BOARD_SIZE; r++) {
var candy = r < BOARD_SIZE ? board[r][c] : null;
var t = candy && !candy.isSpecial ? candy.type : -1;
if (t === runType) {
runLen++;
} else {
if (runLen >= 3 && runType !== null) {
var arr = [];
for (var i = runStart; i < runStart + runLen; i++) arr.push(board[i][c]);
matches.push(arr);
}
runType = t;
runStart = r;
runLen = 1;
}
}
}
return matches;
}
// Helper: find special matches (4+)
function findSpecials(matches) {
var specials = [];
for (var i = 0; i < matches.length; i++) {
if (matches[i].length === 4) specials.push({
candies: matches[i],
type: 'striped'
});
// For MVP, only striped for 4-match
}
return specials;
}
// Helper: remove candies in matches, return positions to fill
function removeMatches(matches, specials) {
var toRemove = [];
for (var i = 0; i < matches.length; i++) {
for (var j = 0; j < matches[i].length; j++) {
if (toRemove.indexOf(matches[i][j]) === -1) toRemove.push(matches[i][j]);
}
}
// Mark special candies
for (var i = 0; i < specials.length; i++) {
var arr = specials[i].candies;
var idx = Math.floor(arr.length / 2);
var c = arr[idx];
// Only create special if not already special
if (!c.isSpecial) {
c.setType(c.type, true);
}
// Remove all except special
for (var j = 0; j < arr.length; j++) {
if (arr[j] !== c && toRemove.indexOf(arr[j]) === -1) toRemove.push(arr[j]);
}
}
// Animate and remove
var removed = 0;
if (toRemove.length > 0) {
LK.getSound('match').play();
}
for (var i = 0; i < toRemove.length; i++) {
(function (candy) {
// Play match sound for each candy popped
LK.getSound('match').play();
candy.pop(function () {
game.removeChild(candy);
});
// Remove from board
board[candy.row][candy.col] = null;
removed++;
})(toRemove[i]);
}
return removed;
}
// Helper: drop candies and fill new
function dropAndFill(onFinish) {
// For each column, drop candies down
var maxDrop = 0;
for (var c = 0; c < BOARD_SIZE; c++) {
var empty = 0;
for (var r = BOARD_SIZE - 1; r >= 0; r--) {
var candy = board[r][c];
if (!candy) {
empty++;
} else if (empty > 0) {
board[r + empty][c] = candy;
board[r][c] = null;
var pos = getPixelPos(r + empty, c);
candy.row = r + empty;
candy.col = c;
candy.dropTo(pos.y);
if (empty > maxDrop) maxDrop = empty;
}
}
// Fill new candies at top
for (var e = 0; e < empty; e++) {
var type = randomCandyType();
var newCandy = new Candy();
newCandy.setType(type, false);
newCandy.row = e;
newCandy.col = c;
newCandy.isSpecial = false;
var pos = getPixelPos(e, c);
newCandy.x = pos.x;
newCandy.y = pos.y - empty * CELL_SIZE;
game.addChild(newCandy);
board[e][c] = newCandy;
newCandy.dropTo(pos.y);
}
}
// Wait for drop animation
LK.setTimeout(onFinish, 240);
}
// Helper: check if board has any matches
function hasAnyMatches() {
var m = findMatches();
return m.length > 0;
}
// Helper: shuffle board if no moves
function shuffleBoard() {
// Simple shuffle: randomize all candies
var candies = [];
for (var r = 0; r < BOARD_SIZE; r++) for (var c = 0; c < BOARD_SIZE; c++) candies.push(board[r][c]);
for (var i = candies.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
// Swap
var trow = candies[i].row,
tcol = candies[i].col;
candies[i].row = candies[j].row;
candies[i].col = candies[j].col;
candies[j].row = trow;
candies[j].col = tcol;
board[candies[i].row][candies[i].col] = candies[i];
board[candies[j].row][candies[j].col] = candies[j];
// Animate
var posI = getPixelPos(candies[i].row, candies[i].col);
var posJ = getPixelPos(candies[j].row, candies[j].col);
candies[i].swapTo(posI.x, posI.y);
candies[j].swapTo(posJ.x, posJ.y);
}
}
// Helper: check if any valid moves exist (MVP: skip, always allow shuffle after 3 failed attempts)
// Initialize board
function initBoard() {
// Remove old candies
for (var r = 0; r < BOARD_SIZE; r++) for (var c = 0; c < BOARD_SIZE; c++) {
if (board[r] && board[r][c]) {
game.removeChild(board[r][c]);
}
}
board = [];
for (var r = 0; r < BOARD_SIZE; r++) {
board[r] = [];
for (var c = 0; c < BOARD_SIZE; c++) {
var type = randomCandyType();
var candy = new Candy();
candy.setType(type, false);
candy.row = r;
candy.col = c;
candy.isSpecial = false;
var pos = getPixelPos(r, c);
candy.x = pos.x;
candy.y = pos.y;
game.addChild(candy);
board[r][c] = candy;
}
}
// Remove initial matches
while (hasAnyMatches()) {
var matches = findMatches();
for (var i = 0; i < matches.length; i++) {
for (var j = 0; j < matches[i].length; j++) {
var c = matches[i][j];
var t;
do {
t = randomCandyType();
} while (t === c.type);
c.setType(t, false);
}
}
}
}
// Start new level
function startLevel() {
movesLeft = MOVES_PER_LEVEL;
score = 0;
level = storage.level || 1;
targetScore = TARGET_SCORE_BASE + (level - 1) * 400;
scoreTxt.setText('Score: 0');
movesTxt.setText('Moves: ' + movesLeft);
targetTxt.setText('Target: ' + targetScore);
selectedCandy = null;
swappingCandy = null;
isAnimating = false;
initBoard();
}
// Handle swap logic
function trySwap(c1, c2) {
if (!c1 || !c2 || isAnimating) return;
if (!areAdjacent(c1, c2)) return;
isAnimating = true;
// Swap in board
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
var pos1 = getPixelPos(c1.row, c1.col);
var pos2 = getPixelPos(c2.row, c2.col);
var done = 0;
c1.swapTo(pos1.x, pos1.y, function () {
done++;
if (done === 2) afterSwap();
});
c2.swapTo(pos2.x, pos2.y, function () {
done++;
if (done === 2) afterSwap();
});
LK.getSound('swap').play();
function afterSwap() {
var matches = findMatches();
if (matches.length === 0) {
// No match, swap back
board[r1][c1c] = c1;
board[r2][c2c] = c2;
c1.row = r1;
c1.col = c1c;
c2.row = r2;
c2.col = c2c;
var pos1b = getPixelPos(c1.row, c1.col);
var pos2b = getPixelPos(c2.row, c2.col);
c1.swapTo(pos1b.x, pos1b.y);
c2.swapTo(pos2b.x, pos2b.y, function () {
isAnimating = false;
LK.getSound('noMatch').play();
});
} else {
// Valid match
movesLeft--;
movesTxt.setText('Moves: ' + movesLeft);
resolveMatches(matches);
}
selectedCandy = null;
swappingCandy = null;
}
}
// Resolve matches recursively
function resolveMatches(matches) {
if (!matches || matches.length === 0) {
isAnimating = false;
// Check for win/lose
if (score >= targetScore) {
// Win
storage.level = level + 1;
if (score > storage.bestScore) storage.bestScore = score;
LK.showYouWin();
} else if (movesLeft <= 0) {
// Lose
storage.level = 1;
// Uncomment the next two lines to reset total score on game over
// totalScore = 0;
// storage.totalScore = 0;
LK.showGameOver();
}
return;
}
LK.getSound('match').play();
// Special candies
var specials = findSpecials(matches);
// Activate special candies (striped) if matched
for (var i = 0; i < matches.length; i++) {
for (var j = 0; j < matches[i].length; j++) {
var candy = matches[i][j];
if (candy && candy.isSpecial) {
// Striped: clear row or column
var isRow = false;
// Check if this match is horizontal or vertical
for (var k = 0; k < matches[i].length - 1; k++) {
if (matches[i][k].row === matches[i][k + 1].row) isRow = true;
}
if (isRow) {
// Clear entire row
for (var c = 0; c < BOARD_SIZE; c++) {
var target = board[candy.row][c];
if (target && !target.isSpecial) {
target.pop(function () {});
game.removeChild(target);
board[candy.row][c] = null;
}
}
} else {
// Clear entire column
for (var r = 0; r < BOARD_SIZE; r++) {
var target = board[r][candy.col];
if (target && !target.isSpecial) {
target.pop(function () {});
game.removeChild(target);
board[r][candy.col] = null;
}
}
}
LK.getSound('special').play();
}
}
}
// Remove matches
var removed = removeMatches(matches, specials);
// Score
score += removed * 60;
if (score > storage.bestScore) storage.bestScore = score;
scoreTxt.setText('Score: ' + score);
// Update total score and persist
totalScore += removed * 60;
storage.totalScore = totalScore;
totalScoreTxt.setText('Total: ' + totalScore);
// Show total score text when candies are matched (merged)
totalScoreTxt.visible = true;
// Play match sound when candies are merged
LK.getSound('match').play();
// Drop and fill
dropAndFill(function () {
// Check for new matches
var newMatches = findMatches();
if (newMatches.length > 0) {
// Chain
LK.getSound('special').play();
resolveMatches(newMatches);
} else {
isAnimating = false;
}
});
}
// Touch/drag logic
var dragStart = null;
var dragStartCandy = null;
game.down = function (x, y, obj) {
if (isAnimating) return;
var pos = getBoardPos(x, y);
if (!pos) return;
var candy = getCandy(pos.row, pos.col);
if (!candy) return;
dragStart = {
x: x,
y: y
};
dragStartCandy = candy;
selectedCandy = candy;
// Highlight (scale up)
tween(candy, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 100
});
};
game.move = function (x, y, obj) {
if (!dragStart || !dragStartCandy || isAnimating) return;
var dx = x - dragStart.x;
var dy = y - dragStart.y;
if (Math.abs(dx) < 40 && Math.abs(dy) < 40) return;
// Determine direction
var dir = null;
if (Math.abs(dx) > Math.abs(dy)) {
dir = dx > 0 ? 'right' : 'left';
} else {
dir = dy > 0 ? 'down' : 'up';
}
var pos = {
row: dragStartCandy.row,
col: dragStartCandy.col
};
if (dir === 'right') pos.col++;
if (dir === 'left') pos.col--;
if (dir === 'down') pos.row++;
if (dir === 'up') pos.row--;
var target = getCandy(pos.row, pos.col);
if (target) {
trySwap(dragStartCandy, target);
}
// Reset highlight
tween(dragStartCandy, {
scaleX: 1,
scaleY: 1
}, {
duration: 100
});
dragStart = null;
dragStartCandy = null;
};
game.up = function (x, y, obj) {
if (selectedCandy) {
tween(selectedCandy, {
scaleX: 1,
scaleY: 1
}, {
duration: 100
});
selectedCandy = null;
}
dragStart = null;
dragStartCandy = null;
};
// Main update (not used for logic, but could be used for hint/shuffle)
game.update = function () {
// No-op for now
};
// Start game
startLevel();
// Play music
LK.playMusic('bgmusic', {
fade: {
start: 0,
end: 0.7,
duration: 1200
}
}); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
level: 1,
bestScore: 0
});
/****
* Classes
****/
// Candy class
var Candy = Container.expand(function () {
var self = Container.call(this);
// Properties
self.type = null; // 0-5 for normal, 6 for striped
self.row = 0;
self.col = 0;
self.isSpecial = false;
// Attach asset
self.setType = function (type, isSpecial) {
self.type = type;
self.isSpecial = !!isSpecial;
self.removeChildren();
var assetId;
if (isSpecial) {
assetId = 'candyStriped';
} else {
assetId = ['candyRed', 'candyGreen', 'candyBlue', 'candyYellow', 'candyPurple', 'candyOrange'][type];
}
var candyGfx = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// For striped, tint to match base color
if (isSpecial) {
var tints = [0xff4b5c, 0x4be04b, 0x4b7bff, 0xffe14b, 0xb44bff, 0xffa14b];
candyGfx.tint = tints[type];
}
};
// Animate pop
self.pop = function (_onFinish) {
tween(self, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0
}, {
duration: 220,
easing: tween.easeIn,
onFinish: function onFinish() {
if (_onFinish) _onFinish();
}
});
};
// Animate drop
self.dropTo = function (newY, onFinish) {
tween(self, {
y: newY
}, {
duration: 220,
easing: tween.bounceOut,
onFinish: onFinish
});
};
// Animate swap
self.swapTo = function (newX, newY, onFinish) {
tween(self, {
x: newX,
y: newY
}, {
duration: 180,
easing: tween.cubicInOut,
onFinish: onFinish
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181828
});
/****
* Game Code
****/
// Music
// Sound effects
// Board background
// Special candy (striped)
// Candy shapes/colors (6 types)
// Board config
var BOARD_SIZE = 8;
var CELL_SIZE = 200;
var BOARD_OFFSET_X = (2048 - BOARD_SIZE * CELL_SIZE) / 2;
var BOARD_OFFSET_Y = 320;
var CANDY_TYPES = 6;
var MOVES_PER_LEVEL = 20;
var TARGET_SCORE_BASE = 1200;
// State
var board = []; // 2D array [row][col] of Candy
var selectedCandy = null;
var swappingCandy = null;
var isAnimating = false;
var movesLeft = MOVES_PER_LEVEL;
var score = 0;
var targetScore = TARGET_SCORE_BASE + (storage.level - 1) * 400;
var level = storage.level || 1;
// GUI
var scoreTxt = new Text2('Score: 0', {
size: 90,
fill: '#fff'
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
scoreTxt.y = 30;
scoreTxt.x = 2048 / 2;
// Total score
var totalScore = storage.totalScore || 0;
var totalScoreTxt = new Text2('Total: ' + totalScore, {
size: 60,
fill: '#fff'
});
totalScoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(totalScoreTxt);
totalScoreTxt.y = 100;
totalScoreTxt.x = 2048 / 2;
var movesTxt = new Text2('Moves: ' + MOVES_PER_LEVEL, {
size: 70,
fill: '#fff'
});
movesTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(movesTxt);
movesTxt.y = 140;
movesTxt.x = 2048 / 2;
var targetTxt = new Text2('Target: ' + targetScore, {
size: 60,
fill: '#fff'
});
targetTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(targetTxt);
targetTxt.y = 220;
targetTxt.x = 2048 / 2;
// Leaderboard button
var leaderboardBtn = new Text2('๐', {
size: 90,
fill: '#fff'
});
leaderboardBtn.anchor.set(1, 0);
LK.gui.top.addChild(leaderboardBtn);
// Place at top right, leaving margin for safe area
leaderboardBtn.x = 2048 - 40;
leaderboardBtn.y = 30;
// Show leaderboard on press
leaderboardBtn.down = function (x, y, obj) {
LK.showLeaderboard();
};
// Board background
var boardBg = LK.getAsset('boardBg', {
anchorX: 0,
anchorY: 0
});
boardBg.x = BOARD_OFFSET_X - 10;
boardBg.y = BOARD_OFFSET_Y - 10;
game.addChild(boardBg);
// Helper: get candy at board position
function getCandy(row, col) {
if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) return null;
return board[row][col];
}
// Helper: get board position from x,y
function getBoardPos(x, y) {
var col = Math.floor((x - BOARD_OFFSET_X) / CELL_SIZE);
var row = Math.floor((y - BOARD_OFFSET_Y) / CELL_SIZE);
if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) return null;
return {
row: row,
col: col
};
}
// Helper: get pixel position from board pos
function getPixelPos(row, col) {
return {
x: BOARD_OFFSET_X + col * CELL_SIZE + CELL_SIZE / 2,
y: BOARD_OFFSET_Y + row * CELL_SIZE + CELL_SIZE / 2
};
}
// Helper: random candy type
function randomCandyType() {
return Math.floor(Math.random() * CANDY_TYPES);
}
// Helper: check if two candies are adjacent
function areAdjacent(c1, c2) {
if (!c1 || !c2) return false;
var dr = Math.abs(c1.row - c2.row);
var dc = Math.abs(c1.col - c2.col);
return dr + dc === 1;
}
// Helper: find all matches on board
function findMatches() {
var matches = [];
// Horizontal
for (var r = 0; r < BOARD_SIZE; r++) {
var runType = null,
runStart = 0,
runLen = 0;
for (var c = 0; c <= BOARD_SIZE; c++) {
var candy = c < BOARD_SIZE ? board[r][c] : null;
var t = candy && !candy.isSpecial ? candy.type : -1;
if (t === runType) {
runLen++;
} else {
if (runLen >= 3 && runType !== null) {
var arr = [];
for (var i = runStart; i < runStart + runLen; i++) arr.push(board[r][i]);
matches.push(arr);
}
runType = t;
runStart = c;
runLen = 1;
}
}
}
// Vertical
for (var c = 0; c < BOARD_SIZE; c++) {
var runType = null,
runStart = 0,
runLen = 0;
for (var r = 0; r <= BOARD_SIZE; r++) {
var candy = r < BOARD_SIZE ? board[r][c] : null;
var t = candy && !candy.isSpecial ? candy.type : -1;
if (t === runType) {
runLen++;
} else {
if (runLen >= 3 && runType !== null) {
var arr = [];
for (var i = runStart; i < runStart + runLen; i++) arr.push(board[i][c]);
matches.push(arr);
}
runType = t;
runStart = r;
runLen = 1;
}
}
}
return matches;
}
// Helper: find special matches (4+)
function findSpecials(matches) {
var specials = [];
for (var i = 0; i < matches.length; i++) {
if (matches[i].length === 4) specials.push({
candies: matches[i],
type: 'striped'
});
// For MVP, only striped for 4-match
}
return specials;
}
// Helper: remove candies in matches, return positions to fill
function removeMatches(matches, specials) {
var toRemove = [];
for (var i = 0; i < matches.length; i++) {
for (var j = 0; j < matches[i].length; j++) {
if (toRemove.indexOf(matches[i][j]) === -1) toRemove.push(matches[i][j]);
}
}
// Mark special candies
for (var i = 0; i < specials.length; i++) {
var arr = specials[i].candies;
var idx = Math.floor(arr.length / 2);
var c = arr[idx];
// Only create special if not already special
if (!c.isSpecial) {
c.setType(c.type, true);
}
// Remove all except special
for (var j = 0; j < arr.length; j++) {
if (arr[j] !== c && toRemove.indexOf(arr[j]) === -1) toRemove.push(arr[j]);
}
}
// Animate and remove
var removed = 0;
if (toRemove.length > 0) {
LK.getSound('match').play();
}
for (var i = 0; i < toRemove.length; i++) {
(function (candy) {
// Play match sound for each candy popped
LK.getSound('match').play();
candy.pop(function () {
game.removeChild(candy);
});
// Remove from board
board[candy.row][candy.col] = null;
removed++;
})(toRemove[i]);
}
return removed;
}
// Helper: drop candies and fill new
function dropAndFill(onFinish) {
// For each column, drop candies down
var maxDrop = 0;
for (var c = 0; c < BOARD_SIZE; c++) {
var empty = 0;
for (var r = BOARD_SIZE - 1; r >= 0; r--) {
var candy = board[r][c];
if (!candy) {
empty++;
} else if (empty > 0) {
board[r + empty][c] = candy;
board[r][c] = null;
var pos = getPixelPos(r + empty, c);
candy.row = r + empty;
candy.col = c;
candy.dropTo(pos.y);
if (empty > maxDrop) maxDrop = empty;
}
}
// Fill new candies at top
for (var e = 0; e < empty; e++) {
var type = randomCandyType();
var newCandy = new Candy();
newCandy.setType(type, false);
newCandy.row = e;
newCandy.col = c;
newCandy.isSpecial = false;
var pos = getPixelPos(e, c);
newCandy.x = pos.x;
newCandy.y = pos.y - empty * CELL_SIZE;
game.addChild(newCandy);
board[e][c] = newCandy;
newCandy.dropTo(pos.y);
}
}
// Wait for drop animation
LK.setTimeout(onFinish, 240);
}
// Helper: check if board has any matches
function hasAnyMatches() {
var m = findMatches();
return m.length > 0;
}
// Helper: shuffle board if no moves
function shuffleBoard() {
// Simple shuffle: randomize all candies
var candies = [];
for (var r = 0; r < BOARD_SIZE; r++) for (var c = 0; c < BOARD_SIZE; c++) candies.push(board[r][c]);
for (var i = candies.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
// Swap
var trow = candies[i].row,
tcol = candies[i].col;
candies[i].row = candies[j].row;
candies[i].col = candies[j].col;
candies[j].row = trow;
candies[j].col = tcol;
board[candies[i].row][candies[i].col] = candies[i];
board[candies[j].row][candies[j].col] = candies[j];
// Animate
var posI = getPixelPos(candies[i].row, candies[i].col);
var posJ = getPixelPos(candies[j].row, candies[j].col);
candies[i].swapTo(posI.x, posI.y);
candies[j].swapTo(posJ.x, posJ.y);
}
}
// Helper: check if any valid moves exist (MVP: skip, always allow shuffle after 3 failed attempts)
// Initialize board
function initBoard() {
// Remove old candies
for (var r = 0; r < BOARD_SIZE; r++) for (var c = 0; c < BOARD_SIZE; c++) {
if (board[r] && board[r][c]) {
game.removeChild(board[r][c]);
}
}
board = [];
for (var r = 0; r < BOARD_SIZE; r++) {
board[r] = [];
for (var c = 0; c < BOARD_SIZE; c++) {
var type = randomCandyType();
var candy = new Candy();
candy.setType(type, false);
candy.row = r;
candy.col = c;
candy.isSpecial = false;
var pos = getPixelPos(r, c);
candy.x = pos.x;
candy.y = pos.y;
game.addChild(candy);
board[r][c] = candy;
}
}
// Remove initial matches
while (hasAnyMatches()) {
var matches = findMatches();
for (var i = 0; i < matches.length; i++) {
for (var j = 0; j < matches[i].length; j++) {
var c = matches[i][j];
var t;
do {
t = randomCandyType();
} while (t === c.type);
c.setType(t, false);
}
}
}
}
// Start new level
function startLevel() {
movesLeft = MOVES_PER_LEVEL;
score = 0;
level = storage.level || 1;
targetScore = TARGET_SCORE_BASE + (level - 1) * 400;
scoreTxt.setText('Score: 0');
movesTxt.setText('Moves: ' + movesLeft);
targetTxt.setText('Target: ' + targetScore);
selectedCandy = null;
swappingCandy = null;
isAnimating = false;
initBoard();
}
// Handle swap logic
function trySwap(c1, c2) {
if (!c1 || !c2 || isAnimating) return;
if (!areAdjacent(c1, c2)) return;
isAnimating = true;
// Swap in board
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
var pos1 = getPixelPos(c1.row, c1.col);
var pos2 = getPixelPos(c2.row, c2.col);
var done = 0;
c1.swapTo(pos1.x, pos1.y, function () {
done++;
if (done === 2) afterSwap();
});
c2.swapTo(pos2.x, pos2.y, function () {
done++;
if (done === 2) afterSwap();
});
LK.getSound('swap').play();
function afterSwap() {
var matches = findMatches();
if (matches.length === 0) {
// No match, swap back
board[r1][c1c] = c1;
board[r2][c2c] = c2;
c1.row = r1;
c1.col = c1c;
c2.row = r2;
c2.col = c2c;
var pos1b = getPixelPos(c1.row, c1.col);
var pos2b = getPixelPos(c2.row, c2.col);
c1.swapTo(pos1b.x, pos1b.y);
c2.swapTo(pos2b.x, pos2b.y, function () {
isAnimating = false;
LK.getSound('noMatch').play();
});
} else {
// Valid match
movesLeft--;
movesTxt.setText('Moves: ' + movesLeft);
resolveMatches(matches);
}
selectedCandy = null;
swappingCandy = null;
}
}
// Resolve matches recursively
function resolveMatches(matches) {
if (!matches || matches.length === 0) {
isAnimating = false;
// Check for win/lose
if (score >= targetScore) {
// Win
storage.level = level + 1;
if (score > storage.bestScore) storage.bestScore = score;
LK.showYouWin();
} else if (movesLeft <= 0) {
// Lose
storage.level = 1;
// Uncomment the next two lines to reset total score on game over
// totalScore = 0;
// storage.totalScore = 0;
LK.showGameOver();
}
return;
}
LK.getSound('match').play();
// Special candies
var specials = findSpecials(matches);
// Activate special candies (striped) if matched
for (var i = 0; i < matches.length; i++) {
for (var j = 0; j < matches[i].length; j++) {
var candy = matches[i][j];
if (candy && candy.isSpecial) {
// Striped: clear row or column
var isRow = false;
// Check if this match is horizontal or vertical
for (var k = 0; k < matches[i].length - 1; k++) {
if (matches[i][k].row === matches[i][k + 1].row) isRow = true;
}
if (isRow) {
// Clear entire row
for (var c = 0; c < BOARD_SIZE; c++) {
var target = board[candy.row][c];
if (target && !target.isSpecial) {
target.pop(function () {});
game.removeChild(target);
board[candy.row][c] = null;
}
}
} else {
// Clear entire column
for (var r = 0; r < BOARD_SIZE; r++) {
var target = board[r][candy.col];
if (target && !target.isSpecial) {
target.pop(function () {});
game.removeChild(target);
board[r][candy.col] = null;
}
}
}
LK.getSound('special').play();
}
}
}
// Remove matches
var removed = removeMatches(matches, specials);
// Score
score += removed * 60;
if (score > storage.bestScore) storage.bestScore = score;
scoreTxt.setText('Score: ' + score);
// Update total score and persist
totalScore += removed * 60;
storage.totalScore = totalScore;
totalScoreTxt.setText('Total: ' + totalScore);
// Show total score text when candies are matched (merged)
totalScoreTxt.visible = true;
// Play match sound when candies are merged
LK.getSound('match').play();
// Drop and fill
dropAndFill(function () {
// Check for new matches
var newMatches = findMatches();
if (newMatches.length > 0) {
// Chain
LK.getSound('special').play();
resolveMatches(newMatches);
} else {
isAnimating = false;
}
});
}
// Touch/drag logic
var dragStart = null;
var dragStartCandy = null;
game.down = function (x, y, obj) {
if (isAnimating) return;
var pos = getBoardPos(x, y);
if (!pos) return;
var candy = getCandy(pos.row, pos.col);
if (!candy) return;
dragStart = {
x: x,
y: y
};
dragStartCandy = candy;
selectedCandy = candy;
// Highlight (scale up)
tween(candy, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 100
});
};
game.move = function (x, y, obj) {
if (!dragStart || !dragStartCandy || isAnimating) return;
var dx = x - dragStart.x;
var dy = y - dragStart.y;
if (Math.abs(dx) < 40 && Math.abs(dy) < 40) return;
// Determine direction
var dir = null;
if (Math.abs(dx) > Math.abs(dy)) {
dir = dx > 0 ? 'right' : 'left';
} else {
dir = dy > 0 ? 'down' : 'up';
}
var pos = {
row: dragStartCandy.row,
col: dragStartCandy.col
};
if (dir === 'right') pos.col++;
if (dir === 'left') pos.col--;
if (dir === 'down') pos.row++;
if (dir === 'up') pos.row--;
var target = getCandy(pos.row, pos.col);
if (target) {
trySwap(dragStartCandy, target);
}
// Reset highlight
tween(dragStartCandy, {
scaleX: 1,
scaleY: 1
}, {
duration: 100
});
dragStart = null;
dragStartCandy = null;
};
game.up = function (x, y, obj) {
if (selectedCandy) {
tween(selectedCandy, {
scaleX: 1,
scaleY: 1
}, {
duration: 100
});
selectedCandy = null;
}
dragStart = null;
dragStartCandy = null;
};
// Main update (not used for logic, but could be used for hint/shuffle)
game.update = function () {
// No-op for now
};
// Start game
startLevel();
// Play music
LK.playMusic('bgmusic', {
fade: {
start: 0,
end: 0.7,
duration: 1200
}
});
Mavi sฬงeker. In-Game asset. 2d. High contrast. No shadows
Yesฬงil seker. In-Game asset. 2d. High contrast. No shadows
Turuncu sฬงeker. In-Game asset. 2d. High contrast. No shadows
Mor sฬงeker. In-Game asset. 2d. High contrast. No shadows
Sarฤฑ sฬงeker. In-Game asset. 2d. High contrast. No shadows
Kฤฑrmฤฑzฤฑ sฬงeker. In-Game asset. 2d. High contrast. No shadows