/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Candy class var Candy = Container.expand(function () { var self = Container.call(this); // Color index: 0-4 self.colorIndex = 0; self.row = 0; self.col = 0; self.isMoving = false; // Used for animation // Attach asset self.setColor = function (idx) { self.colorIndex = idx; if (self.candyAsset) { self.removeChild(self.candyAsset); } var colorIds = ['candyRed', 'candyGreen', 'candyBlue', 'candyYellow', 'candyPurple']; self.candyAsset = self.attachAsset(colorIds[idx], { anchorX: 0.5, anchorY: 0.5 }); }; // Animate to (x, y) self.animateTo = function (x, y, _onFinish) { self.isMoving = true; tween(self, { x: x, y: y }, { duration: 180, easing: tween.cubicInOut, onFinish: function onFinish() { self.isMoving = false; if (_onFinish) _onFinish(); } }); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x222244 }); /**** * Game Code ****/ // --- Game constants --- // 5 candy colors as ellipses // Simple pop sound for matches var BOARD_COLS = 7; var BOARD_ROWS = 9; var CANDY_SIZE = 200; // px, including margin var CANDY_MARGIN = 10; var COLORS = 5; var MOVES_LIMIT = 30; // --- Game state --- var board = []; // 2D array [row][col] of Candy var selectedCandy = null; var swappingCandy = null; var isSwapping = false; var isAnimating = false; var movesLeft = MOVES_LIMIT; var score = 0; // --- UI elements --- var scoreTxt = new Text2('Score: 0', { size: 90, fill: "#fff" }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); var movesTxt = new Text2('Moves: ' + MOVES_LIMIT, { size: 70, fill: "#fff" }); movesTxt.anchor.set(0.5, 0); LK.gui.top.addChild(movesTxt); movesTxt.y = 110; // --- Board positioning --- var boardWidth = BOARD_COLS * CANDY_SIZE; var boardHeight = BOARD_ROWS * CANDY_SIZE; var boardOffsetX = Math.floor((2048 - boardWidth) / 2) + CANDY_SIZE / 2; var boardOffsetY = Math.floor((2732 - boardHeight) / 2) + CANDY_SIZE / 2; // --- Helper functions --- function getCandyPos(row, col) { return { x: boardOffsetX + col * CANDY_SIZE, y: boardOffsetY + row * CANDY_SIZE }; } function randomColor() { return Math.floor(Math.random() * COLORS); } // Returns true if (r1,c1) and (r2,c2) are adjacent function areAdjacent(r1, c1, r2, c2) { return Math.abs(r1 - r2) + Math.abs(c1 - c2) === 1; } // Returns array of {row, col} for all matches (3+ in row or col) function findMatches() { var matches = []; // Horizontal for (var r = 0; r < BOARD_ROWS; r++) { var run = 1; for (var c = 1; c < BOARD_COLS; c++) { var prev = board[r][c - 1]; var curr = board[r][c]; if (prev && curr && prev.colorIndex === curr.colorIndex) { run++; } else { if (run >= 3) { for (var k = 0; k < run; k++) { matches.push({ row: r, col: c - 1 - k }); } } run = 1; } } if (run >= 3) { for (var k = 0; k < run; k++) { matches.push({ row: r, col: BOARD_COLS - 1 - k }); } } } // Vertical for (var c = 0; c < BOARD_COLS; c++) { var run = 1; for (var r = 1; r < BOARD_ROWS; r++) { var prev = board[r - 1][c]; var curr = board[r][c]; if (prev && curr && prev.colorIndex === curr.colorIndex) { run++; } else { if (run >= 3) { for (var k = 0; k < run; k++) { matches.push({ row: r - 1 - k, col: c }); } } run = 1; } } if (run >= 3) { for (var k = 0; k < run; k++) { matches.push({ row: BOARD_ROWS - 1 - k, col: c }); } } } // Remove duplicates var seen = {}; var unique = []; for (var i = 0; i < matches.length; i++) { var key = matches[i].row + ',' + matches[i].col; if (!seen[key]) { unique.push(matches[i]); seen[key] = true; } } return unique; } // Remove candies at given positions, return number removed function removeMatches(matches) { for (var i = 0; i < matches.length; i++) { var r = matches[i].row, c = matches[i].col; var candy = board[r][c]; if (candy) { (function (candy) { tween(candy, { alpha: 0, scaleX: 1.3, scaleY: 1.3 }, { duration: 180, easing: tween.cubicIn, onFinish: function onFinish() { if (candy.parent) candy.parent.removeChild(candy); } }); })(candy); board[r][c] = null; } } if (matches.length > 0) { LK.getSound('pop').play(); } return matches.length; } // Drop candies down to fill empty spaces, return true if any moved function dropCandies(onFinish) { var anyMoved = false; var animCount = 0; for (var c = 0; c < BOARD_COLS; c++) { for (var r = BOARD_ROWS - 1; r >= 0; r--) { if (!board[r][c]) { // Find first non-null above for (var rr = r - 1; rr >= 0; rr--) { if (board[rr][c]) { // Move candy down var candy = board[rr][c]; board[r][c] = candy; board[rr][c] = null; candy.row = r; candy.col = c; var pos = getCandyPos(r, c); animCount++; candy.animateTo(pos.x, pos.y, function () { animCount--; if (animCount === 0 && onFinish) onFinish(); }); anyMoved = true; break; } } } } } // If no candies moved, call onFinish immediately if (!anyMoved && onFinish) { onFinish(); } return anyMoved; } // Fill empty spaces at top with new candies, return true if any added function fillCandies(onFinish) { var anyAdded = false; var animCount = 0; for (var c = 0; c < BOARD_COLS; c++) { for (var r = 0; r < BOARD_ROWS; r++) { if (!board[r][c]) { var candy = new Candy(); var color = randomColor(); candy.setColor(color); candy.row = r; candy.col = c; var pos = getCandyPos(r, c); // Start above board var startY = boardOffsetY - CANDY_SIZE; candy.x = pos.x; candy.y = startY; candy.alpha = 0.7; candy.scaleX = 0.7; candy.scaleY = 0.7; game.addChild(candy); board[r][c] = candy; animCount++; (function (candy, pos) { candy.animateTo(pos.x, pos.y, function () { tween(candy, { alpha: 1, scaleX: 1, scaleY: 1 }, { duration: 100 }); animCount--; if (animCount === 0 && onFinish) onFinish(); }); })(candy, pos); anyAdded = true; } } } if (!anyAdded && onFinish) { onFinish(); } return anyAdded; } // After a move, process all matches, drops, fills, recursively function processBoard(afterAll) { isAnimating = true; var matches = findMatches(); if (matches.length === 0) { isAnimating = false; if (afterAll) afterAll(); return; } score += matches.length * 10; scoreTxt.setText('Score: ' + score); removeMatches(matches); LK.setTimeout(function () { dropCandies(function () { fillCandies(function () { LK.setTimeout(function () { processBoard(afterAll); }, 80); }); }); }, 200); } // Swap two candies (row1,col1) <-> (row2,col2) function swapCandies(r1, c1, r2, c2, animate, onFinish) { var candyA = board[r1][c1]; var candyB = board[r2][c2]; if (!candyA || !candyB) return; // Swap in board board[r1][c1] = candyB; board[r2][c2] = candyA; // Update row/col candyA.row = r2; candyA.col = c2; candyB.row = r1; candyB.col = c1; // Animate if (animate) { var posA = getCandyPos(r2, c2); var posB = getCandyPos(r1, c1); var doneA = false, doneB = false; candyA.animateTo(posA.x, posA.y, function () { doneA = true; if (doneB && onFinish) onFinish(); }); candyB.animateTo(posB.x, posB.y, function () { doneB = true; if (doneA && onFinish) onFinish(); }); } else { var posA = getCandyPos(r2, c2); var posB = getCandyPos(r1, c1); candyA.x = posA.x; candyA.y = posA.y; candyB.x = posB.x; candyB.y = posB.y; if (onFinish) onFinish(); } } // --- Board setup --- function noInitialMatches(r, c, colorIdx) { // Check left 2 if (c >= 2 && board[r][c - 1] && board[r][c - 2]) { if (board[r][c - 1].colorIndex === colorIdx && board[r][c - 2].colorIndex === colorIdx) { return false; } } // Check up 2 if (r >= 2 && board[r - 1][c] && board[r - 2][c]) { if (board[r - 1][c].colorIndex === colorIdx && board[r - 2][c].colorIndex === colorIdx) { return false; } } return true; } // Initialize board for (var r = 0; r < BOARD_ROWS; r++) { board[r] = []; for (var c = 0; c < BOARD_COLS; c++) { var candy = new Candy(); var color = randomColor(); // Avoid initial matches while (!noInitialMatches(r, c, color)) { color = randomColor(); } candy.setColor(color); candy.row = r; candy.col = c; var pos = getCandyPos(r, c); candy.x = pos.x; candy.y = pos.y; game.addChild(candy); board[r][c] = candy; } } // --- Input handling --- // Convert (x, y) to (row, col) on board, or null if outside function posToCell(x, y) { var col = Math.floor((x - boardOffsetX + CANDY_SIZE / 2) / CANDY_SIZE); var row = Math.floor((y - boardOffsetY + CANDY_SIZE / 2) / CANDY_SIZE); if (row < 0 || row >= BOARD_ROWS || col < 0 || col >= BOARD_COLS) return null; return { row: row, col: col }; } // Highlight selected candy (simple scale up) function highlightCandy(candy) { if (!candy) return; tween(candy, { scaleX: 1.18, scaleY: 1.18 }, { duration: 100, easing: tween.cubicOut }); } function unhighlightCandy(candy) { if (!candy) return; tween(candy, { scaleX: 1, scaleY: 1 }, { duration: 100, easing: tween.cubicIn }); } // Touch/drag logic var dragStartCell = null; var dragStartCandy = null; var dragActive = false; game.down = function (x, y, obj) { if (isAnimating) return; var cell = posToCell(x, y); if (!cell) return; var candy = board[cell.row][cell.col]; if (!candy || candy.isMoving) return; dragStartCell = cell; dragStartCandy = candy; dragActive = true; highlightCandy(candy); }; game.move = function (x, y, obj) { if (!dragActive || isAnimating) return; var cell = posToCell(x, y); if (!cell) return; if (!dragStartCell) return; if (cell.row === dragStartCell.row && cell.col === dragStartCell.col) return; if (!areAdjacent(cell.row, cell.col, dragStartCell.row, dragStartCell.col)) return; var targetCandy = board[cell.row][cell.col]; if (!targetCandy || targetCandy.isMoving) return; dragActive = false; unhighlightCandy(dragStartCandy); isSwapping = true; movesLeft--; movesTxt.setText('Moves: ' + movesLeft); // Save original positions for rollback var origA = { row: dragStartCell.row, col: dragStartCell.col }; var origB = { row: cell.row, col: cell.col }; swapCandies(origA.row, origA.col, origB.row, origB.col, true, function () { // After swap, check for matches var matches = findMatches(); if (matches.length === 0) { // No match: swap back with animation swapCandies(origB.row, origB.col, origA.row, origA.col, true, function () { isSwapping = false; if (movesLeft <= 0) { LK.showGameOver(); } }); } else { // Match: process board processBoard(function () { isSwapping = false; if (movesLeft <= 0) { LK.showGameOver(); } }); } }); }; game.up = function (x, y, obj) { if (dragActive) { unhighlightCandy(dragStartCandy); } dragActive = false; dragStartCell = null; dragStartCandy = null; }; // --- Game update loop --- game.update = function () { // No per-frame logic needed; all handled by events and tweens }; // --- End of file ---
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Candy class
var Candy = Container.expand(function () {
var self = Container.call(this);
// Color index: 0-4
self.colorIndex = 0;
self.row = 0;
self.col = 0;
self.isMoving = false; // Used for animation
// Attach asset
self.setColor = function (idx) {
self.colorIndex = idx;
if (self.candyAsset) {
self.removeChild(self.candyAsset);
}
var colorIds = ['candyRed', 'candyGreen', 'candyBlue', 'candyYellow', 'candyPurple'];
self.candyAsset = self.attachAsset(colorIds[idx], {
anchorX: 0.5,
anchorY: 0.5
});
};
// Animate to (x, y)
self.animateTo = function (x, y, _onFinish) {
self.isMoving = true;
tween(self, {
x: x,
y: y
}, {
duration: 180,
easing: tween.cubicInOut,
onFinish: function onFinish() {
self.isMoving = false;
if (_onFinish) _onFinish();
}
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222244
});
/****
* Game Code
****/
// --- Game constants ---
// 5 candy colors as ellipses
// Simple pop sound for matches
var BOARD_COLS = 7;
var BOARD_ROWS = 9;
var CANDY_SIZE = 200; // px, including margin
var CANDY_MARGIN = 10;
var COLORS = 5;
var MOVES_LIMIT = 30;
// --- Game state ---
var board = []; // 2D array [row][col] of Candy
var selectedCandy = null;
var swappingCandy = null;
var isSwapping = false;
var isAnimating = false;
var movesLeft = MOVES_LIMIT;
var score = 0;
// --- UI elements ---
var scoreTxt = new Text2('Score: 0', {
size: 90,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
var movesTxt = new Text2('Moves: ' + MOVES_LIMIT, {
size: 70,
fill: "#fff"
});
movesTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(movesTxt);
movesTxt.y = 110;
// --- Board positioning ---
var boardWidth = BOARD_COLS * CANDY_SIZE;
var boardHeight = BOARD_ROWS * CANDY_SIZE;
var boardOffsetX = Math.floor((2048 - boardWidth) / 2) + CANDY_SIZE / 2;
var boardOffsetY = Math.floor((2732 - boardHeight) / 2) + CANDY_SIZE / 2;
// --- Helper functions ---
function getCandyPos(row, col) {
return {
x: boardOffsetX + col * CANDY_SIZE,
y: boardOffsetY + row * CANDY_SIZE
};
}
function randomColor() {
return Math.floor(Math.random() * COLORS);
}
// Returns true if (r1,c1) and (r2,c2) are adjacent
function areAdjacent(r1, c1, r2, c2) {
return Math.abs(r1 - r2) + Math.abs(c1 - c2) === 1;
}
// Returns array of {row, col} for all matches (3+ in row or col)
function findMatches() {
var matches = [];
// Horizontal
for (var r = 0; r < BOARD_ROWS; r++) {
var run = 1;
for (var c = 1; c < BOARD_COLS; c++) {
var prev = board[r][c - 1];
var curr = board[r][c];
if (prev && curr && prev.colorIndex === curr.colorIndex) {
run++;
} else {
if (run >= 3) {
for (var k = 0; k < run; k++) {
matches.push({
row: r,
col: c - 1 - k
});
}
}
run = 1;
}
}
if (run >= 3) {
for (var k = 0; k < run; k++) {
matches.push({
row: r,
col: BOARD_COLS - 1 - k
});
}
}
}
// Vertical
for (var c = 0; c < BOARD_COLS; c++) {
var run = 1;
for (var r = 1; r < BOARD_ROWS; r++) {
var prev = board[r - 1][c];
var curr = board[r][c];
if (prev && curr && prev.colorIndex === curr.colorIndex) {
run++;
} else {
if (run >= 3) {
for (var k = 0; k < run; k++) {
matches.push({
row: r - 1 - k,
col: c
});
}
}
run = 1;
}
}
if (run >= 3) {
for (var k = 0; k < run; k++) {
matches.push({
row: BOARD_ROWS - 1 - k,
col: c
});
}
}
}
// Remove duplicates
var seen = {};
var unique = [];
for (var i = 0; i < matches.length; i++) {
var key = matches[i].row + ',' + matches[i].col;
if (!seen[key]) {
unique.push(matches[i]);
seen[key] = true;
}
}
return unique;
}
// Remove candies at given positions, return number removed
function removeMatches(matches) {
for (var i = 0; i < matches.length; i++) {
var r = matches[i].row,
c = matches[i].col;
var candy = board[r][c];
if (candy) {
(function (candy) {
tween(candy, {
alpha: 0,
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 180,
easing: tween.cubicIn,
onFinish: function onFinish() {
if (candy.parent) candy.parent.removeChild(candy);
}
});
})(candy);
board[r][c] = null;
}
}
if (matches.length > 0) {
LK.getSound('pop').play();
}
return matches.length;
}
// Drop candies down to fill empty spaces, return true if any moved
function dropCandies(onFinish) {
var anyMoved = false;
var animCount = 0;
for (var c = 0; c < BOARD_COLS; c++) {
for (var r = BOARD_ROWS - 1; r >= 0; r--) {
if (!board[r][c]) {
// Find first non-null above
for (var rr = r - 1; rr >= 0; rr--) {
if (board[rr][c]) {
// Move candy down
var candy = board[rr][c];
board[r][c] = candy;
board[rr][c] = null;
candy.row = r;
candy.col = c;
var pos = getCandyPos(r, c);
animCount++;
candy.animateTo(pos.x, pos.y, function () {
animCount--;
if (animCount === 0 && onFinish) onFinish();
});
anyMoved = true;
break;
}
}
}
}
}
// If no candies moved, call onFinish immediately
if (!anyMoved && onFinish) {
onFinish();
}
return anyMoved;
}
// Fill empty spaces at top with new candies, return true if any added
function fillCandies(onFinish) {
var anyAdded = false;
var animCount = 0;
for (var c = 0; c < BOARD_COLS; c++) {
for (var r = 0; r < BOARD_ROWS; r++) {
if (!board[r][c]) {
var candy = new Candy();
var color = randomColor();
candy.setColor(color);
candy.row = r;
candy.col = c;
var pos = getCandyPos(r, c);
// Start above board
var startY = boardOffsetY - CANDY_SIZE;
candy.x = pos.x;
candy.y = startY;
candy.alpha = 0.7;
candy.scaleX = 0.7;
candy.scaleY = 0.7;
game.addChild(candy);
board[r][c] = candy;
animCount++;
(function (candy, pos) {
candy.animateTo(pos.x, pos.y, function () {
tween(candy, {
alpha: 1,
scaleX: 1,
scaleY: 1
}, {
duration: 100
});
animCount--;
if (animCount === 0 && onFinish) onFinish();
});
})(candy, pos);
anyAdded = true;
}
}
}
if (!anyAdded && onFinish) {
onFinish();
}
return anyAdded;
}
// After a move, process all matches, drops, fills, recursively
function processBoard(afterAll) {
isAnimating = true;
var matches = findMatches();
if (matches.length === 0) {
isAnimating = false;
if (afterAll) afterAll();
return;
}
score += matches.length * 10;
scoreTxt.setText('Score: ' + score);
removeMatches(matches);
LK.setTimeout(function () {
dropCandies(function () {
fillCandies(function () {
LK.setTimeout(function () {
processBoard(afterAll);
}, 80);
});
});
}, 200);
}
// Swap two candies (row1,col1) <-> (row2,col2)
function swapCandies(r1, c1, r2, c2, animate, onFinish) {
var candyA = board[r1][c1];
var candyB = board[r2][c2];
if (!candyA || !candyB) return;
// Swap in board
board[r1][c1] = candyB;
board[r2][c2] = candyA;
// Update row/col
candyA.row = r2;
candyA.col = c2;
candyB.row = r1;
candyB.col = c1;
// Animate
if (animate) {
var posA = getCandyPos(r2, c2);
var posB = getCandyPos(r1, c1);
var doneA = false,
doneB = false;
candyA.animateTo(posA.x, posA.y, function () {
doneA = true;
if (doneB && onFinish) onFinish();
});
candyB.animateTo(posB.x, posB.y, function () {
doneB = true;
if (doneA && onFinish) onFinish();
});
} else {
var posA = getCandyPos(r2, c2);
var posB = getCandyPos(r1, c1);
candyA.x = posA.x;
candyA.y = posA.y;
candyB.x = posB.x;
candyB.y = posB.y;
if (onFinish) onFinish();
}
}
// --- Board setup ---
function noInitialMatches(r, c, colorIdx) {
// Check left 2
if (c >= 2 && board[r][c - 1] && board[r][c - 2]) {
if (board[r][c - 1].colorIndex === colorIdx && board[r][c - 2].colorIndex === colorIdx) {
return false;
}
}
// Check up 2
if (r >= 2 && board[r - 1][c] && board[r - 2][c]) {
if (board[r - 1][c].colorIndex === colorIdx && board[r - 2][c].colorIndex === colorIdx) {
return false;
}
}
return true;
}
// Initialize board
for (var r = 0; r < BOARD_ROWS; r++) {
board[r] = [];
for (var c = 0; c < BOARD_COLS; c++) {
var candy = new Candy();
var color = randomColor();
// Avoid initial matches
while (!noInitialMatches(r, c, color)) {
color = randomColor();
}
candy.setColor(color);
candy.row = r;
candy.col = c;
var pos = getCandyPos(r, c);
candy.x = pos.x;
candy.y = pos.y;
game.addChild(candy);
board[r][c] = candy;
}
}
// --- Input handling ---
// Convert (x, y) to (row, col) on board, or null if outside
function posToCell(x, y) {
var col = Math.floor((x - boardOffsetX + CANDY_SIZE / 2) / CANDY_SIZE);
var row = Math.floor((y - boardOffsetY + CANDY_SIZE / 2) / CANDY_SIZE);
if (row < 0 || row >= BOARD_ROWS || col < 0 || col >= BOARD_COLS) return null;
return {
row: row,
col: col
};
}
// Highlight selected candy (simple scale up)
function highlightCandy(candy) {
if (!candy) return;
tween(candy, {
scaleX: 1.18,
scaleY: 1.18
}, {
duration: 100,
easing: tween.cubicOut
});
}
function unhighlightCandy(candy) {
if (!candy) return;
tween(candy, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.cubicIn
});
}
// Touch/drag logic
var dragStartCell = null;
var dragStartCandy = null;
var dragActive = false;
game.down = function (x, y, obj) {
if (isAnimating) return;
var cell = posToCell(x, y);
if (!cell) return;
var candy = board[cell.row][cell.col];
if (!candy || candy.isMoving) return;
dragStartCell = cell;
dragStartCandy = candy;
dragActive = true;
highlightCandy(candy);
};
game.move = function (x, y, obj) {
if (!dragActive || isAnimating) return;
var cell = posToCell(x, y);
if (!cell) return;
if (!dragStartCell) return;
if (cell.row === dragStartCell.row && cell.col === dragStartCell.col) return;
if (!areAdjacent(cell.row, cell.col, dragStartCell.row, dragStartCell.col)) return;
var targetCandy = board[cell.row][cell.col];
if (!targetCandy || targetCandy.isMoving) return;
dragActive = false;
unhighlightCandy(dragStartCandy);
isSwapping = true;
movesLeft--;
movesTxt.setText('Moves: ' + movesLeft);
// Save original positions for rollback
var origA = {
row: dragStartCell.row,
col: dragStartCell.col
};
var origB = {
row: cell.row,
col: cell.col
};
swapCandies(origA.row, origA.col, origB.row, origB.col, true, function () {
// After swap, check for matches
var matches = findMatches();
if (matches.length === 0) {
// No match: swap back with animation
swapCandies(origB.row, origB.col, origA.row, origA.col, true, function () {
isSwapping = false;
if (movesLeft <= 0) {
LK.showGameOver();
}
});
} else {
// Match: process board
processBoard(function () {
isSwapping = false;
if (movesLeft <= 0) {
LK.showGameOver();
}
});
}
});
};
game.up = function (x, y, obj) {
if (dragActive) {
unhighlightCandy(dragStartCandy);
}
dragActive = false;
dragStartCell = null;
dragStartCandy = null;
};
// --- Game update loop ---
game.update = function () {
// No per-frame logic needed; all handled by events and tweens
};
// --- End of file ---