Show sourcecode
The following files exists in this folder. Click to view.
common.js
maze.js
reaction.js
simon.js
simon.js
190 lines UTF-8 Unix (LF)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
'use strict';
(function() {
const pid = getParticipantId();
const levelEl = document.getElementById('level');
const statusEl = document.getElementById('status');
const buttons = document.querySelectorAll('.simon-btn');
// Audio context for tones
let audioCtx = null;
const FREQUENCIES = [261.63, 329.63, 392.00, 523.25]; // C4, E4, G4, C5
function playTone(index, duration) {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.value = FREQUENCIES[index];
gain.gain.value = 0.3;
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration / 1000);
osc.stop(audioCtx.currentTime + duration / 1000);
}
// Game state
let sequence = [];
let inputIndex = 0;
let level = 0;
let roundData = [];
let responseTimes = [];
let lastPressTime = 0;
let gameStartTime = 0;
let processing = false; // guard against double-clicks
let state = 'IDLE'; // IDLE | SHOWING | AWAITING | GAME_OVER
function lightUp(index, duration) {
return new Promise(function(resolve) {
buttons[index].classList.add('lit');
playTone(index, duration);
setTimeout(function() {
buttons[index].classList.remove('lit');
setTimeout(resolve, 200); // gap between flashes
}, duration);
});
}
async function showSequence() {
state = 'SHOWING';
statusEl.textContent = 'Titta på sekvensen...';
setButtonsDisabled(true);
// Brief pause before starting playback
await sleep(600);
for (let i = 0; i < sequence.length; i++) {
await lightUp(sequence[i], 500);
}
// Now it's the player's turn
state = 'AWAITING';
inputIndex = 0;
responseTimes = [];
lastPressTime = performance.now();
statusEl.textContent = 'Din tur!';
setButtonsDisabled(false);
}
function setButtonsDisabled(disabled) {
buttons.forEach(function(btn) {
if (disabled) {
btn.classList.add('disabled');
} else {
btn.classList.remove('disabled');
}
});
}
function sleep(ms) {
return new Promise(function(resolve) { setTimeout(resolve, ms); });
}
function nextLevel() {
level++;
levelEl.textContent = level;
// Add a random color to the sequence
sequence.push(Math.floor(Math.random() * 4));
showSequence();
}
async function gameOver() {
state = 'GAME_OVER';
setButtonsDisabled(true);
// Record the failed round
roundData.push({
level_number: level,
success: 0,
response_times: responseTimes.slice()
});
const totalTimeMs = Math.round(performance.now() - gameStartTime);
const maxLevel = level - 1; // last successfully completed level
statusEl.textContent = 'Spelet slut! Du nådde nivå ' + maxLevel + '. Sparar resultat...';
// Store for results page
sessionStorage.setItem('simon_level', maxLevel);
sessionStorage.setItem('simon_time', totalTimeMs);
try {
await apiPost('../api/save_simon.php', {
participant_id: pid,
max_level: maxLevel,
total_time_ms: totalTimeMs,
rounds: roundData
});
} catch (e) {
console.error('Failed to save Simon results:', e);
}
setTimeout(function() {
window.location.href = '../results.php';
}, 2000);
}
function handleButtonClick(index) {
if (state !== 'AWAITING' || processing) return;
processing = true;
const now = performance.now();
responseTimes.push(Math.round(now - lastPressTime));
lastPressTime = now;
// Flash the button briefly
buttons[index].classList.add('lit');
playTone(index, 200);
setTimeout(function() {
buttons[index].classList.remove('lit');
processing = false; // allow next click after animation
}, 200);
if (index === sequence[inputIndex]) {
// Correct
inputIndex++;
if (inputIndex === sequence.length) {
// Level complete
setButtonsDisabled(true);
roundData.push({
level_number: level,
success: 1,
response_times: responseTimes.slice()
});
statusEl.textContent = 'Nivå ' + level + ' klar!';
setTimeout(nextLevel, 1000);
}
} else {
// Wrong — game over
processing = true; // keep locked during game over
buttons[index].style.outline = '4px solid #dc2626';
setTimeout(function() { buttons[index].style.outline = ''; }, 500);
gameOver();
}
}
// Attach click handlers
buttons.forEach(function(btn) {
btn.addEventListener('click', function() {
handleButtonClick(parseInt(btn.dataset.index, 10));
});
});
// Instruction overlay — wait for dismiss before starting game
function startGame() {
gameStartTime = performance.now();
nextLevel();
}
const overlay = document.getElementById('simon-instructions-overlay');
if (overlay) {
document.getElementById('simon-start-btn').addEventListener('click', function() {
overlay.style.display = 'none';
startGame();
});
} else {
startGame();
}
})();