Tic Tac Toe Script

<script> /****************************** * Game logic (same as before) ******************************/ const cells = Array.from(document.querySelectorAll('[data-cell]')); const statusDisplay = document.getElementById('status'); const resetButton = document.getElementById('reset-button'); const xScoreDisplay = document.getElementById('x-score'); const oScoreDisplay = document.getElementById('o-score'); const tiesDisplay = document.getElementById('ties'); const modeHuman = document.getElementById('modeHuman'); const modeComputer = document.getElementById('modeComputer'); const historyBtn = document.getElementById('historyBtn'); const overlay = document.getElementById('overlay'); const historyListEl = document.getElementById('historyList'); const closeHistory = document.getElementById('closeHistory'); const clearHistory = document.getElementById('clearHistory'); const congratsEl = document.getElementById('congrats'); const congratsName = document.getElementById('congratsName'); const congratsTitle = document.getElementById('congratsTitle'); let currentPlayer = 'X'; let gameActive = true; let gameState = Array(9).fill(''); let scores = { X: 0, O: 0, ties: 0 }; let history = []; // e.g. [{result:'X',time:'...'}] const winningConditions = [ [0,1,2],[3,4,5],[6,7,8], [0,3,6],[1,4,7],[2,5,8], [0,4,8],[2,4,6] ]; // add click events cells.forEach((cell, idx) => { cell.addEventListener('click', () => playerMove(cell, idx)); }); resetButton.addEventListener('click', resetGame); historyBtn.addEventListener('click', showHistory); closeHistory.addEventListener('click', () => overlay.style.display='none'); clearHistory.addEventListener('click', () => { history=[]; renderHistory(); }); overlay.addEventListener('click', (e)=>{ if(e.target===overlay) overlay.style.display='none'; }); function playerMove(cell, idx){ if (!gameActive || gameState[idx] !== '') return; if (modeComputer.checked && currentPlayer !== 'X') return; // only allow human moves when it's X's turn (computer is O) placeMove(cell, idx, currentPlayer); afterMove(); } function placeMove(cell, idx, player){ gameState[idx] = player; cell.textContent = player; cell.classList.add(player.toLowerCase()); } function afterMove(){ checkResult(); if (!gameActive) return; currentPlayer = (currentPlayer === 'X') ? 'O' : 'X'; statusDisplay.textContent = `${currentPlayer}'s turn`; if (modeComputer.checked && currentPlayer === 'O' && gameActive){ setTimeout(() => computerMove(), 400); } } function computerMove(){ const emptyIdx = gameState.map((v,i)=> v === '' ? i : null).filter(i=>i!==null); for (let i of emptyIdx){ const copy = [...gameState]; copy[i] = 'O'; if (isWinning(copy, 'O')) { clickIndex(i); return; } } for (let i of emptyIdx){ const copy = [...gameState]; copy[i] = 'X'; if (isWinning(copy, 'X')) { clickIndex(i); return; } } if (gameState[4] === '') { clickIndex(4); return; } const corners = [0,2,6,8].filter(i => gameState[i] === ''); if (corners.length){ clickIndex(randomChoice(corners)); return; } if (emptyIdx.length) clickIndex(randomChoice(emptyIdx)); } function clickIndex(i){ const cell = cells[i]; if (!cell) return; placeMove(cell, i, 'O'); afterMove(); } function randomChoice(arr){ return arr[Math.floor(Math.random()*arr.length)]; } function isWinning(state, player){ return winningConditions.some(([a,b,c]) => state[a]===player && state[b]===player && state[c]===player); } function checkResult(){ let roundWon = null; for (const [a,b,c] of winningConditions){ if (gameState[a] && gameState[a] === gameState[b] && gameState[b] === gameState[c]){ roundWon = gameState[a]; break; } } if (roundWon){ gameActive = false; scores[roundWon] += 1; statusDisplay.textContent = `${roundWon} wins!`; updateScores(); pushHistory(roundWon); setTimeout(()=> { resetBoardForNextRound(); }, 800); return; } if (!gameState.includes('')){ gameActive = false; scores.ties += 1; statusDisplay.textContent = `Draw`; updateScores(); pushHistory('Tie'); setTimeout(()=> resetBoardForNextRound(), 800); } } function resetBoardForNextRound(){ currentPlayer = 'X'; gameActive = true; gameState = Array(9).fill(''); cells.forEach(c => { c.textContent=''; c.classList.remove('x','o'); }); statusDisplay.textContent = `${currentPlayer}'s turn`; } function updateScores(){ xScoreDisplay.textContent = scores.X; oScoreDisplay.textContent = scores.O; tiesDisplay.textContent = scores.ties; } function resetGame(){ currentPlayer = 'X'; gameActive = true; gameState = Array(9).fill(''); history = []; scores = { X:0, O:0, ties:0 }; cells.forEach(c => { c.textContent=''; c.classList.remove('x','o'); }); updateScores(); renderHistory(); statusDisplay.textContent = `${currentPlayer}'s turn`; } function pushHistory(result){ const time = new Date().toLocaleString(); const item = { result, time }; history.unshift(item); if (history.length > 50) history.pop(); renderHistory(); if (history.length >= 3){ const latest3 = history.slice(0,3).map(h => h.result); if (latest3[0] === latest3[1] && latest3[1] === latest3[2] && latest3[0] !== 'Tie'){ showCongrats(latest3[0]); } } } function renderHistory(){ historyListEl.innerHTML = ''; if (!history.length){ historyListEl.innerHTML = '<div style="padding:12px;color:#64748b">No games yet</div>'; return; } history.forEach(h => { const div = document.createElement('div'); div.className = 'history-item'; const who = h.result === 'Tie' ? 'Tie' : `Player ${h.result}`; div.innerHTML = `<span>${who}</span><span style="opacity:.7;font-size:.9rem">${h.time}</span>`; historyListEl.appendChild(div); }); } function showHistory(){ renderHistory(); overlay.style.display='flex'; } function showCongrats(winner){ congratsName.textContent = `Player ${winner}`; congratsTitle.textContent = `3 wins in a row!`; congratsEl.style.display = 'flex'; createConfetti(28); setTimeout(()=> { congratsEl.style.display='none'; congratsEl.querySelectorAll('.confetti').forEach(n=>n.remove()); }, 2200); } function createConfetti(n){ const colors = ['#ef4444','#f97316','#f59e0b','#10b981','#06b6d4','#2563eb','#7c3aed']; for (let i=0;i<n;i++){ const el = document.createElement('div'); el.className = 'confetti'; el.style.left = Math.random()*100 + 'vw'; el.style.background = colors[i % colors.length]; el.style.width = (6 + Math.random()*10) + 'px'; el.style.height = (12 + Math.random()*22) + 'px'; el.style.transform = `translateY(-300px) rotate(${Math.random()*360}deg)`; el.style.animationDuration = (1200 + Math.random()*1000) + 'ms'; congratsEl.appendChild(el); } } updateScores(); renderHistory(); // keyboard accessibility document.addEventListener('keydown', (e) => { if (e.key.toLowerCase() === 'r') resetGame(); if (e.key === ' ') { modeComputer.checked = !modeComputer.checked; modeHuman.checked = !modeComputer.checked; } }); /****************************************************** * Local "AI" Assistant (no API) — knowledge-based * - exact matches * - keyword scoring * - fallback generator using project description * - teachable by user ******************************************************/ const assistantToggle = document.getElementById('assistantToggle'); const assistantPanel = document.getElementById('assistantPanel'); const assistantBody = document.getElementById('assistantBody'); const assistantInput = document.getElementById('assistantInput'); const askBtn = document.getElementById('askBtn'); const closeAssistant = document.getElementById('closeAssistant'); const projectDescEl = document.getElementById('projectDescription'); const teachBtn = document.getElementById('teachBtn'); const kbQInput = document.getElementById('kbQ'); const kbAInput = document.getElementById('kbA'); const teachQuick = document.getElementById('teachQuick'); const saveKbBtn = document.getElementById('saveKbBtn'); // small local KB (starts with some entries) let knowledgeBase = [ { q: 'how to play', a: 'Click any empty cell to place your mark. Get three in a row to win.' }, { q: 'what is this project', a: 'This is a browser Tic Tac Toe game with a local AI and history.' }, { q: 'how to play vs computer', a: 'Select "Human vs Computer". You play X and the computer responds as O.' }, { q: 'who built this', a: 'The game is built as a single-page web app and does not require any server.' } ]; // load persisted KB + project description from localStorage (so it survives reload) const STORAGE_KEY = 'local-assistant-v1'; function loadAssistantStorage(){ try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return; const parsed = JSON.parse(raw); if (parsed.kb) knowledgeBase = parsed.kb; if (parsed.projectDescription) projectDescEl.value = parsed.projectDescription; renderAssistantMessage({from:'bot', text: 'Assistant ready. Ask me anything about the project.'}); } catch(e){ console.warn('load err', e) } } function saveAssistantStorage(){ const payload = { kb: knowledgeBase, projectDescription: projectDescEl.value }; localStorage.setItem(STORAGE_KEY, JSON.stringify(payload)); renderAssistantMessage({from:'bot', text: 'Knowledge base saved locally.'}); } saveKbBtn.addEventListener('click', saveAssistantStorage); loadAssistantStorage(); assistantToggle.addEventListener('click', () => { if (assistantPanel.style.display === 'flex') assistantPanel.style.display = 'none'; else assistantPanel.style.display = 'flex'; }); closeAssistant.addEventListener('click', () => assistantPanel.style.display = 'none'); askBtn.addEventListener('click', () => { const q = assistantInput.value.trim(); if (!q) return; processQuestion(q); assistantInput.value = ''; }); assistantInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { askBtn.click(); } }); teachBtn.addEventListener('click', () => { const q = kbQInput.value.trim(); const a = kbAInput.value.trim(); if (!q || !a){ renderAssistantMessage({from:'bot', text:'Provide both a question and an answer to teach.'}); return; } knowledgeBase.unshift({ q: q.toLowerCase(), a }); kbQInput.value = ''; kbAInput.value = ''; renderAssistantMessage({from:'bot', text: 'Learned: "' + q + '" → "' + a + '"' }); }); // quick teach: use last user message and the current assistant input to create KB teachQuick.addEventListener('click', () => { // find last user message in DOM const lastUser = Array.from(assistantBody.querySelectorAll('.from-user')).pop(); const lastText = lastUser ? lastUser.textContent : ''; const answer = assistantInput.value.trim(); if (!lastText || !answer){ renderAssistantMessage({from:'bot', text:'No recent user message to teach or empty answer.'}); return; } knowledgeBase.unshift({ q: lastText.toLowerCase(), a: answer }); assistantInput.value = ''; renderAssistantMessage({from:'bot', text:'Taught answer for: "' + lastText + '"' }); }); function processQuestion(rawQ){ const q = rawQ.trim(); renderAssistantMessage({from:'user', text: q}); const response = answerQuestion(q); // emulate "thinking" delay setTimeout(()=> renderAssistantMessage({from:'bot', text: response}), 250 + Math.random()*400); } function renderAssistantMessage({from, text}){ const div = document.createElement('div'); div.className = 'chat-bubble ' + (from === 'user' ? 'from-user' : 'from-bot'); div.textContent = text; assistantBody.appendChild(div); assistantBody.scrollTop = assistantBody.scrollHeight; } // Answer pipeline function answerQuestion(q){ const ql = q.toLowerCase(); // 1) Direct exact match in KB for (const entry of knowledgeBase){ if (entry.q === ql) return entry.a; } // 2) Simple fuzzy matching: exact words overlap scoring const qWords = tokenize(ql); let best = {score:0, entry:null}; for (const entry of knowledgeBase){ const entryWords = tokenize(entry.q); const score = overlapScore(qWords, entryWords); if (score > best.score){ best = {score, entry}; } } if (best.score >= 0.45 && best.entry) return best.entry.a + ' (from KB)'; // 3) Keyword rules for common questions if (/project|about this|what is this|tell me about/i.test(q)){ // use project description to craft answer const p = projectDescEl.value.trim(); if (p) return generateFromProjectDescription(q, p); } if (/how to play|rules|win|three in a row/i.test(q)) return 'You place X or O in empty squares. First to make three in a row horizontally, vertically or diagonally wins. You can play vs computer or vs a friend.'; if (/computer|bot|ai|opponent/i.test(q)) return 'The local computer opponent uses a simple strategy (win → block → center → corner → random). It runs entirely in your browser, no API needed.'; if (/who built|author|creator/i.test(q)) return 'This single-file web app was created to run locally in the browser. You can edit and teach the assistant via the side panel.'; // 4) fallback: short generative-style answer combining project description and the question const p = projectDescEl.value.trim(); if (p) return generateFromProjectDescription(q, p); // 5) final fallback return "I don't know that yet. You can teach me by adding a Q&A in the Project Info side panel."; } // tokenize + simple score function tokenize(s){ return s.split(/\W+/).filter(Boolean).map(w => w.toLowerCase()); } function overlapScore(aWords, bWords){ if (!aWords.length || !bWords.length) return 0; const aSet = new Set(aWords); let hits = 0; for (const w of bWords) if (aSet.has(w)) hits++; return hits / Math.max(aWords.length, bWords.length); } // tiny template generator using the project description: picks sentences that match keywords function generateFromProjectDescription(q, projectText){ const sentences = projectText.split(/(?<=[.?!])\s+/).filter(Boolean); const qWords = tokenize(q); // score sentences by keyword overlap let scored = sentences.map(s => ({ s, score: overlapScore(qWords, tokenize(s)) })); scored.sort((a,b)=>b.score - a.score); if (scored[0] && scored[0].score > 0) { // produce short answer by combining top 1-2 sentences const top = scored.slice(0,2).map(x=>x.s).join(' '); return top; } // otherwise, produce a short summary + invite to teach return projectText.split('.').slice(0,2).join('. ').trim() + '. If you want more details, teach me with a question and answer in the side panel.'; } // allow saving KB externally (download) function downloadKB(){ const blob = new Blob([JSON.stringify({ kb: knowledgeBase, projectDescription: projectDescEl.value }, null, 2)], {type:'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'project-assistant-kb.json'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } // quick utilities in UI document.getElementById('teachBtn').addEventListener('click', () => {}); document.getElementById('askBtn').addEventListener('click', () => {}); document.getElementById('assistantToggle').addEventListener('click', () => {}); saveKbBtn.addEventListener('click', () => {}); // wire real events already added above // small UX: show welcome renderAssistantMessage({from:'bot', text: 'Hello! Ask me about the project, gameplay, or how the local bot works.'}); // Expose a tiny "assistant responder" so other parts of the page can ask programmatically window.projectAssistant = { ask: (q) => answerQuestion(q), teach: (q, a) => { knowledgeBase.unshift({q:q.toLowerCase(), a}); saveAssistantStorage(); }, getKB: () => knowledgeBase, setProjectDescription: (txt) => { projectDescEl.value = txt; saveAssistantStorage(); } }; // Persist assistant changes automatically when project description loses focus projectDescEl.addEventListener('blur', saveAssistantStorage); // Save on unload window.addEventListener('beforeunload', saveAssistantStorage); </script>

Public Last updated: 2026-08-25 08:38:17 AM