(function(){ const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const targetSpan = document.getElementById('target'); const startBtn = document.getElementById('startBtn'); const messageDiv = document.getElementById('message'); const modeRadios = document.querySelectorAll('input[name="mode"]'); const difficultyDiv = document.getElementById('difficultySelect'); const difficultyRadios = document.querySelectorAll('input[name="difficulty"]'); modeRadios.forEach(radio => { radio.addEventListener('change', () => { if (document.querySelector('input[name="mode"]:checked').value === 'vsAI') { difficultyDiv.style.display = 'block'; } else { difficultyDiv.style.display = 'none'; } }); }); const scoresDiv = document.getElementById('scores'); const playerScoreSpan = document.getElementById('playerScore'); const aiScoreSpan = document.getElementById('aiScore'); let gameMode = 'single'; let playerScore = 0; let aiScore = 0; let aiTimeout = null; let aiAccuracy; const AI_EASY_ACCURACY = 0.3; const AI_HARD_ACCURACY = 0.7; const aiMinDelay = 500; const aiMaxDelay = 2000; let items = []; let remaining = []; let currentTarget = null; const count = 30; const fontSize = 24; ctx.textBaseline = 'alphabetic'; ctx.font = fontSize + 'px sans-serif'; function isOverlapping(x, y, w, h, arr) { return arr.some(item => { return x < item.x + item.width && x + w > item.x && y - h < item.y && y > item.y - item.height; }); } function initGame() { items = []; messageDiv.textContent = ''; messageDiv.className = ''; targetSpan.textContent = '--'; gameMode = document.querySelector('input[name="mode"]:checked').value; if (gameMode === 'vsAI') { const difficulty = document.querySelector('input[name="difficulty"]:checked').value; aiAccuracy = difficulty === 'easy' ? AI_EASY_ACCURACY : AI_HARD_ACCURACY; playerScore = 0; aiScore = 0; playerScoreSpan.textContent = playerScore; aiScoreSpan.textContent = aiScore; scoresDiv.style.display = 'block'; } else { scoresDiv.style.display = 'none'; } if (aiTimeout) { clearTimeout(aiTimeout); aiTimeout = null; } for (let i = 1; i <= count; i++) { const text = i.toString(); const metrics = ctx.measureText(text); const width = metrics.width; const height = fontSize; let x, y, attempts = 0; do { x = Math.random() * (canvas.width - width); y = Math.random() * (canvas.height - height) + height; attempts++; if (attempts > 1000) break; } while (isOverlapping(x, y, width, height, items)); const angle = Math.random() * 2 * Math.PI; items.push({ num: i, x, y, width, height, angle }); } remaining = items.slice(); drawAll(); pickNext(); } function drawAll() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = '#000'; remaining.forEach(item => { ctx.save(); const cx = item.x + item.width / 2; const cy = item.y - item.height / 2; ctx.translate(cx, cy); ctx.rotate(item.angle); ctx.fillText(item.num, -item.width / 2, item.height / 2); ctx.restore(); }); } function pickNext() { if (remaining.length === 0) { currentTarget = null; targetSpan.textContent = '--'; messageDiv.className = 'clear'; if (gameMode === 'vsAI') { if (aiTimeout) { clearTimeout(aiTimeout); aiTimeout = null; } let resultText = ''; if (playerScore > aiScore) resultText = 'あなたの勝ち!'; else if (playerScore < aiScore) resultText = 'AIの勝ち!'; else resultText = '引き分け!'; messageDiv.textContent = 'ゲームクリア!結果: あなた ' + playerScore + ' - AI ' + aiScore + ' ' + resultText; } else { messageDiv.textContent = 'ゲームクリア!'; } return; } const idx = Math.floor(Math.random() * remaining.length); currentTarget = remaining[idx].num; targetSpan.textContent = currentTarget; if (gameMode === 'vsAI') { scheduleAIAttempt(); } } function repositionItems() { const newItems = []; remaining.forEach(orig => { const { num, width, height } = orig; let x, y, attempts = 0; do { x = Math.random() * (canvas.width - width); y = Math.random() * (canvas.height - height) + height; attempts++; if (attempts > 1000) break; } while (isOverlapping(x, y, width, height, newItems)); const angle = Math.random() * 2 * Math.PI; newItems.push({ num, x, y, width, height, angle }); }); return newItems; } function animateReposition(oldItems, newItems, duration, callback) { const startTime = performance.now(); function animate(time) { const t = Math.min((time - startTime) / duration, 1); remaining = oldItems.map((oldItem, i) => { const newItem = newItems[i]; return { num: oldItem.num, width: oldItem.width, height: oldItem.height, x: oldItem.x + (newItem.x - oldItem.x) * t, y: oldItem.y + (newItem.y - oldItem.y) * t, angle: oldItem.angle + (newItem.angle - oldItem.angle) * t }; }); drawAll(); if (t < 1) requestAnimationFrame(animate); else callback(); } requestAnimationFrame(animate); } function drawRedCircle(item) { const cx = item.x + item.width / 2; const cy = item.y - item.height / 2; const r = Math.max(item.width, item.height) / 2 + 5; ctx.strokeStyle = 'red'; ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(cx, cy, r, 0, 2 * Math.PI); ctx.stroke(); } function drawRedCross(item) { const x1 = item.x; const y1 = item.y - item.height; const x2 = item.x + item.width; const y2 = item.y; ctx.strokeStyle = 'red'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.moveTo(x1, y2); ctx.lineTo(x2, y1); ctx.stroke(); } function drawBlueCircle(item) { const cx = item.x + item.width / 2; const cy = item.y - item.height / 2; const r = Math.max(item.width, item.height) / 2 + 5; ctx.strokeStyle = 'blue'; ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(cx, cy, r, 0, 2 * Math.PI); ctx.stroke(); } function drawBlueCross(item) { const x1 = item.x; const y1 = item.y - item.height; const x2 = item.x + item.width; const y2 = item.y; ctx.strokeStyle = 'blue'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.moveTo(x1, y2); ctx.lineTo(x2, y1); ctx.stroke(); } function scheduleAIAttempt() { if (aiTimeout) clearTimeout(aiTimeout); const delay = aiMinDelay + Math.random() * (aiMaxDelay - aiMinDelay); aiTimeout = setTimeout(doAIAttempt, delay); } function doAIAttempt() { aiTimeout = null; if (gameMode !== 'vsAI' || currentTarget === null) return; const correct = Math.random() < aiAccuracy; if (correct) { const idx = remaining.findIndex(item => item.num === currentTarget); currentTarget = null; if (idx === -1) return; const item = remaining[idx]; aiScore++; aiScoreSpan.textContent = aiScore; messageDiv.className = 'correct'; messageDiv.textContent = 'AIが正解!'; drawAll(); drawBlueCircle(item); setTimeout(() => { remaining.splice(idx, 1); const oldItems = remaining.map(it => ({ ...it })); const newItems = repositionItems(); animateReposition(oldItems, newItems, 1000, () => { remaining = newItems; pickNext(); }); }, 500); } else { if (remaining.length > 1) { const wrongItems = remaining.filter(item => item.num !== currentTarget); const wrongItem = wrongItems[Math.floor(Math.random() * wrongItems.length)]; messageDiv.className = 'wrong'; messageDiv.textContent = 'AIが間違えた!'; drawAll(); drawBlueCross(wrongItem); setTimeout(drawAll, 500); } scheduleAIAttempt(); } } canvas.addEventListener('click', e => { if (currentTarget === null) return; const rect = canvas.getBoundingClientRect(); const clickX = e.clientX - rect.left; const clickY = e.clientY - rect.top; for (let i = 0; i < remaining.length; i++) { const item = remaining[i]; if (clickX >= item.x && clickX <= item.x + item.width && clickY <= item.y && clickY >= item.y - item.height) { if (item.num === currentTarget) { currentTarget = null; if (gameMode === 'vsAI') { if (aiTimeout) { clearTimeout(aiTimeout); aiTimeout = null; } playerScore++; playerScoreSpan.textContent = playerScore; } messageDiv.className = 'correct'; messageDiv.textContent = '正解!'; drawAll(); drawRedCircle(item); setTimeout(() => { remaining.splice(i, 1); const oldItems = remaining.map(it => ({ ...it })); const newItems = repositionItems(); animateReposition(oldItems, newItems, 1000, () => { remaining = newItems; pickNext(); }); }, 500); } else { messageDiv.className = 'wrong'; messageDiv.textContent = '違うよ!'; drawAll(); drawRedCross(item); setTimeout(drawAll, 500); } return; } } }); startBtn.addEventListener('click', initGame); })();