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 - This is how the game knows what keys the player is pressing. When a key goes down, the game marks that direction as active. When the key is released, it turns that direction off. The game keeps checking those values to decide how the player should move. This can be tested by going to console and pressing WASD to see which way the character would be moving in a game

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 object knows how to draw itself. A platform, player, and enemy can all appear differently, but the game can still call draw() on each one. This keeps the drawing code organized and makes it easier to add more objects later.

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 stores the main settings for the game, like screen size, gravity, and difficulty. GameSetup lists what objects should be placed into the level. This makes the game easier to change because important settings are kept in one place instead of being scattered everywhere.

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 - This shows how the game sends a score to the backend. The code first sends the request, then checks if it worked, then reads the response. If something goes wrong, the error handler runs so the game does not crash. This is important for the leaderboard because a failed save should not ruin the player’s game.

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 - This shows that API calls take time. The page can keep running while the request is still waiting for a response. When the response finally comes back, the code handles it. This is why games need careful API code, especially for things like saving scores or loading leaderboard data.

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 - Backend data often comes back as text first. JSON.parse() turns that text into real JavaScript data the game can use. After that, the code picks out the important parts, like user, score, and game name, so the leaderboard can show clean information instead of raw backend data.