CardioQuest - ECG Scoring Game

Find abnormal ECG patterns! Avoid clicking on normal ECG.

ECG Waveform Game is an interactive challenge that tests your ability to identify abnormal electrocardiogram (ECG) patterns. In this game, you'll be presented with a continuous scrolling display of ECG waveforms on a canvas. Your task is to carefully observe the scrolling ECG patterns and quickly identify abnormal segments. Abnormal segments can include variations in the QRS complex, missing P waves, absent S waves, or no T waves. Normal segments will also be present. To tag an abnormal segment, click on it when it aligns with the cursor. However, be cautious not to click on normal segments, as this will result in a score deduction. Your score increases when you accurately tag an abnormal segment, but decreases when you mistakenly tag a normal segment. As you accumulate points, the speed of the scrolling waveforms will adjust, making the game progressively more challenging. Keep an eye on the 'Score' display to track your progress and aim for the highest score possible!

For a more advanced version of the game, check out CardioBot!

Game Rules:

  1. Identify and click on abnormal ECG waveform segments as they scroll across the canvas.
  2. Avoid clicking on normal ECG waveform segments to prevent score deductions.
  3. Clicking on an abnormal segment earns you points, while clicking on a normal segment reduces points.
  4. The game speed increases as your score goes up, making it more challenging.
  5. Continuously monitor the 'Score' display to track your current score.
  6. The game ends when you decide to stop or when you reach a specified score goal.
  7. Aim to achieve the highest score and demonstrate your skill in recognizing abnormal ECG patterns!

Game Controls:

Code Repository:


    // this is waveforms.js
    const tValues = new Array(800).fill(0).map((_, index) => (index / 800) * 5 * Math.PI);
    const mu = 2 * Math.PI;
    const sigma = 0.2;
    const amplitude = 3;

    function gaussian(x, mu, sigma) {
        return Math.exp(-((x - mu) ** 2) / (2 * sigma ** 2));
    }

    function refinedRWave(t, mu, sigma, amplitude) {
        return amplitude * gaussian(t, mu, sigma);
    }

    function generateFullWaveform(t, qrsVariationFunction = null) {
        const pWave = (0 <= t && t < Math.PI) ? Math.sin(t) ** 2 : 0;
        const qrs = (qrsVariationFunction === null) ? refinedRWave(t, mu, sigma, amplitude) : qrsVariationFunction(t);
        const sWave = (2.5 * Math.PI <= t && t < 3 * Math.PI) ? -0.5 * Math.sin(t - 2.5 * Math.PI) ** 3 : 0;
        const tWave = (3 * Math.PI <= t && t < 4 * Math.PI) ? 0.5 * Math.sin(t - 3 * Math.PI) ** 2 : 0;
        return 300 - (pWave + qrs + sWave + tWave) * -100;
    }

    // Variations
    function qrsVariation1(t) {
        const pWave = (0 <= t && t < Math.PI) ? Math.sin(t) ** 2 : 0;
        const qrs = refinedRWave(t, mu, 4*sigma, 0.5*amplitude);
        const sWave = (2.5 * Math.PI <= t && t < 3 * Math.PI) ? -0.5 * Math.sin(t - 2.5 * Math.PI) ** 3 : 0;
        const tWave = (3 * Math.PI <= t && t < 4 * Math.PI) ? 0.5 * Math.sin(t - 3 * Math.PI) ** 2 : 0;
        return 300 - (pWave + qrs + sWave + tWave) * -100;
    }

    function qrsVariation2(t) {
        const pWave = (0 <= t && t < Math.PI) ? Math.sin(t) ** 2 : 0;
        const qrs = refinedRWave(t, 1.3 * mu, sigma, 0.8*amplitude); // Different QRS complex
        const sWave = (2.5 * Math.PI <= t && t < 3 * Math.PI) ? -0.5 * Math.sin(t - 2.5 * Math.PI) ** 3 : 0;
        const tWave = (3 * Math.PI <= t && t < 4 * Math.PI) ? 0.5 * Math.sin(t - 3 * Math.PI) ** 2 : 0;
        return 300 - (pWave + qrs + sWave + tWave) * -100;
    }

    function noPWave(t) {
        const pWave = 0;
        const qrs = refinedRWave(t, mu, sigma, amplitude);
        const sWave = (2.5 * Math.PI <= t && t < 3 * Math.PI) ? -0.5 * Math.sin(t - 2.5 * Math.PI) ** 3 : 0;
        const tWave = (3 * Math.PI <= t && t < 4 * Math.PI) ? 0.5 * Math.sin(t - 3 * Math.PI) ** 2 : 0;
        return 300 - (pWave + qrs + sWave + tWave) * -100;
    }

    function noSWave(t) {
        const pWave = (0 <= t && t < Math.PI) ? Math.sin(t) ** 2 : 0;
        const qrs = refinedRWave(t, mu, sigma, amplitude);
        const sWave = 0; // No S wave
        const tWave = (3 * Math.PI <= t && t < 4 * Math.PI) ? 0.5 * Math.sin(t - 3 * Math.PI) ** 2 : 0;
        return 300 - (pWave + qrs + sWave + tWave) * -100;
    }

    function noTWave(t) {
        const pWave = (0 <= t && t < Math.PI) ? Math.sin(t) ** 2 : 0;
        const qrs = refinedRWave(t, mu, sigma, amplitude);
        const sWave = (2.5 * Math.PI <= t && t < 3 * Math.PI) ? -0.5 * Math.sin(t - 2.5 * Math.PI) ** 3 : 0;
        const tWave = 0;
        return 300 - (pWave + qrs + sWave + tWave) * -100;
    }

    let waveforms = [
        tValues.map(t => generateFullWaveform(t)),
        tValues.map(t => generateFullWaveform(t, qrsVariation1)),
        tValues.map(t => generateFullWaveform(t, qrsVariation2)),
        tValues.map(t => noPWave(t)),
        tValues.map(t => noSWave(t)),
        tValues.map(t => noTWave(t))
    ];

    function GenerateWaveform() {
        const abnormalWaveforms = [
            qrsVariation1, qrsVariation2,
            noPWave, noSWave, noTWave
        ];
        const waveformNames = [
            "QRS Variation 1", "QRS Variation 2",
            "No P wave", "No S wave", "No T wave"
        ];
        const isAbnormal = Math.random() < 0.6;
        if (!isAbnormal) {
            const waveformFunction = generateFullWaveform;
            const waveformType = 0; // Normal
            return [tValues.map(t => waveformFunction(t)), waveformType];
        } else {
            const index = Math.floor(Math.random() * abnormalWaveforms.length);
            const waveformFunction = abnormalWaveforms[index];
            const waveformType = 1; // Abnormal
            const newWaveform = tValues.map(t => waveformFunction(t));
            return [newWaveform, waveformType];
        }
    }
    // this is waveforms.js
            

