Spaces:
Running
Running
| <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) ; | |
| border-color: #38bdf8 ; | |
| box-shadow: inset 0 0 10px #38bdf8; | |
| } | |
| .clear-target { | |
| cursor: crosshair ; | |
| 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> |