Show sourcecode
The following files exists in this folder. Click to view.
common.js
maze.js
reaction.js
simon.js
maze.js
278 lines UTF-8 Unix (LF)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
'use strict';
(function() {
const pid = getParticipantId();
const canvas = document.getElementById('maze-canvas');
const ctx = canvas.getContext('2d');
const timerEl = document.getElementById('timer');
const movesEl = document.getElementById('moves');
const instructionsEl = document.getElementById('instructions');
const overlay = document.getElementById('maze-instructions-overlay');
const ROWS = 15;
const COLS = 15;
const CELL = 30; // pixels per cell
// Wall bitmask: 1=top, 2=right, 4=bottom, 8=left
// Fixed 15x15 maze generated via Prim's algorithm (75 dead ends).
// Start: (0,0) top-left. End: (14,14) bottom-right.
const MAZE = [
[13, 1, 7, 11, 11, 9, 5, 5, 1, 7, 9, 5, 1, 1, 3],
[13, 0, 7, 10, 8, 4, 3, 11, 8, 1, 0, 7, 14, 10, 10],
[9, 4, 5, 0, 6, 11, 8, 2, 14, 14, 8, 7, 11, 14, 14],
[8, 3, 11, 12, 1, 6, 14, 12, 3, 11, 12, 5, 4, 1, 7],
[14, 8, 4, 3, 12, 7, 9, 7, 14, 8, 7, 13, 3, 12, 3],
[9, 4, 7, 8, 5, 5, 0, 5, 1, 4, 3, 9, 0, 7, 14],
[8, 5, 3, 8, 5, 3, 12, 3, 12, 7, 14, 10, 14, 11, 11],
[8, 7, 10, 8, 3, 8, 7, 12, 1, 1, 1, 0, 1, 4, 6],
[8, 7, 10, 10, 10, 14, 9, 3, 14, 10, 10, 14, 8, 5, 3],
[8, 7, 14, 10, 8, 5, 2, 14, 9, 2, 12, 3, 14, 11, 10],
[8, 1, 7, 10, 14, 13, 0, 7, 14, 12, 3, 12, 1, 6, 10],
[10, 12, 7, 8, 7, 11, 10, 9, 7, 13, 4, 7, 10, 9, 6],
[12, 1, 3, 8, 7, 12, 0, 0, 1, 1, 1, 7, 10, 8, 7],
[9, 2, 14, 12, 1, 7, 14, 10, 14, 10, 12, 3, 14, 8, 7],
[14, 14, 13, 5, 4, 5, 7, 12, 7, 14, 13, 4, 7, 12, 7]
];
// Key position — placed in the top-right area to force a detour
const KEY_X = 13;
const KEY_Y = 2;
// Player state
let playerX = 0;
let playerY = 0;
let moveCount = 0;
let hasKey = false;
let path = [{x: 0, y: 0, t: 0}];
let startTime = 0;
let timerInterval = null;
let started = false;
let finished = false;
let gameActive = false; // false until instructions dismissed
// Hold-to-move: track which keys are held and use an interval
const keysHeld = {};
const MOVE_INTERVAL = 120; // ms between moves when holding
let moveIntervalId = null;
function hasWall(row, col, direction) {
if (row < 0 || row >= ROWS || col < 0 || col >= COLS) return true;
return (MAZE[row][col] & direction) !== 0;
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = '#1a1a2e';
ctx.lineWidth = 2;
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const x = c * CELL;
const y = r * CELL;
const walls = MAZE[r][c];
ctx.beginPath();
if (walls & 1) {
ctx.moveTo(x, y);
ctx.lineTo(x + CELL, y);
}
if (walls & 2) {
ctx.moveTo(x + CELL, y);
ctx.lineTo(x + CELL, y + CELL);
}
if (walls & 4) {
ctx.moveTo(x, y + CELL);
ctx.lineTo(x + CELL, y + CELL);
}
if (walls & 8) {
ctx.moveTo(x, y);
ctx.lineTo(x, y + CELL);
}
ctx.stroke();
}
}
// Start cell (green)
ctx.fillStyle = 'rgba(22, 163, 74, 0.3)';
ctx.fillRect(0, 0, CELL, CELL);
// End cell — red if locked, green if unlocked
if (hasKey) {
ctx.fillStyle = 'rgba(22, 163, 74, 0.3)';
} else {
ctx.fillStyle = 'rgba(220, 38, 38, 0.15)';
}
ctx.fillRect(14 * CELL, 14 * CELL, CELL, CELL);
// Draw lock icon on exit when locked
if (!hasKey) {
ctx.fillStyle = '#dc2626';
ctx.font = 'bold 18px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('\u{1F512}', 14 * CELL + CELL / 2, 14 * CELL + CELL / 2);
}
// Draw key if not yet collected
if (!hasKey) {
ctx.fillStyle = '#eab308';
ctx.font = 'bold 18px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('\u{1F511}', KEY_X * CELL + CELL / 2, KEY_Y * CELL + CELL / 2);
}
// Player
ctx.fillStyle = '#4361ee';
ctx.beginPath();
ctx.arc(playerX * CELL + CELL / 2, playerY * CELL + CELL / 2, CELL / 3, 0, Math.PI * 2);
ctx.fill();
}
function updateHUD() {
movesEl.textContent = moveCount;
}
function startTimer() {
if (started) return;
started = true;
startTime = performance.now();
timerInterval = setInterval(function() {
const elapsed = ((performance.now() - startTime) / 1000).toFixed(1);
timerEl.textContent = elapsed + 's';
}, 100);
}
async function finishMaze() {
finished = true;
clearInterval(timerInterval);
clearInterval(moveIntervalId);
const totalMs = Math.round(performance.now() - startTime);
timerEl.textContent = (totalMs / 1000).toFixed(1) + 's';
instructionsEl.textContent = 'Labyrint klar! Går vidare till nästa test...';
sessionStorage.setItem('maze_time', totalMs);
sessionStorage.setItem('maze_moves', moveCount);
try {
await apiPost('../api/save_maze.php', {
participant_id: pid,
total_time_ms: totalMs,
path_json: JSON.stringify(path),
total_moves: moveCount
});
} catch (e) {
console.error('Failed to save maze results:', e);
}
setTimeout(function() {
window.location.href = 'simon.php';
}, 2000);
}
function tryMove(dx, dy) {
if (finished || !gameActive) return;
startTimer();
const newX = playerX + dx;
const newY = playerY + dy;
if (newX < 0 || newX >= COLS || newY < 0 || newY >= ROWS) return;
let wallBit = 0;
if (dx === 1) wallBit = 2;
if (dx === -1) wallBit = 8;
if (dy === -1) wallBit = 1;
if (dy === 1) wallBit = 4;
if (hasWall(playerY, playerX, wallBit)) return;
playerX = newX;
playerY = newY;
moveCount++;
path.push({x: playerX, y: playerY, t: Math.round(performance.now() - startTime)});
// Check if player picked up the key
if (!hasKey && playerX === KEY_X && playerY === KEY_Y) {
hasKey = true;
instructionsEl.textContent = 'Nyckel hämtad! Ta dig nu till utgången (nere till höger).';
}
updateHUD();
draw();
// Check win — only if key is collected
if (hasKey && playerX === 14 && playerY === 14) {
finishMaze();
}
}
function getDirection(key) {
switch (key) {
case 'ArrowUp': case 'w': case 'W': return [0, -1];
case 'ArrowDown': case 's': case 'S': return [0, 1];
case 'ArrowLeft': case 'a': case 'A': return [-1, 0];
case 'ArrowRight': case 'd': case 'D': return [1, 0];
default: return null;
}
}
function processHeldKeys() {
for (const key in keysHeld) {
if (keysHeld[key]) {
const dir = getDirection(key);
if (dir) tryMove(dir[0], dir[1]);
}
}
}
document.addEventListener('keydown', function(e) {
if (finished || !gameActive) return;
const dir = getDirection(e.key);
if (!dir) return;
e.preventDefault();
if (!keysHeld[e.key]) {
keysHeld[e.key] = true;
// Immediate move on first press
tryMove(dir[0], dir[1]);
// Start repeat interval if not already running
if (!moveIntervalId) {
moveIntervalId = setInterval(processHeldKeys, MOVE_INTERVAL);
}
}
});
document.addEventListener('keyup', function(e) {
delete keysHeld[e.key];
// Stop interval when no keys are held
if (Object.keys(keysHeld).length === 0 && moveIntervalId) {
clearInterval(moveIntervalId);
moveIntervalId = null;
}
});
// Instruction overlay dismiss
if (overlay) {
overlay.addEventListener('click', function() {
overlay.style.display = 'none';
gameActive = true;
});
document.addEventListener('keydown', function handler(e) {
if (!gameActive && overlay.style.display !== 'none') {
overlay.style.display = 'none';
gameActive = true;
document.removeEventListener('keydown', handler);
}
});
} else {
gameActive = true;
}
// Initial draw
draw();
updateHUD();
})();