/**** * 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();
}
};