/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ var Cell = Container.expand(function (row, col) { var self = Container.call(this); self.row = row; self.col = col; self.value = 0; // 0 = empty, 1 = X (blue), 2 = O (red) var cellBg = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5, alpha: 0.01 // Make almost invisible but still interactive }); // Make the entire cell area interactive self.interactive = true; self.hitArea = new Rectangle(-290, -290, 580, 580); self.marker = null; // Add hover effect to show clickable area self.move = function (x, y, obj) { if (!gameReady || gameOver || self.value !== 0 || gameMode === 'menu') return; if (isAIMode && currentPlayer !== 1) return; cellBg.alpha = 0.2; // Show cell is hoverable }; // Reset hover effect when mouse leaves self.up = function (x, y, obj) { if (self.value === 0) { cellBg.alpha = 0.01; // Reset to almost invisible } }; self.placeMarker = function (type, callback) { if (self.value !== 0) return false; // Prevent placing marker if already has one or is animating if (self.marker) return false; // In AI mode, extra validation if (isAIMode && type === 1 && aiProcessing) return false; if (isAIMode && type === 2 && !aiProcessing) return false; self.value = type; if (type === 1) { // Blue X self.marker = self.attachAsset('xMarker', { anchorX: 0.5, anchorY: 0.5, alpha: 0, scaleX: 0.1, scaleY: 0.1 }); tween(self.marker, { alpha: 0.9, scaleX: 1.0, scaleY: 1.0 }, { duration: 300, easing: tween.easeOut, onFinish: callback }); } else { // Red O self.marker = self.attachAsset('oMarker', { anchorX: 0.5, anchorY: 0.5, alpha: 0, scaleX: 0.1, scaleY: 0.1 }); tween(self.marker, { alpha: 0.9, scaleX: 1.0, scaleY: 1.0 }, { duration: 300, easing: tween.easeOut, onFinish: callback }); } LK.getSound('place').play(); return true; }; self.down = function (x, y, obj) { // Basic validation checks if (!gameReady || gameOver || gameMode === 'menu') return; if (self.value !== 0) return; // Cell already occupied // Prevent placing markers when game board is not visible if (gameBoard.alpha === 0) return; // In AI mode, prevent ALL player input during AI processing or when it's not player's turn if (isAIMode && (currentPlayer !== 1 || aiProcessing)) return; // Additional safety check to prevent race conditions if (isAIMode && currentPlayer === 2) return; // Extra validation: ensure no other animations are running if (isAIMode && aiProcessing) { console.log("Player input blocked - AI is processing"); return; } // Check if any cell is currently animating to prevent interference for (var r = 0; r < 3; r++) { for (var c = 0; c < 3; c++) { var checkCell = grid[r][c]; if (checkCell && checkCell.marker && checkCell.marker.alpha < 0.9 && checkCell.marker.alpha > 0) { return; // Animation in progress, prevent input } } } // Place the marker with animation and wait for completion if (self.placeMarker(currentPlayer, function () { // This callback runs after the marker animation completes checkWin(); if (!gameOver) { if (isAIMode && currentPlayer === 1) { // AI mode - player played, now AI plays currentPlayer = 2; updateTurnDisplay(); // Only start AI processing if not already processing if (!aiProcessing) { aiProcessing = true; LK.setTimeout(function () { if (!gameOver && currentPlayer === 2 && isAIMode) { aiProcessing = false; makeAIMove(); } else { aiProcessing = false; } }, 100); // Small delay to ensure UI updates } } else if (isAIMode && currentPlayer === 2) { // This shouldn't happen in AI mode since AI doesn't click currentPlayer = 1; updateTurnDisplay(); } else { // Two player mode - switch players normally currentPlayer = currentPlayer === 1 ? 2 : 1; updateTurnDisplay(); } } })) { // Marker placement started successfully - disable further input until animation completes } }; return self; }); var WinLine = Container.expand(function () { var self = Container.call(this); self.line = self.attachAsset('winLine', { anchorX: 0.5, anchorY: 0.5, alpha: 0, scaleX: 0 }); self.showWinLine = function (startX, startY, endX, endY, color) { var deltaX = endX - startX; var deltaY = endY - startY; var angle = Math.atan2(deltaY, deltaX); var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); self.x = startX + deltaX / 2; self.y = startY + deltaY / 2; self.line.rotation = angle; self.line.tint = color; self.line.width = distance; tween(self.line, { alpha: 1, scaleX: 1 }, { duration: 500, easing: tween.easeOut }); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x1a1a1a }); /**** * Game Code ****/ var grid = []; var currentPlayer = 1; // 1 = Blue X, 2 = Red O var gameOver = false; var winLine = null; var gameMode = 'menu'; // 'menu', 'ai', 'twoPlayer', 'difficulty' var isAIMode = false; var aiDifficulty = 'normal'; // 'easy', 'normal', 'hard' var aiProcessing = false; // Flag to prevent AI from interfering with player moves var currentLanguage = storage.language || 'tr'; // 'tr' for Turkish, 'en' for English var selectedDifficulty = ''; // Store selected difficulty for confirmation var languageTexts = { tr: { blueTurn: 'Sıra: Mavinin', redTurn: 'Sıra: Kırmızının', blueWin: 'MAVİ TAKIM KAZANDI!', redWin: 'KIRMIZI TAKIM KAZANDI!', draw: 'BERABERE!', playAgain: 'Tekrar Oyna', language: 'Türkçe', vsAI: 'Yapay Zeka ile Oyna', twoPlayer: 'İki Kişilik Oyna', selectMode: 'Oyun Modunu Seçin', back: '← Geri', exit: 'Çıkış', selectDifficulty: 'Zorluk Seçin', easy: 'Kolay', normal: 'Normal', hard: 'Zor', confirmEasy: 'Kolay modu seçmek istiyor musunuz?', confirmNormal: 'Normal modu seçmek istiyor musunuz?', confirmHard: 'Zor modu seçmek istiyor musunuz?', yes: 'Evet', no: 'Hayır' }, en: { blueTurn: 'Turn: Blue', redTurn: 'Turn: Red', blueWin: 'BLUE TEAM WON!', redWin: 'RED TEAM WON!', draw: 'DRAW!', playAgain: 'Play Again', language: 'English', vsAI: 'Play vs AI', twoPlayer: 'Two Player', selectMode: 'Select Game Mode', back: '← Back', exit: 'Exit', selectDifficulty: 'Select Difficulty', easy: 'Easy', normal: 'Normal', hard: 'Hard', confirmEasy: 'Do you want to select Easy mode?', confirmNormal: 'Do you want to select Normal mode?', confirmHard: 'Do you want to select Hard mode?', yes: 'Yes', no: 'No' } }; // Game setup var gameBoard = game.addChild(new Container()); gameBoard.x = 2048 / 2; gameBoard.y = 2732 / 2; gameBoard.scaleX = 1.2; gameBoard.scaleY = 1.2; gameBoard.alpha = 0; // Hidden initially // Create background var background = gameBoard.attachAsset('gridBackground', { anchorX: 0.5, anchorY: 0.5 }); // Create grid lines var verticalLine1 = gameBoard.attachAsset('gridLine', { anchorX: 0.5, anchorY: 0.5, x: -300, height: 1800 }); var verticalLine2 = gameBoard.attachAsset('gridLine', { anchorX: 0.5, anchorY: 0.5, x: 300, height: 1800 }); var horizontalLine1 = gameBoard.attachAsset('gridLine', { anchorX: 0.5, anchorY: 0.5, y: -300, width: 1800, height: 8 }); var horizontalLine2 = gameBoard.attachAsset('gridLine', { anchorX: 0.5, anchorY: 0.5, y: 300, width: 1800, height: 8 }); // Create cells for (var row = 0; row < 3; row++) { grid[row] = []; for (var col = 0; col < 3; col++) { var cell = gameBoard.addChild(new Cell(row, col)); cell.x = (col - 1) * 600; cell.y = (row - 1) * 600; grid[row][col] = cell; } } // UI Elements var blueTurnText = new Text2(languageTexts[currentLanguage].blueTurn, { size: 120, fill: 0x00AAFF }); blueTurnText.anchor.set(0.5, 0); LK.gui.top.addChild(blueTurnText); blueTurnText.y = 150; var redTurnText = new Text2(languageTexts[currentLanguage].redTurn, { size: 120, fill: 0xff0000 }); redTurnText.anchor.set(0.5, 0); LK.gui.top.addChild(redTurnText); redTurnText.y = 150; var languageBtn = new Text2(languageTexts[currentLanguage === 'tr' ? 'en' : 'tr'].language, { size: 60, fill: 0xFFFFFF }); languageBtn.anchor.set(0.5, 0); LK.gui.top.addChild(languageBtn); languageBtn.y = 80; var statusText = new Text2('', { size: 100, fill: 0xFFFFFF }); statusText.anchor.set(0.5, 0.5); LK.gui.center.addChild(statusText); statusText.y = 400; // Create background for play again button var playAgainBg = LK.getAsset('cell', { anchorX: 0.5, anchorY: 0.5, width: 500, height: 120, tint: 0x4CAF50, alpha: 0 }); LK.gui.center.addChild(playAgainBg); playAgainBg.y = 550; var playAgainBtn = new Text2(languageTexts[currentLanguage].playAgain, { size: 90, fill: 0xFFFFFF }); playAgainBtn.anchor.set(0.5, 0.5); playAgainBtn.interactive = false; playAgainBtn.hitArea = new Rectangle(-250, -60, 500, 120); LK.gui.center.addChild(playAgainBtn); playAgainBtn.y = 550; playAgainBtn.alpha = 0; // Menu UI elements var menuTitle = new Text2(languageTexts[currentLanguage].selectMode, { size: 120, fill: 0xFFFFFF }); menuTitle.anchor.set(0.5, 0.5); LK.gui.center.addChild(menuTitle); menuTitle.y = -200; // AI Difficulty selection elements var difficultyTitle = new Text2(languageTexts[currentLanguage].selectDifficulty, { size: 100, fill: 0xFFFFFF }); difficultyTitle.anchor.set(0.5, 0.5); LK.gui.center.addChild(difficultyTitle); difficultyTitle.y = -200; difficultyTitle.alpha = 0; // Create background blocks for difficulty buttons var easyBlock = LK.getAsset('cell', { anchorX: 0.5, anchorY: 0.5, width: 400, height: 120, tint: 0x44FF44, alpha: 0.3 }); LK.gui.center.addChild(easyBlock); easyBlock.y = -100; easyBlock.alpha = 0; var normalBlock = LK.getAsset('cell', { anchorX: 0.5, anchorY: 0.5, width: 400, height: 120, tint: 0xFFAA00, alpha: 0.3 }); LK.gui.center.addChild(normalBlock); normalBlock.y = 50; normalBlock.alpha = 0; var hardBlock = LK.getAsset('cell', { anchorX: 0.5, anchorY: 0.5, width: 400, height: 120, tint: 0xFF4444, alpha: 0.3 }); LK.gui.center.addChild(hardBlock); hardBlock.y = 200; hardBlock.alpha = 0; var easyBtn = new Text2(languageTexts[currentLanguage].easy, { size: 90, fill: 0x44FF44 }); easyBtn.anchor.set(0.5, 0.5); easyBtn.interactive = false; easyBtn.hitArea = new Rectangle(-150, -45, 300, 90); LK.gui.center.addChild(easyBtn); easyBtn.y = -100; easyBtn.alpha = 0; var normalBtn = new Text2(languageTexts[currentLanguage].normal, { size: 90, fill: 0xFFAA00 }); normalBtn.anchor.set(0.5, 0.5); normalBtn.interactive = false; normalBtn.hitArea = new Rectangle(-150, -45, 300, 90); LK.gui.center.addChild(normalBtn); normalBtn.y = 50; normalBtn.alpha = 0; var hardBtn = new Text2(languageTexts[currentLanguage].hard, { size: 90, fill: 0xFF4444 }); hardBtn.anchor.set(0.5, 0.5); hardBtn.interactive = false; hardBtn.hitArea = new Rectangle(-150, -45, 300, 90); LK.gui.center.addChild(hardBtn); hardBtn.y = 200; hardBtn.alpha = 0; // Confirmation dialog elements var confirmationBg = LK.getAsset('gridBackground', { anchorX: 0.5, anchorY: 0.5, width: 1200, height: 600, tint: 0x000000, alpha: 0.8 }); LK.gui.center.addChild(confirmationBg); confirmationBg.alpha = 0; var confirmationText = new Text2('', { size: 80, fill: 0xFFFFFF }); confirmationText.anchor.set(0.5, 0.5); LK.gui.center.addChild(confirmationText); confirmationText.y = -50; confirmationText.alpha = 0; var yesBtn = new Text2(languageTexts[currentLanguage].yes, { size: 80, fill: 0x44FF44 }); yesBtn.anchor.set(0.5, 0.5); yesBtn.interactive = true; LK.gui.center.addChild(yesBtn); yesBtn.x = -150; yesBtn.y = 100; yesBtn.alpha = 0; var noBtn = new Text2(languageTexts[currentLanguage].no, { size: 80, fill: 0xFF4444 }); noBtn.anchor.set(0.5, 0.5); noBtn.interactive = true; LK.gui.center.addChild(noBtn); noBtn.x = 150; noBtn.y = 100; noBtn.alpha = 0; // Add hit areas to confirmation dialog buttons yesBtn.hitArea = new Rectangle(-100, -40, 200, 80); noBtn.hitArea = new Rectangle(-100, -40, 200, 80); // Confirmation dialog button handlers yesBtn.down = function (x, y, obj) { if (selectedDifficulty) { aiDifficulty = selectedDifficulty; hideConfirmationDialog(); startGame('ai'); } }; noBtn.down = function (x, y, obj) { hideConfirmationDialog(); }; // Add glowing animation to blocks function animateDifficultyBlocks() { tween(easyBlock, { alpha: 0.5 }, { duration: 1000, easing: tween.easeInOut, repeat: -1, yoyo: true }); tween(normalBlock, { alpha: 0.5 }, { duration: 1000, easing: tween.easeInOut, repeat: -1, yoyo: true, delay: 333 }); tween(hardBlock, { alpha: 0.5 }, { duration: 1000, easing: tween.easeInOut, repeat: -1, yoyo: true, delay: 666 }); } function showConfirmationDialog(difficulty) { selectedDifficulty = difficulty; gameMode = 'confirmation'; var confirmText = ''; if (difficulty === 'easy') { confirmText = languageTexts[currentLanguage].confirmEasy; } else if (difficulty === 'normal') { confirmText = languageTexts[currentLanguage].confirmNormal; } else if (difficulty === 'hard') { confirmText = languageTexts[currentLanguage].confirmHard; } confirmationText.setText(confirmText); // Enable confirmation button interactions yesBtn.interactive = true; noBtn.interactive = true; // Show confirmation elements tween(confirmationBg, { alpha: 0.8 }, { duration: 200 }); tween(confirmationText, { alpha: 1 }, { duration: 200 }); tween(yesBtn, { alpha: 1 }, { duration: 200 }); tween(noBtn, { alpha: 1 }, { duration: 200 }); } function hideConfirmationDialog() { gameMode = 'difficulty'; selectedDifficulty = ''; // Disable confirmation button interactions yesBtn.interactive = false; noBtn.interactive = false; // Hide confirmation elements tween(confirmationBg, { alpha: 0 }, { duration: 200 }); tween(confirmationText, { alpha: 0 }, { duration: 200 }); tween(yesBtn, { alpha: 0 }, { duration: 200 }); tween(noBtn, { alpha: 0 }, { duration: 200 }); } var vsAIBtn = new Text2(languageTexts[currentLanguage].vsAI, { size: 100, fill: 0x00AAFF }); vsAIBtn.anchor.set(0.5, 0.5); vsAIBtn.interactive = false; vsAIBtn.hitArea = new Rectangle(-200, -50, 400, 100); LK.gui.center.addChild(vsAIBtn); vsAIBtn.y = 0; var twoPlayerBtn = new Text2(languageTexts[currentLanguage].twoPlayer, { size: 100, fill: 0xFF4444 }); twoPlayerBtn.anchor.set(0.5, 0.5); twoPlayerBtn.interactive = false; twoPlayerBtn.hitArea = new Rectangle(-200, -50, 400, 100); LK.gui.center.addChild(twoPlayerBtn); twoPlayerBtn.y = 150; var backBtn = new Text2(languageTexts[currentLanguage].back, { size: 80, fill: 0xFFFFFF }); backBtn.anchor.set(0.5, 0.5); backBtn.interactive = false; backBtn.hitArea = new Rectangle(-150, -40, 300, 80); LK.gui.top.addChild(backBtn); backBtn.x = 250; backBtn.y = 160; backBtn.alpha = 0; var exitBtn = new Text2(languageTexts[currentLanguage].exit, { size: 80, fill: 0xFF0000 }); exitBtn.anchor.set(0.5, 0); exitBtn.interactive = false; exitBtn.hitArea = new Rectangle(-100, -40, 200, 80); LK.gui.top.addChild(exitBtn); exitBtn.y = 80; exitBtn.x = 450; exitBtn.alpha = 0; // Language toggle functionality languageBtn.interactive = true; // Always interactive languageBtn.hitArea = new Rectangle(-100, -40, 200, 80); languageBtn.down = function (x, y, obj) { // Always allow language switching regardless of game state currentLanguage = currentLanguage === 'tr' ? 'en' : 'tr'; storage.language = currentLanguage; updateLanguageTexts(); }; // Menu button handlers vsAIBtn.down = function (x, y, obj) { if (gameMode === 'menu') { showDifficultySelection(); } }; twoPlayerBtn.down = function (x, y, obj) { if (gameMode === 'menu') { startGame('twoPlayer'); } }; // Difficulty button handlers easyBtn.down = function (x, y, obj) { if (gameMode === 'difficulty') { showConfirmationDialog('easy'); } }; normalBtn.down = function (x, y, obj) { if (gameMode === 'difficulty') { showConfirmationDialog('normal'); } }; hardBtn.down = function (x, y, obj) { if (gameMode === 'difficulty') { showConfirmationDialog('hard'); } }; backBtn.down = function (x, y, obj) { if (gameMode === 'confirmation') { hideConfirmationDialog(); } else if (gameMode === 'difficulty') { showMenu(); } else if (gameMode !== 'menu') { resetGame(); showMenu(); } }; exitBtn.down = function (x, y, obj) { if (gameMode !== 'menu') { resetGame(); showMenu(); } }; function updateLanguageTexts() { blueTurnText.setText(languageTexts[currentLanguage].blueTurn); redTurnText.setText(languageTexts[currentLanguage].redTurn); playAgainBtn.setText(languageTexts[currentLanguage].playAgain); languageBtn.setText(languageTexts[currentLanguage === 'tr' ? 'en' : 'tr'].language); menuTitle.setText(languageTexts[currentLanguage].selectMode); vsAIBtn.setText(languageTexts[currentLanguage].vsAI); twoPlayerBtn.setText(languageTexts[currentLanguage].twoPlayer); backBtn.setText(languageTexts[currentLanguage].back); exitBtn.setText(languageTexts[currentLanguage].exit); difficultyTitle.setText(languageTexts[currentLanguage].selectDifficulty); easyBtn.setText(languageTexts[currentLanguage].easy); normalBtn.setText(languageTexts[currentLanguage].normal); hardBtn.setText(languageTexts[currentLanguage].hard); yesBtn.setText(languageTexts[currentLanguage].yes); noBtn.setText(languageTexts[currentLanguage].no); // Update winner/draw text if game is over if (gameOver && statusText.alpha > 0) { var currentText = statusText.text; // Check if current text matches any winner or draw text from either language var isBlueWin = currentText === languageTexts.tr.blueWin || currentText === languageTexts.en.blueWin; var isRedWin = currentText === languageTexts.tr.redWin || currentText === languageTexts.en.redWin; var isDraw = currentText === languageTexts.tr.draw || currentText === languageTexts.en.draw; if (isBlueWin) { statusText.setText(languageTexts[currentLanguage].blueWin); } else if (isRedWin) { statusText.setText(languageTexts[currentLanguage].redWin); } else if (isDraw) { statusText.setText(languageTexts[currentLanguage].draw); } } } // Start in menu mode showMenu(); function updateTurnDisplay() { if (gameMode === 'menu' || gameOver) { blueTurnText.alpha = 0; redTurnText.alpha = 0; } else if (currentPlayer === 1) { blueTurnText.alpha = 1; redTurnText.alpha = 0; } else { blueTurnText.alpha = 0; redTurnText.alpha = 1; } } function showMenu() { gameMode = 'menu'; gameBoard.alpha = 0; menuTitle.alpha = 1; vsAIBtn.alpha = 1; twoPlayerBtn.alpha = 1; // Enable main menu interactions vsAIBtn.interactive = true; twoPlayerBtn.interactive = true; blueTurnText.alpha = 0; redTurnText.alpha = 0; statusText.alpha = 0; playAgainBtn.alpha = 0; playAgainBtn.interactive = false; playAgainBg.alpha = 0; backBtn.alpha = 0; backBtn.interactive = false; exitBtn.alpha = 0; exitBtn.interactive = false; // Hide difficulty selection difficultyTitle.alpha = 0; easyBtn.alpha = 0; normalBtn.alpha = 0; hardBtn.alpha = 0; // Disable difficulty button interactions easyBtn.interactive = false; normalBtn.interactive = false; hardBtn.interactive = false; // Hide blocks // Stop any active animations on difficulty blocks tween.stop(easyBlock); tween.stop(normalBlock); tween.stop(hardBlock); easyBlock.alpha = 0; normalBlock.alpha = 0; hardBlock.alpha = 0; // Hide confirmation dialog confirmationBg.alpha = 0; confirmationText.alpha = 0; yesBtn.alpha = 0; noBtn.alpha = 0; // Disable confirmation dialog interactions yesBtn.interactive = false; noBtn.interactive = false; } function showDifficultySelection() { gameMode = 'difficulty'; // Hide main menu menuTitle.alpha = 0; vsAIBtn.alpha = 0; twoPlayerBtn.alpha = 0; // Disable main menu interactions vsAIBtn.interactive = false; twoPlayerBtn.interactive = false; // Show difficulty selection difficultyTitle.alpha = 1; easyBtn.alpha = 1; normalBtn.alpha = 1; hardBtn.alpha = 1; backBtn.alpha = 1; // Enable difficulty button interactions easyBtn.interactive = true; normalBtn.interactive = true; hardBtn.interactive = true; backBtn.interactive = true; // Show and animate blocks easyBlock.alpha = 0.3; normalBlock.alpha = 0.3; hardBlock.alpha = 0.3; animateDifficultyBlocks(); } function startGame(mode) { gameMode = mode; isAIMode = mode === 'ai'; gameBoard.alpha = 1; menuTitle.alpha = 0; vsAIBtn.alpha = 0; twoPlayerBtn.alpha = 0; // Disable menu interactions vsAIBtn.interactive = false; twoPlayerBtn.interactive = false; // Hide difficulty selection difficultyTitle.alpha = 0; easyBtn.alpha = 0; normalBtn.alpha = 0; hardBtn.alpha = 0; // Disable difficulty button interactions easyBtn.interactive = false; normalBtn.interactive = false; hardBtn.interactive = false; // Hide blocks easyBlock.alpha = 0; normalBlock.alpha = 0; hardBlock.alpha = 0; // Hide confirmation dialog confirmationBg.alpha = 0; confirmationText.alpha = 0; yesBtn.alpha = 0; noBtn.alpha = 0; // Disable confirmation dialog interactions yesBtn.interactive = false; noBtn.interactive = false; // Hide back button in all game modes once game starts backBtn.alpha = 0; backBtn.interactive = false; exitBtn.alpha = 1; // Enable exit button interaction exitBtn.interactive = true; // In AI mode, player (1) always starts first if (isAIMode) { currentPlayer = 1; } else { // In two player mode, random start currentPlayer = Math.random() < 0.5 ? 1 : 2; } updateTurnDisplay(); } function checkWin() { var winner = 0; var winPositions = []; // Check rows for (var row = 0; row < 3; row++) { if (grid[row][0].value !== 0 && grid[row][0].value === grid[row][1].value && grid[row][1].value === grid[row][2].value) { winner = grid[row][0].value; winPositions = [{ row: row, col: 0 }, { row: row, col: 1 }, { row: row, col: 2 }]; break; } } // Check columns if (winner === 0) { for (var col = 0; col < 3; col++) { if (grid[0][col].value !== 0 && grid[0][col].value === grid[1][col].value && grid[1][col].value === grid[2][col].value) { winner = grid[0][col].value; winPositions = [{ row: 0, col: col }, { row: 1, col: col }, { row: 2, col: col }]; break; } } } // Check diagonals if (winner === 0) { if (grid[0][0].value !== 0 && grid[0][0].value === grid[1][1].value && grid[1][1].value === grid[2][2].value) { winner = grid[0][0].value; winPositions = [{ row: 0, col: 0 }, { row: 1, col: 1 }, { row: 2, col: 2 }]; } else if (grid[0][2].value !== 0 && grid[0][2].value === grid[1][1].value && grid[1][1].value === grid[2][0].value) { winner = grid[0][2].value; winPositions = [{ row: 0, col: 2 }, { row: 1, col: 1 }, { row: 2, col: 0 }]; } } if (winner !== 0) { gameOver = true; showWinner(winner, winPositions); return; } // Check for draw var isDraw = true; for (var r = 0; r < 3; r++) { for (var c = 0; c < 3; c++) { if (grid[r][c].value === 0) { isDraw = false; break; } } if (!isDraw) break; } if (isDraw) { gameOver = true; showDraw(); } } function showWinner(winner, positions) { LK.getSound('win').play(); var winnerText = winner === 1 ? languageTexts[currentLanguage].blueWin : languageTexts[currentLanguage].redWin; var winnerColor = 0x000000; // Black color statusText.setText(winnerText); statusText.fill = winnerColor; statusText.alpha = 1; blueTurnText.alpha = 0; redTurnText.alpha = 0; // Show win line winLine = gameBoard.addChild(new WinLine()); var startPos = grid[positions[0].row][positions[0].col]; var endPos = grid[positions[2].row][positions[2].col]; var lineColor = winner === 1 ? 0x00aaff : 0xff4444; winLine.showWinLine(startPos.x, startPos.y, endPos.x, endPos.y, lineColor); // Show play again button with background playAgainBtn.interactive = true; tween(playAgainBg, { alpha: 0.9 }, { duration: 500 }); tween(playAgainBtn, { alpha: 1 }, { duration: 500 }); // Add pulsing animation to make it more prominent tween(playAgainBg, { scaleX: 1.1, scaleY: 1.1 }, { duration: 800, easing: tween.easeInOut, repeat: -1, yoyo: true }); } function showDraw() { statusText.setText(languageTexts[currentLanguage].draw); statusText.fill = 0x000000; // Black color statusText.alpha = 1; blueTurnText.alpha = 0; redTurnText.alpha = 0; // Show play again button with background playAgainBtn.interactive = true; tween(playAgainBg, { alpha: 0.9 }, { duration: 500 }); tween(playAgainBtn, { alpha: 1 }, { duration: 500 }); // Add pulsing animation to make it more prominent tween(playAgainBg, { scaleX: 1.1, scaleY: 1.1 }, { duration: 800, easing: tween.easeInOut, repeat: -1, yoyo: true }); } function makeAIMove() { if (gameOver || currentPlayer !== 2) return; // Extra validation for game state if (!isAIMode || gameMode !== 'ai') return; // Don't proceed if AI is already processing if (aiProcessing) return; // Set AI processing flag to prevent consecutive moves aiProcessing = true; // Add safety timeout to clear flag in case of errors var safetyTimeout = LK.setTimeout(function () { if (aiProcessing && !gameOver) { console.log("AI safety timeout triggered - clearing processing flag"); aiProcessing = false; // Safety clear after 5 seconds } }, 5000); // Variable thinking time based on difficulty var minTime, maxTime; if (aiDifficulty === 'easy') { minTime = 300; maxTime = 800; } else if (aiDifficulty === 'normal') { minTime = 500; maxTime = 1200; } else { // hard minTime = 800; maxTime = 2000; } var thinkingTime = minTime + Math.floor(Math.random() * (maxTime - minTime)); // Use setTimeout to yield frame and prevent blocking LK.setTimeout(function () { // Clear the safety timeout since we're now processing if (safetyTimeout) { LK.clearTimeout(safetyTimeout); safetyTimeout = null; } // Recheck game state after timeout - ensure AI flag is always cleared if (gameOver || currentPlayer !== 2 || !isAIMode || gameMode !== 'ai') { aiProcessing = false; return; } // Additional validation to prevent AI moves during player turn if (isAIMode && currentPlayer === 1) { aiProcessing = false; return; } // Extra safety check - if game state is invalid, clear flag if (!isAIMode || gameMode !== 'ai') { aiProcessing = false; return; } // Find best move using difficulty-specific strategy var bestMove = findBestMove(); var moveSuccess = false; // Validate that we have a valid move and it's still empty if (bestMove && bestMove.value === 0) { // Validate coordinates are within bounds if (bestMove.row >= 0 && bestMove.row < 3 && bestMove.col >= 0 && bestMove.col < 3) { // Double check the cell is still valid before AI places marker var targetCell = grid[bestMove.row][bestMove.col]; if (targetCell && targetCell.value === 0 && !gameOver && currentPlayer === 2 && !targetCell.marker) { if (targetCell.placeMarker(2, function () { // This callback runs after the AI marker animation completes checkWin(); if (!gameOver) { currentPlayer = 1; updateTurnDisplay(); } // Clear AI processing flag after move is complete aiProcessing = false; })) { moveSuccess = true; } } } } // If the best move failed for any reason, find any empty cell as fallback if (!moveSuccess && !gameOver && currentPlayer === 2) { var emptyCells = []; for (var row = 0; row < 3; row++) { for (var col = 0; col < 3; col++) { if (grid[row][col].value === 0 && !grid[row][col].marker) { emptyCells.push(grid[row][col]); } } } // Try to place marker on any empty cell if (emptyCells.length > 0) { var fallbackCell = emptyCells[Math.floor(Math.random() * emptyCells.length)]; if (fallbackCell.placeMarker(2, function () { // This callback runs after the fallback AI marker animation completes checkWin(); if (!gameOver) { currentPlayer = 1; updateTurnDisplay(); } // Clear AI processing flag after move is complete aiProcessing = false; })) { moveSuccess = true; } } } // If no move was made, clear the processing flag if (!moveSuccess) { aiProcessing = false; } }, thinkingTime); } function findBestMove() { var emptyCells = []; var winningMoves = []; var blockingMoves = []; var forkMoves = []; var blockForkMoves = []; var cornerCells = []; var edgeCells = []; var validCells = []; // Collect all valid empty cells for (var row = 0; row < 3; row++) { for (var col = 0; col < 3; col++) { var cell = grid[row][col]; if (cell && cell.value === 0) { emptyCells.push(cell); validCells.push(cell); // Categorize cell positions if ((row === 0 || row === 2) && (col === 0 || col === 2)) { cornerCells.push(cell); } else if (row === 1 && col === 1) { // Center cell - handled separately } else { edgeCells.push(cell); } } } } // Early exit if no valid moves if (validCells.length === 0) { return null; } // Ensure we always have at least one valid cell to return if (emptyCells.length === 0) { return null; } // Easy mode: Make deliberate mistakes and play suboptimally if (aiDifficulty === 'easy') { // 70% chance to make a completely random move if (Math.random() < 0.7) { return validCells[Math.floor(Math.random() * validCells.length)]; } // 20% chance to avoid winning moves (make mistakes) if (Math.random() < 0.2) { var nonWinningMoves = []; for (var i = 0; i < emptyCells.length; i++) { var cell = emptyCells[i]; cell.value = 2; if (!checkWinCondition(2)) { nonWinningMoves.push(cell); } cell.value = 0; } if (nonWinningMoves.length > 0) { return nonWinningMoves[Math.floor(Math.random() * nonWinningMoves.length)]; } } // 10% chance to miss blocking player wins if (Math.random() < 0.1) { var nonBlockingMoves = []; for (var i = 0; i < emptyCells.length; i++) { var cell = emptyCells[i]; cell.value = 1; if (!checkWinCondition(1)) { nonBlockingMoves.push(cell); } cell.value = 0; } if (nonBlockingMoves.length > 0) { return nonBlockingMoves[Math.floor(Math.random() * nonBlockingMoves.length)]; } } // Default: play randomly from all valid moves return validCells[Math.floor(Math.random() * validCells.length)]; } // Normal mode: Balanced play with occasional mistakes if (aiDifficulty === 'normal') { // Priority 1: Check if AI can win immediately (100% success rate for winning moves) for (var i = 0; i < emptyCells.length; i++) { var cell = emptyCells[i]; cell.value = 2; if (checkWinCondition(2)) { cell.value = 0; // Always take winning move in normal mode return cell; } cell.value = 0; } // Priority 2: Block player from winning (98% success rate) for (var i = 0; i < emptyCells.length; i++) { var cell = emptyCells[i]; cell.value = 1; if (checkWinCondition(1)) { cell.value = 0; // Block with 98% probability (2% miss chance for difficulty) if (Math.random() > 0.02) { return cell; } } cell.value = 0; } // Priority 3: Create forks (85% success rate) if (Math.random() < 0.85) { forkMoves = checkForFork(2); if (forkMoves.length > 0) { return forkMoves[0]; } } // Priority 4: Block player forks (80% success rate) if (Math.random() < 0.8) { var playerForkMoves = checkForFork(1); if (playerForkMoves.length > 0) { // When player can create a fork, we need to be smart about blocking // If there's only one fork move, block it if (playerForkMoves.length === 1) { return playerForkMoves[0]; } else { // Multiple fork possibilities - create a threat to force player to defend // Look for a move that creates a winning threat for us for (var i = 0; i < emptyCells.length; i++) { var cell = emptyCells[i]; cell.value = 2; // Check if this creates a winning threat var createsThreats = false; for (var j = 0; j < emptyCells.length; j++) { if (i !== j) { var testCell = emptyCells[j]; testCell.value = 2; if (checkWinCondition(2)) { createsThreats = true; } testCell.value = 0; } } cell.value = 0; // If this move creates a threat and blocks at least one fork path if (createsThreats) { return cell; } } // If no good counter-threat, block one of the fork moves return playerForkMoves[0]; } } } // Priority 5: Basic strategy - prefer center and corners (70% of the time) if (Math.random() < 0.7) { var strategicMoves = []; // Center is most valuable if (grid[1][1].value === 0) { strategicMoves.push(grid[1][1]); strategicMoves.push(grid[1][1]); // Double weight for center } // Corners are second most valuable for (var i = 0; i < cornerCells.length; i++) { strategicMoves.push(cornerCells[i]); } if (strategicMoves.length > 0) { return strategicMoves[Math.floor(Math.random() * strategicMoves.length)]; } } // Priority 6: If no strategic moves, ensure we always return a valid move if (emptyCells.length > 0) { return emptyCells[Math.floor(Math.random() * emptyCells.length)]; } } // Advanced tactic evaluation functions function countWinningLines(row, col, player) { var count = 0; // Check row var rowCount = 0; for (var c = 0; c < 3; c++) { if (grid[row][c].value === player || c === col) { rowCount++; } else if (grid[row][c].value !== 0) { rowCount = -10; // Blocked break; } } if (rowCount === 3) count++; // Check column var colCount = 0; for (var r = 0; r < 3; r++) { if (grid[r][col].value === player || r === row) { colCount++; } else if (grid[r][col].value !== 0) { colCount = -10; // Blocked break; } } if (colCount === 3) count++; // Check diagonals if (row === 0 && col === 0 || row === 1 && col === 1 || row === 2 && col === 2) { var diagCount = 0; for (var i = 0; i < 3; i++) { if (grid[i][i].value === player || i === row && i === col) { diagCount++; } else if (grid[i][i].value !== 0) { diagCount = -10; // Blocked break; } } if (diagCount === 3) count++; } if (row === 0 && col === 2 || row === 1 && col === 1 || row === 2 && col === 0) { var antiDiagCount = 0; for (var i = 0; i < 3; i++) { if (grid[i][2 - i].value === player || i === row && 2 - i === col) { antiDiagCount++; } else if (grid[i][2 - i].value !== 0) { antiDiagCount = -10; // Blocked break; } } if (antiDiagCount === 3) count++; } return count; } // Evaluate move position strength function evaluatePosition(cell) { var score = 0; var row = cell.row; var col = cell.col; // Center is most valuable if (row === 1 && col === 1) score += 4; // Corners are second most valuable else if ((row === 0 || row === 2) && (col === 0 || col === 2)) score += 3; // Edges are least valuable else score += 1; // Check potential winning lines var potentialLines = countWinningLines(row, col, 2); score += potentialLines * 2; return score; } // Advanced position evaluation for hard mode AI function evaluateHardModePosition(cell) { var score = 0; var row = cell.row; var col = cell.col; // Base positional values with strategic weighting if (row === 1 && col === 1) { score += 15; // Center dominance } else if ((row === 0 || row === 2) && (col === 0 || col === 2)) { score += 12; // Corner control } else { score += 8; // Edge positions } // Evaluate line potential (how many lines this position affects) var linesControlled = 0; var lineValues = 0; // Check row control var rowEmpty = 0, rowAI = 0, rowPlayer = 0; for (var c = 0; c < 3; c++) { if (grid[row][c].value === 0) rowEmpty++;else if (grid[row][c].value === 2) rowAI++;else rowPlayer++; } if (rowPlayer === 0) { linesControlled++; lineValues += rowAI * 3 + rowEmpty; } // Check column control var colEmpty = 0, colAI = 0, colPlayer = 0; for (var r = 0; r < 3; r++) { if (grid[r][col].value === 0) colEmpty++;else if (grid[r][col].value === 2) colAI++;else colPlayer++; } if (colPlayer === 0) { linesControlled++; lineValues += colAI * 3 + colEmpty; } // Check main diagonal if (row === col) { var diagEmpty = 0, diagAI = 0, diagPlayer = 0; for (var i = 0; i < 3; i++) { if (grid[i][i].value === 0) diagEmpty++;else if (grid[i][i].value === 2) diagAI++;else diagPlayer++; } if (diagPlayer === 0) { linesControlled++; lineValues += diagAI * 3 + diagEmpty; } } // Check anti-diagonal if (row + col === 2) { var antiDiagEmpty = 0, antiDiagAI = 0, antiDiagPlayer = 0; for (var i = 0; i < 3; i++) { if (grid[i][2 - i].value === 0) antiDiagEmpty++;else if (grid[i][2 - i].value === 2) antiDiagAI++;else antiDiagPlayer++; } if (antiDiagPlayer === 0) { linesControlled++; lineValues += antiDiagAI * 3 + antiDiagEmpty; } } score += linesControlled * 6 + lineValues * 2; // Strategic pattern bonuses // Opposite corner control if (row === 0 && col === 0 && grid[2][2].value === 2 || row === 2 && col === 2 && grid[0][0].value === 2 || row === 0 && col === 2 && grid[2][0].value === 2 || row === 2 && col === 0 && grid[0][2].value === 2) { score += 8; // Diagonal dominance } // Adjacent to AI pieces (connection bonus) var adjacentAI = 0; var checkAdj = [{ r: row - 1, c: col }, { r: row + 1, c: col }, { r: row, c: col - 1 }, { r: row, c: col + 1 }]; for (var i = 0; i < checkAdj.length; i++) { var adj = checkAdj[i]; if (adj.r >= 0 && adj.r < 3 && adj.c >= 0 && adj.c < 3) { if (grid[adj.r][adj.c].value === 2) adjacentAI++; } } score += adjacentAI * 4; return score; } // Check for forks (moves that create two winning threats) function checkForFork(player) { var forkCells = []; for (var i = 0; i < emptyCells.length; i++) { var cell = emptyCells[i]; cell.value = player; var winningLineCount = 0; // Count how many winning moves this creates for (var j = 0; j < emptyCells.length; j++) { if (i !== j) { var testCell = emptyCells[j]; testCell.value = player; if (checkWinCondition(player)) { winningLineCount++; } testCell.value = 0; } } cell.value = 0; if (winningLineCount >= 2) { forkCells.push(cell); } } return forkCells; } // Adjust strategy based on difficulty var aiPersonality = Math.floor(Math.random() * 8); var randomnessFactor = Math.random(); // Difficulty-based mistake chances var winMissChance, blockMissChance, strategyQuality; if (aiDifficulty === 'easy') { // Easy mode doesn't need these values as it plays randomly winMissChance = 1.0; // Always miss winning moves (not that it matters) blockMissChance = 1.0; // Always miss blocking (not that it matters) strategyQuality = 0; // No strategic moves aiPersonality = -1; // No personality, just random } else if (aiDifficulty === 'normal') { winMissChance = 0.02; // 2% chance to miss winning move blockMissChance = 0.05; // 5% chance to miss blocking strategyQuality = 0.6; // Medium quality strategic moves } else { // hard - Perfect play with zero mistakes winMissChance = 0; // NEVER miss winning move blockMissChance = 0; // NEVER miss blocking moves strategyQuality = 1.0; // Perfect strategic moves aiPersonality = Math.floor(Math.random() * 8); // Use all personalities for variety } // Hard mode AI - Perfect play with advanced strategy evaluation if (aiDifficulty === 'hard') { // Priority 1: ALWAYS win if possible (0% miss rate) for (var i = 0; i < emptyCells.length; i++) { var cell = emptyCells[i]; cell.value = 2; if (checkWinCondition(2)) { cell.value = 0; return cell; // IMMEDIATELY take winning move - no randomness } cell.value = 0; } // Priority 2: ALWAYS block player wins (0% miss rate) for (var i = 0; i < emptyCells.length; i++) { var cell = emptyCells[i]; cell.value = 1; if (checkWinCondition(1)) { cell.value = 0; return cell; // IMMEDIATELY block - no randomness } cell.value = 0; } // Priority 3: Create forks (multiple winning threats) forkMoves = checkForFork(2); if (forkMoves.length > 0) { return forkMoves[0]; // Take any fork opportunity } // Priority 4: Block player forks with counter-strategy var playerForkMoves = checkForFork(1); if (playerForkMoves.length > 0) { // If player can create multiple forks, create a counter-threat if (playerForkMoves.length > 1) { // Look for moves that force player to defend while blocking fork for (var i = 0; i < emptyCells.length; i++) { var cell = emptyCells[i]; cell.value = 2; var threatsCreated = 0; for (var j = 0; j < emptyCells.length; j++) { if (i !== j) { var testCell = emptyCells[j]; testCell.value = 2; if (checkWinCondition(2)) { threatsCreated++; } testCell.value = 0; } } cell.value = 0; if (threatsCreated > 0) { return cell; // Counter-threat to force defense } } } // Otherwise block the fork return playerForkMoves[0]; } // Priority 5: Optimal opening strategy if (emptyCells.length === 9) { // First move: always take center or corner if (grid[1][1].value === 0) { return grid[1][1]; // Center is strongest opening } else { return cornerCells[0]; // Fallback to corner } } if (emptyCells.length === 8) { // Second move response if (grid[1][1].value === 1) { // Player took center, take any corner return cornerCells[0]; } else { // Player took corner or edge, take center if (grid[1][1].value === 0) { return grid[1][1]; } } } // Priority 6: Advanced positional strategy with minimax evaluation var bestMove = null; var bestScore = -1000; for (var i = 0; i < emptyCells.length; i++) { var cell = emptyCells[i]; var score = evaluateHardModePosition(cell); // Add advanced tactical bonuses if (cell.row === 1 && cell.col === 1) score += 15; // Center control if ((cell.row === 0 || cell.row === 2) && (cell.col === 0 || cell.col === 2)) score += 12; // Corner control // Evaluate future position strength cell.value = 2; var futureThreats = 0; for (var j = 0; j < emptyCells.length; j++) { if (i !== j) { var testCell = emptyCells[j]; testCell.value = 2; if (checkWinCondition(2)) futureThreats++; testCell.value = 0; } } score += futureThreats * 8; // Bonus for creating future threats cell.value = 0; if (score > bestScore) { bestScore = score; bestMove = cell; } } if (bestMove) { return bestMove; } } // Priority 3: Check for fork opportunities (based on difficulty) if (aiDifficulty !== 'easy' && randomnessFactor < strategyQuality) { forkMoves = checkForFork(2); if (forkMoves.length > 0 && randomnessFactor > 1 - strategyQuality) { return forkMoves[Math.floor(Math.random() * forkMoves.length)]; } } // Priority 4: Block player forks (based on difficulty) if (aiDifficulty !== 'easy' && randomnessFactor < strategyQuality) { blockForkMoves = checkForFork(1); if (blockForkMoves.length > 0 && randomnessFactor > 1 - strategyQuality) { // Force player to defend instead of creating fork return blockForkMoves[Math.floor(Math.random() * blockForkMoves.length)]; } } // Priority 5: Advanced tactical personalities var moveChoices = []; var moveScores = []; if (aiPersonality === 0) { // Perfect player - uses optimal strategy if (emptyCells.length === 9) { // First move: prefer corner moveChoices = cornerCells.slice(); } else if (emptyCells.length === 8) { // Second move response if (grid[1][1].value === 1) { // Player took center, take corner moveChoices = cornerCells.slice(); } else { // Player took corner or edge, take center if (grid[1][1].value === 0) { moveChoices.push(grid[1][1]); } } } else { // Evaluate all positions for (var i = 0; i < emptyCells.length; i++) { var score = evaluatePosition(emptyCells[i]); moveScores.push({ cell: emptyCells[i], score: score }); } moveScores.sort(function (a, b) { return b.score - a.score; }); // Take top scoring moves var topScore = moveScores[0].score; for (var i = 0; i < moveScores.length; i++) { if (moveScores[i].score >= topScore - 1) { moveChoices.push(moveScores[i].cell); } } } } else if (aiPersonality === 1) { // Trap setter - tries to create winning patterns if (grid[1][1].value === 0) { moveChoices.push(grid[1][1]); moveChoices.push(grid[1][1]); // Double weight } // Look for L-shaped patterns if (grid[0][0].value === 2 && grid[2][2].value === 0) { moveChoices.push(grid[2][2]); } if (grid[0][2].value === 2 && grid[2][0].value === 0) { moveChoices.push(grid[2][0]); } // Add corners for trap potential if (cornerCells.length > 0) { moveChoices = moveChoices.concat(cornerCells); } } else if (aiPersonality === 2) { // Defensive player - blocks and controls center if (grid[1][1].value === 0) { moveChoices.push(grid[1][1]); moveChoices.push(grid[1][1]); moveChoices.push(grid[1][1]); // Triple weight for center } // Prefer moves that block multiple lines for (var i = 0; i < emptyCells.length; i++) { var blockCount = countWinningLines(emptyCells[i].row, emptyCells[i].col, 1); if (blockCount >= 2) { moveChoices.push(emptyCells[i]); } } if (moveChoices.length === 0 && cornerCells.length > 0) { moveChoices = cornerCells.slice(); } } else if (aiPersonality === 3) { // Mirror player - plays symmetrically var playerMoves = []; for (var r = 0; r < 3; r++) { for (var c = 0; c < 3; c++) { if (grid[r][c].value === 1) { playerMoves.push({ row: r, col: c }); } } } if (playerMoves.length > 0) { var lastMove = playerMoves[playerMoves.length - 1]; // Try to mirror across center var mirrorRow = 2 - lastMove.row; var mirrorCol = 2 - lastMove.col; if (grid[mirrorRow][mirrorCol].value === 0) { moveChoices.push(grid[mirrorRow][mirrorCol]); moveChoices.push(grid[mirrorRow][mirrorCol]); // Double weight } } // Add strategic positions if (grid[1][1].value === 0) { moveChoices.push(grid[1][1]); } if (cornerCells.length > 0) { moveChoices.push(cornerCells[Math.floor(Math.random() * cornerCells.length)]); } } else if (aiPersonality === 4) { // Edge control player - unusual but effective if (edgeCells.length > 0) { // Prefer edges that connect to existing pieces for (var i = 0; i < edgeCells.length; i++) { var edge = edgeCells[i]; var adjacentAI = 0; // Check adjacent cells var checkPositions = [{ r: edge.row - 1, c: edge.col }, { r: edge.row + 1, c: edge.col }, { r: edge.row, c: edge.col - 1 }, { r: edge.row, c: edge.col + 1 }]; for (var j = 0; j < checkPositions.length; j++) { var pos = checkPositions[j]; if (pos.r >= 0 && pos.r < 3 && pos.c >= 0 && pos.c < 3) { if (grid[pos.r][pos.c].value === 2) { adjacentAI++; } } } if (adjacentAI > 0) { moveChoices.push(edge); moveChoices.push(edge); // Double weight for connected edges } } if (moveChoices.length === 0) { moveChoices = edgeCells.slice(); } } // Sometimes take center if (Math.random() < 0.3 && grid[1][1].value === 0) { moveChoices.push(grid[1][1]); } } else if (aiPersonality === 5) { // Chaos player - creates complex board states var complexityScore = []; for (var i = 0; i < emptyCells.length; i++) { var cell = emptyCells[i]; // Calculate how many lines this move affects var linesAffected = 0; if (true) linesAffected++; // Row if (true) linesAffected++; // Column if (cell.row === cell.col || cell.row + cell.col === 2) { linesAffected++; // Diagonal } complexityScore.push({ cell: cell, complexity: linesAffected }); } // Prefer high complexity moves complexityScore.sort(function (a, b) { return b.complexity - a.complexity; }); for (var i = 0; i < Math.min(3, complexityScore.length); i++) { moveChoices.push(complexityScore[i].cell); } } else if (aiPersonality === 6) { // Pattern player - follows specific winning patterns var patterns = [ // Corner-opposite-corner [{ r: 0, c: 0 }, { r: 2, c: 2 }], [{ r: 0, c: 2 }, { r: 2, c: 0 }], // L-shapes [{ r: 0, c: 0 }, { r: 0, c: 2 }], [{ r: 0, c: 0 }, { r: 2, c: 0 }], [{ r: 2, c: 2 }, { r: 2, c: 0 }], [{ r: 2, c: 2 }, { r: 0, c: 2 }]]; for (var p = 0; p < patterns.length; p++) { var pattern = patterns[p]; var hasFirst = grid[pattern[0].r][pattern[0].c].value === 2; var secondEmpty = grid[pattern[1].r][pattern[1].c].value === 0; if (hasFirst && secondEmpty) { moveChoices.push(grid[pattern[1].r][pattern[1].c]); moveChoices.push(grid[pattern[1].r][pattern[1].c]); // Double weight } } // Default to smart positions if (moveChoices.length === 0) { if (grid[1][1].value === 0) { moveChoices.push(grid[1][1]); } if (cornerCells.length > 0) { moveChoices = moveChoices.concat(cornerCells); } } } else { // Adaptive player - changes strategy based on game state var gamePhase = 9 - emptyCells.length; // 0-8, higher = later if (gamePhase <= 2) { // Early game: control key positions if (grid[1][1].value === 0) { moveChoices.push(grid[1][1]); moveChoices.push(grid[1][1]); } if (cornerCells.length > 0) { moveChoices = moveChoices.concat(cornerCells); } } else if (gamePhase <= 5) { // Mid game: create threats for (var i = 0; i < emptyCells.length; i++) { var threatLevel = countWinningLines(emptyCells[i].row, emptyCells[i].col, 2); if (threatLevel >= 1) { moveChoices.push(emptyCells[i]); if (threatLevel >= 2) { moveChoices.push(emptyCells[i]); // Extra weight for multi-threats } } } } else { // Late game: optimize winning chances for (var i = 0; i < emptyCells.length; i++) { moveChoices.push(emptyCells[i]); } } } // Add calculated randomness if (Math.random() < 0.1) { // 10% chance to add unexpected move if (validCells.length > 0) { var randomCell = validCells[Math.floor(Math.random() * validCells.length)]; moveChoices.push(randomCell); } } // If we have strategic choices, pick from them if (moveChoices.length > 0) { // Sometimes pick the most frequent choice (if duplicates exist) var choiceFrequency = {}; for (var i = 0; i < moveChoices.length; i++) { var key = moveChoices[i].row + ',' + moveChoices[i].col; choiceFrequency[key] = (choiceFrequency[key] || 0) + 1; } // Create weighted selection var weightedChoices = []; for (var i = 0; i < moveChoices.length; i++) { weightedChoices.push(moveChoices[i]); } return weightedChoices[Math.floor(Math.random() * weightedChoices.length)]; } // Easy mode already handled above with helpful logic // Final fallback: intelligent selection var fallbackChoices = []; // Prioritize based on position value (less strategic in easy mode) if (aiDifficulty !== 'easy' || Math.random() < 0.5) { if (grid[1][1].value === 0) { fallbackChoices.push(grid[1][1]); } } if (cornerCells.length > 0 && (aiDifficulty !== 'easy' || Math.random() < 0.6)) { fallbackChoices = fallbackChoices.concat(cornerCells); } if (edgeCells.length > 0 && (aiDifficulty === 'easy' || Math.random() < 0.3)) { fallbackChoices.push(edgeCells[Math.floor(Math.random() * edgeCells.length)]); } if (fallbackChoices.length === 0) { fallbackChoices = validCells; } if (fallbackChoices.length > 0) { return fallbackChoices[Math.floor(Math.random() * fallbackChoices.length)]; } // Ultimate fallback: return first empty cell found for (var row = 0; row < 3; row++) { for (var col = 0; col < 3; col++) { if (grid[row][col].value === 0 && !grid[row][col].marker) { return grid[row][col]; } } } // If still no valid move found, return any empty cell regardless of marker status for (var row = 0; row < 3; row++) { for (var col = 0; col < 3; col++) { if (grid[row][col].value === 0) { return grid[row][col]; } } } return null; } function checkWinCondition(player) { // Check rows for (var row = 0; row < 3; row++) { if (grid[row][0].value === player && grid[row][1].value === player && grid[row][2].value === player) { return true; } } // Check columns for (var col = 0; col < 3; col++) { if (grid[0][col].value === player && grid[1][col].value === player && grid[2][col].value === player) { return true; } } // Check diagonals if (grid[0][0].value === player && grid[1][1].value === player && grid[2][2].value === player) { return true; } if (grid[0][2].value === player && grid[1][1].value === player && grid[2][0].value === player) { return true; } return false; } function resetGame() { gameOver = false; // Immediately clear AI processing to prevent stuck states aiProcessing = false; // Clear any pending AI timeouts LK.clearTimeout(); // Clear any pending timeouts // Stop all active tweens to prevent conflicts tween.stop(); // Force clear any stuck AI state if (isAIMode && currentPlayer === 2) { currentPlayer = 1; // Reset to player's turn } // Start according to game mode if (gameMode === 'ai') { currentPlayer = 1; // Player always starts in AI mode isAIMode = true; } else if (gameMode === 'twoPlayer') { currentPlayer = Math.random() < 0.5 ? 1 : 2; isAIMode = false; } else { // Menu mode currentPlayer = 1; isAIMode = false; } // Clear grid for (var row = 0; row < 3; row++) { for (var col = 0; col < 3; col++) { var cell = grid[row][col]; cell.value = 0; if (cell.marker) { // Stop any active tweens on marker before destroying tween.stop(cell.marker); // Reset marker properties to prevent visual glitches cell.marker.alpha = 0; cell.marker.scaleX = 1; cell.marker.scaleY = 1; cell.marker.tint = 0xffffff; cell.marker.rotation = 0; // Remove from parent before destroying if (cell.marker.parent) { cell.marker.parent.removeChild(cell.marker); } cell.marker.destroy(); cell.marker = null; } } } // Clear UI statusText.setText(''); statusText.alpha = 0; playAgainBtn.alpha = 0; playAgainBg.alpha = 0; // Stop any pulsing animation on play again button tween.stop(playAgainBg); tween.stop(playAgainBtn); playAgainBg.scaleX = 1; playAgainBg.scaleY = 1; playAgainBtn.interactive = false; // Remove win line if (winLine) { // Stop any active tweens on win line to prevent memory leaks tween.stop(winLine.line); tween.stop(winLine); // Stop any tweens on the container too // Reset line properties before destroying winLine.line.alpha = 0; winLine.line.scaleX = 0; winLine.line.tint = 0xffffff; // Reset tint // Remove from parent before destroying if (winLine.parent) { winLine.parent.removeChild(winLine); } winLine.destroy(); winLine = null; } updateTurnDisplay(); } // Play again button handler playAgainBtn.down = function (x, y, obj) { if (gameOver) { resetGame(); } }; // Add small delay to prevent initial freeze when entering game var gameReady = false; LK.setTimeout(function () { gameReady = true; }, 50); // Reduced delay for better responsiveness
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var Cell = Container.expand(function (row, col) {
var self = Container.call(this);
self.row = row;
self.col = col;
self.value = 0; // 0 = empty, 1 = X (blue), 2 = O (red)
var cellBg = self.attachAsset('cell', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.01 // Make almost invisible but still interactive
});
// Make the entire cell area interactive
self.interactive = true;
self.hitArea = new Rectangle(-290, -290, 580, 580);
self.marker = null;
// Add hover effect to show clickable area
self.move = function (x, y, obj) {
if (!gameReady || gameOver || self.value !== 0 || gameMode === 'menu') return;
if (isAIMode && currentPlayer !== 1) return;
cellBg.alpha = 0.2; // Show cell is hoverable
};
// Reset hover effect when mouse leaves
self.up = function (x, y, obj) {
if (self.value === 0) {
cellBg.alpha = 0.01; // Reset to almost invisible
}
};
self.placeMarker = function (type, callback) {
if (self.value !== 0) return false;
// Prevent placing marker if already has one or is animating
if (self.marker) return false;
// In AI mode, extra validation
if (isAIMode && type === 1 && aiProcessing) return false;
if (isAIMode && type === 2 && !aiProcessing) return false;
self.value = type;
if (type === 1) {
// Blue X
self.marker = self.attachAsset('xMarker', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0,
scaleX: 0.1,
scaleY: 0.1
});
tween(self.marker, {
alpha: 0.9,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 300,
easing: tween.easeOut,
onFinish: callback
});
} else {
// Red O
self.marker = self.attachAsset('oMarker', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0,
scaleX: 0.1,
scaleY: 0.1
});
tween(self.marker, {
alpha: 0.9,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 300,
easing: tween.easeOut,
onFinish: callback
});
}
LK.getSound('place').play();
return true;
};
self.down = function (x, y, obj) {
// Basic validation checks
if (!gameReady || gameOver || gameMode === 'menu') return;
if (self.value !== 0) return; // Cell already occupied
// Prevent placing markers when game board is not visible
if (gameBoard.alpha === 0) return;
// In AI mode, prevent ALL player input during AI processing or when it's not player's turn
if (isAIMode && (currentPlayer !== 1 || aiProcessing)) return;
// Additional safety check to prevent race conditions
if (isAIMode && currentPlayer === 2) return;
// Extra validation: ensure no other animations are running
if (isAIMode && aiProcessing) {
console.log("Player input blocked - AI is processing");
return;
}
// Check if any cell is currently animating to prevent interference
for (var r = 0; r < 3; r++) {
for (var c = 0; c < 3; c++) {
var checkCell = grid[r][c];
if (checkCell && checkCell.marker && checkCell.marker.alpha < 0.9 && checkCell.marker.alpha > 0) {
return; // Animation in progress, prevent input
}
}
}
// Place the marker with animation and wait for completion
if (self.placeMarker(currentPlayer, function () {
// This callback runs after the marker animation completes
checkWin();
if (!gameOver) {
if (isAIMode && currentPlayer === 1) {
// AI mode - player played, now AI plays
currentPlayer = 2;
updateTurnDisplay();
// Only start AI processing if not already processing
if (!aiProcessing) {
aiProcessing = true;
LK.setTimeout(function () {
if (!gameOver && currentPlayer === 2 && isAIMode) {
aiProcessing = false;
makeAIMove();
} else {
aiProcessing = false;
}
}, 100); // Small delay to ensure UI updates
}
} else if (isAIMode && currentPlayer === 2) {
// This shouldn't happen in AI mode since AI doesn't click
currentPlayer = 1;
updateTurnDisplay();
} else {
// Two player mode - switch players normally
currentPlayer = currentPlayer === 1 ? 2 : 1;
updateTurnDisplay();
}
}
})) {
// Marker placement started successfully - disable further input until animation completes
}
};
return self;
});
var WinLine = Container.expand(function () {
var self = Container.call(this);
self.line = self.attachAsset('winLine', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0,
scaleX: 0
});
self.showWinLine = function (startX, startY, endX, endY, color) {
var deltaX = endX - startX;
var deltaY = endY - startY;
var angle = Math.atan2(deltaY, deltaX);
var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
self.x = startX + deltaX / 2;
self.y = startY + deltaY / 2;
self.line.rotation = angle;
self.line.tint = color;
self.line.width = distance;
tween(self.line, {
alpha: 1,
scaleX: 1
}, {
duration: 500,
easing: tween.easeOut
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a1a1a
});
/****
* Game Code
****/
var grid = [];
var currentPlayer = 1; // 1 = Blue X, 2 = Red O
var gameOver = false;
var winLine = null;
var gameMode = 'menu'; // 'menu', 'ai', 'twoPlayer', 'difficulty'
var isAIMode = false;
var aiDifficulty = 'normal'; // 'easy', 'normal', 'hard'
var aiProcessing = false; // Flag to prevent AI from interfering with player moves
var currentLanguage = storage.language || 'tr'; // 'tr' for Turkish, 'en' for English
var selectedDifficulty = ''; // Store selected difficulty for confirmation
var languageTexts = {
tr: {
blueTurn: 'Sıra: Mavinin',
redTurn: 'Sıra: Kırmızının',
blueWin: 'MAVİ TAKIM KAZANDI!',
redWin: 'KIRMIZI TAKIM KAZANDI!',
draw: 'BERABERE!',
playAgain: 'Tekrar Oyna',
language: 'Türkçe',
vsAI: 'Yapay Zeka ile Oyna',
twoPlayer: 'İki Kişilik Oyna',
selectMode: 'Oyun Modunu Seçin',
back: '← Geri',
exit: 'Çıkış',
selectDifficulty: 'Zorluk Seçin',
easy: 'Kolay',
normal: 'Normal',
hard: 'Zor',
confirmEasy: 'Kolay modu seçmek istiyor musunuz?',
confirmNormal: 'Normal modu seçmek istiyor musunuz?',
confirmHard: 'Zor modu seçmek istiyor musunuz?',
yes: 'Evet',
no: 'Hayır'
},
en: {
blueTurn: 'Turn: Blue',
redTurn: 'Turn: Red',
blueWin: 'BLUE TEAM WON!',
redWin: 'RED TEAM WON!',
draw: 'DRAW!',
playAgain: 'Play Again',
language: 'English',
vsAI: 'Play vs AI',
twoPlayer: 'Two Player',
selectMode: 'Select Game Mode',
back: '← Back',
exit: 'Exit',
selectDifficulty: 'Select Difficulty',
easy: 'Easy',
normal: 'Normal',
hard: 'Hard',
confirmEasy: 'Do you want to select Easy mode?',
confirmNormal: 'Do you want to select Normal mode?',
confirmHard: 'Do you want to select Hard mode?',
yes: 'Yes',
no: 'No'
}
};
// Game setup
var gameBoard = game.addChild(new Container());
gameBoard.x = 2048 / 2;
gameBoard.y = 2732 / 2;
gameBoard.scaleX = 1.2;
gameBoard.scaleY = 1.2;
gameBoard.alpha = 0; // Hidden initially
// Create background
var background = gameBoard.attachAsset('gridBackground', {
anchorX: 0.5,
anchorY: 0.5
});
// Create grid lines
var verticalLine1 = gameBoard.attachAsset('gridLine', {
anchorX: 0.5,
anchorY: 0.5,
x: -300,
height: 1800
});
var verticalLine2 = gameBoard.attachAsset('gridLine', {
anchorX: 0.5,
anchorY: 0.5,
x: 300,
height: 1800
});
var horizontalLine1 = gameBoard.attachAsset('gridLine', {
anchorX: 0.5,
anchorY: 0.5,
y: -300,
width: 1800,
height: 8
});
var horizontalLine2 = gameBoard.attachAsset('gridLine', {
anchorX: 0.5,
anchorY: 0.5,
y: 300,
width: 1800,
height: 8
});
// Create cells
for (var row = 0; row < 3; row++) {
grid[row] = [];
for (var col = 0; col < 3; col++) {
var cell = gameBoard.addChild(new Cell(row, col));
cell.x = (col - 1) * 600;
cell.y = (row - 1) * 600;
grid[row][col] = cell;
}
}
// UI Elements
var blueTurnText = new Text2(languageTexts[currentLanguage].blueTurn, {
size: 120,
fill: 0x00AAFF
});
blueTurnText.anchor.set(0.5, 0);
LK.gui.top.addChild(blueTurnText);
blueTurnText.y = 150;
var redTurnText = new Text2(languageTexts[currentLanguage].redTurn, {
size: 120,
fill: 0xff0000
});
redTurnText.anchor.set(0.5, 0);
LK.gui.top.addChild(redTurnText);
redTurnText.y = 150;
var languageBtn = new Text2(languageTexts[currentLanguage === 'tr' ? 'en' : 'tr'].language, {
size: 60,
fill: 0xFFFFFF
});
languageBtn.anchor.set(0.5, 0);
LK.gui.top.addChild(languageBtn);
languageBtn.y = 80;
var statusText = new Text2('', {
size: 100,
fill: 0xFFFFFF
});
statusText.anchor.set(0.5, 0.5);
LK.gui.center.addChild(statusText);
statusText.y = 400;
// Create background for play again button
var playAgainBg = LK.getAsset('cell', {
anchorX: 0.5,
anchorY: 0.5,
width: 500,
height: 120,
tint: 0x4CAF50,
alpha: 0
});
LK.gui.center.addChild(playAgainBg);
playAgainBg.y = 550;
var playAgainBtn = new Text2(languageTexts[currentLanguage].playAgain, {
size: 90,
fill: 0xFFFFFF
});
playAgainBtn.anchor.set(0.5, 0.5);
playAgainBtn.interactive = false;
playAgainBtn.hitArea = new Rectangle(-250, -60, 500, 120);
LK.gui.center.addChild(playAgainBtn);
playAgainBtn.y = 550;
playAgainBtn.alpha = 0;
// Menu UI elements
var menuTitle = new Text2(languageTexts[currentLanguage].selectMode, {
size: 120,
fill: 0xFFFFFF
});
menuTitle.anchor.set(0.5, 0.5);
LK.gui.center.addChild(menuTitle);
menuTitle.y = -200;
// AI Difficulty selection elements
var difficultyTitle = new Text2(languageTexts[currentLanguage].selectDifficulty, {
size: 100,
fill: 0xFFFFFF
});
difficultyTitle.anchor.set(0.5, 0.5);
LK.gui.center.addChild(difficultyTitle);
difficultyTitle.y = -200;
difficultyTitle.alpha = 0;
// Create background blocks for difficulty buttons
var easyBlock = LK.getAsset('cell', {
anchorX: 0.5,
anchorY: 0.5,
width: 400,
height: 120,
tint: 0x44FF44,
alpha: 0.3
});
LK.gui.center.addChild(easyBlock);
easyBlock.y = -100;
easyBlock.alpha = 0;
var normalBlock = LK.getAsset('cell', {
anchorX: 0.5,
anchorY: 0.5,
width: 400,
height: 120,
tint: 0xFFAA00,
alpha: 0.3
});
LK.gui.center.addChild(normalBlock);
normalBlock.y = 50;
normalBlock.alpha = 0;
var hardBlock = LK.getAsset('cell', {
anchorX: 0.5,
anchorY: 0.5,
width: 400,
height: 120,
tint: 0xFF4444,
alpha: 0.3
});
LK.gui.center.addChild(hardBlock);
hardBlock.y = 200;
hardBlock.alpha = 0;
var easyBtn = new Text2(languageTexts[currentLanguage].easy, {
size: 90,
fill: 0x44FF44
});
easyBtn.anchor.set(0.5, 0.5);
easyBtn.interactive = false;
easyBtn.hitArea = new Rectangle(-150, -45, 300, 90);
LK.gui.center.addChild(easyBtn);
easyBtn.y = -100;
easyBtn.alpha = 0;
var normalBtn = new Text2(languageTexts[currentLanguage].normal, {
size: 90,
fill: 0xFFAA00
});
normalBtn.anchor.set(0.5, 0.5);
normalBtn.interactive = false;
normalBtn.hitArea = new Rectangle(-150, -45, 300, 90);
LK.gui.center.addChild(normalBtn);
normalBtn.y = 50;
normalBtn.alpha = 0;
var hardBtn = new Text2(languageTexts[currentLanguage].hard, {
size: 90,
fill: 0xFF4444
});
hardBtn.anchor.set(0.5, 0.5);
hardBtn.interactive = false;
hardBtn.hitArea = new Rectangle(-150, -45, 300, 90);
LK.gui.center.addChild(hardBtn);
hardBtn.y = 200;
hardBtn.alpha = 0;
// Confirmation dialog elements
var confirmationBg = LK.getAsset('gridBackground', {
anchorX: 0.5,
anchorY: 0.5,
width: 1200,
height: 600,
tint: 0x000000,
alpha: 0.8
});
LK.gui.center.addChild(confirmationBg);
confirmationBg.alpha = 0;
var confirmationText = new Text2('', {
size: 80,
fill: 0xFFFFFF
});
confirmationText.anchor.set(0.5, 0.5);
LK.gui.center.addChild(confirmationText);
confirmationText.y = -50;
confirmationText.alpha = 0;
var yesBtn = new Text2(languageTexts[currentLanguage].yes, {
size: 80,
fill: 0x44FF44
});
yesBtn.anchor.set(0.5, 0.5);
yesBtn.interactive = true;
LK.gui.center.addChild(yesBtn);
yesBtn.x = -150;
yesBtn.y = 100;
yesBtn.alpha = 0;
var noBtn = new Text2(languageTexts[currentLanguage].no, {
size: 80,
fill: 0xFF4444
});
noBtn.anchor.set(0.5, 0.5);
noBtn.interactive = true;
LK.gui.center.addChild(noBtn);
noBtn.x = 150;
noBtn.y = 100;
noBtn.alpha = 0;
// Add hit areas to confirmation dialog buttons
yesBtn.hitArea = new Rectangle(-100, -40, 200, 80);
noBtn.hitArea = new Rectangle(-100, -40, 200, 80);
// Confirmation dialog button handlers
yesBtn.down = function (x, y, obj) {
if (selectedDifficulty) {
aiDifficulty = selectedDifficulty;
hideConfirmationDialog();
startGame('ai');
}
};
noBtn.down = function (x, y, obj) {
hideConfirmationDialog();
};
// Add glowing animation to blocks
function animateDifficultyBlocks() {
tween(easyBlock, {
alpha: 0.5
}, {
duration: 1000,
easing: tween.easeInOut,
repeat: -1,
yoyo: true
});
tween(normalBlock, {
alpha: 0.5
}, {
duration: 1000,
easing: tween.easeInOut,
repeat: -1,
yoyo: true,
delay: 333
});
tween(hardBlock, {
alpha: 0.5
}, {
duration: 1000,
easing: tween.easeInOut,
repeat: -1,
yoyo: true,
delay: 666
});
}
function showConfirmationDialog(difficulty) {
selectedDifficulty = difficulty;
gameMode = 'confirmation';
var confirmText = '';
if (difficulty === 'easy') {
confirmText = languageTexts[currentLanguage].confirmEasy;
} else if (difficulty === 'normal') {
confirmText = languageTexts[currentLanguage].confirmNormal;
} else if (difficulty === 'hard') {
confirmText = languageTexts[currentLanguage].confirmHard;
}
confirmationText.setText(confirmText);
// Enable confirmation button interactions
yesBtn.interactive = true;
noBtn.interactive = true;
// Show confirmation elements
tween(confirmationBg, {
alpha: 0.8
}, {
duration: 200
});
tween(confirmationText, {
alpha: 1
}, {
duration: 200
});
tween(yesBtn, {
alpha: 1
}, {
duration: 200
});
tween(noBtn, {
alpha: 1
}, {
duration: 200
});
}
function hideConfirmationDialog() {
gameMode = 'difficulty';
selectedDifficulty = '';
// Disable confirmation button interactions
yesBtn.interactive = false;
noBtn.interactive = false;
// Hide confirmation elements
tween(confirmationBg, {
alpha: 0
}, {
duration: 200
});
tween(confirmationText, {
alpha: 0
}, {
duration: 200
});
tween(yesBtn, {
alpha: 0
}, {
duration: 200
});
tween(noBtn, {
alpha: 0
}, {
duration: 200
});
}
var vsAIBtn = new Text2(languageTexts[currentLanguage].vsAI, {
size: 100,
fill: 0x00AAFF
});
vsAIBtn.anchor.set(0.5, 0.5);
vsAIBtn.interactive = false;
vsAIBtn.hitArea = new Rectangle(-200, -50, 400, 100);
LK.gui.center.addChild(vsAIBtn);
vsAIBtn.y = 0;
var twoPlayerBtn = new Text2(languageTexts[currentLanguage].twoPlayer, {
size: 100,
fill: 0xFF4444
});
twoPlayerBtn.anchor.set(0.5, 0.5);
twoPlayerBtn.interactive = false;
twoPlayerBtn.hitArea = new Rectangle(-200, -50, 400, 100);
LK.gui.center.addChild(twoPlayerBtn);
twoPlayerBtn.y = 150;
var backBtn = new Text2(languageTexts[currentLanguage].back, {
size: 80,
fill: 0xFFFFFF
});
backBtn.anchor.set(0.5, 0.5);
backBtn.interactive = false;
backBtn.hitArea = new Rectangle(-150, -40, 300, 80);
LK.gui.top.addChild(backBtn);
backBtn.x = 250;
backBtn.y = 160;
backBtn.alpha = 0;
var exitBtn = new Text2(languageTexts[currentLanguage].exit, {
size: 80,
fill: 0xFF0000
});
exitBtn.anchor.set(0.5, 0);
exitBtn.interactive = false;
exitBtn.hitArea = new Rectangle(-100, -40, 200, 80);
LK.gui.top.addChild(exitBtn);
exitBtn.y = 80;
exitBtn.x = 450;
exitBtn.alpha = 0;
// Language toggle functionality
languageBtn.interactive = true; // Always interactive
languageBtn.hitArea = new Rectangle(-100, -40, 200, 80);
languageBtn.down = function (x, y, obj) {
// Always allow language switching regardless of game state
currentLanguage = currentLanguage === 'tr' ? 'en' : 'tr';
storage.language = currentLanguage;
updateLanguageTexts();
};
// Menu button handlers
vsAIBtn.down = function (x, y, obj) {
if (gameMode === 'menu') {
showDifficultySelection();
}
};
twoPlayerBtn.down = function (x, y, obj) {
if (gameMode === 'menu') {
startGame('twoPlayer');
}
};
// Difficulty button handlers
easyBtn.down = function (x, y, obj) {
if (gameMode === 'difficulty') {
showConfirmationDialog('easy');
}
};
normalBtn.down = function (x, y, obj) {
if (gameMode === 'difficulty') {
showConfirmationDialog('normal');
}
};
hardBtn.down = function (x, y, obj) {
if (gameMode === 'difficulty') {
showConfirmationDialog('hard');
}
};
backBtn.down = function (x, y, obj) {
if (gameMode === 'confirmation') {
hideConfirmationDialog();
} else if (gameMode === 'difficulty') {
showMenu();
} else if (gameMode !== 'menu') {
resetGame();
showMenu();
}
};
exitBtn.down = function (x, y, obj) {
if (gameMode !== 'menu') {
resetGame();
showMenu();
}
};
function updateLanguageTexts() {
blueTurnText.setText(languageTexts[currentLanguage].blueTurn);
redTurnText.setText(languageTexts[currentLanguage].redTurn);
playAgainBtn.setText(languageTexts[currentLanguage].playAgain);
languageBtn.setText(languageTexts[currentLanguage === 'tr' ? 'en' : 'tr'].language);
menuTitle.setText(languageTexts[currentLanguage].selectMode);
vsAIBtn.setText(languageTexts[currentLanguage].vsAI);
twoPlayerBtn.setText(languageTexts[currentLanguage].twoPlayer);
backBtn.setText(languageTexts[currentLanguage].back);
exitBtn.setText(languageTexts[currentLanguage].exit);
difficultyTitle.setText(languageTexts[currentLanguage].selectDifficulty);
easyBtn.setText(languageTexts[currentLanguage].easy);
normalBtn.setText(languageTexts[currentLanguage].normal);
hardBtn.setText(languageTexts[currentLanguage].hard);
yesBtn.setText(languageTexts[currentLanguage].yes);
noBtn.setText(languageTexts[currentLanguage].no);
// Update winner/draw text if game is over
if (gameOver && statusText.alpha > 0) {
var currentText = statusText.text;
// Check if current text matches any winner or draw text from either language
var isBlueWin = currentText === languageTexts.tr.blueWin || currentText === languageTexts.en.blueWin;
var isRedWin = currentText === languageTexts.tr.redWin || currentText === languageTexts.en.redWin;
var isDraw = currentText === languageTexts.tr.draw || currentText === languageTexts.en.draw;
if (isBlueWin) {
statusText.setText(languageTexts[currentLanguage].blueWin);
} else if (isRedWin) {
statusText.setText(languageTexts[currentLanguage].redWin);
} else if (isDraw) {
statusText.setText(languageTexts[currentLanguage].draw);
}
}
}
// Start in menu mode
showMenu();
function updateTurnDisplay() {
if (gameMode === 'menu' || gameOver) {
blueTurnText.alpha = 0;
redTurnText.alpha = 0;
} else if (currentPlayer === 1) {
blueTurnText.alpha = 1;
redTurnText.alpha = 0;
} else {
blueTurnText.alpha = 0;
redTurnText.alpha = 1;
}
}
function showMenu() {
gameMode = 'menu';
gameBoard.alpha = 0;
menuTitle.alpha = 1;
vsAIBtn.alpha = 1;
twoPlayerBtn.alpha = 1;
// Enable main menu interactions
vsAIBtn.interactive = true;
twoPlayerBtn.interactive = true;
blueTurnText.alpha = 0;
redTurnText.alpha = 0;
statusText.alpha = 0;
playAgainBtn.alpha = 0;
playAgainBtn.interactive = false;
playAgainBg.alpha = 0;
backBtn.alpha = 0;
backBtn.interactive = false;
exitBtn.alpha = 0;
exitBtn.interactive = false;
// Hide difficulty selection
difficultyTitle.alpha = 0;
easyBtn.alpha = 0;
normalBtn.alpha = 0;
hardBtn.alpha = 0;
// Disable difficulty button interactions
easyBtn.interactive = false;
normalBtn.interactive = false;
hardBtn.interactive = false;
// Hide blocks
// Stop any active animations on difficulty blocks
tween.stop(easyBlock);
tween.stop(normalBlock);
tween.stop(hardBlock);
easyBlock.alpha = 0;
normalBlock.alpha = 0;
hardBlock.alpha = 0;
// Hide confirmation dialog
confirmationBg.alpha = 0;
confirmationText.alpha = 0;
yesBtn.alpha = 0;
noBtn.alpha = 0;
// Disable confirmation dialog interactions
yesBtn.interactive = false;
noBtn.interactive = false;
}
function showDifficultySelection() {
gameMode = 'difficulty';
// Hide main menu
menuTitle.alpha = 0;
vsAIBtn.alpha = 0;
twoPlayerBtn.alpha = 0;
// Disable main menu interactions
vsAIBtn.interactive = false;
twoPlayerBtn.interactive = false;
// Show difficulty selection
difficultyTitle.alpha = 1;
easyBtn.alpha = 1;
normalBtn.alpha = 1;
hardBtn.alpha = 1;
backBtn.alpha = 1;
// Enable difficulty button interactions
easyBtn.interactive = true;
normalBtn.interactive = true;
hardBtn.interactive = true;
backBtn.interactive = true;
// Show and animate blocks
easyBlock.alpha = 0.3;
normalBlock.alpha = 0.3;
hardBlock.alpha = 0.3;
animateDifficultyBlocks();
}
function startGame(mode) {
gameMode = mode;
isAIMode = mode === 'ai';
gameBoard.alpha = 1;
menuTitle.alpha = 0;
vsAIBtn.alpha = 0;
twoPlayerBtn.alpha = 0;
// Disable menu interactions
vsAIBtn.interactive = false;
twoPlayerBtn.interactive = false;
// Hide difficulty selection
difficultyTitle.alpha = 0;
easyBtn.alpha = 0;
normalBtn.alpha = 0;
hardBtn.alpha = 0;
// Disable difficulty button interactions
easyBtn.interactive = false;
normalBtn.interactive = false;
hardBtn.interactive = false;
// Hide blocks
easyBlock.alpha = 0;
normalBlock.alpha = 0;
hardBlock.alpha = 0;
// Hide confirmation dialog
confirmationBg.alpha = 0;
confirmationText.alpha = 0;
yesBtn.alpha = 0;
noBtn.alpha = 0;
// Disable confirmation dialog interactions
yesBtn.interactive = false;
noBtn.interactive = false;
// Hide back button in all game modes once game starts
backBtn.alpha = 0;
backBtn.interactive = false;
exitBtn.alpha = 1;
// Enable exit button interaction
exitBtn.interactive = true;
// In AI mode, player (1) always starts first
if (isAIMode) {
currentPlayer = 1;
} else {
// In two player mode, random start
currentPlayer = Math.random() < 0.5 ? 1 : 2;
}
updateTurnDisplay();
}
function checkWin() {
var winner = 0;
var winPositions = [];
// Check rows
for (var row = 0; row < 3; row++) {
if (grid[row][0].value !== 0 && grid[row][0].value === grid[row][1].value && grid[row][1].value === grid[row][2].value) {
winner = grid[row][0].value;
winPositions = [{
row: row,
col: 0
}, {
row: row,
col: 1
}, {
row: row,
col: 2
}];
break;
}
}
// Check columns
if (winner === 0) {
for (var col = 0; col < 3; col++) {
if (grid[0][col].value !== 0 && grid[0][col].value === grid[1][col].value && grid[1][col].value === grid[2][col].value) {
winner = grid[0][col].value;
winPositions = [{
row: 0,
col: col
}, {
row: 1,
col: col
}, {
row: 2,
col: col
}];
break;
}
}
}
// Check diagonals
if (winner === 0) {
if (grid[0][0].value !== 0 && grid[0][0].value === grid[1][1].value && grid[1][1].value === grid[2][2].value) {
winner = grid[0][0].value;
winPositions = [{
row: 0,
col: 0
}, {
row: 1,
col: 1
}, {
row: 2,
col: 2
}];
} else if (grid[0][2].value !== 0 && grid[0][2].value === grid[1][1].value && grid[1][1].value === grid[2][0].value) {
winner = grid[0][2].value;
winPositions = [{
row: 0,
col: 2
}, {
row: 1,
col: 1
}, {
row: 2,
col: 0
}];
}
}
if (winner !== 0) {
gameOver = true;
showWinner(winner, winPositions);
return;
}
// Check for draw
var isDraw = true;
for (var r = 0; r < 3; r++) {
for (var c = 0; c < 3; c++) {
if (grid[r][c].value === 0) {
isDraw = false;
break;
}
}
if (!isDraw) break;
}
if (isDraw) {
gameOver = true;
showDraw();
}
}
function showWinner(winner, positions) {
LK.getSound('win').play();
var winnerText = winner === 1 ? languageTexts[currentLanguage].blueWin : languageTexts[currentLanguage].redWin;
var winnerColor = 0x000000; // Black color
statusText.setText(winnerText);
statusText.fill = winnerColor;
statusText.alpha = 1;
blueTurnText.alpha = 0;
redTurnText.alpha = 0;
// Show win line
winLine = gameBoard.addChild(new WinLine());
var startPos = grid[positions[0].row][positions[0].col];
var endPos = grid[positions[2].row][positions[2].col];
var lineColor = winner === 1 ? 0x00aaff : 0xff4444;
winLine.showWinLine(startPos.x, startPos.y, endPos.x, endPos.y, lineColor);
// Show play again button with background
playAgainBtn.interactive = true;
tween(playAgainBg, {
alpha: 0.9
}, {
duration: 500
});
tween(playAgainBtn, {
alpha: 1
}, {
duration: 500
});
// Add pulsing animation to make it more prominent
tween(playAgainBg, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 800,
easing: tween.easeInOut,
repeat: -1,
yoyo: true
});
}
function showDraw() {
statusText.setText(languageTexts[currentLanguage].draw);
statusText.fill = 0x000000; // Black color
statusText.alpha = 1;
blueTurnText.alpha = 0;
redTurnText.alpha = 0;
// Show play again button with background
playAgainBtn.interactive = true;
tween(playAgainBg, {
alpha: 0.9
}, {
duration: 500
});
tween(playAgainBtn, {
alpha: 1
}, {
duration: 500
});
// Add pulsing animation to make it more prominent
tween(playAgainBg, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 800,
easing: tween.easeInOut,
repeat: -1,
yoyo: true
});
}
function makeAIMove() {
if (gameOver || currentPlayer !== 2) return;
// Extra validation for game state
if (!isAIMode || gameMode !== 'ai') return;
// Don't proceed if AI is already processing
if (aiProcessing) return;
// Set AI processing flag to prevent consecutive moves
aiProcessing = true;
// Add safety timeout to clear flag in case of errors
var safetyTimeout = LK.setTimeout(function () {
if (aiProcessing && !gameOver) {
console.log("AI safety timeout triggered - clearing processing flag");
aiProcessing = false; // Safety clear after 5 seconds
}
}, 5000);
// Variable thinking time based on difficulty
var minTime, maxTime;
if (aiDifficulty === 'easy') {
minTime = 300;
maxTime = 800;
} else if (aiDifficulty === 'normal') {
minTime = 500;
maxTime = 1200;
} else {
// hard
minTime = 800;
maxTime = 2000;
}
var thinkingTime = minTime + Math.floor(Math.random() * (maxTime - minTime));
// Use setTimeout to yield frame and prevent blocking
LK.setTimeout(function () {
// Clear the safety timeout since we're now processing
if (safetyTimeout) {
LK.clearTimeout(safetyTimeout);
safetyTimeout = null;
}
// Recheck game state after timeout - ensure AI flag is always cleared
if (gameOver || currentPlayer !== 2 || !isAIMode || gameMode !== 'ai') {
aiProcessing = false;
return;
}
// Additional validation to prevent AI moves during player turn
if (isAIMode && currentPlayer === 1) {
aiProcessing = false;
return;
}
// Extra safety check - if game state is invalid, clear flag
if (!isAIMode || gameMode !== 'ai') {
aiProcessing = false;
return;
}
// Find best move using difficulty-specific strategy
var bestMove = findBestMove();
var moveSuccess = false;
// Validate that we have a valid move and it's still empty
if (bestMove && bestMove.value === 0) {
// Validate coordinates are within bounds
if (bestMove.row >= 0 && bestMove.row < 3 && bestMove.col >= 0 && bestMove.col < 3) {
// Double check the cell is still valid before AI places marker
var targetCell = grid[bestMove.row][bestMove.col];
if (targetCell && targetCell.value === 0 && !gameOver && currentPlayer === 2 && !targetCell.marker) {
if (targetCell.placeMarker(2, function () {
// This callback runs after the AI marker animation completes
checkWin();
if (!gameOver) {
currentPlayer = 1;
updateTurnDisplay();
}
// Clear AI processing flag after move is complete
aiProcessing = false;
})) {
moveSuccess = true;
}
}
}
}
// If the best move failed for any reason, find any empty cell as fallback
if (!moveSuccess && !gameOver && currentPlayer === 2) {
var emptyCells = [];
for (var row = 0; row < 3; row++) {
for (var col = 0; col < 3; col++) {
if (grid[row][col].value === 0 && !grid[row][col].marker) {
emptyCells.push(grid[row][col]);
}
}
}
// Try to place marker on any empty cell
if (emptyCells.length > 0) {
var fallbackCell = emptyCells[Math.floor(Math.random() * emptyCells.length)];
if (fallbackCell.placeMarker(2, function () {
// This callback runs after the fallback AI marker animation completes
checkWin();
if (!gameOver) {
currentPlayer = 1;
updateTurnDisplay();
}
// Clear AI processing flag after move is complete
aiProcessing = false;
})) {
moveSuccess = true;
}
}
}
// If no move was made, clear the processing flag
if (!moveSuccess) {
aiProcessing = false;
}
}, thinkingTime);
}
function findBestMove() {
var emptyCells = [];
var winningMoves = [];
var blockingMoves = [];
var forkMoves = [];
var blockForkMoves = [];
var cornerCells = [];
var edgeCells = [];
var validCells = [];
// Collect all valid empty cells
for (var row = 0; row < 3; row++) {
for (var col = 0; col < 3; col++) {
var cell = grid[row][col];
if (cell && cell.value === 0) {
emptyCells.push(cell);
validCells.push(cell);
// Categorize cell positions
if ((row === 0 || row === 2) && (col === 0 || col === 2)) {
cornerCells.push(cell);
} else if (row === 1 && col === 1) {
// Center cell - handled separately
} else {
edgeCells.push(cell);
}
}
}
}
// Early exit if no valid moves
if (validCells.length === 0) {
return null;
}
// Ensure we always have at least one valid cell to return
if (emptyCells.length === 0) {
return null;
}
// Easy mode: Make deliberate mistakes and play suboptimally
if (aiDifficulty === 'easy') {
// 70% chance to make a completely random move
if (Math.random() < 0.7) {
return validCells[Math.floor(Math.random() * validCells.length)];
}
// 20% chance to avoid winning moves (make mistakes)
if (Math.random() < 0.2) {
var nonWinningMoves = [];
for (var i = 0; i < emptyCells.length; i++) {
var cell = emptyCells[i];
cell.value = 2;
if (!checkWinCondition(2)) {
nonWinningMoves.push(cell);
}
cell.value = 0;
}
if (nonWinningMoves.length > 0) {
return nonWinningMoves[Math.floor(Math.random() * nonWinningMoves.length)];
}
}
// 10% chance to miss blocking player wins
if (Math.random() < 0.1) {
var nonBlockingMoves = [];
for (var i = 0; i < emptyCells.length; i++) {
var cell = emptyCells[i];
cell.value = 1;
if (!checkWinCondition(1)) {
nonBlockingMoves.push(cell);
}
cell.value = 0;
}
if (nonBlockingMoves.length > 0) {
return nonBlockingMoves[Math.floor(Math.random() * nonBlockingMoves.length)];
}
}
// Default: play randomly from all valid moves
return validCells[Math.floor(Math.random() * validCells.length)];
}
// Normal mode: Balanced play with occasional mistakes
if (aiDifficulty === 'normal') {
// Priority 1: Check if AI can win immediately (100% success rate for winning moves)
for (var i = 0; i < emptyCells.length; i++) {
var cell = emptyCells[i];
cell.value = 2;
if (checkWinCondition(2)) {
cell.value = 0;
// Always take winning move in normal mode
return cell;
}
cell.value = 0;
}
// Priority 2: Block player from winning (98% success rate)
for (var i = 0; i < emptyCells.length; i++) {
var cell = emptyCells[i];
cell.value = 1;
if (checkWinCondition(1)) {
cell.value = 0;
// Block with 98% probability (2% miss chance for difficulty)
if (Math.random() > 0.02) {
return cell;
}
}
cell.value = 0;
}
// Priority 3: Create forks (85% success rate)
if (Math.random() < 0.85) {
forkMoves = checkForFork(2);
if (forkMoves.length > 0) {
return forkMoves[0];
}
}
// Priority 4: Block player forks (80% success rate)
if (Math.random() < 0.8) {
var playerForkMoves = checkForFork(1);
if (playerForkMoves.length > 0) {
// When player can create a fork, we need to be smart about blocking
// If there's only one fork move, block it
if (playerForkMoves.length === 1) {
return playerForkMoves[0];
} else {
// Multiple fork possibilities - create a threat to force player to defend
// Look for a move that creates a winning threat for us
for (var i = 0; i < emptyCells.length; i++) {
var cell = emptyCells[i];
cell.value = 2;
// Check if this creates a winning threat
var createsThreats = false;
for (var j = 0; j < emptyCells.length; j++) {
if (i !== j) {
var testCell = emptyCells[j];
testCell.value = 2;
if (checkWinCondition(2)) {
createsThreats = true;
}
testCell.value = 0;
}
}
cell.value = 0;
// If this move creates a threat and blocks at least one fork path
if (createsThreats) {
return cell;
}
}
// If no good counter-threat, block one of the fork moves
return playerForkMoves[0];
}
}
}
// Priority 5: Basic strategy - prefer center and corners (70% of the time)
if (Math.random() < 0.7) {
var strategicMoves = [];
// Center is most valuable
if (grid[1][1].value === 0) {
strategicMoves.push(grid[1][1]);
strategicMoves.push(grid[1][1]); // Double weight for center
}
// Corners are second most valuable
for (var i = 0; i < cornerCells.length; i++) {
strategicMoves.push(cornerCells[i]);
}
if (strategicMoves.length > 0) {
return strategicMoves[Math.floor(Math.random() * strategicMoves.length)];
}
}
// Priority 6: If no strategic moves, ensure we always return a valid move
if (emptyCells.length > 0) {
return emptyCells[Math.floor(Math.random() * emptyCells.length)];
}
}
// Advanced tactic evaluation functions
function countWinningLines(row, col, player) {
var count = 0;
// Check row
var rowCount = 0;
for (var c = 0; c < 3; c++) {
if (grid[row][c].value === player || c === col) {
rowCount++;
} else if (grid[row][c].value !== 0) {
rowCount = -10; // Blocked
break;
}
}
if (rowCount === 3) count++;
// Check column
var colCount = 0;
for (var r = 0; r < 3; r++) {
if (grid[r][col].value === player || r === row) {
colCount++;
} else if (grid[r][col].value !== 0) {
colCount = -10; // Blocked
break;
}
}
if (colCount === 3) count++;
// Check diagonals
if (row === 0 && col === 0 || row === 1 && col === 1 || row === 2 && col === 2) {
var diagCount = 0;
for (var i = 0; i < 3; i++) {
if (grid[i][i].value === player || i === row && i === col) {
diagCount++;
} else if (grid[i][i].value !== 0) {
diagCount = -10; // Blocked
break;
}
}
if (diagCount === 3) count++;
}
if (row === 0 && col === 2 || row === 1 && col === 1 || row === 2 && col === 0) {
var antiDiagCount = 0;
for (var i = 0; i < 3; i++) {
if (grid[i][2 - i].value === player || i === row && 2 - i === col) {
antiDiagCount++;
} else if (grid[i][2 - i].value !== 0) {
antiDiagCount = -10; // Blocked
break;
}
}
if (antiDiagCount === 3) count++;
}
return count;
}
// Evaluate move position strength
function evaluatePosition(cell) {
var score = 0;
var row = cell.row;
var col = cell.col;
// Center is most valuable
if (row === 1 && col === 1) score += 4;
// Corners are second most valuable
else if ((row === 0 || row === 2) && (col === 0 || col === 2)) score += 3;
// Edges are least valuable
else score += 1;
// Check potential winning lines
var potentialLines = countWinningLines(row, col, 2);
score += potentialLines * 2;
return score;
}
// Advanced position evaluation for hard mode AI
function evaluateHardModePosition(cell) {
var score = 0;
var row = cell.row;
var col = cell.col;
// Base positional values with strategic weighting
if (row === 1 && col === 1) {
score += 15; // Center dominance
} else if ((row === 0 || row === 2) && (col === 0 || col === 2)) {
score += 12; // Corner control
} else {
score += 8; // Edge positions
}
// Evaluate line potential (how many lines this position affects)
var linesControlled = 0;
var lineValues = 0;
// Check row control
var rowEmpty = 0,
rowAI = 0,
rowPlayer = 0;
for (var c = 0; c < 3; c++) {
if (grid[row][c].value === 0) rowEmpty++;else if (grid[row][c].value === 2) rowAI++;else rowPlayer++;
}
if (rowPlayer === 0) {
linesControlled++;
lineValues += rowAI * 3 + rowEmpty;
}
// Check column control
var colEmpty = 0,
colAI = 0,
colPlayer = 0;
for (var r = 0; r < 3; r++) {
if (grid[r][col].value === 0) colEmpty++;else if (grid[r][col].value === 2) colAI++;else colPlayer++;
}
if (colPlayer === 0) {
linesControlled++;
lineValues += colAI * 3 + colEmpty;
}
// Check main diagonal
if (row === col) {
var diagEmpty = 0,
diagAI = 0,
diagPlayer = 0;
for (var i = 0; i < 3; i++) {
if (grid[i][i].value === 0) diagEmpty++;else if (grid[i][i].value === 2) diagAI++;else diagPlayer++;
}
if (diagPlayer === 0) {
linesControlled++;
lineValues += diagAI * 3 + diagEmpty;
}
}
// Check anti-diagonal
if (row + col === 2) {
var antiDiagEmpty = 0,
antiDiagAI = 0,
antiDiagPlayer = 0;
for (var i = 0; i < 3; i++) {
if (grid[i][2 - i].value === 0) antiDiagEmpty++;else if (grid[i][2 - i].value === 2) antiDiagAI++;else antiDiagPlayer++;
}
if (antiDiagPlayer === 0) {
linesControlled++;
lineValues += antiDiagAI * 3 + antiDiagEmpty;
}
}
score += linesControlled * 6 + lineValues * 2;
// Strategic pattern bonuses
// Opposite corner control
if (row === 0 && col === 0 && grid[2][2].value === 2 || row === 2 && col === 2 && grid[0][0].value === 2 || row === 0 && col === 2 && grid[2][0].value === 2 || row === 2 && col === 0 && grid[0][2].value === 2) {
score += 8; // Diagonal dominance
}
// Adjacent to AI pieces (connection bonus)
var adjacentAI = 0;
var checkAdj = [{
r: row - 1,
c: col
}, {
r: row + 1,
c: col
}, {
r: row,
c: col - 1
}, {
r: row,
c: col + 1
}];
for (var i = 0; i < checkAdj.length; i++) {
var adj = checkAdj[i];
if (adj.r >= 0 && adj.r < 3 && adj.c >= 0 && adj.c < 3) {
if (grid[adj.r][adj.c].value === 2) adjacentAI++;
}
}
score += adjacentAI * 4;
return score;
}
// Check for forks (moves that create two winning threats)
function checkForFork(player) {
var forkCells = [];
for (var i = 0; i < emptyCells.length; i++) {
var cell = emptyCells[i];
cell.value = player;
var winningLineCount = 0;
// Count how many winning moves this creates
for (var j = 0; j < emptyCells.length; j++) {
if (i !== j) {
var testCell = emptyCells[j];
testCell.value = player;
if (checkWinCondition(player)) {
winningLineCount++;
}
testCell.value = 0;
}
}
cell.value = 0;
if (winningLineCount >= 2) {
forkCells.push(cell);
}
}
return forkCells;
}
// Adjust strategy based on difficulty
var aiPersonality = Math.floor(Math.random() * 8);
var randomnessFactor = Math.random();
// Difficulty-based mistake chances
var winMissChance, blockMissChance, strategyQuality;
if (aiDifficulty === 'easy') {
// Easy mode doesn't need these values as it plays randomly
winMissChance = 1.0; // Always miss winning moves (not that it matters)
blockMissChance = 1.0; // Always miss blocking (not that it matters)
strategyQuality = 0; // No strategic moves
aiPersonality = -1; // No personality, just random
} else if (aiDifficulty === 'normal') {
winMissChance = 0.02; // 2% chance to miss winning move
blockMissChance = 0.05; // 5% chance to miss blocking
strategyQuality = 0.6; // Medium quality strategic moves
} else {
// hard - Perfect play with zero mistakes
winMissChance = 0; // NEVER miss winning move
blockMissChance = 0; // NEVER miss blocking moves
strategyQuality = 1.0; // Perfect strategic moves
aiPersonality = Math.floor(Math.random() * 8); // Use all personalities for variety
}
// Hard mode AI - Perfect play with advanced strategy evaluation
if (aiDifficulty === 'hard') {
// Priority 1: ALWAYS win if possible (0% miss rate)
for (var i = 0; i < emptyCells.length; i++) {
var cell = emptyCells[i];
cell.value = 2;
if (checkWinCondition(2)) {
cell.value = 0;
return cell; // IMMEDIATELY take winning move - no randomness
}
cell.value = 0;
}
// Priority 2: ALWAYS block player wins (0% miss rate)
for (var i = 0; i < emptyCells.length; i++) {
var cell = emptyCells[i];
cell.value = 1;
if (checkWinCondition(1)) {
cell.value = 0;
return cell; // IMMEDIATELY block - no randomness
}
cell.value = 0;
}
// Priority 3: Create forks (multiple winning threats)
forkMoves = checkForFork(2);
if (forkMoves.length > 0) {
return forkMoves[0]; // Take any fork opportunity
}
// Priority 4: Block player forks with counter-strategy
var playerForkMoves = checkForFork(1);
if (playerForkMoves.length > 0) {
// If player can create multiple forks, create a counter-threat
if (playerForkMoves.length > 1) {
// Look for moves that force player to defend while blocking fork
for (var i = 0; i < emptyCells.length; i++) {
var cell = emptyCells[i];
cell.value = 2;
var threatsCreated = 0;
for (var j = 0; j < emptyCells.length; j++) {
if (i !== j) {
var testCell = emptyCells[j];
testCell.value = 2;
if (checkWinCondition(2)) {
threatsCreated++;
}
testCell.value = 0;
}
}
cell.value = 0;
if (threatsCreated > 0) {
return cell; // Counter-threat to force defense
}
}
}
// Otherwise block the fork
return playerForkMoves[0];
}
// Priority 5: Optimal opening strategy
if (emptyCells.length === 9) {
// First move: always take center or corner
if (grid[1][1].value === 0) {
return grid[1][1]; // Center is strongest opening
} else {
return cornerCells[0]; // Fallback to corner
}
}
if (emptyCells.length === 8) {
// Second move response
if (grid[1][1].value === 1) {
// Player took center, take any corner
return cornerCells[0];
} else {
// Player took corner or edge, take center
if (grid[1][1].value === 0) {
return grid[1][1];
}
}
}
// Priority 6: Advanced positional strategy with minimax evaluation
var bestMove = null;
var bestScore = -1000;
for (var i = 0; i < emptyCells.length; i++) {
var cell = emptyCells[i];
var score = evaluateHardModePosition(cell);
// Add advanced tactical bonuses
if (cell.row === 1 && cell.col === 1) score += 15; // Center control
if ((cell.row === 0 || cell.row === 2) && (cell.col === 0 || cell.col === 2)) score += 12; // Corner control
// Evaluate future position strength
cell.value = 2;
var futureThreats = 0;
for (var j = 0; j < emptyCells.length; j++) {
if (i !== j) {
var testCell = emptyCells[j];
testCell.value = 2;
if (checkWinCondition(2)) futureThreats++;
testCell.value = 0;
}
}
score += futureThreats * 8; // Bonus for creating future threats
cell.value = 0;
if (score > bestScore) {
bestScore = score;
bestMove = cell;
}
}
if (bestMove) {
return bestMove;
}
}
// Priority 3: Check for fork opportunities (based on difficulty)
if (aiDifficulty !== 'easy' && randomnessFactor < strategyQuality) {
forkMoves = checkForFork(2);
if (forkMoves.length > 0 && randomnessFactor > 1 - strategyQuality) {
return forkMoves[Math.floor(Math.random() * forkMoves.length)];
}
}
// Priority 4: Block player forks (based on difficulty)
if (aiDifficulty !== 'easy' && randomnessFactor < strategyQuality) {
blockForkMoves = checkForFork(1);
if (blockForkMoves.length > 0 && randomnessFactor > 1 - strategyQuality) {
// Force player to defend instead of creating fork
return blockForkMoves[Math.floor(Math.random() * blockForkMoves.length)];
}
}
// Priority 5: Advanced tactical personalities
var moveChoices = [];
var moveScores = [];
if (aiPersonality === 0) {
// Perfect player - uses optimal strategy
if (emptyCells.length === 9) {
// First move: prefer corner
moveChoices = cornerCells.slice();
} else if (emptyCells.length === 8) {
// Second move response
if (grid[1][1].value === 1) {
// Player took center, take corner
moveChoices = cornerCells.slice();
} else {
// Player took corner or edge, take center
if (grid[1][1].value === 0) {
moveChoices.push(grid[1][1]);
}
}
} else {
// Evaluate all positions
for (var i = 0; i < emptyCells.length; i++) {
var score = evaluatePosition(emptyCells[i]);
moveScores.push({
cell: emptyCells[i],
score: score
});
}
moveScores.sort(function (a, b) {
return b.score - a.score;
});
// Take top scoring moves
var topScore = moveScores[0].score;
for (var i = 0; i < moveScores.length; i++) {
if (moveScores[i].score >= topScore - 1) {
moveChoices.push(moveScores[i].cell);
}
}
}
} else if (aiPersonality === 1) {
// Trap setter - tries to create winning patterns
if (grid[1][1].value === 0) {
moveChoices.push(grid[1][1]);
moveChoices.push(grid[1][1]); // Double weight
}
// Look for L-shaped patterns
if (grid[0][0].value === 2 && grid[2][2].value === 0) {
moveChoices.push(grid[2][2]);
}
if (grid[0][2].value === 2 && grid[2][0].value === 0) {
moveChoices.push(grid[2][0]);
}
// Add corners for trap potential
if (cornerCells.length > 0) {
moveChoices = moveChoices.concat(cornerCells);
}
} else if (aiPersonality === 2) {
// Defensive player - blocks and controls center
if (grid[1][1].value === 0) {
moveChoices.push(grid[1][1]);
moveChoices.push(grid[1][1]);
moveChoices.push(grid[1][1]); // Triple weight for center
}
// Prefer moves that block multiple lines
for (var i = 0; i < emptyCells.length; i++) {
var blockCount = countWinningLines(emptyCells[i].row, emptyCells[i].col, 1);
if (blockCount >= 2) {
moveChoices.push(emptyCells[i]);
}
}
if (moveChoices.length === 0 && cornerCells.length > 0) {
moveChoices = cornerCells.slice();
}
} else if (aiPersonality === 3) {
// Mirror player - plays symmetrically
var playerMoves = [];
for (var r = 0; r < 3; r++) {
for (var c = 0; c < 3; c++) {
if (grid[r][c].value === 1) {
playerMoves.push({
row: r,
col: c
});
}
}
}
if (playerMoves.length > 0) {
var lastMove = playerMoves[playerMoves.length - 1];
// Try to mirror across center
var mirrorRow = 2 - lastMove.row;
var mirrorCol = 2 - lastMove.col;
if (grid[mirrorRow][mirrorCol].value === 0) {
moveChoices.push(grid[mirrorRow][mirrorCol]);
moveChoices.push(grid[mirrorRow][mirrorCol]); // Double weight
}
}
// Add strategic positions
if (grid[1][1].value === 0) {
moveChoices.push(grid[1][1]);
}
if (cornerCells.length > 0) {
moveChoices.push(cornerCells[Math.floor(Math.random() * cornerCells.length)]);
}
} else if (aiPersonality === 4) {
// Edge control player - unusual but effective
if (edgeCells.length > 0) {
// Prefer edges that connect to existing pieces
for (var i = 0; i < edgeCells.length; i++) {
var edge = edgeCells[i];
var adjacentAI = 0;
// Check adjacent cells
var checkPositions = [{
r: edge.row - 1,
c: edge.col
}, {
r: edge.row + 1,
c: edge.col
}, {
r: edge.row,
c: edge.col - 1
}, {
r: edge.row,
c: edge.col + 1
}];
for (var j = 0; j < checkPositions.length; j++) {
var pos = checkPositions[j];
if (pos.r >= 0 && pos.r < 3 && pos.c >= 0 && pos.c < 3) {
if (grid[pos.r][pos.c].value === 2) {
adjacentAI++;
}
}
}
if (adjacentAI > 0) {
moveChoices.push(edge);
moveChoices.push(edge); // Double weight for connected edges
}
}
if (moveChoices.length === 0) {
moveChoices = edgeCells.slice();
}
}
// Sometimes take center
if (Math.random() < 0.3 && grid[1][1].value === 0) {
moveChoices.push(grid[1][1]);
}
} else if (aiPersonality === 5) {
// Chaos player - creates complex board states
var complexityScore = [];
for (var i = 0; i < emptyCells.length; i++) {
var cell = emptyCells[i];
// Calculate how many lines this move affects
var linesAffected = 0;
if (true) linesAffected++; // Row
if (true) linesAffected++; // Column
if (cell.row === cell.col || cell.row + cell.col === 2) {
linesAffected++; // Diagonal
}
complexityScore.push({
cell: cell,
complexity: linesAffected
});
}
// Prefer high complexity moves
complexityScore.sort(function (a, b) {
return b.complexity - a.complexity;
});
for (var i = 0; i < Math.min(3, complexityScore.length); i++) {
moveChoices.push(complexityScore[i].cell);
}
} else if (aiPersonality === 6) {
// Pattern player - follows specific winning patterns
var patterns = [
// Corner-opposite-corner
[{
r: 0,
c: 0
}, {
r: 2,
c: 2
}], [{
r: 0,
c: 2
}, {
r: 2,
c: 0
}],
// L-shapes
[{
r: 0,
c: 0
}, {
r: 0,
c: 2
}], [{
r: 0,
c: 0
}, {
r: 2,
c: 0
}], [{
r: 2,
c: 2
}, {
r: 2,
c: 0
}], [{
r: 2,
c: 2
}, {
r: 0,
c: 2
}]];
for (var p = 0; p < patterns.length; p++) {
var pattern = patterns[p];
var hasFirst = grid[pattern[0].r][pattern[0].c].value === 2;
var secondEmpty = grid[pattern[1].r][pattern[1].c].value === 0;
if (hasFirst && secondEmpty) {
moveChoices.push(grid[pattern[1].r][pattern[1].c]);
moveChoices.push(grid[pattern[1].r][pattern[1].c]); // Double weight
}
}
// Default to smart positions
if (moveChoices.length === 0) {
if (grid[1][1].value === 0) {
moveChoices.push(grid[1][1]);
}
if (cornerCells.length > 0) {
moveChoices = moveChoices.concat(cornerCells);
}
}
} else {
// Adaptive player - changes strategy based on game state
var gamePhase = 9 - emptyCells.length; // 0-8, higher = later
if (gamePhase <= 2) {
// Early game: control key positions
if (grid[1][1].value === 0) {
moveChoices.push(grid[1][1]);
moveChoices.push(grid[1][1]);
}
if (cornerCells.length > 0) {
moveChoices = moveChoices.concat(cornerCells);
}
} else if (gamePhase <= 5) {
// Mid game: create threats
for (var i = 0; i < emptyCells.length; i++) {
var threatLevel = countWinningLines(emptyCells[i].row, emptyCells[i].col, 2);
if (threatLevel >= 1) {
moveChoices.push(emptyCells[i]);
if (threatLevel >= 2) {
moveChoices.push(emptyCells[i]); // Extra weight for multi-threats
}
}
}
} else {
// Late game: optimize winning chances
for (var i = 0; i < emptyCells.length; i++) {
moveChoices.push(emptyCells[i]);
}
}
}
// Add calculated randomness
if (Math.random() < 0.1) {
// 10% chance to add unexpected move
if (validCells.length > 0) {
var randomCell = validCells[Math.floor(Math.random() * validCells.length)];
moveChoices.push(randomCell);
}
}
// If we have strategic choices, pick from them
if (moveChoices.length > 0) {
// Sometimes pick the most frequent choice (if duplicates exist)
var choiceFrequency = {};
for (var i = 0; i < moveChoices.length; i++) {
var key = moveChoices[i].row + ',' + moveChoices[i].col;
choiceFrequency[key] = (choiceFrequency[key] || 0) + 1;
}
// Create weighted selection
var weightedChoices = [];
for (var i = 0; i < moveChoices.length; i++) {
weightedChoices.push(moveChoices[i]);
}
return weightedChoices[Math.floor(Math.random() * weightedChoices.length)];
}
// Easy mode already handled above with helpful logic
// Final fallback: intelligent selection
var fallbackChoices = [];
// Prioritize based on position value (less strategic in easy mode)
if (aiDifficulty !== 'easy' || Math.random() < 0.5) {
if (grid[1][1].value === 0) {
fallbackChoices.push(grid[1][1]);
}
}
if (cornerCells.length > 0 && (aiDifficulty !== 'easy' || Math.random() < 0.6)) {
fallbackChoices = fallbackChoices.concat(cornerCells);
}
if (edgeCells.length > 0 && (aiDifficulty === 'easy' || Math.random() < 0.3)) {
fallbackChoices.push(edgeCells[Math.floor(Math.random() * edgeCells.length)]);
}
if (fallbackChoices.length === 0) {
fallbackChoices = validCells;
}
if (fallbackChoices.length > 0) {
return fallbackChoices[Math.floor(Math.random() * fallbackChoices.length)];
}
// Ultimate fallback: return first empty cell found
for (var row = 0; row < 3; row++) {
for (var col = 0; col < 3; col++) {
if (grid[row][col].value === 0 && !grid[row][col].marker) {
return grid[row][col];
}
}
}
// If still no valid move found, return any empty cell regardless of marker status
for (var row = 0; row < 3; row++) {
for (var col = 0; col < 3; col++) {
if (grid[row][col].value === 0) {
return grid[row][col];
}
}
}
return null;
}
function checkWinCondition(player) {
// Check rows
for (var row = 0; row < 3; row++) {
if (grid[row][0].value === player && grid[row][1].value === player && grid[row][2].value === player) {
return true;
}
}
// Check columns
for (var col = 0; col < 3; col++) {
if (grid[0][col].value === player && grid[1][col].value === player && grid[2][col].value === player) {
return true;
}
}
// Check diagonals
if (grid[0][0].value === player && grid[1][1].value === player && grid[2][2].value === player) {
return true;
}
if (grid[0][2].value === player && grid[1][1].value === player && grid[2][0].value === player) {
return true;
}
return false;
}
function resetGame() {
gameOver = false;
// Immediately clear AI processing to prevent stuck states
aiProcessing = false;
// Clear any pending AI timeouts
LK.clearTimeout(); // Clear any pending timeouts
// Stop all active tweens to prevent conflicts
tween.stop();
// Force clear any stuck AI state
if (isAIMode && currentPlayer === 2) {
currentPlayer = 1; // Reset to player's turn
}
// Start according to game mode
if (gameMode === 'ai') {
currentPlayer = 1; // Player always starts in AI mode
isAIMode = true;
} else if (gameMode === 'twoPlayer') {
currentPlayer = Math.random() < 0.5 ? 1 : 2;
isAIMode = false;
} else {
// Menu mode
currentPlayer = 1;
isAIMode = false;
}
// Clear grid
for (var row = 0; row < 3; row++) {
for (var col = 0; col < 3; col++) {
var cell = grid[row][col];
cell.value = 0;
if (cell.marker) {
// Stop any active tweens on marker before destroying
tween.stop(cell.marker);
// Reset marker properties to prevent visual glitches
cell.marker.alpha = 0;
cell.marker.scaleX = 1;
cell.marker.scaleY = 1;
cell.marker.tint = 0xffffff;
cell.marker.rotation = 0;
// Remove from parent before destroying
if (cell.marker.parent) {
cell.marker.parent.removeChild(cell.marker);
}
cell.marker.destroy();
cell.marker = null;
}
}
}
// Clear UI
statusText.setText('');
statusText.alpha = 0;
playAgainBtn.alpha = 0;
playAgainBg.alpha = 0;
// Stop any pulsing animation on play again button
tween.stop(playAgainBg);
tween.stop(playAgainBtn);
playAgainBg.scaleX = 1;
playAgainBg.scaleY = 1;
playAgainBtn.interactive = false;
// Remove win line
if (winLine) {
// Stop any active tweens on win line to prevent memory leaks
tween.stop(winLine.line);
tween.stop(winLine); // Stop any tweens on the container too
// Reset line properties before destroying
winLine.line.alpha = 0;
winLine.line.scaleX = 0;
winLine.line.tint = 0xffffff; // Reset tint
// Remove from parent before destroying
if (winLine.parent) {
winLine.parent.removeChild(winLine);
}
winLine.destroy();
winLine = null;
}
updateTurnDisplay();
}
// Play again button handler
playAgainBtn.down = function (x, y, obj) {
if (gameOver) {
resetGame();
}
};
// Add small delay to prevent initial freeze when entering game
var gameReady = false;
LK.setTimeout(function () {
gameReady = true;
}, 50); // Reduced delay for better responsiveness