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