User prompt
rakip kale ve bizim kalemizi oluştur. oyuncular kalelere giremez. top kaleye ulaştığında gol olur ve oyun başa döner
User prompt
topu aldıktan sonra herşey duruyor top sürme ve şut mekaniğini düzenle
User prompt
oyuncuya tıklandığında çıkan hedef göstergeleri için yeni texture oluştur
User prompt
oyuncunun gideceği yeri gösterme ye de yeni bi texture ata
User prompt
saha karelerine textur ekleyebilir misin arka plan olacak
User prompt
duvarların texturunu wall olarak değiştirebilir misin
User prompt
saha kareleri için yeni texture oluştur
User prompt
duvarların texturunu wall olarak ayarla
User prompt
topuve tüm oyuncuların görünüşünü %75 büyüt
Code edit (1 edits merged)
Please save this source code
User prompt
Grid Knight Football
Initial prompt
## 1. Saha (Oyun Alanı) ve Koordinat Sistemi - **Saha**, bir ızgara (grid) sistemidir. Örneğin: 21 hücre genişliğinde (X), 31 hücre yüksekliğinde (Y). - Her hücre eşit büyüklüktedir (ör: 75x75 piksel). - Saha, ekranın ortasına yerleştirilir. Her objenin (oyuncu, top, duvar) pozisyonu grid koordinatları ile tutulur: `gridX` ve `gridY`. - Ekrandaki gerçek pozisyonlar, bu grid koordinatları üzerinden hesaplanır. --- ## 2. Oyuncular ve AI - **İki takım** vardır: Bir insan oyuncusu (player) ve bir yapay zeka (AI). - Her takımda birden fazla oyuncu olabilir (ör: 5'er kişi). - Her oyuncunun grid üzerinde bir pozisyonu vardır (`gridX`, `gridY`). - Oyuncular sırayla hamle yapar. Sıra kimdeyse sadece o takım hareket edebilir. --- ## 3. Hareket Sistemi - Oyuncular, satrançtaki "at" gibi (L şeklinde) hareket eder. Yani bir hamlede 8 farklı pozisyona gidebilirler. - Hareket edilecek pozisyonun: - Saha sınırları içinde olması, - Başka bir oyuncu veya duvar tarafından işgal edilmemesi gerekir. - Hareketler, dokunmatik/mouse ile seçilen oyuncu üzerinden ve sadece geçerli hamleler için gösterilir. --- ## 4. Top ve Şut Mekaniği - Top, gridde bir hücrede bulunur ve bir oyuncunun pozisyonunda olabilir. - Oyuncu topa sahipken, belirli bir yönde (ör: yukarıya veya aşağıya) "şut" atabilir. - Şut, topu 5 hücreye kadar düz bir çizgide ilerletir. Ancak yol üzerinde engel (duvar veya başka oyuncu) olmamalı. - Şutun geçerli olup olmadığı, matematiksel olarak yolun her hücresinin boş olup olmadığına bakılarak kontrol edilir. --- ## 5. Duvarlar (Engeller) - Duvarlar, Tetris blokları gibi farklı şekillerde (O, I, L, J, S, Z, T) oluşturulur. - Her duvar, gridde 4 hücreyi kaplar. - Duvarlar rastgele bir pozisyonda ve rastgele bir şekille oluşturulur. Ancak: - Kale (goal) hücrelerine, oyuncu/top üstüne veya başka bir duvarın üstüne gelmemesi gerekir. - Duvarlar, hareketi ve şutları engeller. --- ## 6. Oyun Akışı ve Trigger (Tetikleyici) Mantığı - Oyun, sırayla oynanır: Önce oyuncu, sonra AI hamle yapar. - Her hamlede şu kontroller yapılır: - Oyuncu/AI hareket ettiğinde, yeni pozisyonun geçerli olup olmadığı kontrol edilir. - Oyuncu/AI topa sahip olup olmadığı kontrol edilir. - Topun veya oyuncunun kale hücresine ulaşıp ulaşmadığı kontrol edilir (gol olup olmadığı). - Şut atıldığında, topun yolunun boş olup olmadığı kontrol edilir. - Bir oyuncu/AI gol atarsa, skor güncellenir ve pozisyonlar sıfırlanır. - Her hamle sonunda, yeni bir duvar eklenip eklenmeyeceğine bakılır. ### Trigger (Tetikleyici) Yapıları - **Bir pozisyona ulaşınca tetikleme:** Her objenin önceki (`lastX`, `lastY`) ve şimdiki (`x`, `y`) pozisyonu tutulur. Örneğin: ```js if (self.lastY <= GOAL_Y && self.y > GOAL_Y) { // Gol çizgisine ulaşıldı } ``` - **Bir objeyle çarpışınca tetikleme:** Her objede `lastWasIntersecting` tutulur. ```js if (self.lastWasIntersecting === false && self.intersects(goal)) { // Gol oldu } ``` - **Bir yolun boş olup olmadığını kontrol etme:** Şut atarken, topun gideceği yolun her hücresinde engel olup olmadığına bakılır. --- ## 7. Matematiksel ve Mantıksal İşlemler - **Grid sınırları:** `x >= 0 && x < GRID_W && y >= 0 && y < GRID_H` - **Bir hücrenin dolu olup olmadığını kontrol etme:** - Oyuncular, AI'lar ve duvarlar için ayrı ayrı kontrol edilir. - **Hareket seçenekleri:** - At (knight) hareketleri için 8 olası hamle hesaplanır ve geçerli olanlar filtrelenir. - **Şut yolu:** - Topun gideceği yol, bir dizi hücre olarak hesaplanır ve her biri için engel kontrolü yapılır. --- ## 8. Oyun Sonu ve Reset - Bir takım belirlenen sayıda gol atınca oyun biter. - Oyun motoru (LK), oyunu otomatik olarak resetler ve tüm değişkenler baştan oluşturulur. --- ## 9. Özet - Her şey grid tabanlıdır. - Oyuncular ve AI sırayla oynar. - Hareketler ve şutlar, matematiksel olarak geçerli olup olmadığına göre tetiklenir. - Duvarlar, hareket ve şutları engeller. - Oyun akışı, trigger mantığı ve kontrollerle yönetilir.
/**** * 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();