Webbserver - Love Blomberg

Show sourcecode

The following files exists in this folder. Click to view.

public_html/GYA2/js/

common.js
maze.js
reaction.js
simon.js

maze.js

278 lines UTF-8 Unix (LF)
'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();
})();