// game_logic.js
document.addEventListener("DOMContentLoaded", () => {
    const HEIGHT = 600;
    const WIDTH = 800;

    // colors
    const BLUE = "#0477bf";
    const GREEN = "#04bf68";
    const YELLOW = "#f2c641";
    const RED = "#f25835";
    const BLACK = "#0d0d0d";

    const canvas = document.getElementById("game-canvas");
    canvas.width = WIDTH;
    canvas.height = HEIGHT;
    const ctx = canvas.getContext("2d");
    const font = "36px sans-serif";

    let score = 0; // Initialize the player's score
    let gameSpeed = 2; // Initialize the game speed (you can adjust this value)

    let waveformType; // 0 for normal, 1 for abnormal
    let currentWaveform = []; // Store the current waveform points

    // Define variables to manage the scrolling waveform
    let waveformX = -WIDTH; // Initial x-coordinate of the waveform
    // let waveformX = -currentWaveform.length; // Set initial position off the left edge

    function DrawWaveform(x, y, waveformColor) {
        ctx.strokeStyle = waveformColor;
        ctx.lineWidth = 5;

        ctx.beginPath();
        // ctx.moveTo(x, y); // Move the starting point to (x, y)

        // Loop through the currentWaveform points and draw the waveform
        for (let i = 0; i < currentWaveform.length; i++) {
            ctx.lineTo(x + i, y - currentWaveform[i]); // Draw a line segment
        }

        ctx.stroke();
        // ctx.closePath();
    }

    // Function to draw the score
    function drawScore() {
        ctx.fillStyle = BLUE;
        ctx.font = `bold ${font}`;
        ctx.fillText(`Score: ${score}`, 40, 40);
    }

    // Function to clear the canvas
    function clearCanvas() {
        // ctx.clearRect(waveformX, 0, currentWaveform.length, HEIGHT);
        ctx.clearRect(0, 0, WIDTH, HEIGHT);
    }


    // Define variables for different types of scores
    // let score = 0;
    let hits = 0;
    let falseAlarms = 0;
    let correctRejections = 0;
    let misses = 0;

    let currentWaveformClicked = false;
    let waveformCounted = false;

    // Function to handle user clicks
    function handleClick(event) {
        // Check if the current waveform has already been clicked
        if (!currentWaveformClicked) {
            if (event.clientX >= waveformX && event.clientX <= waveformX + currentWaveform.length) {
                if (waveformType === 1) {
                    // HIT: Player clicked on an abnormal segment, increase score
                    hits++;
                } else {
                    // FALSE ALARM: Player clicked on a normal segment, increase false alarms
                    falseAlarms++;
                }
                // Mark the current waveform as clicked
                currentWaveformClicked = true;

                // Update the overall score based on your scoring logic
                score = hits * 10 - falseAlarms * 10 + correctRejections * 10 - misses * 10;
                score = Math.max(score, -50); // Limit the score to not drop below -100
            }
        }
    }

    // Add a click event listener to the canvas
    canvas.addEventListener("click", handleClick);

    // In the game loop or a timer, check if the player hasn't interacted and update correct rejections and misses accordingly
    function updateCorrectRejectionsAndMisses() {

        // Check if the current waveform has completely scrolled off the canvas
        // if (waveformX < -currentWaveform.length) {
        if (waveformX <= -currentWaveform.length && !waveformCounted) {
            // console.log(`Waveform Type: ${waveformType}`);
            if (!waveformCounted) {
                if (waveformType === 0 && !currentWaveformClicked) {
                    // CORRECT REJECTION: Player correctly didn't click on a normal segment
                    correctRejections++;
                } else if (waveformType === 1 && !currentWaveformClicked) {
                    // MISS: Player missed clicking on an abnormal segment
                    misses++;
                }
                // Mark the current waveform as counted
                waveformCounted = true;

                // Reset the flags for the next waveform
                currentWaveformClicked = false;

                // Update the overall score based on your scoring logic
                score = hits * 10 - falseAlarms * 10 + correctRejections * 10 - misses * 10;
                score = Math.max(score, -50); // Limit the score to not drop below -100
            }
        }
    }

    // Function to display all counters on the canvas
    function drawCounters() {
        // Hits and correct rejections in green
        ctx.fillStyle = GREEN;
        // ctx.font = `bold ${font}`; // Make the font bold
        ctx.fillText(`Hits: ${hits}`, 40, 80);
        ctx.fillText(`Correct Rejections: ${correctRejections}`, 40, 160);

        // False alarms and misses in red
        ctx.fillStyle = RED;
        ctx.fillText(`False Alarms: ${falseAlarms}`, 40, 120);
        ctx.fillText(`Misses: ${misses}`, 40, 200);
    }

    // Game loop that scrolls a waveform across the canvas using GenerateWaveform
    function gameLoop() {
        clearCanvas();

        // Increase game speed based on the player's score
        gameSpeed = 5 + Math.floor(score / 50);

        // Move the waveform leftward (scrolling effect)
        waveformX -= gameSpeed;

        // Call the function to update correct rejections and misses at the beginning of the loop
        updateCorrectRejectionsAndMisses();

        // Draw the current waveform
        const waveformY = HEIGHT + 100; // You can adjust the vertical position
        DrawWaveform(waveformX, waveformY, BLACK);

        // Check if the waveform has completely scrolled off the canvas
        if (waveformX < -currentWaveform.length) {
            // Generate a new waveform
            let [newWaveform, newWaveformType] = GenerateWaveform();

            waveformCounted = false;
            currentWaveformClicked = false;

            // Set the x-coordinate for the new waveform
            waveformX = WIDTH; // Starts just off the right edge

            currentWaveform = newWaveform;
            waveformType = newWaveformType;

            console.log(`Waveform Type: ${waveformType}`);
        }

        // Draw the score
        drawScore();

        // Draw all counters on the canvas
        drawCounters();

        // Request the next animation frame
        requestAnimationFrame(gameLoop);
    }

    // Start the game loop
    gameLoop();
});