/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Tile class for each tile on the board var Tile = Container.expand(function () { var self = Container.call(this); self.value = 3; // default, will be set on creation self.row = 0; self.col = 0; // Attach the correct asset for the tile value self.tileAsset = null; function getTileAssetId(val) { // Only defined values have unique assets var ids = [3, 6, 12, 24, 48, 96, 192, 384, 768, 1536, 3072]; if (ids.indexOf(val) !== -1) return 'tile' + val; // Fallback to closest lower for (var i = ids.length - 1; i >= 0; i--) { if (val >= ids[i]) return 'tile' + ids[i]; } return 'tile3'; } // Attach asset for the current value function setTileAsset(val) { var assetId = getTileAssetId(val); // Remove old asset if present if (self.tileAsset) { self.removeChild(self.tileAsset); self.tileAsset.destroy(); self.tileAsset = null; } self.tileAsset = self.attachAsset(assetId, { width: 400, height: 400, anchorX: 0.5, anchorY: 0.5 }); // Always ensure tileAsset is at the bottom if (self.valueText && self.children.indexOf(self.valueText) !== -1) { self.setChildIndex(self.tileAsset, 0); } } // Text for the tile value // Helper to get a contrasting shadow color for a given tile value function getShadowColor(val) { // Light tiles get dark shadow, dark tiles get light shadow // Use the tile asset color as a base var colorMap = { 3: 0xf9f6f2, 6: 0xede0c8, 12: 0xf2b179, 24: 0xf59563, 48: 0xf67c5f, 96: 0xf65e3b, 192: 0xffd600, 384: 0xffb400, 768: 0xff8c00, 1536: 0xff4d00, 3072: 0xff1e56 }; var base = colorMap[val] !== undefined ? colorMap[val] : 0xf9f6f2; // Calculate perceived brightness var r = base >> 16 & 0xff, g = base >> 8 & 0xff, b = base & 0xff; var brightness = 0.299 * r + 0.587 * g + 0.114 * b; // If bright, use dark shadow; if dark, use light shadow return brightness > 180 ? "#444444" : "#fff8"; } // Helper to get a stylish font for the tile value function getTileFont(val) { // Use a bold, rounded font for style // Just return the font string, do not set any property here return "'GillSans-Bold', Impact, 'Arial Black', Tahoma, sans-serif"; } // Create the value text with shadow and style self.valueText = new Text2('3', { size: 120, fill: 0x333333, font: getTileFont(3), shadow: { color: getShadowColor(3), blur: 12, offsetX: 0, offsetY: 8 }, stroke: "#ffffff88", strokeThickness: 6 }); self.valueText.anchor.set(0.5, 0.5); self.addChild(self.valueText); // Set tile value and update appearance self.setValue = function (val) { self.value = val; self.valueText.setText(val + ''); // Update font, shadow, and stroke for new value self.valueText.setStyle({ font: getTileFont(val), shadow: { color: getShadowColor(val), blur: 12, offsetX: 0, offsetY: 8 }, stroke: "#ffffff88", strokeThickness: 6 }); setTileAsset(val); }; // Animate pop-in self.pop = function () { self.scaleX = self.scaleY = 0.2; tween(self, { scaleX: 1, scaleY: 1 }, { duration: 120, easing: tween.easeOut }); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0xf7f7f7 }); /**** * Game Code ****/ // Global mute state for merge sound // Pleasant merge sound effect // --- Constants --- // Unique tile assets for each value var mergeSoundMuted = false; var GRID_SIZE = 4; var TILE_SIZE = 400; var TILE_MARGIN = 32; var BOARD_SIZE = GRID_SIZE * TILE_SIZE + (GRID_SIZE + 1) * TILE_MARGIN; var BOARD_X = (2048 - BOARD_SIZE) / 2; var BOARD_Y = (2732 - BOARD_SIZE) / 2 + 100; // --- State --- var board = []; // 2D array of tiles or null var tileNodes = []; // 2D array of Tile objects or null var score = 0; var bestTile = 3; var moving = false; // Prevent input during animation // --- GUI --- var scoreTxt = new Text2('Score: 0', { size: 90, fill: 0x333333 }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); var bestTxt = new Text2('Best: 3', { size: 60, fill: 0x888888 }); bestTxt.anchor.set(0.5, 0); LK.gui.top.addChild(bestTxt); bestTxt.y = 110; // --- Merge Sound Mute Toggle Button --- var mergeMuteBtn = new Text2('🔊', { size: 80, fill: 0x666666, font: "'GillSans-Bold', Impact, 'Arial Black', Tahoma, sans-serif", stroke: "#fff8", strokeThickness: 4 }); mergeMuteBtn.anchor.set(0.5, 0.5); // Place at top right, but not in the top left 100x100 area mergeMuteBtn.x = LK.gui.width - 120; mergeMuteBtn.y = 80; mergeMuteBtn.interactive = true; mergeMuteBtn.buttonMode = true; mergeMuteBtn.updateIcon = function () { mergeMuteBtn.setText(mergeSoundMuted ? "🔇" : "🔊"); mergeMuteBtn.setStyle({ fill: mergeSoundMuted ? "#bbbbbb" : "#666666" }); }; mergeMuteBtn.updateIcon(); mergeMuteBtn.down = function (x, y, obj) { mergeSoundMuted = !mergeSoundMuted; mergeMuteBtn.updateIcon(); }; LK.gui.top.addChild(mergeMuteBtn); // --- Board background --- var boardBg = LK.getAsset('boardBg', { width: BOARD_SIZE, height: BOARD_SIZE, color: 0xbbb9b6, anchorX: 0, anchorY: 0, x: BOARD_X, y: BOARD_Y }); game.addChild(boardBg); // --- Board grid background tiles --- for (var r = 0; r < GRID_SIZE; r++) { for (var c = 0; c < GRID_SIZE; c++) { var cellBg = LK.getAsset('cellBg', { width: TILE_SIZE, height: TILE_SIZE, color: 0xcdc1b4, anchorX: 0.5, anchorY: 0.5, x: BOARD_X + TILE_MARGIN + c * (TILE_SIZE + TILE_MARGIN) + TILE_SIZE / 2, y: BOARD_Y + TILE_MARGIN + r * (TILE_SIZE + TILE_MARGIN) + TILE_SIZE / 2 }); game.addChild(cellBg); } } // --- Helper: get position for a tile --- function getTilePos(row, col) { return { x: BOARD_X + TILE_MARGIN + col * (TILE_SIZE + TILE_MARGIN) + TILE_SIZE / 2, y: BOARD_Y + TILE_MARGIN + row * (TILE_SIZE + TILE_MARGIN) + TILE_SIZE / 2 }; } // --- Helper: spawn a new tile (3 or 6) in a random empty cell --- function spawnTile() { var empties = []; for (var r = 0; r < GRID_SIZE; r++) { for (var c = 0; c < GRID_SIZE; c++) { if (board[r][c] === null) { empties.push({ r: r, c: c }); } } } if (empties.length === 0) return false; var idx = Math.floor(Math.random() * empties.length); var pos = empties[idx]; var val = Math.random() < 0.8 ? 3 : 6; addTile(pos.r, pos.c, val, true); return true; } // --- Helper: add a tile to the board and scene --- function addTile(row, col, value, animate) { var tile = new Tile(); tile.setValue(value); tile.row = row; tile.col = col; var pos = getTilePos(row, col); tile.x = pos.x; tile.y = pos.y; board[row][col] = value; tileNodes[row][col] = tile; game.addChild(tile); if (animate) tile.pop(); } // --- Helper: remove a tile from the board and scene --- function removeTile(row, col) { if (tileNodes[row][col]) { tileNodes[row][col].destroy(); } board[row][col] = null; tileNodes[row][col] = null; } // --- Helper: update score and best tile display --- function updateScore(add) { score += add; scoreTxt.setText('Score: ' + score); } function updateBestTile(val) { if (val > bestTile) { bestTile = val; bestTxt.setText('Best: ' + bestTile); } } // --- Helper: check if any moves are possible --- function movesAvailable() { for (var r = 0; r < GRID_SIZE; r++) { for (var c = 0; c < GRID_SIZE; c++) { if (board[r][c] === null) return true; var v = board[r][c]; // Check right if (c < GRID_SIZE - 1 && board[r][c + 1] === v) return true; // Check down if (r < GRID_SIZE - 1 && board[r + 1][c] === v) return true; } } return false; } // --- Helper: check for win (tile 3072) --- function checkWin() { for (var r = 0; r < GRID_SIZE; r++) { for (var c = 0; c < GRID_SIZE; c++) { if (board[r][c] === 3072) { LK.showYouWin(); return true; } } } return false; } // --- Helper: move/merge tiles in a direction --- // dir: {x: -1, y:0} for left, {x:1,y:0} for right, etc. function moveTiles(dir) { if (moving) return; moving = true; // Prepare for move var moved = false; var merged = []; for (var r = 0; r < GRID_SIZE; r++) { merged[r] = []; for (var c = 0; c < GRID_SIZE; c++) merged[r][c] = false; } // Helper to get traversal order for classic 2048 function getTraversal(dir) { var rows = [], cols = []; for (var i = 0; i < GRID_SIZE; i++) { rows.push(i); cols.push(i); } if (dir.x === 1) cols = cols.reverse(); if (dir.y === 1) rows = rows.reverse(); return { rows: rows, cols: cols }; } var traversal = getTraversal(dir); var movedAny = false; // --- Animation-aware move/merge logic --- var moveDuration = 120; // ms for slide var mergeDuration = 100; // ms for merge pop // Prepare to track original tile nodes and their moves var moveActions = []; var mergeActions = []; var oldTileNodes = []; for (var r = 0; r < GRID_SIZE; r++) { oldTileNodes[r] = []; for (var c = 0; c < GRID_SIZE; c++) { oldTileNodes[r][c] = tileNodes[r][c]; } } // Build new board and move/merge actions var newBoard = []; var newTileNodes = []; for (var r = 0; r < GRID_SIZE; r++) { newBoard[r] = []; newTileNodes[r] = []; for (var c = 0; c < GRID_SIZE; c++) { newBoard[r][c] = null; newTileNodes[r][c] = null; } } // For each cell in traversal order, slide and merge for (var i = 0; i < traversal.rows.length; i++) { for (var j = 0; j < traversal.cols.length; j++) { var r = traversal.rows[i]; var c = traversal.cols[j]; var val = board[r][c]; if (val === null) continue; var targetR = r, targetC = c; // Slide as far as possible in the direction while (true) { var testR = targetR + (dir.y || 0); var testC = targetC + (dir.x || 0); if (testR < 0 || testR >= GRID_SIZE || testC < 0 || testC >= GRID_SIZE) break; if (newBoard[testR][testC] === null) { targetR = testR; targetC = testC; } else if (newBoard[testR][testC] === val && !merged[testR][testC]) { // Merge! targetR = testR; targetC = testC; break; } else { break; } } // If merge is possible if (newBoard[targetR][targetC] === val && !merged[targetR][targetC]) { // Merge newBoard[targetR][targetC] = val * 2; merged[targetR][targetC] = true; updateScore(val * 2); updateBestTile(val * 2); movedAny = true; // Animate: move to merge, then pop moveActions.push({ tile: oldTileNodes[r][c], from: { row: r, col: c }, to: { row: targetR, col: targetC }, merge: true, value: val * 2 }); // Mark for removal after animation mergeActions.push({ row: targetR, col: targetC, value: val * 2 }); } else { // Move to empty spot if (targetR !== r || targetC !== c) movedAny = true; newBoard[targetR][targetC] = val; // Animate: move only moveActions.push({ tile: oldTileNodes[r][c], from: { row: r, col: c }, to: { row: targetR, col: targetC }, merge: false, value: val }); } } } // Remove all old tiles from board state (not from scene yet) for (var r = 0; r < GRID_SIZE; r++) { for (var c = 0; c < GRID_SIZE; c++) { board[r][c] = null; tileNodes[r][c] = null; } } // Animate all moves var animCount = moveActions.length; if (animCount === 0) { moving = false; return; } for (var i = 0; i < moveActions.length; i++) { (function (action) { var tile = action.tile; if (!tile) { animCount--; return; } var pos = getTilePos(action.to.row, action.to.col); // Bring to top for merges if (action.merge) { tile.zIndex = 1000; } tween(tile, { x: pos.x, y: pos.y }, { duration: moveDuration, easing: tween.easeInOut, onFinish: function onFinish() { // If merge, destroy this tile and create a new one with pop if (action.merge) { // Play merge sound if not muted if (!mergeSoundMuted) { LK.getSound('mergeSfx').play(); } tile.destroy(); // Remove any tile at merge target (should not exist, but safety) if (tileNodes[action.to.row][action.to.col]) { tileNodes[action.to.row][action.to.col].destroy(); } // Add merged tile with pop var mergedTile = new Tile(); mergedTile.setValue(action.value); mergedTile.row = action.to.row; mergedTile.col = action.to.col; var mpos = getTilePos(action.to.row, action.to.col); mergedTile.x = mpos.x; mergedTile.y = mpos.y; game.addChild(mergedTile); mergedTile.pop(); tileNodes[action.to.row][action.to.col] = mergedTile; board[action.to.row][action.to.col] = action.value; } else { // Just move, update tileNodes and board tile.row = action.to.row; tile.col = action.to.col; tileNodes[action.to.row][action.to.col] = tile; board[action.to.row][action.to.col] = action.value; } animCount--; if (animCount === 0) { // After all animations, spawn new tile if needed // Only spawn a new tile if there were moves and NO merges if (movedAny && mergeActions.length === 0) { spawnTile(); } // Update score and best tile display after every move scoreTxt.setText('Score: ' + score); bestTxt.setText('Best: ' + bestTile); // Check for win checkWin(); // Check for game over if (!movesAvailable()) { LK.setTimeout(function () { LK.showGameOver(); }, 400); } moving = false; } } }); })(moveActions[i]); } return; // Update score and best tile display after every move scoreTxt.setText('Score: ' + score); bestTxt.setText('Best: ' + bestTile); // Check for win checkWin(); // Check for game over if (!movesAvailable()) { LK.setTimeout(function () { LK.showGameOver(); }, 400); } moving = false; } // --- Input handling --- // Touch/drag swipe detection var touchStartX = null, touchStartY = null, touchStartTime = null; game.down = function (x, y, obj) { touchStartX = x; touchStartY = y; touchStartTime = Date.now(); }; game.up = function (x, y, obj) { if (touchStartX === null || moving) return; var dx = x - touchStartX; var dy = y - touchStartY; var adx = Math.abs(dx), ady = Math.abs(dy); if (adx < 50 && ady < 50) { touchStartX = null; touchStartY = null; return; } if (adx > ady) { if (dx > 0) moveTiles({ x: 1, y: 0 }); // right else moveTiles({ x: -1, y: 0 }); // left } else { if (dy > 0) moveTiles({ x: 0, y: 1 }); // down else moveTiles({ x: 0, y: -1 }); // up } touchStartX = null; touchStartY = null; }; // Keyboard controls (for desktop) game.move = function (x, y, obj) { // No-op for drag, but we want to support keyboard if (obj && obj.event && obj.event.type === 'keydown' && !moving) { var key = obj.event.key; if (key === 'ArrowLeft') moveTiles({ x: -1, y: 0 });else if (key === 'ArrowRight') moveTiles({ x: 1, y: 0 });else if (key === 'ArrowUp') moveTiles({ x: 0, y: -1 });else if (key === 'ArrowDown') moveTiles({ x: 0, y: 1 }); } }; // --- Game initialization --- function resetGame() { // Clear board for (var r = 0; r < GRID_SIZE; r++) { for (var c = 0; c < GRID_SIZE; c++) { if (tileNodes[r] && tileNodes[r][c]) { tileNodes[r][c].destroy(); } } } board = []; tileNodes = []; for (var r = 0; r < GRID_SIZE; r++) { board[r] = []; tileNodes[r] = []; for (var c = 0; c < GRID_SIZE; c++) { board[r][c] = null; tileNodes[r][c] = null; } } score = 0; bestTile = 3; scoreTxt.setText('Score: 0'); bestTxt.setText('Best: 3'); // Ensure score and best tile are displayed clearly after reset scoreTxt.anchor.set(0.5, 0); bestTxt.anchor.set(0.5, 0); moving = false; // Spawn two tiles spawnTile(); spawnTile(); } resetGame(); // --- Game tick (not used, but required for LK) --- game.update = function () { // No per-frame logic needed };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Tile class for each tile on the board
var Tile = Container.expand(function () {
var self = Container.call(this);
self.value = 3; // default, will be set on creation
self.row = 0;
self.col = 0;
// Attach the correct asset for the tile value
self.tileAsset = null;
function getTileAssetId(val) {
// Only defined values have unique assets
var ids = [3, 6, 12, 24, 48, 96, 192, 384, 768, 1536, 3072];
if (ids.indexOf(val) !== -1) return 'tile' + val;
// Fallback to closest lower
for (var i = ids.length - 1; i >= 0; i--) {
if (val >= ids[i]) return 'tile' + ids[i];
}
return 'tile3';
}
// Attach asset for the current value
function setTileAsset(val) {
var assetId = getTileAssetId(val);
// Remove old asset if present
if (self.tileAsset) {
self.removeChild(self.tileAsset);
self.tileAsset.destroy();
self.tileAsset = null;
}
self.tileAsset = self.attachAsset(assetId, {
width: 400,
height: 400,
anchorX: 0.5,
anchorY: 0.5
});
// Always ensure tileAsset is at the bottom
if (self.valueText && self.children.indexOf(self.valueText) !== -1) {
self.setChildIndex(self.tileAsset, 0);
}
}
// Text for the tile value
// Helper to get a contrasting shadow color for a given tile value
function getShadowColor(val) {
// Light tiles get dark shadow, dark tiles get light shadow
// Use the tile asset color as a base
var colorMap = {
3: 0xf9f6f2,
6: 0xede0c8,
12: 0xf2b179,
24: 0xf59563,
48: 0xf67c5f,
96: 0xf65e3b,
192: 0xffd600,
384: 0xffb400,
768: 0xff8c00,
1536: 0xff4d00,
3072: 0xff1e56
};
var base = colorMap[val] !== undefined ? colorMap[val] : 0xf9f6f2;
// Calculate perceived brightness
var r = base >> 16 & 0xff,
g = base >> 8 & 0xff,
b = base & 0xff;
var brightness = 0.299 * r + 0.587 * g + 0.114 * b;
// If bright, use dark shadow; if dark, use light shadow
return brightness > 180 ? "#444444" : "#fff8";
}
// Helper to get a stylish font for the tile value
function getTileFont(val) {
// Use a bold, rounded font for style
// Just return the font string, do not set any property here
return "'GillSans-Bold', Impact, 'Arial Black', Tahoma, sans-serif";
}
// Create the value text with shadow and style
self.valueText = new Text2('3', {
size: 120,
fill: 0x333333,
font: getTileFont(3),
shadow: {
color: getShadowColor(3),
blur: 12,
offsetX: 0,
offsetY: 8
},
stroke: "#ffffff88",
strokeThickness: 6
});
self.valueText.anchor.set(0.5, 0.5);
self.addChild(self.valueText);
// Set tile value and update appearance
self.setValue = function (val) {
self.value = val;
self.valueText.setText(val + '');
// Update font, shadow, and stroke for new value
self.valueText.setStyle({
font: getTileFont(val),
shadow: {
color: getShadowColor(val),
blur: 12,
offsetX: 0,
offsetY: 8
},
stroke: "#ffffff88",
strokeThickness: 6
});
setTileAsset(val);
};
// Animate pop-in
self.pop = function () {
self.scaleX = self.scaleY = 0.2;
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeOut
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xf7f7f7
});
/****
* Game Code
****/
// Global mute state for merge sound
// Pleasant merge sound effect
// --- Constants ---
// Unique tile assets for each value
var mergeSoundMuted = false;
var GRID_SIZE = 4;
var TILE_SIZE = 400;
var TILE_MARGIN = 32;
var BOARD_SIZE = GRID_SIZE * TILE_SIZE + (GRID_SIZE + 1) * TILE_MARGIN;
var BOARD_X = (2048 - BOARD_SIZE) / 2;
var BOARD_Y = (2732 - BOARD_SIZE) / 2 + 100;
// --- State ---
var board = []; // 2D array of tiles or null
var tileNodes = []; // 2D array of Tile objects or null
var score = 0;
var bestTile = 3;
var moving = false; // Prevent input during animation
// --- GUI ---
var scoreTxt = new Text2('Score: 0', {
size: 90,
fill: 0x333333
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
var bestTxt = new Text2('Best: 3', {
size: 60,
fill: 0x888888
});
bestTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(bestTxt);
bestTxt.y = 110;
// --- Merge Sound Mute Toggle Button ---
var mergeMuteBtn = new Text2('🔊', {
size: 80,
fill: 0x666666,
font: "'GillSans-Bold', Impact, 'Arial Black', Tahoma, sans-serif",
stroke: "#fff8",
strokeThickness: 4
});
mergeMuteBtn.anchor.set(0.5, 0.5);
// Place at top right, but not in the top left 100x100 area
mergeMuteBtn.x = LK.gui.width - 120;
mergeMuteBtn.y = 80;
mergeMuteBtn.interactive = true;
mergeMuteBtn.buttonMode = true;
mergeMuteBtn.updateIcon = function () {
mergeMuteBtn.setText(mergeSoundMuted ? "🔇" : "🔊");
mergeMuteBtn.setStyle({
fill: mergeSoundMuted ? "#bbbbbb" : "#666666"
});
};
mergeMuteBtn.updateIcon();
mergeMuteBtn.down = function (x, y, obj) {
mergeSoundMuted = !mergeSoundMuted;
mergeMuteBtn.updateIcon();
};
LK.gui.top.addChild(mergeMuteBtn);
// --- Board background ---
var boardBg = LK.getAsset('boardBg', {
width: BOARD_SIZE,
height: BOARD_SIZE,
color: 0xbbb9b6,
anchorX: 0,
anchorY: 0,
x: BOARD_X,
y: BOARD_Y
});
game.addChild(boardBg);
// --- Board grid background tiles ---
for (var r = 0; r < GRID_SIZE; r++) {
for (var c = 0; c < GRID_SIZE; c++) {
var cellBg = LK.getAsset('cellBg', {
width: TILE_SIZE,
height: TILE_SIZE,
color: 0xcdc1b4,
anchorX: 0.5,
anchorY: 0.5,
x: BOARD_X + TILE_MARGIN + c * (TILE_SIZE + TILE_MARGIN) + TILE_SIZE / 2,
y: BOARD_Y + TILE_MARGIN + r * (TILE_SIZE + TILE_MARGIN) + TILE_SIZE / 2
});
game.addChild(cellBg);
}
}
// --- Helper: get position for a tile ---
function getTilePos(row, col) {
return {
x: BOARD_X + TILE_MARGIN + col * (TILE_SIZE + TILE_MARGIN) + TILE_SIZE / 2,
y: BOARD_Y + TILE_MARGIN + row * (TILE_SIZE + TILE_MARGIN) + TILE_SIZE / 2
};
}
// --- Helper: spawn a new tile (3 or 6) in a random empty cell ---
function spawnTile() {
var empties = [];
for (var r = 0; r < GRID_SIZE; r++) {
for (var c = 0; c < GRID_SIZE; c++) {
if (board[r][c] === null) {
empties.push({
r: r,
c: c
});
}
}
}
if (empties.length === 0) return false;
var idx = Math.floor(Math.random() * empties.length);
var pos = empties[idx];
var val = Math.random() < 0.8 ? 3 : 6;
addTile(pos.r, pos.c, val, true);
return true;
}
// --- Helper: add a tile to the board and scene ---
function addTile(row, col, value, animate) {
var tile = new Tile();
tile.setValue(value);
tile.row = row;
tile.col = col;
var pos = getTilePos(row, col);
tile.x = pos.x;
tile.y = pos.y;
board[row][col] = value;
tileNodes[row][col] = tile;
game.addChild(tile);
if (animate) tile.pop();
}
// --- Helper: remove a tile from the board and scene ---
function removeTile(row, col) {
if (tileNodes[row][col]) {
tileNodes[row][col].destroy();
}
board[row][col] = null;
tileNodes[row][col] = null;
}
// --- Helper: update score and best tile display ---
function updateScore(add) {
score += add;
scoreTxt.setText('Score: ' + score);
}
function updateBestTile(val) {
if (val > bestTile) {
bestTile = val;
bestTxt.setText('Best: ' + bestTile);
}
}
// --- Helper: check if any moves are possible ---
function movesAvailable() {
for (var r = 0; r < GRID_SIZE; r++) {
for (var c = 0; c < GRID_SIZE; c++) {
if (board[r][c] === null) return true;
var v = board[r][c];
// Check right
if (c < GRID_SIZE - 1 && board[r][c + 1] === v) return true;
// Check down
if (r < GRID_SIZE - 1 && board[r + 1][c] === v) return true;
}
}
return false;
}
// --- Helper: check for win (tile 3072) ---
function checkWin() {
for (var r = 0; r < GRID_SIZE; r++) {
for (var c = 0; c < GRID_SIZE; c++) {
if (board[r][c] === 3072) {
LK.showYouWin();
return true;
}
}
}
return false;
}
// --- Helper: move/merge tiles in a direction ---
// dir: {x: -1, y:0} for left, {x:1,y:0} for right, etc.
function moveTiles(dir) {
if (moving) return;
moving = true;
// Prepare for move
var moved = false;
var merged = [];
for (var r = 0; r < GRID_SIZE; r++) {
merged[r] = [];
for (var c = 0; c < GRID_SIZE; c++) merged[r][c] = false;
}
// Helper to get traversal order for classic 2048
function getTraversal(dir) {
var rows = [],
cols = [];
for (var i = 0; i < GRID_SIZE; i++) {
rows.push(i);
cols.push(i);
}
if (dir.x === 1) cols = cols.reverse();
if (dir.y === 1) rows = rows.reverse();
return {
rows: rows,
cols: cols
};
}
var traversal = getTraversal(dir);
var movedAny = false;
// --- Animation-aware move/merge logic ---
var moveDuration = 120; // ms for slide
var mergeDuration = 100; // ms for merge pop
// Prepare to track original tile nodes and their moves
var moveActions = [];
var mergeActions = [];
var oldTileNodes = [];
for (var r = 0; r < GRID_SIZE; r++) {
oldTileNodes[r] = [];
for (var c = 0; c < GRID_SIZE; c++) {
oldTileNodes[r][c] = tileNodes[r][c];
}
}
// Build new board and move/merge actions
var newBoard = [];
var newTileNodes = [];
for (var r = 0; r < GRID_SIZE; r++) {
newBoard[r] = [];
newTileNodes[r] = [];
for (var c = 0; c < GRID_SIZE; c++) {
newBoard[r][c] = null;
newTileNodes[r][c] = null;
}
}
// For each cell in traversal order, slide and merge
for (var i = 0; i < traversal.rows.length; i++) {
for (var j = 0; j < traversal.cols.length; j++) {
var r = traversal.rows[i];
var c = traversal.cols[j];
var val = board[r][c];
if (val === null) continue;
var targetR = r,
targetC = c;
// Slide as far as possible in the direction
while (true) {
var testR = targetR + (dir.y || 0);
var testC = targetC + (dir.x || 0);
if (testR < 0 || testR >= GRID_SIZE || testC < 0 || testC >= GRID_SIZE) break;
if (newBoard[testR][testC] === null) {
targetR = testR;
targetC = testC;
} else if (newBoard[testR][testC] === val && !merged[testR][testC]) {
// Merge!
targetR = testR;
targetC = testC;
break;
} else {
break;
}
}
// If merge is possible
if (newBoard[targetR][targetC] === val && !merged[targetR][targetC]) {
// Merge
newBoard[targetR][targetC] = val * 2;
merged[targetR][targetC] = true;
updateScore(val * 2);
updateBestTile(val * 2);
movedAny = true;
// Animate: move to merge, then pop
moveActions.push({
tile: oldTileNodes[r][c],
from: {
row: r,
col: c
},
to: {
row: targetR,
col: targetC
},
merge: true,
value: val * 2
});
// Mark for removal after animation
mergeActions.push({
row: targetR,
col: targetC,
value: val * 2
});
} else {
// Move to empty spot
if (targetR !== r || targetC !== c) movedAny = true;
newBoard[targetR][targetC] = val;
// Animate: move only
moveActions.push({
tile: oldTileNodes[r][c],
from: {
row: r,
col: c
},
to: {
row: targetR,
col: targetC
},
merge: false,
value: val
});
}
}
}
// Remove all old tiles from board state (not from scene yet)
for (var r = 0; r < GRID_SIZE; r++) {
for (var c = 0; c < GRID_SIZE; c++) {
board[r][c] = null;
tileNodes[r][c] = null;
}
}
// Animate all moves
var animCount = moveActions.length;
if (animCount === 0) {
moving = false;
return;
}
for (var i = 0; i < moveActions.length; i++) {
(function (action) {
var tile = action.tile;
if (!tile) {
animCount--;
return;
}
var pos = getTilePos(action.to.row, action.to.col);
// Bring to top for merges
if (action.merge) {
tile.zIndex = 1000;
}
tween(tile, {
x: pos.x,
y: pos.y
}, {
duration: moveDuration,
easing: tween.easeInOut,
onFinish: function onFinish() {
// If merge, destroy this tile and create a new one with pop
if (action.merge) {
// Play merge sound if not muted
if (!mergeSoundMuted) {
LK.getSound('mergeSfx').play();
}
tile.destroy();
// Remove any tile at merge target (should not exist, but safety)
if (tileNodes[action.to.row][action.to.col]) {
tileNodes[action.to.row][action.to.col].destroy();
}
// Add merged tile with pop
var mergedTile = new Tile();
mergedTile.setValue(action.value);
mergedTile.row = action.to.row;
mergedTile.col = action.to.col;
var mpos = getTilePos(action.to.row, action.to.col);
mergedTile.x = mpos.x;
mergedTile.y = mpos.y;
game.addChild(mergedTile);
mergedTile.pop();
tileNodes[action.to.row][action.to.col] = mergedTile;
board[action.to.row][action.to.col] = action.value;
} else {
// Just move, update tileNodes and board
tile.row = action.to.row;
tile.col = action.to.col;
tileNodes[action.to.row][action.to.col] = tile;
board[action.to.row][action.to.col] = action.value;
}
animCount--;
if (animCount === 0) {
// After all animations, spawn new tile if needed
// Only spawn a new tile if there were moves and NO merges
if (movedAny && mergeActions.length === 0) {
spawnTile();
}
// Update score and best tile display after every move
scoreTxt.setText('Score: ' + score);
bestTxt.setText('Best: ' + bestTile);
// Check for win
checkWin();
// Check for game over
if (!movesAvailable()) {
LK.setTimeout(function () {
LK.showGameOver();
}, 400);
}
moving = false;
}
}
});
})(moveActions[i]);
}
return;
// Update score and best tile display after every move
scoreTxt.setText('Score: ' + score);
bestTxt.setText('Best: ' + bestTile);
// Check for win
checkWin();
// Check for game over
if (!movesAvailable()) {
LK.setTimeout(function () {
LK.showGameOver();
}, 400);
}
moving = false;
}
// --- Input handling ---
// Touch/drag swipe detection
var touchStartX = null,
touchStartY = null,
touchStartTime = null;
game.down = function (x, y, obj) {
touchStartX = x;
touchStartY = y;
touchStartTime = Date.now();
};
game.up = function (x, y, obj) {
if (touchStartX === null || moving) return;
var dx = x - touchStartX;
var dy = y - touchStartY;
var adx = Math.abs(dx),
ady = Math.abs(dy);
if (adx < 50 && ady < 50) {
touchStartX = null;
touchStartY = null;
return;
}
if (adx > ady) {
if (dx > 0) moveTiles({
x: 1,
y: 0
}); // right
else moveTiles({
x: -1,
y: 0
}); // left
} else {
if (dy > 0) moveTiles({
x: 0,
y: 1
}); // down
else moveTiles({
x: 0,
y: -1
}); // up
}
touchStartX = null;
touchStartY = null;
};
// Keyboard controls (for desktop)
game.move = function (x, y, obj) {
// No-op for drag, but we want to support keyboard
if (obj && obj.event && obj.event.type === 'keydown' && !moving) {
var key = obj.event.key;
if (key === 'ArrowLeft') moveTiles({
x: -1,
y: 0
});else if (key === 'ArrowRight') moveTiles({
x: 1,
y: 0
});else if (key === 'ArrowUp') moveTiles({
x: 0,
y: -1
});else if (key === 'ArrowDown') moveTiles({
x: 0,
y: 1
});
}
};
// --- Game initialization ---
function resetGame() {
// Clear board
for (var r = 0; r < GRID_SIZE; r++) {
for (var c = 0; c < GRID_SIZE; c++) {
if (tileNodes[r] && tileNodes[r][c]) {
tileNodes[r][c].destroy();
}
}
}
board = [];
tileNodes = [];
for (var r = 0; r < GRID_SIZE; r++) {
board[r] = [];
tileNodes[r] = [];
for (var c = 0; c < GRID_SIZE; c++) {
board[r][c] = null;
tileNodes[r][c] = null;
}
}
score = 0;
bestTile = 3;
scoreTxt.setText('Score: 0');
bestTxt.setText('Best: 3');
// Ensure score and best tile are displayed clearly after reset
scoreTxt.anchor.set(0.5, 0);
bestTxt.anchor.set(0.5, 0);
moving = false;
// Spawn two tiles
spawnTile();
spawnTile();
}
resetGame();
// --- Game tick (not used, but required for LK) ---
game.update = function () {
// No per-frame logic needed
};
Design a subtle, minimalistic background for the game. Use a soft pastel or muted gradient (e.g., light beige, soft gray, or pale blue). Avoid patterns or distractions — the focus should remain on the tiles. The background should give a calm, clean feeling and enhance the overall aesthetic without drawing attention.. In-Game asset. 2d. High contrast. No shadows
Prompt: Create a Dark Brown Hollow-Style Background Design a sleek and minimal background using a dark brown color (such as chocolate or espresso tones). Use a hollow or framed appearance — not a solid fill — to keep the center light and uncluttered. The central area should feel open, possibly with a slight transparency or soft inner shadow. The darker brown edges should add contrast and warmth without overpowering the game tiles.. In-Game asset. 2d. High contrast. No shadows
Design a background similar to the original 2048 game. Use a warm, soft beige or light brown tone as the main background. Include a grid layout with rounded square slots where tiles appear. Each slot should have a slightly darker shade than the background to show the empty grid clearly. Keep the overall design minimal, clean, and visually balanced.. In-Game asset. 2d. High contrast. No shadows