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
