User prompt
Display the current score and the highest tile achieved after every move in a clear and readable format.
User prompt
After each valid move, spawn a new tile with a value of either 3 or 6 at a random empty position on the grid.
User prompt
Update the merge logic to ensure tiles merge only if their values are exactly the same. Tiles with different values should never merge.
User prompt
In addition to the existing game rules, ensure the merging logic correctly checks and performs addition only when two tiles have the exact same value. Specifically: Tiles should merge only if their values are equal. When merged, the resulting tile's value must be the sum of the two tiles. Prevent any merges of tiles with different values (e.g., 3 and 6 should NOT merge). Include clear validation in the code to avoid incorrect merges or unexpected results.
Code edit (1 edits merged)
Please save this source code
User prompt
Threes Merge: 3072 Puzzle
Initial prompt
Create a grid-based sliding puzzle game inspired by 2048, but instead of powers of 2, the tiles should be multiples of 3 (e.g., 3, 6, 12, 24, 48, etc.). The rules should be: Start with a 4x4 grid containing two tiles with values of 3 or 6. When two tiles with the same value are combined, they merge into one tile whose value is the sum (e.g., 6 + 6 = 12). After each move (up/down/left/right), a new tile (3 or 6) spawns in an empty cell. The game ends when there are no valid moves left. The goal is to reach a tile with the value 3072 (instead of 2048). Add the following features: Keyboard controls for movement (WASD or arrows) Display the grid and current score after every move Optional: track and show the highest tile reached Keep the code clean and easy to convert to GUI later (e.g., Unity, WinForms, or web)
/**** * 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