Sprint View

Input/Output

9 min read

Input and Output

Table of Contents

Input is how the game receives information — from the keyboard, from an API, from a config file. Output is how the game sends information back — drawing to the canvas, posting a score, displaying text.

Keyboard Input

The game listens for key presses using event listeners. When a key is pressed or released, a function runs that updates the player’s movement flags. Assessment evidence: Key event handlers respond correctly to arrow keys, space, and WASD controls.

Code Runner Challenge

Keyboard Input

View IPYNB Source
%%js

//CODE_RUNNER: Keyboard Input

const keys = {
    left:  false,
    right: false,
    up:    false,
};

document.addEventListener("keydown", function(event) {
    if (event.key === "ArrowLeft"  || event.key === "a") {
        keys.left = true;
        console.log("Key pressed: LEFT — player moves left");
    }
    if (event.key === "ArrowRight" || event.key === "d") {
        keys.right = true;
        console.log("Key pressed: RIGHT — player moves right");
    }
    if (event.key === "ArrowUp" || event.key === "w" || event.key === " ") {
        keys.up = true;
        console.log("Key pressed: UP/SPACE — player jumps");
    }
});

document.addEventListener("keyup", function(event) {
    if (event.key === "ArrowLeft"  || event.key === "a") {
        keys.left = false;
        console.log("Key released: LEFT — player stops");
    }
    if (event.key === "ArrowRight" || event.key === "d") {
        keys.right = false;
        console.log("Key released: RIGHT — player stops");
    }
    if (event.key === "ArrowUp" || event.key === "w" || event.key === " ") {
        keys.up = false;
        console.log("Key released: UP/SPACE");
    }
});

console.log("Event listeners ready — press arrow keys or WASD to test");
Lines: 1 Characters: 0
Output
Click "Run" in code control panel to see output ...

Explanation — addEventListener watches for keyboard events on the whole page. When a key is pressed, keydown fires and sets the matching flag to true. When the key is released, keyup fires and sets it back to false. The game loop reads these flags every frame to decide which direction to move the player.

Canvas Rendering

The Canvas API lets JavaScript draw images, shapes, and text directly onto an HTML canvas element. Every game object has a draw() method that is called every frame to paint it onto the screen.

Code Runner Challenge

Canvas Rendering

View IPYNB Source
%%js

//CODE_RUNNER: Canvas Rendering

// Run this to see how draw() builds the canvas output

class GameObject {
    constructor(data, gameEnv) {
        this.x      = data.x;
        this.y      = data.y;
        this.width  = data.width;
        this.height = data.height;
        this.color  = data.color;
        this.label  = data.label;
    }

    draw() {
        console.log(`Drawing ${this.label} at (${this.x}, ${this.y}) — size: ${this.width}x${this.height}`);
    }
}

class Platform extends GameObject {
    draw() {
        console.log(`Platform drawn at y=${this.y}, width=${this.width} — fills the floor`);
    }
}

class Player extends GameObject {
    draw() {
        console.log(`Player sprite drawn at (${this.x}, ${this.y})`);
    }
}

class Enemy extends GameObject {
    draw() {
        console.log(`Enemy drawn at (${this.x}, ${this.y})`);
    }
}

// Simulate a game frame
const gameObjects = [
    new Platform({ x: 0,   y: 400, width: 800, height: 20,  color: "gray",  label: "Platform" }),
    new Player  ({ x: 100, y: 350, width: 48,  height: 48,  color: "blue",  label: "Player"   }),
    new Enemy   ({ x: 500, y: 352, width: 40,  height: 40,  color: "red",   label: "Enemy"    }),
];

console.log("--- Rendering one game frame ---");
gameObjects.forEach(obj => obj.draw());
Lines: 1 Characters: 0
Output
Click "Run" in code control panel to see output ...

Explanation — Each class has its own draw() method that describes what gets painted for that object. The game loop calls draw() on every object in gameObjects every frame using forEach

GameEnv Configuration

GameEnv is a central configuration object that stores the canvas size, difficulty settings, and environment state. Everything in the game reads from it instead of using hardcoded values.

// Run this to see GameEnv being created and read by game objects

const GameEnv = {
    canvas:     null,
    width:      800,
    height:     450,
    difficulty: "normal",
    gravity:    0.4,
    isPaused:   false,

    create(canvasId) {
        console.log(`GameEnv created — canvas: ${canvasId}, size: ${this.width}x${this.height}`);
        console.log(`Difficulty: ${this.difficulty}, Gravity: ${this.gravity}`);
    }
};

// GameSetup tells the level which objects to build
const GameSetup = {
    player: {
        data:  { x: 100, y: 300, width: 48, height: 48, speed: 4 },
        class: "Player",
    },
    enemies: [
        { data: { x: 400, y: 300, speed: 2 }, class: "Enemy" },
        { data: { x: 600, y: 300, speed: 3 }, class: "Enemy" },
    ],
};

GameEnv.create("gameCanvas");

console.log("\n--- Reading GameSetup ---");
console.log("Player start position:", GameSetup.player.data.x, GameSetup.player.data.y);
console.log("Number of enemies:", GameSetup.enemies.length);
GameSetup.enemies.forEach((e, i) => {
    console.log(`Enemy ${i + 1} — x: ${e.data.x}, speed: ${e.data.speed}`);
});

Explanation — GameEnv holds all the values the game needs to know about the environment — canvas size, gravity, difficulty. GameEnv.create() initializes it. GameSetup is a separate config object that lists every game object the level should instantiate and what data to pass in. Instead of hardcoding values everywhere, everything reads from these two objects, so changing the difficulty or canvas size in one place updates the whole game.

