/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // --- Ball --- var Ball = Container.expand(function () { var self = Container.call(this); self.gridX = 0; self.gridY = 0; self.ballAsset = self.attachAsset('ball', { width: cellSize * 0.6 * 1.75, height: cellSize * 0.6 * 1.75, color: 0xffffff, shape: 'ellipse', anchorX: 0.5, anchorY: 0.5 }); self.setGridPos = function (gx, gy) { self.gridX = gx; self.gridY = gy; self.x = gridOriginX + gx * cellSize + cellSize / 2; self.y = gridOriginY + gy * cellSize + cellSize / 2; }; self.flash = function () { LK.effects.flashObject(self, 0xffa500, 200); }; return self; }); // --- PlayerPawn: For both player and AI pawns --- var PlayerPawn = Container.expand(function () { var self = Container.call(this); // Use different colors for player and AI self.isAI = false; self.gridX = 0; self.gridY = 0; self.hasBall = false; self.pawnAsset = null; self.init = function (isAI) { self.isAI = isAI; var color = isAI ? 0x1e90ff : 0x32cd32; self.pawnAsset = self.attachAsset('pawn_' + (isAI ? 'ai' : 'player'), { width: (cellSize - 8) * 1.75, height: (cellSize - 8) * 1.75, color: color, shape: 'ellipse', anchorX: 0.5, anchorY: 0.5 }); }; self.setGridPos = function (gx, gy) { self.gridX = gx; self.gridY = gy; self.x = gridOriginX + gx * cellSize + cellSize / 2; self.y = gridOriginY + gy * cellSize + cellSize / 2; }; self.flash = function () { LK.effects.flashObject(self, 0xffff00, 300); }; return self; }); // --- Wall: Tetris block wall, occupies multiple cells --- var Wall = Container.expand(function () { var self = Container.call(this); self.cells = []; // [{x, y}] self.wallId = null; self.blocks = []; self.init = function (cells, wallId) { self.cells = cells; self.wallId = wallId; for (var i = 0; i < cells.length; i++) { var block = self.attachAsset('wall', { width: cellSize - 6, height: cellSize - 6, color: 0x888888, shape: 'box', anchorX: 0.5, anchorY: 0.5, x: (cells[i].x - cells[0].x) * cellSize, y: (cells[i].y - cells[0].y) * cellSize }); self.blocks.push(block); } // Position wall at first cell self.x = gridOriginX + cells[0].x * cellSize + cellSize / 2; self.y = gridOriginY + cells[0].y * cellSize + cellSize / 2; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x1a1a1a }); /**** * Game Code ****/ // --- GridCell: For grid logic, not a display object --- // --- Grid Setup --- var GridCell = function GridCell(x, y) { this.x = x; this.y = y; this.occupied = null; // null, 'player', 'ai', 'wall', 'ball' this.wallId = null; // If wall, which wall }; var gridCols = 21; var gridRows = 31; var cellSize = Math.floor(Math.min(2048 / gridCols, 2732 / gridRows) * 0.95); var gridWidth = gridCols * cellSize; var gridHeight = gridRows * cellSize; var gridOriginX = Math.floor((2048 - gridWidth) / 2); var gridOriginY = Math.floor((2732 - gridHeight) / 2); // --- Game State --- var grid = []; for (var y = 0; y < gridRows; y++) { var row = []; for (var x = 0; x < gridCols; x++) { row.push(new GridCell(x, y)); } grid.push(row); } var playerPawns = []; var aiPawns = []; var ball = null; var walls = []; var gameElements = []; // Track all game elements for management (future use) var wallIdCounter = 1; var turn = 'player'; // 'player' or 'ai' var selectedPawn = null; var validMoves = []; var validShots = []; var gameLocked = false; var playerScore = 0; var aiScore = 0; var goalToWin = 3; var scoreTxt = null; var infoTxt = null; // --- Draw grid cell backgrounds --- var goalCols = []; for (var i = Math.floor(gridCols / 2) - 2; i <= Math.floor(gridCols / 2) + 2; i++) { goalCols.push(i); } for (var gy = 0; gy < gridRows; gy++) { for (var gx = 0; gx < gridCols; gx++) { var cellBg = LK.getAsset('grid_cell', { width: cellSize, height: cellSize, anchorX: 0, anchorY: 0, x: gridOriginX + gx * cellSize, y: gridOriginY + gy * cellSize }); game.addChild(cellBg); } } // --- Draw grid lines (for visual aid) --- for (var gx = 0; gx <= gridCols; gx++) { var vline = LK.getAsset('wall_block', { width: 4, height: gridHeight, color: 0x222222, shape: 'box', anchorX: 0.5, anchorY: 0, x: gridOriginX + gx * cellSize - 2, y: gridOriginY }); game.addChild(vline); } for (var gy = 0; gy <= gridRows; gy++) { var hline = LK.getAsset('wall_block', { width: gridWidth, height: 4, color: 0x222222, shape: 'box', anchorX: 0, anchorY: 0.5, x: gridOriginX, y: gridOriginY + gy * cellSize - 2 }); game.addChild(hline); } // --- Place Pawns --- function placePawns() { // Player: 3 pawns, bottom center var pxs = [Math.floor(gridCols / 2) - 2, Math.floor(gridCols / 2), Math.floor(gridCols / 2) + 2]; for (var i = 0; i < 3; i++) { var pawn = new PlayerPawn(); pawn.init(false); pawn.setGridPos(pxs[i], gridRows - 3); playerPawns.push(pawn); gameElements.push(pawn); game.addChild(pawn); grid[gridRows - 3][pxs[i]].occupied = 'player'; } // AI: 3 pawns, top center var axs = [Math.floor(gridCols / 2) - 2, Math.floor(gridCols / 2), Math.floor(gridCols / 2) + 2]; for (var i = 0; i < 3; i++) { var pawn = new PlayerPawn(); pawn.init(true); pawn.setGridPos(axs[i], 2); aiPawns.push(pawn); gameElements.push(pawn); game.addChild(pawn); grid[2][axs[i]].occupied = 'ai'; } } placePawns(); // --- Place Ball --- function placeBall(center) { if (ball) { ball.destroy(); } ball = new Ball(); gameElements.push(ball); var bx = center ? Math.floor(gridCols / 2) : playerPawns[1].gridX; var by = center ? Math.floor(gridRows / 2) : playerPawns[1].gridY - 1; ball.setGridPos(bx, by); game.addChild(ball); grid[by][bx].occupied = 'ball'; } placeBall(true); // --- Draw Goals (visual only, with goal image asset) --- var goalWidth = 5; var goalImgW = cellSize * goalWidth * 1.05; var goalImgH = cellSize * 1.2; var goalColsStart = Math.floor(gridCols / 2) - 2; var goalColsEnd = Math.floor(gridCols / 2) + 2; // Top goal (AI's goal) var goalTopImg = LK.getAsset('goal', { width: goalImgW, height: goalImgH, anchorX: 0.5, anchorY: 1, x: gridOriginX + (goalColsStart + goalColsEnd) / 2 * cellSize + cellSize / 2, y: gridOriginY + 2 // slight offset for visual }); game.addChild(goalTopImg); // Bottom goal (Player's goal) var goalBotImg = LK.getAsset('goal', { width: goalImgW, height: goalImgH, anchorX: 0.5, anchorY: 0, x: gridOriginX + (goalColsStart + goalColsEnd) / 2 * cellSize + cellSize / 2, y: gridOriginY + gridHeight - 2 // slight offset for visual }); game.addChild(goalBotImg); // --- Score Display --- scoreTxt = new Text2('0 : 0', { size: 120, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // --- Info Text --- infoTxt = new Text2('', { size: 60, fill: "#fff" }); infoTxt.anchor.set(0.5, 0); LK.gui.bottom.addChild(infoTxt); // --- Utility Functions --- function updateScore() { scoreTxt.setText(playerScore + ' : ' + aiScore); } function setInfo(msg) { infoTxt.setText(msg); } function clearInfo() { infoTxt.setText(''); } function isInsideGrid(x, y) { return x >= 0 && x < gridCols && y >= 0 && y < gridRows; } function isGoal(x, y, forPlayer) { // Top row for player, bottom row for AI var goalCols = []; for (var i = Math.floor(gridCols / 2) - 2; i <= Math.floor(gridCols / 2) + 2; i++) { goalCols.push(i); } if (forPlayer && y === 0 && goalCols.indexOf(x) !== -1) return true; if (!forPlayer && y === gridRows - 1 && goalCols.indexOf(x) !== -1) return true; return false; } function getPawnAt(x, y) { for (var i = 0; i < playerPawns.length; i++) { if (playerPawns[i].gridX === x && playerPawns[i].gridY === y) return playerPawns[i]; } for (var i = 0; i < aiPawns.length; i++) { if (aiPawns[i].gridX === x && aiPawns[i].gridY === y) return aiPawns[i]; } return null; } function getWallAt(x, y) { for (var i = 0; i < walls.length; i++) { var wall = walls[i]; for (var j = 0; j < wall.cells.length; j++) { if (wall.cells[j].x === x && wall.cells[j].y === y) return wall; } } return null; } function cellBlocked(x, y) { if (!isInsideGrid(x, y)) return true; var occ = grid[y][x].occupied; return occ === 'player' || occ === 'ai' || occ === 'wall'; } function cellFree(x, y) { if (!isInsideGrid(x, y)) return false; var occ = grid[y][x].occupied; return occ === null || occ === 'ball'; } function knightMoves(x, y) { var moves = [{ dx: 1, dy: 3 }, { dx: 3, dy: 1 }, { dx: -1, dy: 3 }, { dx: -3, dy: 1 }, { dx: 1, dy: -3 }, { dx: 3, dy: -1 }, { dx: -1, dy: -3 }, { dx: -3, dy: -1 }, // Extra moves (as requested) { dx: 2, dy: 1 }, { dx: -1, dy: 1 }, { dx: -2, dy: -1 }, { dx: 1, dy: -1 }]; var res = []; for (var i = 0; i < moves.length; i++) { var nx = x + moves[i].dx; var ny = y + moves[i].dy; if (isInsideGrid(nx, ny) && cellFree(nx, ny)) { res.push({ x: nx, y: ny }); } } return res; } function straightLineShots(x, y) { // Up, Down, Left, Right var dirs = [{ dx: 0, dy: -1 }, { dx: 0, dy: 1 }, { dx: -1, dy: 0 }, { dx: 1, dy: 0 }]; var shots = []; var maxShotDistance = 7; for (var d = 0; d < dirs.length; d++) { var nx = x, ny = y; var distance = 0; while (distance < maxShotDistance) { // limit to 7 blocks nx += dirs[d].dx; ny += dirs[d].dy; distance++; if (!isInsideGrid(nx, ny)) break; if (cellBlocked(nx, ny)) break; shots.push({ x: nx, y: ny, dir: dirs[d] }); if (isGoal(nx, ny, turn === 'player')) break; } } return shots; } function highlightCells(cells, color) { for (var i = 0; i < cells.length; i++) { var hl = LK.getAsset('target_indicator', { width: cellSize - 10, height: cellSize - 10, anchorX: 0.5, anchorY: 0.5, x: gridOriginX + cells[i].x * cellSize + cellSize / 2, y: gridOriginY + cells[i].y * cellSize + cellSize / 2, alpha: 0.7 }); game.addChild(hl); highlightOverlays.push(hl); } } function clearHighlights() { for (var i = 0; i < highlightOverlays.length; i++) { highlightOverlays[i].destroy(); } highlightOverlays = []; } var highlightOverlays = []; // --- Wall Generation (Tetris shapes) --- var tetrisShapes = [ // I [{ x: 0, y: 0 }, { x: 0, y: 1 }, { x: 0, y: 2 }, { x: 0, y: 3 }], // O [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }], // T [{ x: 0, y: 0 }, { x: -1, y: 1 }, { x: 0, y: 1 }, { x: 1, y: 1 }], // L [{ x: 0, y: 0 }, { x: 0, y: 1 }, { x: 0, y: 2 }, { x: 1, y: 2 }], // S [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 0, y: 1 }, { x: -1, y: 1 }]]; function randomWallShape() { var idx = Math.floor(Math.random() * tetrisShapes.length); return tetrisShapes[idx]; } function canPlaceWallAt(shape, ox, oy) { for (var i = 0; i < shape.length; i++) { var x = ox + shape[i].x; var y = oy + shape[i].y; if (!isInsideGrid(x, y)) return false; if (grid[y][x].occupied) return false; // Don't block ball or pawns if (ball && ball.gridX === x && ball.gridY === y) return false; if (getPawnAt(x, y)) return false; } return true; } function placeRandomWall() { // Try up to 10 times to find a spot for (var tries = 0; tries < 10; tries++) { var shape = randomWallShape(); var ox = Math.floor(Math.random() * (gridCols - 2)) + 1; var oy = Math.floor(Math.random() * (gridRows - 2)) + 1; if (canPlaceWallAt(shape, ox, oy)) { var cells = []; for (var i = 0; i < shape.length; i++) { var x = ox + shape[i].x; var y = oy + shape[i].y; cells.push({ x: x, y: y }); } var wall = new Wall(); wall.init(cells, wallIdCounter); wall.wallId = wallIdCounter; wallIdCounter++; walls.push(wall); gameElements.push(wall); game.addChild(wall); for (var i = 0; i < cells.length; i++) { grid[cells[i].y][cells[i].x].occupied = 'wall'; grid[cells[i].y][cells[i].x].wallId = wall.wallId; } break; } } } // --- Turn Logic --- function startPlayerTurn() { turn = 'player'; setInfo("Your turn: Tap a pawn to move or shoot"); clearHighlights(); selectedPawn = null; validMoves = []; validShots = []; gameLocked = false; } function startAITurn() { turn = 'ai'; setInfo("AI's turn..."); clearHighlights(); selectedPawn = null; validMoves = []; validShots = []; gameLocked = true; LK.setTimeout(aiTakeTurn, 700); } function endTurn() { // After each turn, maybe spawn a wall if (Math.random() < 0.05) { placeRandomWall(); } if (turn === 'player') { startAITurn(); } else { startPlayerTurn(); } } // --- Ball Movement and Goal Check --- function moveBallTo(x, y, onFinish) { // Remove old ball from grid grid[ball.gridY][ball.gridX].occupied = null; ball.setGridPos(x, y); grid[y][x].occupied = 'ball'; ball.flash(); if (onFinish) LK.setTimeout(onFinish, 200); } function shootBall(fromX, fromY, toX, toY, dir, onFinish) { // Animate ball along the path, but limit to 7 blocks max var path = []; var nx = fromX, ny = fromY; var maxShotDistance = 7; var distance = 0; while (distance < maxShotDistance) { nx += dir.dx; ny += dir.dy; distance++; if (!isInsideGrid(nx, ny)) break; if (cellBlocked(nx, ny)) break; path.push({ x: nx, y: ny }); if (isGoal(nx, ny, turn === 'player')) break; if (isGoal(nx, ny, turn === 'ai')) break; } function animateStep(idx) { if (idx >= path.length) { if (onFinish) onFinish(); return; } moveBallTo(path[idx].x, path[idx].y, function () { animateStep(idx + 1); }); } animateStep(0); } // --- Player Input Handling --- game.down = function (x, y, obj) { if (gameLocked) return; // Convert to grid coordinates var gx = Math.floor((x - gridOriginX) / cellSize); var gy = Math.floor((y - gridOriginY) / cellSize); if (!isInsideGrid(gx, gy)) return; // If no pawn selected, select a pawn if (!selectedPawn) { for (var i = 0; i < playerPawns.length; i++) { var pawn = playerPawns[i]; if (pawn.gridX === gx && pawn.gridY === gy) { selectedPawn = pawn; pawn.flash(); // Show knight moves validMoves = knightMoves(gx, gy); highlightCells(validMoves, 0x00ff00); // If pawn is adjacent to ball, allow to pick up if (Math.abs(ball.gridX - gx) + Math.abs(ball.gridY - gy) === 1) { validMoves.push({ x: ball.gridX, y: ball.gridY }); } // If pawn is on ball, allow to shoot if (gx === ball.gridX && gy === ball.gridY) { validShots = straightLineShots(gx, gy); highlightCells(validShots, 0xffa500); } return; } } } else { // If clicked on a valid move, move pawn for (var i = 0; i < validMoves.length; i++) { if (validMoves[i].x === gx && validMoves[i].y === gy) { // Move pawn grid[selectedPawn.gridY][selectedPawn.gridX].occupied = null; selectedPawn.setGridPos(gx, gy); grid[gy][gx].occupied = 'player'; // If moved onto ball, pick up if (gx === ball.gridX && gy === ball.gridY) { selectedPawn.hasBall = true; setInfo("You have the ball! Move again or tap a direction to shoot."); // Allow dribbling: highlight knight moves and shots again validMoves = knightMoves(gx, gy); validShots = straightLineShots(gx, gy); clearHighlights(); highlightCells(validMoves, 0x00ff00); highlightCells(validShots, 0xffa500); return; } clearHighlights(); endTurn(); return; } } // If pawn has ball and clicked on valid shot, shoot if (selectedPawn.hasBall) { // Check for shot for (var i = 0; i < validShots.length; i++) { if (validShots[i].x === gx && validShots[i].y === gy) { selectedPawn.hasBall = false; clearHighlights(); shootBall(selectedPawn.gridX, selectedPawn.gridY, gx, gy, validShots[i].dir, function () { // Check for goal if (isGoal(gx, gy, true)) { playerScore++; updateScore(); setInfo("GOAL! You scored!"); // Beautiful goal effect: flash screen, ball animation, and shake goal LK.effects.flashScreen(0x00ff00, 400); // Animate ball scaling up and fading out for a goal "pop" tween.to(ball, { scaleX: 2, scaleY: 2, alpha: 0 }, 350, { easing: 'easeOutCubic' }); // Animate bottom goal shaking tween.to(goalBotImg, { x: goalBotImg.x + 20 }, 60, { yoyo: true, repeat: 3 }); tween.to(goalBotImg, { x: goalBotImg.x - 20 }, 60, { yoyo: true, repeat: 3 }); if (playerScore >= goalToWin) { LK.setTimeout(function () { LK.showYouWin(); }, 600); return; } LK.setTimeout(function () { resetAfterGoal(false); }, 1200); } else { endTurn(); } }); return; } } // Check for dribble move (knight move with ball) for (var i = 0; i < validMoves.length; i++) { if (validMoves[i].x === gx && validMoves[i].y === gy) { // Move pawn and ball together grid[selectedPawn.gridY][selectedPawn.gridX].occupied = null; selectedPawn.setGridPos(gx, gy); grid[gy][gx].occupied = 'player'; moveBallTo(gx, gy); // Allow shoot after dribble setInfo("Tap a direction to shoot!"); validShots = straightLineShots(gx, gy); validMoves = []; // Only allow shoot after dribble, not another move clearHighlights(); highlightCells(validShots, 0xffa500); return; } } } // Deselect if clicked elsewhere clearHighlights(); selectedPawn = null; validMoves = []; validShots = []; } }; // --- AI Logic --- function aiTakeTurn() { // --- AI STRATEGY: All 3 pawns act, with passing, shooting, and blocking --- // 1. Try to shoot if any pawn is on the ball // 2. Try to pass if a pawn can reach the ball and another can shoot // 3. Otherwise, move pawns to block or approach ball // Helper: Find all possible moves for a pawn function pawnMoves(pawn) { return knightMoves(pawn.gridX, pawn.gridY); } // 1. Try to shoot if any pawn is on the ball for (var i = 0; i < aiPawns.length; i++) { var pawn = aiPawns[i]; if (pawn.gridX === ball.gridX && pawn.gridY === ball.gridY) { var shots = straightLineShots(pawn.gridX, pawn.gridY); // Prefer shooting downwards (towards player goal) var bestShot = null; for (var j = 0; j < shots.length; j++) { if (shots[j].dir.dy > 0) { bestShot = shots[j]; break; } } if (!bestShot && shots.length > 0) bestShot = shots[0]; if (bestShot) { shootBall(pawn.gridX, pawn.gridY, bestShot.x, bestShot.y, bestShot.dir, function () { if (isGoal(bestShot.x, bestShot.y, false)) { aiScore++; updateScore(); setInfo("AI scored!"); LK.effects.flashScreen(0xff0000, 400); tween.to(ball, { scaleX: 2, scaleY: 2, alpha: 0 }, 350, { easing: 'easeOutCubic' }); tween.to(goalTopImg, { x: goalTopImg.x + 20 }, 60, { yoyo: true, repeat: 3 }); tween.to(goalTopImg, { x: goalTopImg.x - 20 }, 60, { yoyo: true, repeat: 3 }); if (aiScore >= goalToWin) { LK.setTimeout(function () { LK.showGameOver(); }, 600); return; } LK.setTimeout(function () { resetAfterGoal(true); }, 1200); } else { endTurn(); } }); return; } } } // 2. Try to pass: If a pawn can move onto the ball, and another pawn can shoot from there var passFound = false; for (var i = 0; i < aiPawns.length; i++) { var mover = aiPawns[i]; var moves = pawnMoves(mover); for (var j = 0; j < moves.length; j++) { if (moves[j].x === ball.gridX && moves[j].y === ball.gridY) { // Simulate: If this pawn moves onto ball, can another pawn shoot next turn? for (var k = 0; k < aiPawns.length; k++) { if (k === i) continue; var shooter = aiPawns[k]; // Can shooter reach the new ball position in one move? var shooterMoves = knightMoves(shooter.gridX, shooter.gridY); for (var m = 0; m < shooterMoves.length; m++) { if (shooterMoves[m].x === moves[j].x && shooterMoves[m].y === moves[j].y) { // Simulate shooter on ball, can shoot? var shots = straightLineShots(moves[j].x, moves[j].y); var bestShot = null; for (var n = 0; n < shots.length; n++) { if (shots[n].dir.dy > 0) { bestShot = shots[n]; break; } } if (!bestShot && shots.length > 0) bestShot = shots[0]; if (bestShot) { // Move mover onto ball grid[mover.gridY][mover.gridX].occupied = null; mover.setGridPos(moves[j].x, moves[j].y); grid[moves[j].y][moves[j].x].occupied = 'ai'; mover.hasBall = true; // Next turn, shooter will shoot passFound = true; endTurn(); return; } } } } } } } // 3. Otherwise, move all pawns: one towards ball, others block or approach // Find the pawn closest to the ball var bestPawn = null; var minDist = 9999; for (var i = 0; i < aiPawns.length; i++) { var pawn = aiPawns[i]; var dist = Math.abs(pawn.gridX - ball.gridX) + Math.abs(pawn.gridY - ball.gridY); if (dist < minDist) { minDist = dist; bestPawn = pawn; } } // Move bestPawn towards ball if (bestPawn) { var moves = knightMoves(bestPawn.gridX, bestPawn.gridY); var bestMove = null; minDist = 9999; for (var i = 0; i < moves.length; i++) { var dist = Math.abs(moves[i].x - ball.gridX) + Math.abs(moves[i].y - ball.gridY); if (dist < minDist) { minDist = dist; bestMove = moves[i]; } } if (bestMove) { grid[bestPawn.gridY][bestPawn.gridX].occupied = null; bestPawn.setGridPos(bestMove.x, bestMove.y); grid[bestMove.y][bestMove.x].occupied = 'ai'; // If moved onto ball, pick up and shoot next turn if (bestMove.x === ball.gridX && bestMove.y === ball.gridY) { bestPawn.hasBall = true; } } } // Move other pawns to block or approach player pawns for (var i = 0; i < aiPawns.length; i++) { var pawn = aiPawns[i]; if (pawn === bestPawn) continue; // Try to block: move towards the player pawn closest to the ball var closestPlayer = null; var minPlayerDist = 9999; for (var j = 0; j < playerPawns.length; j++) { var pdist = Math.abs(playerPawns[j].gridX - ball.gridX) + Math.abs(playerPawns[j].gridY - ball.gridY); if (pdist < minPlayerDist) { minPlayerDist = pdist; closestPlayer = playerPawns[j]; } } if (closestPlayer) { var moves = knightMoves(pawn.gridX, pawn.gridY); var bestBlock = null; var minBlockDist = 9999; for (var k = 0; k < moves.length; k++) { var dist = Math.abs(moves[k].x - closestPlayer.gridX) + Math.abs(moves[k].y - closestPlayer.gridY); if (dist < minBlockDist && cellFree(moves[k].x, moves[k].y)) { minBlockDist = dist; bestBlock = moves[k]; } } if (bestBlock) { grid[pawn.gridY][pawn.gridX].occupied = null; pawn.setGridPos(bestBlock.x, bestBlock.y); grid[bestBlock.y][bestBlock.x].occupied = 'ai'; } } } endTurn(); } // --- Reset After Goal --- function resetAfterGoal(aiScored) { // Remove all walls for (var i = 0; i < walls.length; i++) { walls[i].destroy(); } walls = []; gameElements = []; // Clear grid for (var y = 0; y < gridRows; y++) { for (var x = 0; x < gridCols; x++) { grid[y][x].occupied = null; grid[y][x].wallId = null; } } // Reset pawns for (var i = 0; i < playerPawns.length; i++) { playerPawns[i].destroy(); } for (var i = 0; i < aiPawns.length; i++) { aiPawns[i].destroy(); } playerPawns = []; aiPawns = []; placePawns(); // Reset ball at center and show it for a moment before resuming play placeBall(true); clearHighlights(); selectedPawn = null; validMoves = []; validShots = []; gameLocked = false; setInfo(''); // Wait 900ms before resuming play, so the ball is visible at center after a goal LK.setTimeout(function () { if (aiScored) { startPlayerTurn(); } else { startAITurn(); } }, 900); } // --- Game Update (not used for logic, but could be for animations) --- game.update = function () { // Check if ball is in the top goal area (AI's goal, y === 0, goalCols) if (ball && goalCols.indexOf(ball.gridX) !== -1 && ball.gridY === 0) { // Player scores! playerScore++; updateScore(); setInfo("GOAL! You scored!"); LK.effects.flashScreen(0x00ff00, 400); tween.to(ball, { scaleX: 2, scaleY: 2, alpha: 0 }, 350, { easing: 'easeOutCubic' }); tween.to(goalTopImg, { x: goalTopImg.x + 20 }, 60, { yoyo: true, repeat: 3 }); tween.to(goalTopImg, { x: goalTopImg.x - 20 }, 60, { yoyo: true, repeat: 3 }); if (playerScore >= goalToWin) { LK.setTimeout(function () { LK.showYouWin(); }, 600); } else { LK.setTimeout(function () { resetAfterGoal(false); }, 1200); } } // Check if ball is in the bottom goal area (Player's goal, y === gridRows-1, goalCols) if (ball && goalCols.indexOf(ball.gridX) !== -1 && ball.gridY === gridRows - 1) { // AI scores! aiScore++; updateScore(); setInfo("AI scored!"); LK.effects.flashScreen(0xff0000, 400); tween.to(ball, { scaleX: 2, scaleY: 2, alpha: 0 }, 350, { easing: 'easeOutCubic' }); tween.to(goalBotImg, { x: goalBotImg.x + 20 }, 60, { yoyo: true, repeat: 3 }); tween.to(goalBotImg, { x: goalBotImg.x - 20 }, 60, { yoyo: true, repeat: 3 }); if (aiScore >= goalToWin) { LK.setTimeout(function () { LK.showGameOver(); }, 600); } else { LK.setTimeout(function () { resetAfterGoal(true); }, 1200); } } }; // --- Start Game --- updateScore(); startPlayerTurn();
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// --- Ball ---
var Ball = Container.expand(function () {
var self = Container.call(this);
self.gridX = 0;
self.gridY = 0;
self.ballAsset = self.attachAsset('ball', {
width: cellSize * 0.6 * 1.75,
height: cellSize * 0.6 * 1.75,
color: 0xffffff,
shape: 'ellipse',
anchorX: 0.5,
anchorY: 0.5
});
self.setGridPos = function (gx, gy) {
self.gridX = gx;
self.gridY = gy;
self.x = gridOriginX + gx * cellSize + cellSize / 2;
self.y = gridOriginY + gy * cellSize + cellSize / 2;
};
self.flash = function () {
LK.effects.flashObject(self, 0xffa500, 200);
};
return self;
});
// --- PlayerPawn: For both player and AI pawns ---
var PlayerPawn = Container.expand(function () {
var self = Container.call(this);
// Use different colors for player and AI
self.isAI = false;
self.gridX = 0;
self.gridY = 0;
self.hasBall = false;
self.pawnAsset = null;
self.init = function (isAI) {
self.isAI = isAI;
var color = isAI ? 0x1e90ff : 0x32cd32;
self.pawnAsset = self.attachAsset('pawn_' + (isAI ? 'ai' : 'player'), {
width: (cellSize - 8) * 1.75,
height: (cellSize - 8) * 1.75,
color: color,
shape: 'ellipse',
anchorX: 0.5,
anchorY: 0.5
});
};
self.setGridPos = function (gx, gy) {
self.gridX = gx;
self.gridY = gy;
self.x = gridOriginX + gx * cellSize + cellSize / 2;
self.y = gridOriginY + gy * cellSize + cellSize / 2;
};
self.flash = function () {
LK.effects.flashObject(self, 0xffff00, 300);
};
return self;
});
// --- Wall: Tetris block wall, occupies multiple cells ---
var Wall = Container.expand(function () {
var self = Container.call(this);
self.cells = []; // [{x, y}]
self.wallId = null;
self.blocks = [];
self.init = function (cells, wallId) {
self.cells = cells;
self.wallId = wallId;
for (var i = 0; i < cells.length; i++) {
var block = self.attachAsset('wall', {
width: cellSize - 6,
height: cellSize - 6,
color: 0x888888,
shape: 'box',
anchorX: 0.5,
anchorY: 0.5,
x: (cells[i].x - cells[0].x) * cellSize,
y: (cells[i].y - cells[0].y) * cellSize
});
self.blocks.push(block);
}
// Position wall at first cell
self.x = gridOriginX + cells[0].x * cellSize + cellSize / 2;
self.y = gridOriginY + cells[0].y * cellSize + cellSize / 2;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a1a1a
});
/****
* Game Code
****/
// --- GridCell: For grid logic, not a display object ---
// --- Grid Setup ---
var GridCell = function GridCell(x, y) {
this.x = x;
this.y = y;
this.occupied = null; // null, 'player', 'ai', 'wall', 'ball'
this.wallId = null; // If wall, which wall
};
var gridCols = 21;
var gridRows = 31;
var cellSize = Math.floor(Math.min(2048 / gridCols, 2732 / gridRows) * 0.95);
var gridWidth = gridCols * cellSize;
var gridHeight = gridRows * cellSize;
var gridOriginX = Math.floor((2048 - gridWidth) / 2);
var gridOriginY = Math.floor((2732 - gridHeight) / 2);
// --- Game State ---
var grid = [];
for (var y = 0; y < gridRows; y++) {
var row = [];
for (var x = 0; x < gridCols; x++) {
row.push(new GridCell(x, y));
}
grid.push(row);
}
var playerPawns = [];
var aiPawns = [];
var ball = null;
var walls = [];
var gameElements = []; // Track all game elements for management (future use)
var wallIdCounter = 1;
var turn = 'player'; // 'player' or 'ai'
var selectedPawn = null;
var validMoves = [];
var validShots = [];
var gameLocked = false;
var playerScore = 0;
var aiScore = 0;
var goalToWin = 3;
var scoreTxt = null;
var infoTxt = null;
// --- Draw grid cell backgrounds ---
var goalCols = [];
for (var i = Math.floor(gridCols / 2) - 2; i <= Math.floor(gridCols / 2) + 2; i++) {
goalCols.push(i);
}
for (var gy = 0; gy < gridRows; gy++) {
for (var gx = 0; gx < gridCols; gx++) {
var cellBg = LK.getAsset('grid_cell', {
width: cellSize,
height: cellSize,
anchorX: 0,
anchorY: 0,
x: gridOriginX + gx * cellSize,
y: gridOriginY + gy * cellSize
});
game.addChild(cellBg);
}
}
// --- Draw grid lines (for visual aid) ---
for (var gx = 0; gx <= gridCols; gx++) {
var vline = LK.getAsset('wall_block', {
width: 4,
height: gridHeight,
color: 0x222222,
shape: 'box',
anchorX: 0.5,
anchorY: 0,
x: gridOriginX + gx * cellSize - 2,
y: gridOriginY
});
game.addChild(vline);
}
for (var gy = 0; gy <= gridRows; gy++) {
var hline = LK.getAsset('wall_block', {
width: gridWidth,
height: 4,
color: 0x222222,
shape: 'box',
anchorX: 0,
anchorY: 0.5,
x: gridOriginX,
y: gridOriginY + gy * cellSize - 2
});
game.addChild(hline);
}
// --- Place Pawns ---
function placePawns() {
// Player: 3 pawns, bottom center
var pxs = [Math.floor(gridCols / 2) - 2, Math.floor(gridCols / 2), Math.floor(gridCols / 2) + 2];
for (var i = 0; i < 3; i++) {
var pawn = new PlayerPawn();
pawn.init(false);
pawn.setGridPos(pxs[i], gridRows - 3);
playerPawns.push(pawn);
gameElements.push(pawn);
game.addChild(pawn);
grid[gridRows - 3][pxs[i]].occupied = 'player';
}
// AI: 3 pawns, top center
var axs = [Math.floor(gridCols / 2) - 2, Math.floor(gridCols / 2), Math.floor(gridCols / 2) + 2];
for (var i = 0; i < 3; i++) {
var pawn = new PlayerPawn();
pawn.init(true);
pawn.setGridPos(axs[i], 2);
aiPawns.push(pawn);
gameElements.push(pawn);
game.addChild(pawn);
grid[2][axs[i]].occupied = 'ai';
}
}
placePawns();
// --- Place Ball ---
function placeBall(center) {
if (ball) {
ball.destroy();
}
ball = new Ball();
gameElements.push(ball);
var bx = center ? Math.floor(gridCols / 2) : playerPawns[1].gridX;
var by = center ? Math.floor(gridRows / 2) : playerPawns[1].gridY - 1;
ball.setGridPos(bx, by);
game.addChild(ball);
grid[by][bx].occupied = 'ball';
}
placeBall(true);
// --- Draw Goals (visual only, with goal image asset) ---
var goalWidth = 5;
var goalImgW = cellSize * goalWidth * 1.05;
var goalImgH = cellSize * 1.2;
var goalColsStart = Math.floor(gridCols / 2) - 2;
var goalColsEnd = Math.floor(gridCols / 2) + 2;
// Top goal (AI's goal)
var goalTopImg = LK.getAsset('goal', {
width: goalImgW,
height: goalImgH,
anchorX: 0.5,
anchorY: 1,
x: gridOriginX + (goalColsStart + goalColsEnd) / 2 * cellSize + cellSize / 2,
y: gridOriginY + 2 // slight offset for visual
});
game.addChild(goalTopImg);
// Bottom goal (Player's goal)
var goalBotImg = LK.getAsset('goal', {
width: goalImgW,
height: goalImgH,
anchorX: 0.5,
anchorY: 0,
x: gridOriginX + (goalColsStart + goalColsEnd) / 2 * cellSize + cellSize / 2,
y: gridOriginY + gridHeight - 2 // slight offset for visual
});
game.addChild(goalBotImg);
// --- Score Display ---
scoreTxt = new Text2('0 : 0', {
size: 120,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// --- Info Text ---
infoTxt = new Text2('', {
size: 60,
fill: "#fff"
});
infoTxt.anchor.set(0.5, 0);
LK.gui.bottom.addChild(infoTxt);
// --- Utility Functions ---
function updateScore() {
scoreTxt.setText(playerScore + ' : ' + aiScore);
}
function setInfo(msg) {
infoTxt.setText(msg);
}
function clearInfo() {
infoTxt.setText('');
}
function isInsideGrid(x, y) {
return x >= 0 && x < gridCols && y >= 0 && y < gridRows;
}
function isGoal(x, y, forPlayer) {
// Top row for player, bottom row for AI
var goalCols = [];
for (var i = Math.floor(gridCols / 2) - 2; i <= Math.floor(gridCols / 2) + 2; i++) {
goalCols.push(i);
}
if (forPlayer && y === 0 && goalCols.indexOf(x) !== -1) return true;
if (!forPlayer && y === gridRows - 1 && goalCols.indexOf(x) !== -1) return true;
return false;
}
function getPawnAt(x, y) {
for (var i = 0; i < playerPawns.length; i++) {
if (playerPawns[i].gridX === x && playerPawns[i].gridY === y) return playerPawns[i];
}
for (var i = 0; i < aiPawns.length; i++) {
if (aiPawns[i].gridX === x && aiPawns[i].gridY === y) return aiPawns[i];
}
return null;
}
function getWallAt(x, y) {
for (var i = 0; i < walls.length; i++) {
var wall = walls[i];
for (var j = 0; j < wall.cells.length; j++) {
if (wall.cells[j].x === x && wall.cells[j].y === y) return wall;
}
}
return null;
}
function cellBlocked(x, y) {
if (!isInsideGrid(x, y)) return true;
var occ = grid[y][x].occupied;
return occ === 'player' || occ === 'ai' || occ === 'wall';
}
function cellFree(x, y) {
if (!isInsideGrid(x, y)) return false;
var occ = grid[y][x].occupied;
return occ === null || occ === 'ball';
}
function knightMoves(x, y) {
var moves = [{
dx: 1,
dy: 3
}, {
dx: 3,
dy: 1
}, {
dx: -1,
dy: 3
}, {
dx: -3,
dy: 1
}, {
dx: 1,
dy: -3
}, {
dx: 3,
dy: -1
}, {
dx: -1,
dy: -3
}, {
dx: -3,
dy: -1
},
// Extra moves (as requested)
{
dx: 2,
dy: 1
}, {
dx: -1,
dy: 1
}, {
dx: -2,
dy: -1
}, {
dx: 1,
dy: -1
}];
var res = [];
for (var i = 0; i < moves.length; i++) {
var nx = x + moves[i].dx;
var ny = y + moves[i].dy;
if (isInsideGrid(nx, ny) && cellFree(nx, ny)) {
res.push({
x: nx,
y: ny
});
}
}
return res;
}
function straightLineShots(x, y) {
// Up, Down, Left, Right
var dirs = [{
dx: 0,
dy: -1
}, {
dx: 0,
dy: 1
}, {
dx: -1,
dy: 0
}, {
dx: 1,
dy: 0
}];
var shots = [];
var maxShotDistance = 7;
for (var d = 0; d < dirs.length; d++) {
var nx = x,
ny = y;
var distance = 0;
while (distance < maxShotDistance) {
// limit to 7 blocks
nx += dirs[d].dx;
ny += dirs[d].dy;
distance++;
if (!isInsideGrid(nx, ny)) break;
if (cellBlocked(nx, ny)) break;
shots.push({
x: nx,
y: ny,
dir: dirs[d]
});
if (isGoal(nx, ny, turn === 'player')) break;
}
}
return shots;
}
function highlightCells(cells, color) {
for (var i = 0; i < cells.length; i++) {
var hl = LK.getAsset('target_indicator', {
width: cellSize - 10,
height: cellSize - 10,
anchorX: 0.5,
anchorY: 0.5,
x: gridOriginX + cells[i].x * cellSize + cellSize / 2,
y: gridOriginY + cells[i].y * cellSize + cellSize / 2,
alpha: 0.7
});
game.addChild(hl);
highlightOverlays.push(hl);
}
}
function clearHighlights() {
for (var i = 0; i < highlightOverlays.length; i++) {
highlightOverlays[i].destroy();
}
highlightOverlays = [];
}
var highlightOverlays = [];
// --- Wall Generation (Tetris shapes) ---
var tetrisShapes = [
// I
[{
x: 0,
y: 0
}, {
x: 0,
y: 1
}, {
x: 0,
y: 2
}, {
x: 0,
y: 3
}],
// O
[{
x: 0,
y: 0
}, {
x: 1,
y: 0
}, {
x: 0,
y: 1
}, {
x: 1,
y: 1
}],
// T
[{
x: 0,
y: 0
}, {
x: -1,
y: 1
}, {
x: 0,
y: 1
}, {
x: 1,
y: 1
}],
// L
[{
x: 0,
y: 0
}, {
x: 0,
y: 1
}, {
x: 0,
y: 2
}, {
x: 1,
y: 2
}],
// S
[{
x: 0,
y: 0
}, {
x: 1,
y: 0
}, {
x: 0,
y: 1
}, {
x: -1,
y: 1
}]];
function randomWallShape() {
var idx = Math.floor(Math.random() * tetrisShapes.length);
return tetrisShapes[idx];
}
function canPlaceWallAt(shape, ox, oy) {
for (var i = 0; i < shape.length; i++) {
var x = ox + shape[i].x;
var y = oy + shape[i].y;
if (!isInsideGrid(x, y)) return false;
if (grid[y][x].occupied) return false;
// Don't block ball or pawns
if (ball && ball.gridX === x && ball.gridY === y) return false;
if (getPawnAt(x, y)) return false;
}
return true;
}
function placeRandomWall() {
// Try up to 10 times to find a spot
for (var tries = 0; tries < 10; tries++) {
var shape = randomWallShape();
var ox = Math.floor(Math.random() * (gridCols - 2)) + 1;
var oy = Math.floor(Math.random() * (gridRows - 2)) + 1;
if (canPlaceWallAt(shape, ox, oy)) {
var cells = [];
for (var i = 0; i < shape.length; i++) {
var x = ox + shape[i].x;
var y = oy + shape[i].y;
cells.push({
x: x,
y: y
});
}
var wall = new Wall();
wall.init(cells, wallIdCounter);
wall.wallId = wallIdCounter;
wallIdCounter++;
walls.push(wall);
gameElements.push(wall);
game.addChild(wall);
for (var i = 0; i < cells.length; i++) {
grid[cells[i].y][cells[i].x].occupied = 'wall';
grid[cells[i].y][cells[i].x].wallId = wall.wallId;
}
break;
}
}
}
// --- Turn Logic ---
function startPlayerTurn() {
turn = 'player';
setInfo("Your turn: Tap a pawn to move or shoot");
clearHighlights();
selectedPawn = null;
validMoves = [];
validShots = [];
gameLocked = false;
}
function startAITurn() {
turn = 'ai';
setInfo("AI's turn...");
clearHighlights();
selectedPawn = null;
validMoves = [];
validShots = [];
gameLocked = true;
LK.setTimeout(aiTakeTurn, 700);
}
function endTurn() {
// After each turn, maybe spawn a wall
if (Math.random() < 0.05) {
placeRandomWall();
}
if (turn === 'player') {
startAITurn();
} else {
startPlayerTurn();
}
}
// --- Ball Movement and Goal Check ---
function moveBallTo(x, y, onFinish) {
// Remove old ball from grid
grid[ball.gridY][ball.gridX].occupied = null;
ball.setGridPos(x, y);
grid[y][x].occupied = 'ball';
ball.flash();
if (onFinish) LK.setTimeout(onFinish, 200);
}
function shootBall(fromX, fromY, toX, toY, dir, onFinish) {
// Animate ball along the path, but limit to 7 blocks max
var path = [];
var nx = fromX,
ny = fromY;
var maxShotDistance = 7;
var distance = 0;
while (distance < maxShotDistance) {
nx += dir.dx;
ny += dir.dy;
distance++;
if (!isInsideGrid(nx, ny)) break;
if (cellBlocked(nx, ny)) break;
path.push({
x: nx,
y: ny
});
if (isGoal(nx, ny, turn === 'player')) break;
if (isGoal(nx, ny, turn === 'ai')) break;
}
function animateStep(idx) {
if (idx >= path.length) {
if (onFinish) onFinish();
return;
}
moveBallTo(path[idx].x, path[idx].y, function () {
animateStep(idx + 1);
});
}
animateStep(0);
}
// --- Player Input Handling ---
game.down = function (x, y, obj) {
if (gameLocked) return;
// Convert to grid coordinates
var gx = Math.floor((x - gridOriginX) / cellSize);
var gy = Math.floor((y - gridOriginY) / cellSize);
if (!isInsideGrid(gx, gy)) return;
// If no pawn selected, select a pawn
if (!selectedPawn) {
for (var i = 0; i < playerPawns.length; i++) {
var pawn = playerPawns[i];
if (pawn.gridX === gx && pawn.gridY === gy) {
selectedPawn = pawn;
pawn.flash();
// Show knight moves
validMoves = knightMoves(gx, gy);
highlightCells(validMoves, 0x00ff00);
// If pawn is adjacent to ball, allow to pick up
if (Math.abs(ball.gridX - gx) + Math.abs(ball.gridY - gy) === 1) {
validMoves.push({
x: ball.gridX,
y: ball.gridY
});
}
// If pawn is on ball, allow to shoot
if (gx === ball.gridX && gy === ball.gridY) {
validShots = straightLineShots(gx, gy);
highlightCells(validShots, 0xffa500);
}
return;
}
}
} else {
// If clicked on a valid move, move pawn
for (var i = 0; i < validMoves.length; i++) {
if (validMoves[i].x === gx && validMoves[i].y === gy) {
// Move pawn
grid[selectedPawn.gridY][selectedPawn.gridX].occupied = null;
selectedPawn.setGridPos(gx, gy);
grid[gy][gx].occupied = 'player';
// If moved onto ball, pick up
if (gx === ball.gridX && gy === ball.gridY) {
selectedPawn.hasBall = true;
setInfo("You have the ball! Move again or tap a direction to shoot.");
// Allow dribbling: highlight knight moves and shots again
validMoves = knightMoves(gx, gy);
validShots = straightLineShots(gx, gy);
clearHighlights();
highlightCells(validMoves, 0x00ff00);
highlightCells(validShots, 0xffa500);
return;
}
clearHighlights();
endTurn();
return;
}
}
// If pawn has ball and clicked on valid shot, shoot
if (selectedPawn.hasBall) {
// Check for shot
for (var i = 0; i < validShots.length; i++) {
if (validShots[i].x === gx && validShots[i].y === gy) {
selectedPawn.hasBall = false;
clearHighlights();
shootBall(selectedPawn.gridX, selectedPawn.gridY, gx, gy, validShots[i].dir, function () {
// Check for goal
if (isGoal(gx, gy, true)) {
playerScore++;
updateScore();
setInfo("GOAL! You scored!");
// Beautiful goal effect: flash screen, ball animation, and shake goal
LK.effects.flashScreen(0x00ff00, 400);
// Animate ball scaling up and fading out for a goal "pop"
tween.to(ball, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, 350, {
easing: 'easeOutCubic'
});
// Animate bottom goal shaking
tween.to(goalBotImg, {
x: goalBotImg.x + 20
}, 60, {
yoyo: true,
repeat: 3
});
tween.to(goalBotImg, {
x: goalBotImg.x - 20
}, 60, {
yoyo: true,
repeat: 3
});
if (playerScore >= goalToWin) {
LK.setTimeout(function () {
LK.showYouWin();
}, 600);
return;
}
LK.setTimeout(function () {
resetAfterGoal(false);
}, 1200);
} else {
endTurn();
}
});
return;
}
}
// Check for dribble move (knight move with ball)
for (var i = 0; i < validMoves.length; i++) {
if (validMoves[i].x === gx && validMoves[i].y === gy) {
// Move pawn and ball together
grid[selectedPawn.gridY][selectedPawn.gridX].occupied = null;
selectedPawn.setGridPos(gx, gy);
grid[gy][gx].occupied = 'player';
moveBallTo(gx, gy);
// Allow shoot after dribble
setInfo("Tap a direction to shoot!");
validShots = straightLineShots(gx, gy);
validMoves = []; // Only allow shoot after dribble, not another move
clearHighlights();
highlightCells(validShots, 0xffa500);
return;
}
}
}
// Deselect if clicked elsewhere
clearHighlights();
selectedPawn = null;
validMoves = [];
validShots = [];
}
};
// --- AI Logic ---
function aiTakeTurn() {
// --- AI STRATEGY: All 3 pawns act, with passing, shooting, and blocking ---
// 1. Try to shoot if any pawn is on the ball
// 2. Try to pass if a pawn can reach the ball and another can shoot
// 3. Otherwise, move pawns to block or approach ball
// Helper: Find all possible moves for a pawn
function pawnMoves(pawn) {
return knightMoves(pawn.gridX, pawn.gridY);
}
// 1. Try to shoot if any pawn is on the ball
for (var i = 0; i < aiPawns.length; i++) {
var pawn = aiPawns[i];
if (pawn.gridX === ball.gridX && pawn.gridY === ball.gridY) {
var shots = straightLineShots(pawn.gridX, pawn.gridY);
// Prefer shooting downwards (towards player goal)
var bestShot = null;
for (var j = 0; j < shots.length; j++) {
if (shots[j].dir.dy > 0) {
bestShot = shots[j];
break;
}
}
if (!bestShot && shots.length > 0) bestShot = shots[0];
if (bestShot) {
shootBall(pawn.gridX, pawn.gridY, bestShot.x, bestShot.y, bestShot.dir, function () {
if (isGoal(bestShot.x, bestShot.y, false)) {
aiScore++;
updateScore();
setInfo("AI scored!");
LK.effects.flashScreen(0xff0000, 400);
tween.to(ball, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, 350, {
easing: 'easeOutCubic'
});
tween.to(goalTopImg, {
x: goalTopImg.x + 20
}, 60, {
yoyo: true,
repeat: 3
});
tween.to(goalTopImg, {
x: goalTopImg.x - 20
}, 60, {
yoyo: true,
repeat: 3
});
if (aiScore >= goalToWin) {
LK.setTimeout(function () {
LK.showGameOver();
}, 600);
return;
}
LK.setTimeout(function () {
resetAfterGoal(true);
}, 1200);
} else {
endTurn();
}
});
return;
}
}
}
// 2. Try to pass: If a pawn can move onto the ball, and another pawn can shoot from there
var passFound = false;
for (var i = 0; i < aiPawns.length; i++) {
var mover = aiPawns[i];
var moves = pawnMoves(mover);
for (var j = 0; j < moves.length; j++) {
if (moves[j].x === ball.gridX && moves[j].y === ball.gridY) {
// Simulate: If this pawn moves onto ball, can another pawn shoot next turn?
for (var k = 0; k < aiPawns.length; k++) {
if (k === i) continue;
var shooter = aiPawns[k];
// Can shooter reach the new ball position in one move?
var shooterMoves = knightMoves(shooter.gridX, shooter.gridY);
for (var m = 0; m < shooterMoves.length; m++) {
if (shooterMoves[m].x === moves[j].x && shooterMoves[m].y === moves[j].y) {
// Simulate shooter on ball, can shoot?
var shots = straightLineShots(moves[j].x, moves[j].y);
var bestShot = null;
for (var n = 0; n < shots.length; n++) {
if (shots[n].dir.dy > 0) {
bestShot = shots[n];
break;
}
}
if (!bestShot && shots.length > 0) bestShot = shots[0];
if (bestShot) {
// Move mover onto ball
grid[mover.gridY][mover.gridX].occupied = null;
mover.setGridPos(moves[j].x, moves[j].y);
grid[moves[j].y][moves[j].x].occupied = 'ai';
mover.hasBall = true;
// Next turn, shooter will shoot
passFound = true;
endTurn();
return;
}
}
}
}
}
}
}
// 3. Otherwise, move all pawns: one towards ball, others block or approach
// Find the pawn closest to the ball
var bestPawn = null;
var minDist = 9999;
for (var i = 0; i < aiPawns.length; i++) {
var pawn = aiPawns[i];
var dist = Math.abs(pawn.gridX - ball.gridX) + Math.abs(pawn.gridY - ball.gridY);
if (dist < minDist) {
minDist = dist;
bestPawn = pawn;
}
}
// Move bestPawn towards ball
if (bestPawn) {
var moves = knightMoves(bestPawn.gridX, bestPawn.gridY);
var bestMove = null;
minDist = 9999;
for (var i = 0; i < moves.length; i++) {
var dist = Math.abs(moves[i].x - ball.gridX) + Math.abs(moves[i].y - ball.gridY);
if (dist < minDist) {
minDist = dist;
bestMove = moves[i];
}
}
if (bestMove) {
grid[bestPawn.gridY][bestPawn.gridX].occupied = null;
bestPawn.setGridPos(bestMove.x, bestMove.y);
grid[bestMove.y][bestMove.x].occupied = 'ai';
// If moved onto ball, pick up and shoot next turn
if (bestMove.x === ball.gridX && bestMove.y === ball.gridY) {
bestPawn.hasBall = true;
}
}
}
// Move other pawns to block or approach player pawns
for (var i = 0; i < aiPawns.length; i++) {
var pawn = aiPawns[i];
if (pawn === bestPawn) continue;
// Try to block: move towards the player pawn closest to the ball
var closestPlayer = null;
var minPlayerDist = 9999;
for (var j = 0; j < playerPawns.length; j++) {
var pdist = Math.abs(playerPawns[j].gridX - ball.gridX) + Math.abs(playerPawns[j].gridY - ball.gridY);
if (pdist < minPlayerDist) {
minPlayerDist = pdist;
closestPlayer = playerPawns[j];
}
}
if (closestPlayer) {
var moves = knightMoves(pawn.gridX, pawn.gridY);
var bestBlock = null;
var minBlockDist = 9999;
for (var k = 0; k < moves.length; k++) {
var dist = Math.abs(moves[k].x - closestPlayer.gridX) + Math.abs(moves[k].y - closestPlayer.gridY);
if (dist < minBlockDist && cellFree(moves[k].x, moves[k].y)) {
minBlockDist = dist;
bestBlock = moves[k];
}
}
if (bestBlock) {
grid[pawn.gridY][pawn.gridX].occupied = null;
pawn.setGridPos(bestBlock.x, bestBlock.y);
grid[bestBlock.y][bestBlock.x].occupied = 'ai';
}
}
}
endTurn();
}
// --- Reset After Goal ---
function resetAfterGoal(aiScored) {
// Remove all walls
for (var i = 0; i < walls.length; i++) {
walls[i].destroy();
}
walls = [];
gameElements = [];
// Clear grid
for (var y = 0; y < gridRows; y++) {
for (var x = 0; x < gridCols; x++) {
grid[y][x].occupied = null;
grid[y][x].wallId = null;
}
}
// Reset pawns
for (var i = 0; i < playerPawns.length; i++) {
playerPawns[i].destroy();
}
for (var i = 0; i < aiPawns.length; i++) {
aiPawns[i].destroy();
}
playerPawns = [];
aiPawns = [];
placePawns();
// Reset ball at center and show it for a moment before resuming play
placeBall(true);
clearHighlights();
selectedPawn = null;
validMoves = [];
validShots = [];
gameLocked = false;
setInfo('');
// Wait 900ms before resuming play, so the ball is visible at center after a goal
LK.setTimeout(function () {
if (aiScored) {
startPlayerTurn();
} else {
startAITurn();
}
}, 900);
}
// --- Game Update (not used for logic, but could be for animations) ---
game.update = function () {
// Check if ball is in the top goal area (AI's goal, y === 0, goalCols)
if (ball && goalCols.indexOf(ball.gridX) !== -1 && ball.gridY === 0) {
// Player scores!
playerScore++;
updateScore();
setInfo("GOAL! You scored!");
LK.effects.flashScreen(0x00ff00, 400);
tween.to(ball, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, 350, {
easing: 'easeOutCubic'
});
tween.to(goalTopImg, {
x: goalTopImg.x + 20
}, 60, {
yoyo: true,
repeat: 3
});
tween.to(goalTopImg, {
x: goalTopImg.x - 20
}, 60, {
yoyo: true,
repeat: 3
});
if (playerScore >= goalToWin) {
LK.setTimeout(function () {
LK.showYouWin();
}, 600);
} else {
LK.setTimeout(function () {
resetAfterGoal(false);
}, 1200);
}
}
// Check if ball is in the bottom goal area (Player's goal, y === gridRows-1, goalCols)
if (ball && goalCols.indexOf(ball.gridX) !== -1 && ball.gridY === gridRows - 1) {
// AI scores!
aiScore++;
updateScore();
setInfo("AI scored!");
LK.effects.flashScreen(0xff0000, 400);
tween.to(ball, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, 350, {
easing: 'easeOutCubic'
});
tween.to(goalBotImg, {
x: goalBotImg.x + 20
}, 60, {
yoyo: true,
repeat: 3
});
tween.to(goalBotImg, {
x: goalBotImg.x - 20
}, 60, {
yoyo: true,
repeat: 3
});
if (aiScore >= goalToWin) {
LK.setTimeout(function () {
LK.showGameOver();
}, 600);
} else {
LK.setTimeout(function () {
resetAfterGoal(true);
}, 1200);
}
}
};
// --- Start Game ---
updateScore();
startPlayerTurn();