Code edit (1 edits merged)
Please save this source code
User prompt
make the highscore button beside start button from the left side by 100px
User prompt
Add introbackground before game start, & startbutton with blinking animation continuously and smoothly, the start button close to the bottom by 200px, the highscore button on the left corner and close to it by area 200x200 ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
change the mahjong pieces names to be names of vegetables or from nature potato-tomato-flower-tree....etc
Code edit (1 edits merged)
Please save this source code
User prompt
Shape Match Mahjong
Initial prompt
Game of mahjong each time different shapes, game have a score, introbackground and start game button close to the bottom by 200px & before the game start, the button start game have animation of blinking continuously smoothly, HighScore button on the right side in the intro before game start, background of scores when highscore button is clicked
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1", { highScore: 0 }); /**** * Classes ****/ //Library for using the camera (the background becomes the user's camera video feed) and the microphone. It can access face coordinates for interactive play, as well detect microphone volume / voice interactions //var facekit = LK.import('@upit/facekit.v1'); //Classes can only be defined here. You cannot create inline classes in the games code. var Tile = Container.expand(function (assetId, row, col, layer) { var self = Container.call(this); self.assetId = assetId; self.row = row; self.col = col; self.layer = layer || 0; self.isMatched = false; self.isCovered = false; // Check if it's a shape asset and adjust rendering var isShapeAsset = shapeAssets.indexOf(assetId) !== -1; if (isShapeAsset) { var tileGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); } else { var tileGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5, scaleX: 2, scaleY: 3 }); } self.down = function (x, y, obj) { if (!self.isMatched && !self.isCovered) { tileSelected(self); } }; self.destroy = function () { self.isMatched = true; // tween the tile out or fade it tween(self, { alpha: 0, scaleX: 0, scaleY: 0 }, { duration: 300, onFinish: function onFinish() { // Remove from game scene self.parent.removeChild(self); // Remove from tiles array var index = tiles.indexOf(self); if (index > -1) { tiles.splice(index, 1); } // Uncover tiles below this one updateCoveredTiles(); // Check for level completion if (tiles.length === 0) { isTransitioningLevel = true; // Set flag to prevent game over LK.clearInterval(levelTimer); // Show level complete message var levelCompleteText = new Text2('Level ' + currentLevel + ' Complete!', { size: 120, fill: 0xFFD700 }); levelCompleteText.anchor.set(0.5, 0.5); levelCompleteText.x = gameWidth / 2; levelCompleteText.y = gameHeight / 2 - 100; game.addChild(levelCompleteText); // Calculate score bonus for completing level var timeBonus = timeRemaining * 5; var levelBonus = currentLevel * 100; var totalBonus = timeBonus + levelBonus; LK.setScore(LK.getScore() + totalBonus); scoreTxt.setText('Score: ' + LK.getScore()); // Show bonus info var bonusInfoText = new Text2('Time Bonus: ' + timeBonus + '\nLevel Bonus: ' + levelBonus, { size: 60, fill: 0xFFFFFF }); bonusInfoText.anchor.set(0.5, 0.5); bonusInfoText.x = gameWidth / 2; bonusInfoText.y = gameHeight / 2 + 50; game.addChild(bonusInfoText); if (currentLevel < maxLevel) { // Show next level info var nextLevelText = new Text2('Next: Level ' + (currentLevel + 1), { size: 80, fill: 0xFFFFFF }); nextLevelText.anchor.set(0.5, 0.5); nextLevelText.x = gameWidth / 2; nextLevelText.y = gameHeight / 2 + 150; game.addChild(nextLevelText); // Increment level currentLevel++; // Start next level after delay LK.setTimeout(function () { levelCompleteText.parent.removeChild(levelCompleteText); bonusInfoText.parent.removeChild(bonusInfoText); nextLevelText.parent.removeChild(nextLevelText); startLevel(); }, 3000); } else { // Completed all 10 levels - start over at level 1 with score intact var continueText = new Text2('Amazing! Starting new game cycle...', { size: 80, fill: 0xFFD700 }); continueText.anchor.set(0.5, 0.5); continueText.x = gameWidth / 2; continueText.y = gameHeight / 2 + 200; game.addChild(continueText); LK.setTimeout(function () { levelCompleteText.parent.removeChild(levelCompleteText); bonusInfoText.parent.removeChild(bonusInfoText); continueText.parent.removeChild(continueText); currentLevel = 1; // Reset to level 1 startLevel(); // Continue playing }, 3000); } } else if (tiles.length > 0 && !isTransitioningLevel) { // Check if there are any valid matches left LK.setTimeout(function () { // Double-check tiles still exist and we're not transitioning if (tiles.length > 0 && !isTransitioningLevel && !hasAvailableMatches()) { // No moves available - restart level isTransitioningLevel = true; LK.clearInterval(levelTimer); // Show no moves message var noMovesText = new Text2('No Moves Left!', { size: 120, fill: 0xFF6B6B }); noMovesText.anchor.set(0.5, 0.5); noMovesText.x = gameWidth / 2; noMovesText.y = gameHeight / 2 - 100; game.addChild(noMovesText); var restartText = new Text2('Restarting Level...', { size: 80, fill: 0xFFFFFF }); restartText.anchor.set(0.5, 0.5); restartText.x = gameWidth / 2; restartText.y = gameHeight / 2 + 50; game.addChild(restartText); // Restart level after delay LK.setTimeout(function () { noMovesText.parent.removeChild(noMovesText); restartText.parent.removeChild(restartText); startLevel(); }, 2000); } }, 100); } } }); }; return self; //You must return self if you want other classes to be able to inherit from this class }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0xd1a400 // Init game with a steelblue background }); /**** * Game Code ****/ // Declare shape assets array globally so it's accessible in Tile class //Storage library which should be used for persistent game data //Minimalistic tween library which should be used for animations over time, including tinting / colouring an object, scaling, rotating, or changing any game object property. //Only include the plugins you need to create the game. //We have access to the following plugins. (Note that the variable names used are mandetory for each plugin) // Initialize assets used in this game. Scale them according to what is needed for the game. // or via static code analysis based on their usage in the code. // Assets are automatically created and loaded either dynamically during gameplay /* Supported Types: 1. Shape: - Simple geometric figures with these properties: * width: (required) pixel width of the shape. * height: (required) pixel height of the shape. * color: (required) color of the shape. * shape: (required) type of shape. Valid options: 'box', 'ellipse'. 2. Image: - Imported images with these properties: * width: (required) pixel resolution width. * height: (required) pixel resolution height. * id: (required) identifier for the image. * flipX: (optional) horizontal flip. Valid values: 0 (no flip), 1 (flip). * flipY: (optional) vertical flip. Valid values: 0 (no flip), 1 (flip). * orientation: (optional) rotation in multiples of 90 degrees, clockwise. Valid values: - 0: No rotation. - 1: Rotate 90 degrees. - 2: Rotate 180 degrees. - 3: Rotate 270 degrees. Note: Width and height remain unchanged upon flipping. 3. Sound: - Sound effects with these properties: * id: (required) identifier for the sound. * volume: (optional) custom volume. Valid values are a float from 0 to 1. 4. Music: - In contract to sound effects, only one music can be played at a time - Music is using the same API to initilize just like sound. - Music loops by default - Music with these config options: * id: (required) identifier for the sound. * volume: (optional) custom volume. Valid values are a float from 0 to 1. * start: (optional) a float from 0 to 1 used for cropping and indicates the start of the cropping * end: (optional) a float from 0 to 1 used for cropping and indicates the end of the cropping */ // Global variables var shapeAssets = ['circle_tile']; var selectedTile = null; var tiles = []; var scoreTxt; var highScoreTxt; var gameBoard; // Container for tiles var currentLevel = 1; var maxLevel = 10; // 10 levels total var levelTimer; var timeRemaining; var timerText; var levelText; var consecutiveMatches = 0; // Track consecutive successful matches var bonusText; // Display bonus multiplier var isTransitioningLevel = false; // Flag to prevent game over during level transitions // Game dimensions var gameWidth = 2048; var gameHeight = 2732; // Tile dimensions and spacing var tileWidth = 200; var tileHeight = 300; var tileSpacing = 20; // Game board dimensions var boardCols = 8; var boardRows = 6; var boardLayers = 2; // Two layers of tiles var boardWidth = boardCols * (tileWidth + tileSpacing) - tileSpacing; var boardHeight = boardRows * (tileHeight + tileSpacing) - tileSpacing; // Available nature-themed assets var availableAssets = ['potato', 'tomato', 'carrot', 'flower', 'tree', 'apple', 'corn', 'mushroom', 'pumpkin', 'sunflower', 'broccoli', 'strawberry']; // Shape assets array already declared above // Level configurations for different shapes var levelConfigs = { 1: { shape: 'rectangle', cols: 6, rows: 4, layers: 2 }, 2: { shape: 'rectangle', cols: 8, rows: 6, layers: 2 }, 3: { shape: 'triangle', cols: 8, rows: 5, layers: 2 }, 4: { shape: 'circle', cols: 6, rows: 6, layers: 2 }, 5: { shape: 'rectangle', cols: 9, rows: 6, layers: 3 }, 6: { shape: 'triangle', cols: 9, rows: 6, layers: 3 }, 7: { shape: 'circle', cols: 8, rows: 8, layers: 3 }, 8: { shape: 'rectangle', cols: 10, rows: 7, layers: 3 }, 9: { shape: 'triangle', cols: 9, rows: 7, layers: 4 }, 10: { shape: 'circle', cols: 9, rows: 7, layers: 4 } }; // Function to get random asset based on level function getRandomAssetForLevel(level) { // All levels use nature assets return availableAssets[Math.floor(Math.random() * availableAssets.length)]; } // Function to get time limit for current level function getTimeLimitForLevel(level) { if (level <= 3) { return 300; // 5 minutes } else if (level <= 6) { return 180; // 3 minutes } else { return 120; // 2 minutes } } // Intro screen container var introContainer; // Function to generate the game board layout function generateBoardLayout(rows, cols, layers) { var config = levelConfigs[currentLevel]; var shape = config.shape; var layout = []; // Generate shape-specific layouts for (var layer = 0; layer < layers; layer++) { layout[layer] = []; var validPositions = []; // Get valid positions based on shape for (var r = 0; r < rows; r++) { layout[layer][r] = []; for (var c = 0; c < cols; c++) { var isValid = false; if (shape === 'rectangle') { isValid = true; } else if (shape === 'triangle') { // Pyramid shape - start with 1-2 tiles at top, increase by 1-2 per row var tilesInRow = Math.min(cols, 2 + r * 2); var startCol = Math.floor((cols - tilesInRow) / 2); isValid = c >= startCol && c < startCol + tilesInRow; } else if (shape === 'circle') { // Circle shape - use distance from center var centerX = (cols - 1) / 2; var centerY = (rows - 1) / 2; var radiusX = cols / 2 - 0.5; var radiusY = rows / 2 - 0.5; var dx = (c - centerX) / radiusX; var dy = (r - centerY) / radiusY; isValid = dx * dx + dy * dy <= 1; } if (isValid) { validPositions.push({ r: r, c: c }); layout[layer][r][c] = null; } else { layout[layer][r][c] = null; } } } // Ensure even number of valid positions if (validPositions.length % 2 !== 0) { validPositions.pop(); } // Create pairs of tiles var allTiles = []; for (var i = 0; i < validPositions.length / 2; i++) { var randomAssetId = getRandomAssetForLevel(currentLevel); allTiles.push(randomAssetId); allTiles.push(randomAssetId); } // Shuffle the tiles for (var i = allTiles.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = allTiles[i]; allTiles[i] = allTiles[j]; allTiles[j] = temp; } // Place tiles in valid positions for (var i = 0; i < validPositions.length; i++) { var pos = validPositions[i]; layout[layer][pos.r][pos.c] = allTiles[i]; } } return layout; } // Function to create and position tiles function createBoard(layout) { gameBoard = new Container(); game.addChild(gameBoard); // Calculate actual board dimensions based on level config var config = levelConfigs[currentLevel]; var actualBoardWidth = config.cols * (tileWidth + tileSpacing) - tileSpacing; var actualBoardHeight = config.rows * (tileHeight + tileSpacing) - tileSpacing; // Center the board on the screen gameBoard.x = (gameWidth - actualBoardWidth) / 2; gameBoard.y = (gameHeight - actualBoardHeight) / 2 + 100; // Adjust vertical position to avoid top menu // Create tiles for each layer for (var layer = 0; layer < layout.length; layer++) { var layerOffset = layer * 30; // Offset each layer for visual depth for (var r = 0; r < layout[layer].length; r++) { for (var c = 0; c < layout[layer][r].length; c++) { var assetId = layout[layer][r][c]; if (assetId) { var tile = new Tile(assetId, r, c, layer); tile.x = c * (tileWidth + tileSpacing) + tileWidth / 2 + layerOffset; tile.y = r * (tileHeight + tileSpacing) + tileHeight / 2 - layerOffset; gameBoard.addChild(tile); tiles.push(tile); } } } } // Sort tiles by layer so upper layers are drawn on top tiles.sort(function (a, b) { return a.layer - b.layer; }); // Update which tiles are covered updateCoveredTiles(); } // Function to update which tiles are covered by tiles above them function updateCoveredTiles() { // Reset all tiles to uncovered for (var i = 0; i < tiles.length; i++) { tiles[i].isCovered = false; tiles[i].alpha = 1; } // Check each tile to see if it's covered for (var i = 0; i < tiles.length; i++) { var tile = tiles[i]; // Check if any tile above this one overlaps it for (var j = 0; j < tiles.length; j++) { var otherTile = tiles[j]; if (otherTile.layer > tile.layer && !otherTile.isMatched) { // Check if tiles overlap (considering the offset) var layerDiff = otherTile.layer - tile.layer; var offsetX = layerDiff * 30; var offsetY = layerDiff * 30; var overlapX = Math.abs(tile.x - offsetX - otherTile.x) < tileWidth; var overlapY = Math.abs(tile.y + offsetY - otherTile.y) < tileHeight; if (overlapX && overlapY) { tile.isCovered = true; tile.alpha = 0.5; // Make covered tiles semi-transparent break; } } } } } // Function to handle tile selection and matching function tileSelected(tile) { if (selectedTile === null) { selectedTile = tile; // Highlight the selected tile (optional) // tween(selectedTile, { tint: 0xffff00 }, { duration: 100 }); } else if (selectedTile === tile) { // Deselect the same tile // tween(selectedTile, { tint: 0xffffff }, { duration: 100 }); selectedTile = null; } else { // Check for a match if (selectedTile.assetId === tile.assetId) { // Match found LK.getSound('match_sound').play(); consecutiveMatches++; // Increment consecutive matches var baseScore = 10; var multiplier = consecutiveMatches; // 1x, 2x, 3x, etc. var scoreToAdd = baseScore * multiplier; LK.setScore(LK.getScore() + scoreToAdd); scoreTxt.setText('Score: ' + LK.getScore()); // Show bonus multiplier if greater than 1 if (multiplier > 1) { showBonusText(multiplier, scoreToAdd); } selectedTile.destroy(); tile.destroy(); selectedTile = null; } else { // No match - reset consecutive matches consecutiveMatches = 0; LK.getSound('no_match_sound').play(); // Optionally indicate no match (e.g., flash tiles red) // tween(selectedTile, { tint: 0xff0000 }, { duration: 200, onFinish: function() { tween(selectedTile, { tint: 0xffffff }, { duration: 200 }); } }); // tween(tile, { tint: 0xff0000 }, { duration: 200, onFinish: function() { tween(tile, { tint: 0xffffff }, { duration: 200 }); } }); selectedTile = null; } } } // Initialize score and high score display (created but not shown until game starts) scoreTxt = new Text2('Score: 0', { size: 80, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); scoreTxt.y = 20; // Position below the top menu area highScoreTxt = new Text2('High Score: ' + storage.highScore, { size: 80, fill: 0xFFFFFF }); highScoreTxt.anchor.set(0.5, 0); highScoreTxt.y = 120; // Position below the score display // Create intro container introContainer = new Container(); game.addChild(introContainer); // Add intro background var introBg = introContainer.attachAsset('Introbackground', { anchorX: 0.5, anchorY: 0.5, x: gameWidth / 2, y: gameHeight / 2 }); // Get the actual dimensions of the background asset var bgWidth = introBg.width; var bgHeight = introBg.height; // Calculate scale to cover the entire screen var scaleX = gameWidth / bgWidth; var scaleY = gameHeight / bgHeight; // Use the larger scale to ensure full coverage var scale = Math.max(scaleX, scaleY); // Apply the scale introBg.scale.set(scale, scale); // Start button with image asset var startButton = introContainer.attachAsset('Startbutton', { anchorX: 0.5, anchorY: 0.5, x: gameWidth / 2, y: gameHeight - 200 // 200px from bottom }); // Smooth continuous blinking animation using tween function animateStartButton() { tween(startButton, { alpha: 0.3 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { tween(startButton, { alpha: 1 }, { duration: 1000, easing: tween.easeInOut, onFinish: animateStartButton // Loop the animation }); } }); } animateStartButton(); // Handle tap on start button to begin the game startButton.down = function (x, y, obj) { tween.stop(startButton); // Stop the blinking animation introContainer.parent.removeChild(introContainer); startGame(); }; // High score button beside start button from the left side by 600px var highScoreButton = introContainer.attachAsset('Highscorebutton', { anchorX: 0.5, anchorY: 0.5, x: gameWidth / 2 - 600, // 100px to the left of start button y: gameHeight - 200 // Same Y position as start button }); // Handle tap on high score button highScoreButton.down = function (x, y, obj) { // Create high score display overlay var highScoreOverlay = new Container(); introContainer.addChild(highScoreOverlay); // Create a dark semi-transparent background for scores var bgOverlay = LK.getAsset('box', { width: gameWidth, height: gameHeight, color: 0x000000, shape: 'box', anchorX: 0.5, anchorY: 0.5, x: gameWidth / 2, y: gameHeight / 2, alpha: 0.8 }); highScoreOverlay.addChild(bgOverlay); // High score text var hsText = new Text2('Your High Score\n' + storage.highScore, { size: 120, fill: 0xFFFFFF }); hsText.anchor.set(0.5, 0.5); hsText.x = gameWidth / 2; hsText.y = gameHeight / 2 - 300; highScoreOverlay.addChild(hsText); // Leaderboard title var leaderboardTitle = new Text2('Top Players', { size: 100, fill: 0xFFD700 }); leaderboardTitle.anchor.set(0.5, 0.5); leaderboardTitle.x = gameWidth / 2; leaderboardTitle.y = gameHeight / 2 - 100; highScoreOverlay.addChild(leaderboardTitle); // Create mock leaderboard data since LK.getLeaderboard() doesn't exist var leaderboard = [{ name: 'Player 1', score: 1000 }, { name: 'Player 2', score: 850 }, { name: 'Player 3', score: 720 }, { name: 'Player 4', score: 650 }, { name: 'Player 5', score: 500 }]; var leaderboardY = gameHeight / 2 + 50; // Display top 5 players for (var i = 0; i < Math.min(5, leaderboard.length); i++) { var entry = leaderboard[i]; var rankText = new Text2(i + 1 + '. ' + entry.name + ' - ' + entry.score, { size: 70, fill: 0xFFFFFF }); rankText.anchor.set(0.5, 0.5); rankText.x = gameWidth / 2; rankText.y = leaderboardY + i * 80; highScoreOverlay.addChild(rankText); } // Close button var closeText = new Text2('Tap to Close', { size: 80, fill: 0xFFFF00 }); closeText.anchor.set(0.5, 0.5); closeText.x = gameWidth / 2; closeText.y = gameHeight / 2 + 500; highScoreOverlay.addChild(closeText); // Handle tap to close overlay highScoreOverlay.down = function (x, y, obj) { highScoreOverlay.parent.removeChild(highScoreOverlay); }; }; // Function to start the game function startGame() { currentLevel = 1; LK.setScore(0); scoreTxt.setText('Score: 0'); LK.gui.top.addChild(scoreTxt); // Add level display levelText = new Text2('Level: ' + currentLevel, { size: 80, fill: 0xFFFFFF }); levelText.anchor.set(0.5, 0); levelText.y = 120; LK.gui.top.addChild(levelText); // Add timer display timerText = new Text2('Time: 5:00', { size: 80, fill: 0xFFFFFF }); timerText.anchor.set(0.5, 0); timerText.y = 220; LK.gui.top.addChild(timerText); startLevel(); LK.playMusic('bg_music'); } // Function to start a new level function startLevel() { // Clear any existing timer if (levelTimer) { LK.clearInterval(levelTimer); } // Clear existing board if (gameBoard) { gameBoard.destroy(); } tiles = []; selectedTile = null; consecutiveMatches = 0; // Reset consecutive matches for new level isTransitioningLevel = false; // Reset transition flag // Update level text levelText.setText('Level: ' + currentLevel); // Set time limit for current level timeRemaining = getTimeLimitForLevel(currentLevel); updateTimerDisplay(); // Create new board with level-specific configuration var config = levelConfigs[currentLevel]; var boardLayout = generateBoardLayout(config.rows, config.cols, config.layers); createBoard(boardLayout); // Start timer levelTimer = LK.setInterval(function () { timeRemaining--; updateTimerDisplay(); if (timeRemaining <= 0) { LK.clearInterval(levelTimer); isTransitioningLevel = true; // Show time up message var timeUpText = new Text2('Time Up!', { size: 120, fill: 0xFF6B6B }); timeUpText.anchor.set(0.5, 0.5); timeUpText.x = gameWidth / 2; timeUpText.y = gameHeight / 2 - 100; game.addChild(timeUpText); var restartText = new Text2('Restarting Level...', { size: 80, fill: 0xFFFFFF }); restartText.anchor.set(0.5, 0.5); restartText.x = gameWidth / 2; restartText.y = gameHeight / 2 + 50; game.addChild(restartText); // Restart level after delay LK.setTimeout(function () { timeUpText.parent.removeChild(timeUpText); restartText.parent.removeChild(restartText); startLevel(); }, 2000); } }, 1000); } // Function to update timer display function updateTimerDisplay() { var minutes = Math.floor(timeRemaining / 60); var seconds = timeRemaining % 60; var timeString = minutes + ':' + (seconds < 10 ? '0' : '') + seconds; timerText.setText('Time: ' + timeString); } // Function to show bonus multiplier text function showBonusText(multiplier, points) { // Remove existing bonus text if any if (bonusText && bonusText.parent) { bonusText.parent.removeChild(bonusText); } // Create new bonus text bonusText = new Text2(multiplier + 'x COMBO! +' + points, { size: 120, fill: 0xFFD700 // Gold color }); bonusText.anchor.set(0.5, 0.5); bonusText.x = gameWidth / 2; bonusText.y = gameHeight / 2; game.addChild(bonusText); // Animate the bonus text tween(bonusText, { y: gameHeight / 2 - 200, alpha: 0 }, { duration: 1500, onFinish: function onFinish() { if (bonusText.parent) { bonusText.parent.removeChild(bonusText); } } }); } // Function to check if any valid matches are available function hasAvailableMatches() { // Create a map to track available tiles by assetId var availableTilesByAsset = {}; // Group all uncovered and unmatched tiles by their assetId for (var i = 0; i < tiles.length; i++) { var tile = tiles[i]; if (!tile.isMatched && !tile.isCovered) { if (!availableTilesByAsset[tile.assetId]) { availableTilesByAsset[tile.assetId] = []; } availableTilesByAsset[tile.assetId].push(tile); } } // Check if any asset has at least 2 available tiles (a pair) for (var assetId in availableTilesByAsset) { if (availableTilesByAsset[assetId].length >= 2) { return true; } } return false; } // Game update loop (currently not needed for this game's logic, but included for structure) game.update = function () { // Update high score if current score is higher if (LK.getScore() > storage.highScore) { storage.highScore = LK.getScore(); } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
highScore: 0
});
/****
* Classes
****/
//Library for using the camera (the background becomes the user's camera video feed) and the microphone. It can access face coordinates for interactive play, as well detect microphone volume / voice interactions
//var facekit = LK.import('@upit/facekit.v1');
//Classes can only be defined here. You cannot create inline classes in the games code.
var Tile = Container.expand(function (assetId, row, col, layer) {
var self = Container.call(this);
self.assetId = assetId;
self.row = row;
self.col = col;
self.layer = layer || 0;
self.isMatched = false;
self.isCovered = false;
// Check if it's a shape asset and adjust rendering
var isShapeAsset = shapeAssets.indexOf(assetId) !== -1;
if (isShapeAsset) {
var tileGraphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
} else {
var tileGraphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2,
scaleY: 3
});
}
self.down = function (x, y, obj) {
if (!self.isMatched && !self.isCovered) {
tileSelected(self);
}
};
self.destroy = function () {
self.isMatched = true;
// tween the tile out or fade it
tween(self, {
alpha: 0,
scaleX: 0,
scaleY: 0
}, {
duration: 300,
onFinish: function onFinish() {
// Remove from game scene
self.parent.removeChild(self);
// Remove from tiles array
var index = tiles.indexOf(self);
if (index > -1) {
tiles.splice(index, 1);
}
// Uncover tiles below this one
updateCoveredTiles();
// Check for level completion
if (tiles.length === 0) {
isTransitioningLevel = true; // Set flag to prevent game over
LK.clearInterval(levelTimer);
// Show level complete message
var levelCompleteText = new Text2('Level ' + currentLevel + ' Complete!', {
size: 120,
fill: 0xFFD700
});
levelCompleteText.anchor.set(0.5, 0.5);
levelCompleteText.x = gameWidth / 2;
levelCompleteText.y = gameHeight / 2 - 100;
game.addChild(levelCompleteText);
// Calculate score bonus for completing level
var timeBonus = timeRemaining * 5;
var levelBonus = currentLevel * 100;
var totalBonus = timeBonus + levelBonus;
LK.setScore(LK.getScore() + totalBonus);
scoreTxt.setText('Score: ' + LK.getScore());
// Show bonus info
var bonusInfoText = new Text2('Time Bonus: ' + timeBonus + '\nLevel Bonus: ' + levelBonus, {
size: 60,
fill: 0xFFFFFF
});
bonusInfoText.anchor.set(0.5, 0.5);
bonusInfoText.x = gameWidth / 2;
bonusInfoText.y = gameHeight / 2 + 50;
game.addChild(bonusInfoText);
if (currentLevel < maxLevel) {
// Show next level info
var nextLevelText = new Text2('Next: Level ' + (currentLevel + 1), {
size: 80,
fill: 0xFFFFFF
});
nextLevelText.anchor.set(0.5, 0.5);
nextLevelText.x = gameWidth / 2;
nextLevelText.y = gameHeight / 2 + 150;
game.addChild(nextLevelText);
// Increment level
currentLevel++;
// Start next level after delay
LK.setTimeout(function () {
levelCompleteText.parent.removeChild(levelCompleteText);
bonusInfoText.parent.removeChild(bonusInfoText);
nextLevelText.parent.removeChild(nextLevelText);
startLevel();
}, 3000);
} else {
// Completed all 10 levels - start over at level 1 with score intact
var continueText = new Text2('Amazing! Starting new game cycle...', {
size: 80,
fill: 0xFFD700
});
continueText.anchor.set(0.5, 0.5);
continueText.x = gameWidth / 2;
continueText.y = gameHeight / 2 + 200;
game.addChild(continueText);
LK.setTimeout(function () {
levelCompleteText.parent.removeChild(levelCompleteText);
bonusInfoText.parent.removeChild(bonusInfoText);
continueText.parent.removeChild(continueText);
currentLevel = 1; // Reset to level 1
startLevel(); // Continue playing
}, 3000);
}
} else if (tiles.length > 0 && !isTransitioningLevel) {
// Check if there are any valid matches left
LK.setTimeout(function () {
// Double-check tiles still exist and we're not transitioning
if (tiles.length > 0 && !isTransitioningLevel && !hasAvailableMatches()) {
// No moves available - restart level
isTransitioningLevel = true;
LK.clearInterval(levelTimer);
// Show no moves message
var noMovesText = new Text2('No Moves Left!', {
size: 120,
fill: 0xFF6B6B
});
noMovesText.anchor.set(0.5, 0.5);
noMovesText.x = gameWidth / 2;
noMovesText.y = gameHeight / 2 - 100;
game.addChild(noMovesText);
var restartText = new Text2('Restarting Level...', {
size: 80,
fill: 0xFFFFFF
});
restartText.anchor.set(0.5, 0.5);
restartText.x = gameWidth / 2;
restartText.y = gameHeight / 2 + 50;
game.addChild(restartText);
// Restart level after delay
LK.setTimeout(function () {
noMovesText.parent.removeChild(noMovesText);
restartText.parent.removeChild(restartText);
startLevel();
}, 2000);
}
}, 100);
}
}
});
};
return self; //You must return self if you want other classes to be able to inherit from this class
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xd1a400 // Init game with a steelblue background
});
/****
* Game Code
****/
// Declare shape assets array globally so it's accessible in Tile class
//Storage library which should be used for persistent game data
//Minimalistic tween library which should be used for animations over time, including tinting / colouring an object, scaling, rotating, or changing any game object property.
//Only include the plugins you need to create the game.
//We have access to the following plugins. (Note that the variable names used are mandetory for each plugin)
// Initialize assets used in this game. Scale them according to what is needed for the game.
// or via static code analysis based on their usage in the code.
// Assets are automatically created and loaded either dynamically during gameplay
/*
Supported Types:
1. Shape:
- Simple geometric figures with these properties:
* width: (required) pixel width of the shape.
* height: (required) pixel height of the shape.
* color: (required) color of the shape.
* shape: (required) type of shape. Valid options: 'box', 'ellipse'.
2. Image:
- Imported images with these properties:
* width: (required) pixel resolution width.
* height: (required) pixel resolution height.
* id: (required) identifier for the image.
* flipX: (optional) horizontal flip. Valid values: 0 (no flip), 1 (flip).
* flipY: (optional) vertical flip. Valid values: 0 (no flip), 1 (flip).
* orientation: (optional) rotation in multiples of 90 degrees, clockwise. Valid values:
- 0: No rotation.
- 1: Rotate 90 degrees.
- 2: Rotate 180 degrees.
- 3: Rotate 270 degrees.
Note: Width and height remain unchanged upon flipping.
3. Sound:
- Sound effects with these properties:
* id: (required) identifier for the sound.
* volume: (optional) custom volume. Valid values are a float from 0 to 1.
4. Music:
- In contract to sound effects, only one music can be played at a time
- Music is using the same API to initilize just like sound.
- Music loops by default
- Music with these config options:
* id: (required) identifier for the sound.
* volume: (optional) custom volume. Valid values are a float from 0 to 1.
* start: (optional) a float from 0 to 1 used for cropping and indicates the start of the cropping
* end: (optional) a float from 0 to 1 used for cropping and indicates the end of the cropping
*/
// Global variables
var shapeAssets = ['circle_tile'];
var selectedTile = null;
var tiles = [];
var scoreTxt;
var highScoreTxt;
var gameBoard; // Container for tiles
var currentLevel = 1;
var maxLevel = 10; // 10 levels total
var levelTimer;
var timeRemaining;
var timerText;
var levelText;
var consecutiveMatches = 0; // Track consecutive successful matches
var bonusText; // Display bonus multiplier
var isTransitioningLevel = false; // Flag to prevent game over during level transitions
// Game dimensions
var gameWidth = 2048;
var gameHeight = 2732;
// Tile dimensions and spacing
var tileWidth = 200;
var tileHeight = 300;
var tileSpacing = 20;
// Game board dimensions
var boardCols = 8;
var boardRows = 6;
var boardLayers = 2; // Two layers of tiles
var boardWidth = boardCols * (tileWidth + tileSpacing) - tileSpacing;
var boardHeight = boardRows * (tileHeight + tileSpacing) - tileSpacing;
// Available nature-themed assets
var availableAssets = ['potato', 'tomato', 'carrot', 'flower', 'tree', 'apple', 'corn', 'mushroom', 'pumpkin', 'sunflower', 'broccoli', 'strawberry'];
// Shape assets array already declared above
// Level configurations for different shapes
var levelConfigs = {
1: {
shape: 'rectangle',
cols: 6,
rows: 4,
layers: 2
},
2: {
shape: 'rectangle',
cols: 8,
rows: 6,
layers: 2
},
3: {
shape: 'triangle',
cols: 8,
rows: 5,
layers: 2
},
4: {
shape: 'circle',
cols: 6,
rows: 6,
layers: 2
},
5: {
shape: 'rectangle',
cols: 9,
rows: 6,
layers: 3
},
6: {
shape: 'triangle',
cols: 9,
rows: 6,
layers: 3
},
7: {
shape: 'circle',
cols: 8,
rows: 8,
layers: 3
},
8: {
shape: 'rectangle',
cols: 10,
rows: 7,
layers: 3
},
9: {
shape: 'triangle',
cols: 9,
rows: 7,
layers: 4
},
10: {
shape: 'circle',
cols: 9,
rows: 7,
layers: 4
}
};
// Function to get random asset based on level
function getRandomAssetForLevel(level) {
// All levels use nature assets
return availableAssets[Math.floor(Math.random() * availableAssets.length)];
}
// Function to get time limit for current level
function getTimeLimitForLevel(level) {
if (level <= 3) {
return 300; // 5 minutes
} else if (level <= 6) {
return 180; // 3 minutes
} else {
return 120; // 2 minutes
}
}
// Intro screen container
var introContainer;
// Function to generate the game board layout
function generateBoardLayout(rows, cols, layers) {
var config = levelConfigs[currentLevel];
var shape = config.shape;
var layout = [];
// Generate shape-specific layouts
for (var layer = 0; layer < layers; layer++) {
layout[layer] = [];
var validPositions = [];
// Get valid positions based on shape
for (var r = 0; r < rows; r++) {
layout[layer][r] = [];
for (var c = 0; c < cols; c++) {
var isValid = false;
if (shape === 'rectangle') {
isValid = true;
} else if (shape === 'triangle') {
// Pyramid shape - start with 1-2 tiles at top, increase by 1-2 per row
var tilesInRow = Math.min(cols, 2 + r * 2);
var startCol = Math.floor((cols - tilesInRow) / 2);
isValid = c >= startCol && c < startCol + tilesInRow;
} else if (shape === 'circle') {
// Circle shape - use distance from center
var centerX = (cols - 1) / 2;
var centerY = (rows - 1) / 2;
var radiusX = cols / 2 - 0.5;
var radiusY = rows / 2 - 0.5;
var dx = (c - centerX) / radiusX;
var dy = (r - centerY) / radiusY;
isValid = dx * dx + dy * dy <= 1;
}
if (isValid) {
validPositions.push({
r: r,
c: c
});
layout[layer][r][c] = null;
} else {
layout[layer][r][c] = null;
}
}
}
// Ensure even number of valid positions
if (validPositions.length % 2 !== 0) {
validPositions.pop();
}
// Create pairs of tiles
var allTiles = [];
for (var i = 0; i < validPositions.length / 2; i++) {
var randomAssetId = getRandomAssetForLevel(currentLevel);
allTiles.push(randomAssetId);
allTiles.push(randomAssetId);
}
// Shuffle the tiles
for (var i = allTiles.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = allTiles[i];
allTiles[i] = allTiles[j];
allTiles[j] = temp;
}
// Place tiles in valid positions
for (var i = 0; i < validPositions.length; i++) {
var pos = validPositions[i];
layout[layer][pos.r][pos.c] = allTiles[i];
}
}
return layout;
}
// Function to create and position tiles
function createBoard(layout) {
gameBoard = new Container();
game.addChild(gameBoard);
// Calculate actual board dimensions based on level config
var config = levelConfigs[currentLevel];
var actualBoardWidth = config.cols * (tileWidth + tileSpacing) - tileSpacing;
var actualBoardHeight = config.rows * (tileHeight + tileSpacing) - tileSpacing;
// Center the board on the screen
gameBoard.x = (gameWidth - actualBoardWidth) / 2;
gameBoard.y = (gameHeight - actualBoardHeight) / 2 + 100; // Adjust vertical position to avoid top menu
// Create tiles for each layer
for (var layer = 0; layer < layout.length; layer++) {
var layerOffset = layer * 30; // Offset each layer for visual depth
for (var r = 0; r < layout[layer].length; r++) {
for (var c = 0; c < layout[layer][r].length; c++) {
var assetId = layout[layer][r][c];
if (assetId) {
var tile = new Tile(assetId, r, c, layer);
tile.x = c * (tileWidth + tileSpacing) + tileWidth / 2 + layerOffset;
tile.y = r * (tileHeight + tileSpacing) + tileHeight / 2 - layerOffset;
gameBoard.addChild(tile);
tiles.push(tile);
}
}
}
}
// Sort tiles by layer so upper layers are drawn on top
tiles.sort(function (a, b) {
return a.layer - b.layer;
});
// Update which tiles are covered
updateCoveredTiles();
}
// Function to update which tiles are covered by tiles above them
function updateCoveredTiles() {
// Reset all tiles to uncovered
for (var i = 0; i < tiles.length; i++) {
tiles[i].isCovered = false;
tiles[i].alpha = 1;
}
// Check each tile to see if it's covered
for (var i = 0; i < tiles.length; i++) {
var tile = tiles[i];
// Check if any tile above this one overlaps it
for (var j = 0; j < tiles.length; j++) {
var otherTile = tiles[j];
if (otherTile.layer > tile.layer && !otherTile.isMatched) {
// Check if tiles overlap (considering the offset)
var layerDiff = otherTile.layer - tile.layer;
var offsetX = layerDiff * 30;
var offsetY = layerDiff * 30;
var overlapX = Math.abs(tile.x - offsetX - otherTile.x) < tileWidth;
var overlapY = Math.abs(tile.y + offsetY - otherTile.y) < tileHeight;
if (overlapX && overlapY) {
tile.isCovered = true;
tile.alpha = 0.5; // Make covered tiles semi-transparent
break;
}
}
}
}
}
// Function to handle tile selection and matching
function tileSelected(tile) {
if (selectedTile === null) {
selectedTile = tile;
// Highlight the selected tile (optional)
// tween(selectedTile, { tint: 0xffff00 }, { duration: 100 });
} else if (selectedTile === tile) {
// Deselect the same tile
// tween(selectedTile, { tint: 0xffffff }, { duration: 100 });
selectedTile = null;
} else {
// Check for a match
if (selectedTile.assetId === tile.assetId) {
// Match found
LK.getSound('match_sound').play();
consecutiveMatches++; // Increment consecutive matches
var baseScore = 10;
var multiplier = consecutiveMatches; // 1x, 2x, 3x, etc.
var scoreToAdd = baseScore * multiplier;
LK.setScore(LK.getScore() + scoreToAdd);
scoreTxt.setText('Score: ' + LK.getScore());
// Show bonus multiplier if greater than 1
if (multiplier > 1) {
showBonusText(multiplier, scoreToAdd);
}
selectedTile.destroy();
tile.destroy();
selectedTile = null;
} else {
// No match - reset consecutive matches
consecutiveMatches = 0;
LK.getSound('no_match_sound').play();
// Optionally indicate no match (e.g., flash tiles red)
// tween(selectedTile, { tint: 0xff0000 }, { duration: 200, onFinish: function() { tween(selectedTile, { tint: 0xffffff }, { duration: 200 }); } });
// tween(tile, { tint: 0xff0000 }, { duration: 200, onFinish: function() { tween(tile, { tint: 0xffffff }, { duration: 200 }); } });
selectedTile = null;
}
}
}
// Initialize score and high score display (created but not shown until game starts)
scoreTxt = new Text2('Score: 0', {
size: 80,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
scoreTxt.y = 20; // Position below the top menu area
highScoreTxt = new Text2('High Score: ' + storage.highScore, {
size: 80,
fill: 0xFFFFFF
});
highScoreTxt.anchor.set(0.5, 0);
highScoreTxt.y = 120; // Position below the score display
// Create intro container
introContainer = new Container();
game.addChild(introContainer);
// Add intro background
var introBg = introContainer.attachAsset('Introbackground', {
anchorX: 0.5,
anchorY: 0.5,
x: gameWidth / 2,
y: gameHeight / 2
});
// Get the actual dimensions of the background asset
var bgWidth = introBg.width;
var bgHeight = introBg.height;
// Calculate scale to cover the entire screen
var scaleX = gameWidth / bgWidth;
var scaleY = gameHeight / bgHeight;
// Use the larger scale to ensure full coverage
var scale = Math.max(scaleX, scaleY);
// Apply the scale
introBg.scale.set(scale, scale);
// Start button with image asset
var startButton = introContainer.attachAsset('Startbutton', {
anchorX: 0.5,
anchorY: 0.5,
x: gameWidth / 2,
y: gameHeight - 200 // 200px from bottom
});
// Smooth continuous blinking animation using tween
function animateStartButton() {
tween(startButton, {
alpha: 0.3
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(startButton, {
alpha: 1
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: animateStartButton // Loop the animation
});
}
});
}
animateStartButton();
// Handle tap on start button to begin the game
startButton.down = function (x, y, obj) {
tween.stop(startButton); // Stop the blinking animation
introContainer.parent.removeChild(introContainer);
startGame();
};
// High score button beside start button from the left side by 600px
var highScoreButton = introContainer.attachAsset('Highscorebutton', {
anchorX: 0.5,
anchorY: 0.5,
x: gameWidth / 2 - 600,
// 100px to the left of start button
y: gameHeight - 200 // Same Y position as start button
});
// Handle tap on high score button
highScoreButton.down = function (x, y, obj) {
// Create high score display overlay
var highScoreOverlay = new Container();
introContainer.addChild(highScoreOverlay);
// Create a dark semi-transparent background for scores
var bgOverlay = LK.getAsset('box', {
width: gameWidth,
height: gameHeight,
color: 0x000000,
shape: 'box',
anchorX: 0.5,
anchorY: 0.5,
x: gameWidth / 2,
y: gameHeight / 2,
alpha: 0.8
});
highScoreOverlay.addChild(bgOverlay);
// High score text
var hsText = new Text2('Your High Score\n' + storage.highScore, {
size: 120,
fill: 0xFFFFFF
});
hsText.anchor.set(0.5, 0.5);
hsText.x = gameWidth / 2;
hsText.y = gameHeight / 2 - 300;
highScoreOverlay.addChild(hsText);
// Leaderboard title
var leaderboardTitle = new Text2('Top Players', {
size: 100,
fill: 0xFFD700
});
leaderboardTitle.anchor.set(0.5, 0.5);
leaderboardTitle.x = gameWidth / 2;
leaderboardTitle.y = gameHeight / 2 - 100;
highScoreOverlay.addChild(leaderboardTitle);
// Create mock leaderboard data since LK.getLeaderboard() doesn't exist
var leaderboard = [{
name: 'Player 1',
score: 1000
}, {
name: 'Player 2',
score: 850
}, {
name: 'Player 3',
score: 720
}, {
name: 'Player 4',
score: 650
}, {
name: 'Player 5',
score: 500
}];
var leaderboardY = gameHeight / 2 + 50;
// Display top 5 players
for (var i = 0; i < Math.min(5, leaderboard.length); i++) {
var entry = leaderboard[i];
var rankText = new Text2(i + 1 + '. ' + entry.name + ' - ' + entry.score, {
size: 70,
fill: 0xFFFFFF
});
rankText.anchor.set(0.5, 0.5);
rankText.x = gameWidth / 2;
rankText.y = leaderboardY + i * 80;
highScoreOverlay.addChild(rankText);
}
// Close button
var closeText = new Text2('Tap to Close', {
size: 80,
fill: 0xFFFF00
});
closeText.anchor.set(0.5, 0.5);
closeText.x = gameWidth / 2;
closeText.y = gameHeight / 2 + 500;
highScoreOverlay.addChild(closeText);
// Handle tap to close overlay
highScoreOverlay.down = function (x, y, obj) {
highScoreOverlay.parent.removeChild(highScoreOverlay);
};
};
// Function to start the game
function startGame() {
currentLevel = 1;
LK.setScore(0);
scoreTxt.setText('Score: 0');
LK.gui.top.addChild(scoreTxt);
// Add level display
levelText = new Text2('Level: ' + currentLevel, {
size: 80,
fill: 0xFFFFFF
});
levelText.anchor.set(0.5, 0);
levelText.y = 120;
LK.gui.top.addChild(levelText);
// Add timer display
timerText = new Text2('Time: 5:00', {
size: 80,
fill: 0xFFFFFF
});
timerText.anchor.set(0.5, 0);
timerText.y = 220;
LK.gui.top.addChild(timerText);
startLevel();
LK.playMusic('bg_music');
}
// Function to start a new level
function startLevel() {
// Clear any existing timer
if (levelTimer) {
LK.clearInterval(levelTimer);
}
// Clear existing board
if (gameBoard) {
gameBoard.destroy();
}
tiles = [];
selectedTile = null;
consecutiveMatches = 0; // Reset consecutive matches for new level
isTransitioningLevel = false; // Reset transition flag
// Update level text
levelText.setText('Level: ' + currentLevel);
// Set time limit for current level
timeRemaining = getTimeLimitForLevel(currentLevel);
updateTimerDisplay();
// Create new board with level-specific configuration
var config = levelConfigs[currentLevel];
var boardLayout = generateBoardLayout(config.rows, config.cols, config.layers);
createBoard(boardLayout);
// Start timer
levelTimer = LK.setInterval(function () {
timeRemaining--;
updateTimerDisplay();
if (timeRemaining <= 0) {
LK.clearInterval(levelTimer);
isTransitioningLevel = true;
// Show time up message
var timeUpText = new Text2('Time Up!', {
size: 120,
fill: 0xFF6B6B
});
timeUpText.anchor.set(0.5, 0.5);
timeUpText.x = gameWidth / 2;
timeUpText.y = gameHeight / 2 - 100;
game.addChild(timeUpText);
var restartText = new Text2('Restarting Level...', {
size: 80,
fill: 0xFFFFFF
});
restartText.anchor.set(0.5, 0.5);
restartText.x = gameWidth / 2;
restartText.y = gameHeight / 2 + 50;
game.addChild(restartText);
// Restart level after delay
LK.setTimeout(function () {
timeUpText.parent.removeChild(timeUpText);
restartText.parent.removeChild(restartText);
startLevel();
}, 2000);
}
}, 1000);
}
// Function to update timer display
function updateTimerDisplay() {
var minutes = Math.floor(timeRemaining / 60);
var seconds = timeRemaining % 60;
var timeString = minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
timerText.setText('Time: ' + timeString);
}
// Function to show bonus multiplier text
function showBonusText(multiplier, points) {
// Remove existing bonus text if any
if (bonusText && bonusText.parent) {
bonusText.parent.removeChild(bonusText);
}
// Create new bonus text
bonusText = new Text2(multiplier + 'x COMBO! +' + points, {
size: 120,
fill: 0xFFD700 // Gold color
});
bonusText.anchor.set(0.5, 0.5);
bonusText.x = gameWidth / 2;
bonusText.y = gameHeight / 2;
game.addChild(bonusText);
// Animate the bonus text
tween(bonusText, {
y: gameHeight / 2 - 200,
alpha: 0
}, {
duration: 1500,
onFinish: function onFinish() {
if (bonusText.parent) {
bonusText.parent.removeChild(bonusText);
}
}
});
}
// Function to check if any valid matches are available
function hasAvailableMatches() {
// Create a map to track available tiles by assetId
var availableTilesByAsset = {};
// Group all uncovered and unmatched tiles by their assetId
for (var i = 0; i < tiles.length; i++) {
var tile = tiles[i];
if (!tile.isMatched && !tile.isCovered) {
if (!availableTilesByAsset[tile.assetId]) {
availableTilesByAsset[tile.assetId] = [];
}
availableTilesByAsset[tile.assetId].push(tile);
}
}
// Check if any asset has at least 2 available tiles (a pair)
for (var assetId in availableTilesByAsset) {
if (availableTilesByAsset[assetId].length >= 2) {
return true;
}
}
return false;
}
// Game update loop (currently not needed for this game's logic, but included for structure)
game.update = function () {
// Update high score if current score is higher
if (LK.getScore() > storage.highScore) {
storage.highScore = LK.getScore();
}
};