API Integration

The leaderboard uses fetch to POST a new score and GET scores from a backend server. This comes directly from Leaderboard.js in the project. Every fetch is wrapped in a .then()/.catch() chain so errors are handled cleanly without crashing the game.

// This is the real submitScore method from Leaderboard.js
// It POSTs a score to the backend SCORE_COUNTER endpoint

const javaURI = "https://spring.opencodingsociety.com";

function submitScore(username, score, gameName) {
    const url = `${javaURI}/api/events/SCORE_COUNTER`;

    const requestBody = {
        payload: {
            user:     username,
            score:    score,
            gameName: gameName
        }
    };

    console.log("Posting score to:", url);
    console.log("Payload:", JSON.stringify(requestBody));

    // POST to backend using .then() API chaining
    fetch(url, {
        method:  "POST",
        headers: { "Content-Type": "application/json" },
        body:    JSON.stringify(requestBody)
    })
        .then(res => {
            if (!res.ok) {
                throw new Error(`POST failed: ${res.status}`);
            }
            return res.json();
        })
        .then(savedEntry => {
            console.log("Score saved successfully:", savedEntry);
        })
        .catch(error => {
            // If backend is down, fall back to localStorage
            console.log("Backend unavailable — saving locally:", error.message);
            const stored = JSON.parse(localStorage.getItem("scores") || "[]");
            stored.push({ username, score, gameName });
            localStorage.setItem("scores", JSON.stringify(stored));
            console.log("Score saved to localStorage as fallback");
        });
}

submitScore("mario", 4500, "MarioGame");

Explanation — fetch sends an HTTP POST request to the backend. The .then() chain handles the response step by step — first checking if the response was OK, then parsing the JSON. If anything fails, .catch() runs instead of crashing the game. This is the same pattern used in the real Leaderboard.js — if the backend is unavailable, the score is saved to localStorage as a fallback so the player never loses their data.

Asynchronous I/O

async/await and .then() chains let the game wait for an API response without freezing. Leaderboard.js uses .then() chaining throughout. The key idea is that fetch runs in the background while the rest of the game keeps going.

Code Runner Challenge

Async I/O

View IPYNB Source
%%js

//CODE_RUNNER: Async I/O

// Run this to see async behaviour — notice which line prints first

// .then() chain style — used in Leaderboard.js
function fetchLeaderboard() {
    console.log("1. Starting fetch...");

    fetch("https://spring.opencodingsociety.com/api/events/SCORE_COUNTER")
        .then(res => {
            console.log("3. Response received");
            return res.json();
        })
        .then(data => {
            console.log("4. Data ready — entries:", data.length);
        })
        .catch(error => {
            console.log("3. Fetch failed:", error.message);
        });

    console.log("2. This prints immediately — fetch runs in the background");
}

// async/await style — same result, reads like normal code
async function fetchLeaderboardAsync() {
    console.log("A. Starting async fetch...");
    try {
        const res  = await fetch("https://spring.opencodingsociety.com/api/events/SCORE_COUNTER");
        const data = await res.json();
        console.log("B. Data ready — entries:", data.length);
    } catch (error) {
        console.log("B. Fetch failed:", error.message);
    }
}

fetchLeaderboard();
fetchLeaderboardAsync();
Lines: 1 Characters: 0
Output
Click "Run" in code control panel to see output ...

Explanation — The key thing to notice is that line “2. This prints immediately” appears in the console before “3. Response received”. That proves fetch runs in the background — the rest of the code does not wait for it. Leaderboard.js uses .then() chaining for this reason, which lets it do sequential steps — fetch → transform → display — in a clean readable chain. async/await does the exact same thing but reads more like normal top-to-bottom code.

JSON Parsing

When the API sends back data, it arrives as a raw JSON string. JSON.parse() converts it into a real JavaScript object. In Leaderboard.js, the backend returns an array of score events that need to be transformed before display.

Code Runner Challenge

JSON

View IPYNB Source
%%js

//CODE_RUNNER: JSON

// This is the shape of data the backend sends back
const rawResponse = '[{"id":1,"payload":{"user":"mario","score":4500,"gameName":"MarioGame"},"timestamp":"2026-05-18"},{"id":2,"payload":{"user":"luigi","score":3200,"gameName":"MarioGame"},"timestamp":"2026-05-18"}]';

console.log("Raw API response type:", typeof rawResponse);

// JSON.parse() converts the string into a real JavaScript array
const data = JSON.parse(rawResponse);

console.log("After parse — type:", typeof data);
console.log("Number of entries:", data.length);

// This is the same transform Leaderboard.js does after fetching
const transformed = data.map(event => ({
    id:       event.id,
    user:     event.payload?.user  || "Anonymous",
    score:    event.payload?.score || 0,
    gameName: event.payload?.gameName || "Unknown",
}));

console.log("\n--- Transformed leaderboard ---");
transformed
    .sort((a, b) => b.score - a.score)
    .forEach((entry, i) => {
        // Object destructuring — pulls values out cleanly
        const { user, score, gameName } = entry;
        console.log(`${i + 1}. ${user}${score} pts (${gameName})`);
    });
Lines: 1 Characters: 0
Output
Click "Run" in code control panel to see output ...

Explanation — Before JSON.parse(), rawResponse is just a string — you cannot read .score from it. After parsing it becomes a real JavaScript array that you can loop through and read properties from. The .map() then transforms each raw backend event into a cleaner object with just the fields the display needs. Object destructuring (const { user, score, gameName } = entry) pulls three values out in one line instead of writing three separate assignments — the same pattern used in Leaderboard.js when rendering the score table.

Course Timeline