User prompt
Create the logic for a 2D match-3 game swap mechanic: When two adjacent pieces are swapped, save their original positions. Perform the swap visually. Check if the swap creates a valid match (3 or more identical pieces in a row or column). If a valid match exists, keep the swap and remove matched pieces. If no valid match, smoothly move the pieces back to their original positions. Ensure the player clearly sees the swap and the rollback if invalid. No special pieces or power-ups needed now, just basic swap and match logic with rollback.
User prompt
Design the logic for a 2D match-3 puzzle game where players swap two adjacent game pieces (e.g., candies or balls). After swapping, the game should check if the move creates a valid match (three or more identical pieces in a row or column). If a valid match occurs, the swap remains permanent and matching pieces are cleared. If no valid match is found, the swapped pieces should smoothly return to their original positions, effectively undoing the move. Include these features in the prompt: Track the original positions of the two swapped pieces before moving. Perform the swap visually (with animation if possible). Implement a match detection function that verifies if the new board state contains valid matches. If no match is detected, animate the pieces back to their original spots. Ensure smooth user experience with proper timing so that players see the swap and the return if invalid. Support extensibility for adding special pieces or power-ups in the future.
User prompt
once the circle changes place, if it doesn't work, let it go back to its place
User prompt
Please fix the bug: 'TypeError: Cannot read properties of null (reading 'row')' in or related to this line: 'swapCandies(cell.row, cell.col, dragStartCell.row, dragStartCell.col, true, function () {' Line Number: 498
Code edit (1 edits merged)
Please save this source code
User prompt
Candy Swap Match-3
Initial prompt
Create a simple 2D match-3 puzzle game where players swap adjacent colorful candies to make rows or columns of three or more identical candies. When matched, candies disappear, and new ones fall from the top to fill the empty spaces. The goal is to clear as many candies as possible within a limited number of moves. Keep the visuals bright and colorful with basic animations for swapping and matching. Include a score system that increases when players make matches. No complex power-ups or special candies at the start — just straightforward match-3 gameplay.
/**** * 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 ---
===================================================================
--- original.js
+++ change.js
@@ -53,11 +53,11 @@
/****
* Game Code
****/
-// Simple pop sound for matches
-// 5 candy colors as ellipses
// --- 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;
@@ -440,9 +440,9 @@
unhighlightCandy(dragStartCandy);
isSwapping = true;
movesLeft--;
movesTxt.setText('Moves: ' + movesLeft);
- // Track original positions for extensibility and animation
+ // Save original positions for rollback
var origA = {
row: dragStartCell.row,
col: dragStartCell.col
};
@@ -453,26 +453,15 @@
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
- if (dragStartCell) {
- // Prevent further input during swap-back
- isAnimating = true;
- // Animate candies back to their original positions
- swapCandies(origB.row, origB.col, origA.row, origA.col, true, function () {
- isSwapping = false;
- isAnimating = false;
- if (movesLeft <= 0) {
- LK.showGameOver();
- }
- });
- } else {
+ // 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;