tic-tac-infinity / index.html
Ghosugy's picture
An advanced game that combines intelligence, planning, and animation.
426f48a verified
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tic Tac Infinity ⚡</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: '#6366f1', // Indigo 500
secondary: '#ec4899', // Pink 500
dark: '#0f172a', // Slate 900
surface: '#1e293b', // Slate 800
},
animation: {
'pulse-fast': 'pulse 1s cubic-bezier(0.4, 0, 0.6, 1) infinite',
'bounce-short': 'bounce 0.5s infinite',
'pop-in': 'popIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards',
'glow': 'glow 2s ease-in-out infinite alternate',
'shake': 'shake 0.5s cubic-bezier(.36,.07,.19,.97) both',
},
keyframes: {
popIn: {
'0%': { opacity: '0', transform: 'scale(0.5)' },
'100%': { opacity: '1', transform: 'scale(1)' },
},
glow: {
'from': { boxShadow: '0 0 10px #6366f1' },
'to': { boxShadow: '0 0 20px #ec4899, 0 0 10px #6366f1' },
},
shake: {
'10%, 90%': { transform: 'translate3d(-1px, 0, 0)' },
'20%, 80%': { transform: 'translate3d(2px, 0, 0)' },
'30%, 50%, 70%': { transform: 'translate3d(-4px, 0, 0)' },
'40%, 60%': { transform: 'translate3d(4px, 0, 0)' }
}
}
}
}
}
</script>
<!-- Icons -->
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Rajdhani:wght@300;500;700&display=swap');
body {
font-family: 'Rajdhani', sans-serif;
overflow-x: hidden;
background-color: #0f172a;
color: #f8fafc;
}
.font-display {
font-family: 'Orbitron', sans-serif;
}
/* Dynamic Background */
.bg-grid {
background-size: 50px 50px;
background-image: linear-gradient(to right, rgba(255, 255, 255, 0.05) 1px, transparent 1px),
linear-gradient(to bottom, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
mask-image: radial-gradient(circle at center, black 40%, transparent 100%);
-webkit-mask-image: radial-gradient(circle at center, black 40%, transparent 100%);
animation: moveGrid 20s linear infinite;
}
@keyframes moveGrid {
0% { transform: translateY(0); }
100% { transform: translateY(50px); }
}
/* Neon Skins */
.skin-neon-x { color: #00f2ff; text-shadow: 0 0 10px #00f2ff, 0 0 20px #00f2ff; }
.skin-neon-o { color: #ff00aa; text-shadow: 0 0 10px #ff00aa, 0 0 20px #ff00aa; }
.skin-fire-x { color: #ffaa00; text-shadow: 0 0 10px #ff4400, 0 0 20px #ff0000; }
.skin-fire-o { color: #ffdd00; text-shadow: 0 0 10px #ff8800, 0 0 20px #ff4400; }
.skin-glass-x {
color: rgba(255,255,255,0.8);
text-shadow: 0 0 5px rgba(255,255,255,0.5);
backdrop-filter: blur(2px);
stroke: white;
stroke-width: 1px;
}
.skin-glass-o {
color: rgba(255,255,255,0.8);
text-shadow: 0 0 5px rgba(255,255,255,0.5);
backdrop-filter: blur(2px);
stroke: white;
stroke-width: 1px;
}
/* Board Styles */
.cell {
transition: all 0.2s ease;
}
.cell:hover:not(.taken) {
background: rgba(255, 255, 255, 0.1);
transform: scale(1.02);
}
.cell.taken {
cursor: default;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #1e293b;
}
::-webkit-scrollbar-thumb {
background: #475569;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #6366f1;
}
/* Modal */
.modal {
transition: opacity 0.3s ease, visibility 0.3s;
}
.modal.hidden {
opacity: 0;
visibility: hidden;
pointer-events: none;
}
.modal:not(.hidden) {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
/* Ability Effects */
.frozen {
background: rgba(60, 200, 255, 0.2) !important;
border-color: #38bdf8 !important;
box-shadow: inset 0 0 10px #38bdf8;
}
.clear-target {
cursor: crosshair !important;
box-shadow: inset 0 0 15px #ef4444;
}
</style>
</head>
<body class="h-screen w-screen flex flex-col relative overflow-hidden selection:bg-primary selection:text-white">
<!-- Background -->
<div class="fixed inset-0 pointer-events-none z-0">
<div class="absolute inset-0 bg-gradient-to-br from-dark via-slate-900 to-indigo-950"></div>
<div class="absolute inset-0 bg-grid opacity-30"></div>
</div>
<!-- UI Container -->
<div id="app" class="relative z-10 flex flex-col h-full w-full max-w-7xl mx-auto p-4">
<!-- Header -->
<header class="flex justify-between items-center mb-6 p-4 bg-surface/50 backdrop-blur-md rounded-xl border border-white/10 shadow-lg">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-gradient-to-tr from-primary to-secondary rounded-lg flex items-center justify-center shadow-lg shadow-primary/30">
<i data-feather="grid" class="text-white w-6 h-6"></i>
</div>
<h1 class="font-display text-2xl font-bold tracking-wider bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-400">TIC TAC <span class="text-primary">INFINITY</span></h1>
</div>
<div class="flex items-center gap-6">
<div class="flex flex-col items-end">
<span class="text-xs text-gray-400 uppercase tracking-widest">XP Level</span>
<div class="flex items-center gap-2">
<span id="xp-display" class="font-bold text-secondary">0 XP</span>
<div class="w-24 h-2 bg-gray-700 rounded-full overflow-hidden">
<div id="xp-bar" class="h-full bg-gradient-to-r from-primary to-secondary w-0 transition-all duration-500"></div>
</div>
</div>
</div>
<button id="settings-btn" class="p-2 hover:bg-white/10 rounded-full transition-colors">
<i data-feather="settings" class="text-gray-300"></i>
</button>
</div>
</header>
<!-- Main Content Area -->
<main class="flex-1 relative">
<!-- View: Menu -->
<section id="view-menu" class="absolute inset-0 flex flex-col items-center justify-center transition-all duration-500">
<h2 class="font-display text-4xl md:text-6xl font-bold mb-10 text-center animate-pop-in">
Choose Your <span class="text-transparent bg-clip-text bg-gradient-to-r from-primary to-secondary">Arena</span>
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 w-full max-w-4xl px-4">
<!-- Classic -->
<button onclick="game.start(3, 'classic')" class="group relative bg-surface/80 hover:bg-surface border border-white/10 p-8 rounded-2xl text-left transition-all hover:scale-[1.02] hover:border-primary/50 hover:shadow-lg hover:shadow-primary/20">
<div class="absolute top-4 right-4 opacity-50 group-hover:opacity-100 transition-opacity"><i data-feather="activity"></i></div>
<h3 class="font-display text-2xl font-bold mb-2 text-white group-hover:text-primary">Classic Mode</h3>
<p class="text-gray-400 text-sm">The traditional 3x3 battle. Test your wits against a friend or AI.</p>
</button>
<!-- Expanded -->
<button onclick="game.start(4, 'expanded')" class="group relative bg-surface/80 hover:bg-surface border border-white/10 p-8 rounded-2xl text-left transition-all hover:scale-[1.02] hover:border-secondary/50 hover:shadow-lg hover:shadow-secondary/20">
<div class="absolute top-4 right-4 opacity-50 group-hover:opacity-100 transition-opacity"><i data-feather="maximize-2"></i></div>
<h3 class="font-display text-2xl font-bold mb-2 text-white group-hover:text-secondary">Expanded Mode</h3>
<p class="text-gray-400 text-sm">4x4 or 5x5 grids. More space, more strategy, less draws.</p>
</button>
<!-- Time Attack -->
<button onclick="game.start(3, 'time')" class="group relative bg-surface/80 hover:bg-surface border border-white/10 p-8 rounded-2xl text-left transition-all hover:scale-[1.02] hover:border-orange-500/50 hover:shadow-lg hover:shadow-orange-500/20">
<div class="absolute top-4 right-4 opacity-50 group-hover:opacity-100 transition-opacity"><i data-feather="clock"></i></div>
<h3 class="font-display text-2xl font-bold mb-2 text-white group-hover:text-orange-400">Time Attack</h3>
<p class="text-gray-400 text-sm">Think fast! You have 5 seconds per move or lose your turn.</p>
</button>
<!-- AI Simulation -->
<button onclick="game.start(3, 'ai-vs-ai')" class="group relative bg-surface/80 hover:bg-surface border border-white/10 p-8 rounded-2xl text-left transition-all hover:scale-[1.02] hover:border-green-500/50 hover:shadow-lg hover:shadow-green-500/20">
<div class="absolute top-4 right-4 opacity-50 group-hover:opacity-100 transition-opacity"><i data-feather="cpu"></i></div>
<h3 class="font-display text-2xl font-bold mb-2 text-white group-hover:text-green-400">AI Battle</h3>
<p class="text-gray-400 text-sm">Watch two advanced AI's battle each other. Learn their moves.</p>
</button>
</div>
</section>
<!-- View: Game -->
<section id="view-game" class="absolute inset-0 flex flex-col md:flex-row gap-8 items-center justify-center opacity-0 pointer-events-none transition-opacity duration-500">
<!-- Sidebar / HUD -->
<div class="w-full md:w-1/3 flex flex-col gap-4 order-2 md:order-1">
<!-- Player Card -->
<div id="p1-card" class="bg-surface/60 backdrop-blur border-l-4 border-primary p-4 rounded-lg flex justify-between items-center transition-all">
<div>
<h4 class="font-bold text-white text-lg">Player 1 (X)</h4>
<p class="text-xs text-gray-400">Human / CPU</p>
</div>
<div class="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center text-primary font-display font-bold">X</div>
</div>
<!-- Timer -->
<div id="timer-container" class="hidden bg-surface/60 backdrop-blur p-4 rounded-lg text-center">
<span class="text-gray-400 text-xs uppercase">Time Left</span>
<div class="text-3xl font-display font-bold text-white mt-1" id="timer-display">5.00</div>
<div class="w-full bg-gray-700 h-1 mt-2 rounded-full overflow-hidden">
<div id="timer-bar" class="bg-gradient-to-r from-green-400 to-red-500 h-full w-full transition-all duration-100 linear"></div>
</div>
</div>
<div id="p2-card" class="bg-surface/60 backdrop-blur border-l-4 border-secondary p-4 rounded-lg flex justify-between items-center transition-all opacity-60">
<div>
<h4 class="font-bold text-white text-lg">Player 2 (O)</h4>
<p class="text-xs text-gray-400">Human / CPU</p>
</div>
<div class="w-10 h-10 rounded-full bg-secondary/20 flex items-center justify-center text-secondary font-display font-bold">O</div>
</div>
<!-- Abilities -->
<div class="bg-surface/40 backdrop-blur p-4 rounded-lg mt-4">
<h4 class="text-xs uppercase text-gray-500 font-bold mb-3 tracking-widest">Special Abilities</h4>
<div class="grid grid-cols-2 gap-2">
<button id="ability-clear" onclick="game.activateAbility('clear')" class="p-2 bg-red-500/10 border border-red-500/30 rounded hover:bg-red-500/30 text-xs flex flex-col items-center gap-1 transition-all disabled:opacity-30 disabled:cursor-not-allowed">
<i data-feather="trash-2" class="w-4 h-4 text-red-400"></i>
<span class="text-red-200">Clear</span>
</button>
<button id="ability-freeze" onclick="game.activateAbility('freeze')" class="p-2 bg-blue-500/10 border border-blue-500/30 rounded hover:bg-blue-500/30 text-xs flex flex-col items-center gap-1 transition-all disabled:opacity-30 disabled:cursor-not-allowed">
<i data-feather="snowflake" class="w-4 h-4 text-blue-400"></i>
<span class="text-blue-200">Freeze</span>
</button>
</div>
<p class="text-[10px] text-gray-500 mt-2 text-center">One use per game</p>
</div>
<button onclick="app.showMenu()" class="mt-auto w-full py-3 border border-white/20 rounded-lg text-gray-300 hover:bg-white/10 hover:text-white transition-colors text-sm font-bold tracking-wider">
EXIT GAME
</button>
</div>
<!-- Game Board -->
<div class="relative w-full md:w-2/3 flex flex-col items-center justify-center order-1 md:order-2">
<div id="turn-indicator" class="mb-6 font-display text-2xl font-bold text-white animate-pulse">X's Turn</div>
<div id="game-board" class="relative bg-surface/30 backdrop-blur-xl p-4 rounded-2xl shadow-2xl border border-white/5 transition-transform duration-200">
<!-- Grid generated by JS -->
</div>
<!-- Win Line Overlay -->
<div id="win-line" class="absolute hidden bg-white rounded-full shadow-[0_0_20px_rgba(255,255,255,0.8)] pointer-events-none z-20"></div>
</div>
</section>
<!-- View: Settings -->
<section id="view-settings" class="absolute inset-0 flex flex-col items-center justify-center bg-dark/90 backdrop-blur-sm z-50 hidden">
<div class="bg-surface border border-white/10 p-8 rounded-2xl w-full max-w-md shadow-2xl">
<div class="flex justify-between items-center mb-6">
<h2 class="font-display text-2xl font-bold">Settings</h2>
<button onclick="app.toggleSettings()" class="p-1 hover:bg-white/10 rounded"><i data-feather="x"></i></button>
</div>
<!-- Skins -->
<div class="mb-8">
<h3 class="text-sm font-bold text-gray-400 uppercase tracking-widest mb-4">Select Skin (Requires XP)</h3>
<div class="grid grid-cols-3 gap-3">
<button onclick="app.setSkin('default')" class="p-4 border border-gray-600 rounded-xl flex flex-col items-center gap-2 hover:border-primary transition-colors bg-white/5">
<span class="font-display font-bold text-xl">X</span>
<span class="text-xs text-gray-400">Default</span>
</button>
<button onclick="app.setSkin('neon')" class="p-4 border border-gray-600 rounded-xl flex flex-col items-center gap-2 hover:border-primary transition-colors bg-black/40 relative overflow-hidden group">
<div class="absolute inset-0 bg-gradient-to-br from-cyan-500/20 to-purple-500/20 opacity-0 group-hover:opacity-100 transition-opacity"></div>
<span class="font-display font-bold text-xl skin-neon-x relative z-10">X</span>
<span class="text-xs text-gray-400 relative z-10">Neon (500XP)</span>
</button>
<button onclick="app.setSkin('fire')" class="p-4 border border-gray-600 rounded-xl flex flex-col items-center gap-2 hover:border-primary transition-colors bg-black/40 relative overflow-hidden group">
<div class="absolute inset-0 bg-gradient-to-br from-orange-500/20 to-red-500/20 opacity-0 group-hover:opacity-100 transition-opacity"></div>
<span class="font-display font-bold text-xl skin-fire-x relative z-10">X</span>
<span class="text-xs text-gray-400 relative z-10">Fire (1000XP)</span>
</button>
</div>
</div>
<div class="flex justify-between items-center mt-6 pt-6 border-t border-white/10">
<span class="text-gray-300">Light/Dark Mode</span>
<button id="theme-toggle" class="bg-white/10 p-2 rounded-full"><i data-feather="moon"></i></button>
</div>
</div>
</section>
</main>
</div>
<!-- Modal: Game Over -->
<div id="modal-gameover" class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm hidden">
<div class="bg-surface border border-white/10 p-8 rounded-2xl text-center max-w-sm w-full shadow-2xl transform scale-95 transition-all duration-300" id="modal-content">
<div class="w-20 h-20 mx-auto bg-gradient-to-tr from-primary to-secondary rounded-full flex items-center justify-center mb-6 shadow-lg shadow-primary/40">
<i data-feather="award" class="w-10 h-10 text-white"></i>
</div>
<h2 id="modal-title" class="font-display text-3xl font-bold mb-2 text-white">Victory!</h2>
<p id="modal-msg" class="text-gray-400 mb-8">Player X has dominated the grid.</p>
<div class="flex gap-4">
<button onclick="app.showMenu()" class="flex-1 py-3 bg-white/5 border border-white/10 rounded-lg hover:bg-white/10 transition text-gray-300">Menu</button>
<button onclick="game.restart()" class="flex-1 py-3 bg-gradient-to-r from-primary to-secondary rounded-lg hover:opacity-90 transition text-white font-bold shadow-lg shadow-primary/25">Rematch</button>
</div>
</div>
</div>
<script>
// --- Audio System (Synthesized to avoid external assets) ---
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const Sound = {
playTone: (freq, type, duration) => {
if(audioCtx.state === 'suspended') audioCtx.resume();
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + duration);
},
moveX: () => Sound.playTone(440, 'sine', 0.15), // A4
moveO: () => Sound.playTone(330, 'sine', 0.15), // E4
win: () => {
setTimeout(() => Sound.playTone(523.25, 'triangle', 0.2), 0);
setTimeout(() => Sound.playTone(659.25, 'triangle', 0.2), 200);
setTimeout(() => Sound.playTone(783.99, 'triangle', 0.4), 400);
},
draw: () => {
setTimeout(() => Sound.playTone(200, 'sawtooth', 0.3), 0);
setTimeout(() => Sound.playTone(150, 'sawtooth', 0.4), 300);
}
};
// --- App & State Management ---
const app = {
state: {
xp: parseInt(localStorage.getItem('ttti_xp') || '0'),
skin: localStorage.getItem('ttti_skin') || 'default',
lang: 'en'
},
init: () => {
app.updateXP();
feather.replace();
document.getElementById('theme-toggle').addEventListener('click', () => {
document.documentElement.classList.toggle('dark');
const isDark = document.documentElement.classList.contains('dark');
// Ideally save preference here
});
document.getElementById('settings-btn').addEventListener('click', app.toggleSettings);
},
toggleSettings: () => {
const el = document.getElementById('view-settings');
el.classList.toggle('hidden');
},
setSkin: (skinName) => {
// Logic check for XP could go here
app.state.skin = skinName;
localStorage.setItem('ttti_skin', skinName);
game.applySkins();
},
addXP: (amount) => {
app.state.xp += amount;
localStorage.setItem('ttti_xp', app.state.xp);
app.updateXP();
},
updateXP: () => {
const xpDisplay = document.getElementById('xp-display');
const xpBar = document.getElementById('xp-bar');
// Animate number
const start = parseInt(xpDisplay.innerText);
const end = app.state.xp;
let current = start;
const step = () => {
current += Math.ceil((end - current) / 10);
if(current >= end) current = end;
xpDisplay.innerText = `${current} XP`;
if(current < end) requestAnimationFrame(step);
};
step();
// Update bar (max level 2000xp for loop visual)
const max = 2000;
const pct = Math.min((app.state.xp % max) / max * 100, 100);
xpBar.style.width = `${pct}%`;
},
showMenu: () => {
document.getElementById('view-menu').classList.remove('opacity-0', 'pointer-events-none');
document.getElementById('view-game').classList.add('opacity-0', 'pointer-events-none');
document.getElementById('modal-gameover').classList.add('hidden');
},
showGame: () => {
document.getElementById('view-menu').classList.add('opacity-0', 'pointer-events-none');
document.getElementById('view-game').classList.remove('opacity-0', 'pointer-events-none');
},
showGameOver: (title, msg) => {
const modal = document.getElementById('modal-gameover');
document.getElementById('modal-title').innerText = title;
document.getElementById('modal-msg').innerText = msg;
modal.classList.remove('hidden');
setTimeout(() => {
document.getElementById('modal-content').classList.remove('scale-95');
document.getElementById('modal-content').classList.add('scale-100');
}, 10);
}
};
// --- Game Logic ---
const game = {
size: 3,
mode: 'classic',
board: [], // 2D array
currentPlayer: 'X',
active: false,
abilities: { X: { clear: true, freeze: true }, O: { clear: true, freeze: true } },
pendingAbility: null, // 'clear' or 'freeze'
timer: null,
timeLeft: 5,
aiLevel: 'medium', // easy, medium, hard
isAiVsAi: false,
start: (size, mode) => {
game.size = size;
game.mode = mode;
game.board = Array(size).fill(null).map(() => Array(size).fill(null));
game.currentPlayer = 'X';
game.active = true;
game.isAiVsAi = mode === 'ai-vs-ai';
// Reset UI
document.getElementById('game-board').innerHTML = '';
app.showGame();
game.renderBoard();
game.updateHUD();
// Setup Mode Specifics
if (mode === 'time') {
document.getElementById('timer-container').classList.remove('hidden');
game.resetTimer();
} else {
document.getElementById('timer-container').classList.add('hidden');
clearInterval(game.timer);
}
// Abilities reset
game.abilities = {
X: { clear: true, freeze: true },
O: { clear: true, freeze: true }
};
game.updateAbilityButtons();
// If AI vs AI or AI starts (assuming P1 is always X, P2 O in this demo structure unless changed)
// Let's assume P1 is Human, P2 is AI for standard modes.
if (game.isAiVsAi) {
setTimeout(game.aiMove, 1000);
}
},
renderBoard: () => {
const container = document.getElementById('game-board');
container.style.gridTemplateColumns = `repeat(${game.size}, minmax(0, 1fr))`;
container.style.display = 'grid';
container.style.gap = '10px';
// Adjust cell size based on grid size to fit screen
const cellSize = game.size === 3 ? '100px' : (game.size === 4 ? '80px' : '60px');
for(let r=0; r<game.size; r++) {
for(let c=0; c<game.size; c++) {
const cell = document.createElement('div');
cell.className = `cell w-full aspect-square bg-white/5 rounded-lg flex items-center justify-center text-4xl md:text-5xl font-display font-bold border border-white/5 relative cursor-pointer ${cellSize}`;
cell.dataset.r = r;
cell.dataset.c = c;
cell.onclick = () => game.handleCellClick(r, c);
container.appendChild(cell);
}
}
game.applySkins();
},
applySkins: () => {
const skin = app.state.skin;
const cells = document.querySelectorAll('.cell');
cells.forEach(cell => {
const txt = cell.innerText;
if(txt === 'X') {
cell.className = cell.className.replace(/skin-\w+-x/g, '');
cell.classList.add(`skin-${skin}-x`);
} else if (txt === 'O') {
cell.className = cell.className.replace(/skin-\w+-o/g, '');
cell.classList.add(`skin-${skin}-o`);
}
});
},
handleCellClick: (r, c) => {
if (!game.active) return;
// Handle Abilities
if (game.pendingAbility) {
game.executeAbility(r, c);
return;
}
// Standard Move
if (game.board[r][c] !== null) return;
// If current player is AI (human vs AI), block clicks
if (!game.isAiVsAi && game.currentPlayer === 'O' && game.mode !== 'ai-vs-ai') return;
game.placeMark(r, c, game.currentPlayer);
},
placeMark: (r, c, player) => {
game.board[r][c] = player;
// UI Update
const cell = document.querySelector(`.cell[data-r='${r}'][data-c='${c}']`);
cell.innerText = player;
cell.classList.add('taken', 'animate-pop-in');
if(app.state.skin !== 'default') {
cell.classList.add(player === 'X' ? `skin-${app.state.skin}-x` : `skin-${app.state.skin}-o`);
}
// Sound
player === 'X' ? Sound.moveX() : Sound.moveO();
// Check Win
const win = game.checkWin(player);
if (win) {
game.endGame(player, win);
return;
}
// Check Draw
if (game.checkDraw()) {
game.endGame('draw');
return;
}
// Switch Turn
game.switchTurn();
},
switchTurn: () => {
game.currentPlayer = game.currentPlayer === 'X' ? 'O' : 'X';
game.updateHUD();
game.resetTimer();
// AI Move Trigger
if (!game.isAiVsAi && game.currentPlayer === 'O' && game.mode !== 'classic') {
// In Classic, assuming Human vs Human unless specified,
// but for "AI Mode" in prompt, usually vs CPU.
// Let's assume for non-AIvAI modes, P2 is AI.
setTimeout(game.aiMove, 600 + Math.random() * 800); // Thinking delay
} else if (game.isAiVsAi) {
setTimeout(game.aiMove, 600);
}
},
activateAbility: (type) => {
if (!game.abilities[game.currentPlayer][type]) return;
if (game.isAiVsAi && game.currentPlayer === 'O') return; // Humans only for now
if (game.pendingAbility === type) {
// Cancel
game.pendingAbility = null;
document.getElementById('game-board').classList.remove('clear-target');
return;
}
game.pendingAbility = type;
// Visual feedback
const boardEl = document.getElementById('game-board');
boardEl.classList.remove('shake', 'clear-target');
void boardEl.offsetWidth; // trigger reflow
boardEl.classList.add('animate-shake');
if(type === 'clear') boardEl.classList.add('clear-target');
// Show instruction
document.getElementById('turn-indicator').innerText = `Select target for ${type.toUpperCase()}...`;
},
executeAbility: (r, c) => {
const type = game.pendingAbility;
const targetCell = document.querySelector(`.cell[data-r='${r}'][data-c='${c}']`);
if (type === 'freeze') {
if (game.board[r][c] !== null) return; // Must be empty
// Mark frozen
game.board[r][c] = 'FROZEN';
targetCell.classList.add('frozen');
targetCell.innerHTML = '<i data-feather="snowflake" class="text-blue-400 w-6 h-6"></i>';
feather.replace();
}
else if (type === 'clear') {
// Must be opponent piece
const opponent = game.currentPlayer === 'X' ? 'O' : 'X';
if (game.board[r][c] !== opponent) return;
game.board[r][c] = null;
targetCell.innerText = '';
targetCell.className = targetCell.className.replace('taken', '').replace(/skin-\w+-[xo]/, '');
Sound.playTone(150, 'square', 0.1); // Delete sound
}
// Consume ability
game.abilities[game.currentPlayer][type] = false;
game.updateAbilityButtons();
game.pendingAbility = null;
document.getElementById('game-board').classList.remove('clear-target');
game.updateHUD();
},
updateAbilityButtons: () => {
const p = game.currentPlayer;
document.getElementById('ability-clear').disabled = !game.abilities[p].clear;
document.getElementById('ability-freeze').disabled = !game.abilities[p].freeze;
// Visual dimming
['clear', 'freeze'].forEach(abl => {
const btn = document.getElementById(`ability-${abl}`);
if(!game.abilities[p][abl]) btn.classList.add('opacity-30');
else btn.classList.remove('opacity-30');
});
},
updateHUD: () => {
const indicator = document.getElementById('turn-indicator');
const p1Card = document.getElementById('p1-card');
const p2Card = document.getElementById('p2-card');
indicator.innerText = `${game.currentPlayer}'s Turn`;
indicator.className = `font-display text-2xl font-bold mb-6 animate-pop-in ${game.currentPlayer === 'X' ? 'text-primary' : 'text-secondary'}`;
if(game.currentPlayer === 'X') {
p1Card.classList.remove('opacity-60');
p2Card.classList.add('opacity-60');
} else {
p1Card.classList.add('opacity-60');
p2Card.classList.remove('opacity-60');
}
game.updateAbilityButtons();
},
resetTimer: () => {
if (game.mode !== 'time') return;
clearInterval(game.timer);
game.timeLeft = 5;
game.timer = setInterval(() => {
game.timeLeft -= 0.1;
const display = document.getElementById('timer-display');
const bar = document.getElementById('timer-bar');
display.innerText = Math.max(0, game.timeLeft).toFixed(2);
bar.style.width = `${(game.timeLeft / 5) * 100}%`;
if (game.timeLeft <= 2) display.classList.add('text-red-500', 'animate-pulse');
else display.classList.remove('text-red-500', 'animate-pulse');
if (game.timeLeft <= 0) {
clearInterval(game.timer);
game.switchTurn(); // Lose turn
}
}, 100);
},
checkWin: (player) => {
const b = game.board;
const n = game.size;
const winLength = n; // For N x N, usually need N in a row (expanded rules)
// Rows
for(let r=0; r<n; r++) {
let count = 0;
for(let c=0; c<n; c++) {
if(b[r][c] === player) count++;
else count = 0;
if(count === winLength) return [{r,c-2}, {r,c-1}, {r,c}]; // simplified for logic
}
}
// Cols
for(let c=0; c<n; c++) {
let count = 0;
for(let r=0; r<n; r++) {
if(b[r][c] === player) count++;
else count = 0;
if(count === winLength) return [{r-2,c}, {r-1,c}, {r,c}];
}
}
// Diagonals
// Check all diagonals logic is complex for generic N,
// let's stick to main 2 for simplicity in this demo or use a simplified sweep
// Implementing a sweep for main/anti
// Main
for(let r=0; r<=n-winLength; r++){
for(let c=0; c<=n-winLength; c++){
let won = true;
let path = [];
for(let k=0; k<winLength; k++){
if(b[r+k][c+k] !== player) { won=false; break; }
path.push({r:r+k, c:c+k});
}
if(won) return path;
}
}
// Anti
for(let r=0; r<=n-winLength; r++){
for(let c=winLength-1; c<n; c++){
let won = true;
let path = [];
for(let k=0; k<winLength; k++){
if(b[r+k][c-k] !== player) { won=false; break; }
path.push({r:r+k, c:c-k});
}
if(won) return path;
}
}
return null;
},
checkDraw: () => {
return game.board.every(row => row.every(cell => cell !== null));
},
endGame: (winner, path) => {
game.active = false;
clearInterval(game.timer);
if (winner === 'draw') {
Sound.draw();
app.showGameOver("Draw", "A close match. No victor today.");
} else {
Sound.win();
if(path) game.highlightWin(path);
app.addXP(game.mode === 'classic' ? 100 : 200);
app.showGameOver(`${winner} Wins!`, "Dominance achieved on the grid.");
}
},
highlightWin: (path) => {
const container = document.getElementById('game-board');
const containerRect = container.getBoundingClientRect();
const winLine = document.getElementById('win-line');
// Calculate line position
const startCell = document.querySelector(`.cell[data-r='${path[0].r}'][data-c='${path[0].c}']`);
const endCell = document.querySelector(`.cell[data-r='${path[path.length-1].r}'][data-c='${path[path.length-1].c}']`);
const startRect = startCell.getBoundingClientRect();
const endRect = endCell.getBoundingClientRect();
const x1 = startRect.left - containerRect.left + startRect.width/2;
const y1 = startRect.top - containerRect.top + startRect.height/2;
const x2 = endRect.left - containerRect.left + endRect.width/2;
const y2 = endRect.top - containerRect.top + endRect.height/2;
const length = Math.sqrt((x2-x1)**2 + (y2-y1)**2);
const angle = Math.atan2(y2-y1, x2-x1) * 180 / Math.PI;
winLine.style.width = `${length}px`;
winLine.style.height = '6px';
winLine.style.left = `${x1}px`;
winLine.style.top = `${y1 - 3}px`; // Center vertically
winLine.style.transform = `rotate(${angle}deg)`;
winLine.style.transformOrigin = '0 50%';
winLine.classList.remove('hidden');
winLine.classList.add('animate-pulse');
// Visual pop on winning cells
path.forEach(pos => {
const cell = document.querySelector(`.cell[data-r='${pos.r}'][data-c='${pos.c}']`);
cell.classList.add('bg-white/20');
});
},
restart: () => {
document.getElementById('modal-gameover').classList.add('hidden');
document.getElementById('win-line').classList.add('hidden');
game.start(game.size, game.mode);
},
// --- AI Logic ---
aiMove: () => {
if (!game.active) return;
const size = game.size;
// 1. Easy: Random
if (game.mode === 'expanded' || Math.random() < 0.2) { // Heuristic for Expanded
game.aiHeuristic();
return;
}
// 2. Hard: Minimax (for 3x3 only due to performance)
if (size === 3 && (game.mode === 'classic' || game.mode === 'time')) {
const bestMove = game.minimax(game.board, 'O').index;
if (bestMove) game.placeMark(bestMove.r, bestMove.c, 'O');
else game.aiHeuristic(); // Fallback
} else {
game.aiHeuristic();
}
},
aiHeuristic: () => {
// Prioritize: Win > Block > Center > Random
const size = game.size;
let available = [];
for(let r=0; r<size; r++)
for(let c=0; c<size; c++)
if(game.board[r][c] === null) available.push({r,c});
if(available.length === 0) return;
// Try to win
for(let move of available) {
game.board[move.r][move.c] = 'O';
if(game.checkWin('O')) {
game.board[move.r][move.c] = null;
game.placeMark(move.r, move.c, 'O');
return;
}
game.board[move.r][move.c] = null;
}
// Try to block
for(let move of available) {
game.board[move.r][move.c] = 'X';
if(game.checkWin('X')) {
game.board[move.r][move.c] = null;
game.placeMark(move.r, move.c, 'O');
return;
}
game.board[move.r][move.c] = null;
}
// Random move
const move = available[Math.floor(Math.random() * available.length)];
game.placeMark(move.r, move.c, 'O');
},
minimax: (newBoard, player) => {
const availSpots = [];
for(let r=0; r<game.size; r++)
for(let c=0; c<game.size; c++)
if(newBoard[r][c] === null) availSpots.push({r,c});
if (game.checkWinBoard(newBoard, 'X')) return { score: -10 };
if (game.checkWinBoard(newBoard, 'O')) return { score: 10 };
if (availSpots.length === 0) return { score: 0 };
const moves = [];
for (let i = 0; i < availSpots.length; i++) {
const move = availSpots[i];
newBoard[move.r][move.c] = player;
const result = game.minimax(newBoard, player === 'O' ? 'X' : 'O');
move.score = result.score;
newBoard[move.r][move.c] = null;
moves.push(move);
}
let bestMove;
if (player === 'O') {
let bestScore = -10000;
for (let i = 0; i < moves.length; i++) {
if (moves[i].score > bestScore) {
bestScore = moves[i].score;
bestMove = i;
}
}
} else {
let bestScore = 10000;
for (let i = 0; i < moves.length; i++) {
if (moves[i].score < bestScore) {
bestScore = moves[i].score;
bestMove = i;
}
}
}
return moves[bestMove];
},
checkWinBoard: (board, player) => {
// Helper to check win on a temp board
const original = game.board;
game.board = board;
const res = game.checkWin(player) !== null;
game.board = original;
return res;
}
};
// Initialize App
window.addEventListener('DOMContentLoaded', app.init);
</script>
<script src="https://huggingface.co/deepsite/deepsite-badge.js"></script>
</body>
</html>