概要
脳トレアプリとして有名なDual N Backをできる簡易アプリを作成してみました。
コード全体
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dual N-Back Game</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
margin: 0;
padding: 0;
}
#game-board {
display: grid;
grid-template-columns: repeat(3, 100px);
grid-template-rows: repeat(3, 100px);
gap: 10px;
justify-content: center;
margin: 20px auto;
}
.cell {
width: 100px;
height: 100px;
background-color: lightgray;
border: 1px solid #ccc;
}
.cell.active {
background-color: yellow;
}
button {
padding: 10px 20px;
font-size: 16px;
margin: 10px;
}
#feedback {
font-size: 20px;
color: red;
margin: 10px;
}
</style>
</head>
<body>
<h1>Dual N-Back Game</h1>
<div>
<label for="n-value">N値を設定:</label>
<input type="number" id="n-value" value="2" min="1" style="width: 50px;">
</div>
<div id="game-board"></div>
<button id="start-button">スタート</button>
<button id="stop-button" disabled>ストップ</button>
<button id="panel-match-button" disabled>パネル Match!</button>
<button id="audio-match-button" disabled>音声 Match!</button>
<div id="feedback"></div>
<div>
<p>正解数: <span id="correct-count">0</span></p>
<p>不正解数: <span id="wrong-count">0</span></p>
</div>
<script>
const board = document.getElementById("game-board");
const startButton = document.getElementById("start-button");
const stopButton = document.getElementById("stop-button");
const panelMatchButton = document.getElementById("panel-match-button");
const audioMatchButton = document.getElementById("audio-match-button");
const feedback = document.getElementById("feedback");
const correctCountEl = document.getElementById("correct-count");
const wrongCountEl = document.getElementById("wrong-count");
const nInput = document.getElementById("n-value");
const cells = [];
const alphabet = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'];
let n = 2;
let sequence = [];
let audioSequence = [];
let currentStep = 0;
let panelMatchHappened = false;
let audioMatchHappened = false;
let activeIndex = -1;
let gameInterval;
let correctCount = 0;
let wrongCount = 0;
let displayMessage = '';
// ゲームボードの初期化
for (let i = 0; i < 9; i++) {
const cell = document.createElement("div");
cell.classList.add("cell");
board.appendChild(cell);
cells.push(cell);
}
function highlightCell(index) {
cells.forEach((cell, i) => {
cell.classList.toggle("active", i === index);
});
}
function speakText(text) {
const utterance = new SpeechSynthesisUtterance(text);
speechSynthesis.speak(utterance);
}
function displayFeedback(message, isCorrect = null) {
feedback.textContent = message;
feedback.style.color = isCorrect === null ? 'red' : isCorrect ? 'green' : 'red';
setTimeout(() => {
feedback.textContent = '';
}, 1000); // 1秒後にフィードバックを消去
}
function startGame() {
n = parseInt(nInput.value) || 2;
resetGame();
sequence = [];
audioSequence = [];
currentStep = 0;
gameInterval = setInterval(nextStep, 4000); // 3秒の間隔
startButton.disabled = true;
stopButton.disabled = false;
panelMatchButton.disabled = false;
audioMatchButton.disabled = false;
feedback.textContent = "ゲーム開始!";
}
function stopGame() {
clearInterval(gameInterval);
startButton.disabled = false;
stopButton.disabled = true;
panelMatchButton.disabled = true;
audioMatchButton.disabled = true;
feedback.textContent = "ゲーム終了!";
highlightCell(-1);
}
function resetGame() {
correctCount = 0;
wrongCount = 0;
correctCountEl.textContent = correctCount;
wrongCountEl.textContent = wrongCount;
}
function nextStep() {
if (currentStep >= sequence.length) {
let nextIndex;
let audio_nextIndex;
if (currentStep >= n && Math.random() < 0.3) { // 1/3の確率でMatchを発生
nextIndex = sequence[currentStep - n];
panelMatchHappened = true;
} else {
nextIndex = Math.floor(Math.random() * cells.length);
panelMatchHappened = false;
}
if (currentStep >= n && Math.random() < 0.3) { // 1/3の確率でMatchを発生
audio_nextIndex = audioSequence[currentStep - n];
audioMatchHappened = true;
} else {
audio_nextIndex = alphabet[Math.floor(Math.random() * alphabet.length)];
audioMatchHappened = false;
}
sequence.push(nextIndex);
audioSequence.push(audio_nextIndex); // ランダムなアルファベットを音声配列に追加
}
activeIndex = sequence[currentStep];
highlightCell(activeIndex);
speakText(audioSequence[currentStep]); // 音声を再生
setTimeout(() => {
highlightCell(-1);
// パネルが表示された後、マッチを見逃した場合の判定
if (panelMatchHappened) {
displayMessage += "マッチを見逃しました!";
wrongCount++;
wrongCountEl.textContent = wrongCount;
panelMatchHappened = false; // リセット
}
if (audioMatchHappened) {
displayMessage +="音声Matchを見逃しました!";
wrongCount++;
wrongCountEl.textContent = wrongCount;
audioMatchHappened = false; // リセット
}
displayFeedback(displayMessage);
displayMessage = "";
}, 3000); // パネルが1秒後に消える
currentStep++;
}
panelMatchButton.addEventListener("click", () => {
if (panelMatchHappened) {
displayFeedback("パネル正解!", true);
correctCount++;
correctCountEl.textContent = correctCount;
panelMatchHappened = false; // 正解時にリセット
} else {
displayFeedback("パネル間違い!");
wrongCount++;
wrongCountEl.textContent = wrongCount;
}
});
audioMatchButton.addEventListener("click", () => {
if (audioMatchHappened) {
displayFeedback("音声正解!", true);
correctCount++;
correctCountEl.textContent = correctCount;
audioMatchHappened = false; // 正解時にリセット
} else {
displayFeedback("音声間違い!");
wrongCount++;
wrongCountEl.textContent = wrongCount;
}
});
startButton.addEventListener("click", startGame);
stopButton.addEventListener("click", stopGame);
</script>
</body>
</html>
ポイント
- タイルのマッチだけではなく、
SpeechSynthesisUtterance
を使うことで音声に対応してDual N Backにした - N部分は動的設定に対応
- そのままだとマッチ確率が低いのでマッチ確率を30%に設定