Spaces:
Running
Running
| (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); | |
| })(); | |