๐ŸŒ

Input/Output and API Integration

This page focuses on input/output and API integration in my Peppa Pig Fighting Game. It explains how the game receives keyboard input, updates HUD and DOM output, communicates with a web API, handles async code, parses JSON, and stores game data with localStorage.

Learning Objective Project Evidence Required Assessment Method
Keyboard Input Section 1 explains keyboard events for attacks, menu choices, and player interaction. Testing key handlers during gameplay.
HUD / DOM Output Section 2 explains HUD and DOM output for health, messages, buttons, and leaderboard elements. Gameplay demo and element inspection.
API Integration Section 3 explains fetch() calls for music and leaderboard-style data. Review API requests, response checks, and error handling.
Asynchronous I/O Section 4 explains async, await, promises, and .then() for web data. Review async code patterns.
JSON Handling Section 5 explains response.json(), JSON.stringify(), and stored JSON data. Review parsed API responses and saved leaderboard data.
LocalStorage Section 6 explains storing player name, game mode, character choices, and local leaderboard scores. Inspect saved browser data and test reload behavior.

โœ…

Specific Input/Output and API Requirements


1. Keyboard Input

Requirement: Use keyboard events for attacks, menu choices, and player interaction

Keyboard input is how the player controls the game. My project listens for keydown events, checks which key was pressed, and then changes game state. Instead of instantly doing every action inside the key handler, the code sets request flags such as attackRequested and player2AttackRequested. The update loop then decides whether the attack is allowed.

document.addEventListener('keydown', this.boundKeyDown);

handleKeyDown(event) {
    if (event.code === 'Space') {
        this.attackRequested = true;
        event.preventDefault();
    }

    if (this.gameMode === 'twoPlayer' && event.code === 'Enter') {
        this.player2AttackRequested = true;
        event.preventDefault();
    }
}

This proves the keyboard input requirement because the game receives real player key presses and turns them into attack requests during gameplay.

2. HUD / DOM Output

Requirement: Update health, messages, buttons, and leaderboard elements

Output is what the game shows back to the player. My project uses the DOM to update health, enemy health, messages, leaderboard entries, buttons, and the API demo output. The canvas shows the moving game, but the DOM gives readable feedback such as health values, win/loss messages, score updates, and leaderboard results.

updateHud(message = null) {
    const playerHpEl = document.getElementById(`peppa-player-hp-${this.config.levelId}`);
    const enemyHpEl = document.getElementById(`peppa-enemy-hp-${this.config.levelId}`);
    const messageEl = document.getElementById(`peppa-message-${this.config.levelId}`);

    if (playerHpEl) playerHpEl.textContent = `Player HP: ${this.playerHealth}`;
    if (enemyHpEl) enemyHpEl.textContent = `Enemy HP: ${this.enemyHealth}`;
    if (messageEl && message) messageEl.textContent = message;
}
const leaderboardDiv = document.getElementById('leaderboard');

this.leaderboard.forEach(entry => {
    const item = document.createElement('div');
    item.textContent = `${entry.name}: ${entry.score}`;
    leaderboardDiv.appendChild(item);
});

This proves the HUD / DOM output requirement because the project changes what the user sees based on live game data.

3. API Integration

Requirement: Use fetch() calls for music and leaderboard-style data

API integration means the project communicates with an outside web service. My game uses fetch() to request music data from the iTunes Search API. It also includes leaderboard-style API code that can send score data to a backend endpoint when apiBase is configured.

const searchTerm = 'Rick Astley Never Gonna Give You Up';
const url = 'https://itunes.apple.com/search?entity=song&limit=5&term=' +
  encodeURIComponent(searchTerm);

const response = await fetch(url);

if (!response.ok) {
  throw new Error('iTunes API request failed: ' + response.status);
}

const data = await response.json();
const track = data.results.find(song => song.previewUrl);
const response = await fetch(`${this.apiBase}/leaderboard`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
        name: this.playerName || 'Player',
        score: score,
        levelId: this.config.levelId,
        levelTitle: this.config.levelTitle
    })
});

This proves API integration because the project sends web requests, checks responses, receives data, and uses that data inside the game.

4. Asynchronous I/O

Requirement: Use async, await, promises, and .then() for web data

Asynchronous I/O is needed because web requests do not finish instantly. My project uses async, await, promises, and .then() so the game can wait for API responses, leaderboard saves, and audio playback without freezing the page.

async fetchPreviewUrl() {
    const response = await fetch(this.endpoint);

    if (!response.ok) {
      throw new Error('API request failed (' + response.status + ')');
    }

    const data = await response.json();
    const tracks = data && Array.isArray(data.results) ? data.results : [];
    const track = tracks.find(item => item && item.previewUrl);

    return track.previewUrl;
}
this.saveScore(score)
    .then(() => this.loadLeaderboard())
    .then(() => this.renderLeaderboard());

This proves asynchronous I/O because the project waits for web data and score actions in an organized way instead of assuming everything happens instantly.

5. JSON Handling

Requirement: Use response.json(), JSON.stringify(), and stored JSON data

JSON handling is used when the game receives API data or saves structured data. The iTunes API returns JSON, so the game converts the response with response.json(). The leaderboard and localStorage systems use JSON.stringify() and JSON.parse() to save and reload arrays of score objects.

const data = await response.json();
const track = data.results.find(song => song.previewUrl);

const existing = JSON.parse(localStorage.getItem(key) || '[]');

existing.push({
    name: name || 'Player',
    score: score,
    timestamp: Date.now()
});

localStorage.setItem(key, JSON.stringify(existing));

This proves JSON handling because the project parses API JSON, creates JSON-style objects, and stores structured leaderboard data.

6. LocalStorage

Requirement: Store player name, game mode, character choices, and local leaderboard scores

localStorage lets the game remember information after the page reloads. My project uses it to save the selected game mode, player name, character choices, and local leaderboard scores. This makes the game more reliable on GitHub Pages because it can still remember scores even without a live backend server.

const savedName = localStorage.getItem('peppaPlayerName');

if (savedName && savedName.trim()) {
    this.playerName = savedName.trim();
}

localStorage.setItem('peppaPlayerName', this.playerName);
saveScoreLocal(score) {
    const leaderboardKey = `peppa-leaderboard-${this.config.levelId}`;
    const existing = this.loadLeaderboardLocal();

    existing.push({
        name: this.playerName || 'Player',
        score: score,
        timestamp: Date.now()
    });

    existing.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
    localStorage.setItem(leaderboardKey, JSON.stringify(existing.slice(0, 10)));
}

This proves the localStorage requirement because the project saves and reloads data in the browser, allowing player choices and scores to persist.

๐ŸŒ

API Integration


Using a Live Web API for Game Music

The API part of my Peppa Pig Fighting Game uses the iTunes Search API to request real music data from the internet. Instead of hard-coding one song into the game, the code searches for music related to Peppa Pig, receives JSON data, finds a playable preview URL, and uses that preview as background music in the game.

Music API screenshot showing the iTunes Search API or music feature working

This follows the same Web API flow used in class:

Connection to the Game

The game's PeppaMusic class uses this exact same pattern โ€” it calls the iTunes API to fetch background music previews and plays them during gameplay. The same fetch() โ†’ await response.json() โ†’ extract field โ†’ play flow powers both the leaderboard and the in-game music.

๐ŸŽต Live Demo โ€” Rickroll with the iTunes API

Click the button to make a real iTunes API call, find "Never Gonna Give You Up", extract its previewUrl, and play the 30-second preview in your browser.

Waiting for API call... Click play to fetch song data from iTunes. No request has been sent yet.

Rickroll API Code Example

async playRickrollPreview() {
  const searchTerm = 'Rick Astley Never Gonna Give You Up';
  const url = 'https://itunes.apple.com/search?entity=song&limit=5&term=' +
    encodeURIComponent(searchTerm);

  const response = await fetch(url);

  if (!response.ok) {
    throw new Error('iTunes API request failed: ' + response.status);
  }

  const data = await response.json();
  const track = data.results.find(song => song.previewUrl);

  const audio = new Audio(track.previewUrl);
  audio.volume = 0.35;
  await audio.play();
}

Building the API Request URL

const filter = "peppa pig theme";
const url = "https://itunes.apple.com/search?term=" + encodeURIComponent(filter);

console.log(url);

The iTunes API needs a search term inside the URL. I use encodeURIComponent() because spaces and special characters need to be converted into a safe URL format. For example, "peppa pig theme" becomes "peppa%20pig%20theme".

Fetching and Checking the API Response

const url = "https://itunes.apple.com/search?term=" + encodeURIComponent("peppa pig theme");

fetch(url, { method: 'GET', mode: 'cors' })
  .then(response => {
    if (response.status !== 200) {
      throw new Error('Database response error: ' + response.status);
    }
    return response.json();
  })
  .then(data => {
    console.log('Track count:', data.results.length);
  })
  .catch(err => console.error(err));

This code sends a GET request to the iTunes Search API. The most important part is checking the status code before using the response. If the response is not successful, the code throws an error instead of trying to use broken data.

Reading JSON Data from the API

const data = {
  results: [
    {
      artistName: 'The Wiggles',
      trackName: 'Peppa Pig Theme',
      artworkUrl100: 'https://example.com/image.jpg',
      previewUrl: 'https://example.com/preview.mp4'
    }
  ]
};

for (const row of data.results) {
  const artist = row.artistName;
  const track = row.trackName;
  const imageUrl = row.artworkUrl100;
  const previewUrl = row.previewUrl;

  console.log(artist, track, imageUrl, previewUrl);
}

JSON is the format returned by the API. After the response is parsed, the data can be used like a normal JavaScript object. In this project, the most important value is previewUrl, because that is the audio preview the game can play.

Playing API Music with the Audio API

const previewUrl = 'https://example.com/song.mp4';
const audio = new Audio(previewUrl);

audio.volume = 0.35;
audio.loop = true;

audio.play().catch(err => {
  console.warn('Autoplay blocked until user gesture:', err);
});

The Audio API lets the browser create and control sound. The game creates a new Audio object from the API preview URL, lowers the volume, loops the track, and tries to play it. The catch() block is important because some browsers block audio until the player clicks or presses a key.

Controller Class for Cleaner API Code

class PeppaMusicApiController {
  constructor(endpoint) {
    this.endpoint = endpoint;
  }

  async fetchPreviewUrl() {
    const response = await fetch(this.endpoint);

    if (!response.ok) {
      throw new Error('API request failed (' + response.status + ')');
    }

    const data = await response.json();
    const tracks = data && Array.isArray(data.results) ? data.results : [];
    const track = tracks.find(item => item && item.previewUrl);

    if (!track) {
      throw new Error('No playable preview URL found');
    }

    return track.previewUrl;
  }

  async startMusic() {
    const previewUrl = await this.fetchPreviewUrl();
    const audio = new Audio(previewUrl);

    audio.volume = 0.35;
    audio.loop = true;

    await audio.play();
  }
}

console.log('PeppaMusicApiController loaded');

This class separates the API logic from the rest of the game. The fetchPreviewUrl() method handles the web request and JSON parsing, while startMusic() handles the audio playback. This makes the project cleaner because the game does not need to repeat API code every time it wants to play music.

Error Handling

async startMusicSafely(controller) {
  try {
    const previewUrl = await controller.fetchPreviewUrl();
    const audio = new Audio(previewUrl);

    audio.volume = 0.35;
    audio.loop = true;

    await audio.play();
    console.log('Music started successfully');
  } catch (error) {
    console.warn('Failed to play music:', error);
  }
}

Error handling is important because APIs can fail, internet connections can break, or the browser can block autoplay. With try/catch, the game can continue running even if the music does not play.


๐Ÿ’พ

LocalStorage and Data Persistence


Remembering Game State in the Browser

localStorage is used so the game can remember player preferences and scores after the page reloads. This includes the selected game mode, player name, character choices, and local leaderboard scores.

peppaGameMode peppaPlayerName peppaPlayer1CharImage peppaPlayer2CharImage peppa-leaderboard-levelId
ensurePlayerName() {
    if (this.playerName && this.playerName.trim()) return;

    const savedName = localStorage.getItem('peppaPlayerName');
    if (savedName && savedName.trim()) {
        this.playerName = savedName.trim();
        return;
    }

    let inputName = window.prompt('Enter your name for the leaderboard:', '');
    if (!inputName || !inputName.trim()) inputName = 'Player';

    this.playerName = inputName.trim().slice(0, 20);
    localStorage.setItem('peppaPlayerName', this.playerName);
}

This method first checks whether a player name already exists, then checks browser storage, then asks the player for a name only if needed. The name is limited to 20 characters before saving.

API Fallback to Local Leaderboard

The game can save scores to a backend API when apiBase exists. If no API is configured, it automatically saves the leaderboard locally instead.

async saveScore(score) {
    if (!this.apiBase) {
        this.saveScoreLocal(score);
        return { success: true };
    }

    const response = await fetch(`${this.apiBase}/leaderboard`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            name: this.playerName || 'Player',
            score: score,
            levelId: this.config.levelId,
            levelTitle: this.config.levelTitle
        })
    });
}

Local Leaderboard Storage

saveScoreLocal(score) {
    const leaderboardKey = `peppa-leaderboard-${this.config.levelId}`;
    const existing = this.loadLeaderboardLocal();

    existing.push({
        name: this.playerName || 'Player',
        score: score,
        timestamp: Date.now()
    });

    existing.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
    const topScores = existing.slice(0, 10);
    localStorage.setItem(leaderboardKey, JSON.stringify(topScores));
}

The leaderboard is stored separately for each level using a key like peppa-leaderboard-lvl1. The game sorts scores from highest to lowest and keeps the top 10.

Why this is important: The game still has persistent scores even without a live backend server, which makes the project more reliable for demos and GitHub Pages deployment.


โŒจ๏ธ

Keyboard Input and Player Actions


The game uses keyboard input to turn player actions into game-state changes. The keydown listener does not directly fire every laser or update every sprite by itself. Instead, it sets request flags, and the main update loop reads those flags during gameplay.

document.addEventListener('keydown', this.boundKeyDown);

handleKeyDown(event) {
    if (event.code === 'Space') {
        this.attackRequested = true;
        event.preventDefault();
    }

    if (this.gameMode === 'twoPlayer' && event.code === 'Enter') {
        this.player2AttackRequested = true;
        event.preventDefault();
    }
}

This shows keyboard input because the player controls the game through key presses. Player 1 can use Space to attack, while Player 2 can use Enter in two-player mode. The reason this is organized with flags is that the update loop can control timing, cooldowns, and game state instead of letting one key press instantly bypass the battle logic.

Why this matters: Input is separated from gameplay processing. The keyboard method records the player's request, and the update loop decides whether the action is allowed based on cooldowns, player mode, and battle state.


๐Ÿ“Ÿ

HUD Output and Screen Feedback


Output appears through the HUD, messages, and leaderboard areas. While the canvas shows the moving game objects, the DOM displays readable feedback such as health, score, battle messages, and saved leaderboard results. This gives the player immediate information about what is happening.

updateHud(message = null) {
    const playerHpEl = document.getElementById(`peppa-player-hp-${this.config.levelId}`);
    const enemyHpEl = document.getElementById(`peppa-enemy-hp-${this.config.levelId}`);
    const messageEl = document.getElementById(`peppa-message-${this.config.levelId}`);

    if (playerHpEl) playerHpEl.textContent = `Player HP: ${this.playerHealth}`;
    if (enemyHpEl) enemyHpEl.textContent = `Enemy HP: ${this.enemyHealth}`;
    if (messageEl && message) messageEl.textContent = message;
}

This code is output because it changes what the user sees on the page. When the player gets hit, collects coins, defeats a boss, or loses the battle, the HUD can update without refreshing the whole page.


๐Ÿ–ฅ๏ธ

DOM Manipulation and Dynamic Page Output


DOM manipulation is used for parts of the project that are outside the canvas, such as the leaderboard, menu buttons, and text panels. This section is different from the API section because it focuses on how data becomes visible on the page after it is already loaded or stored.

const leaderboardDiv = document.getElementById('leaderboard');

this.leaderboard.forEach(entry => {
    const item = document.createElement('div');
    item.textContent = `${entry.name}: ${entry.score}`;
    leaderboardDiv.appendChild(item);
});

This creates new HTML elements for leaderboard entries. Instead of writing every score manually in the HTML, the game loops through stored score data, creates a new element for each score, and appends it to the leaderboard. That makes the page update dynamically as the game data changes.

const button = document.getElementById('startButton');

button.addEventListener('click', () => {
    this.start();
});

Button events are another input/output connection. The player clicks a DOM element, JavaScript receives that input, and the game responds by starting or changing the game state.


๐Ÿงช

Code Runner Challenge


To make my blog more interactive, I added a Code Runner Challenge section. This lets the reader experiment with the same storage ideas used in my Peppa Pig Fighting Game, including localStorage, JSON.stringify, JSON.parse, sorting scores, and displaying output.

Code Runner Challenge

Experiment with localStorage and JSON handling. This mimics saving game settings and leaderboard data.

1
Lines: 1   Characters: 0

Output

Click "Run" in the code control panel to see output ...

This challenge demonstrates the same concepts as my game storage code: localStorage, JSON.stringify, JSON.parse, sorting scores, and error handling.

Why this belongs in my blog: My Peppa Pig Fighting Game saves game mode, character selections, player name, and leaderboard scores with localStorage. This challenge is a smaller, safer version of that same workflow.

Connection to the project: The code acts like a mock local leaderboard. It shows how I understand storing data, reading it back, sorting it, and displaying useful results to the user.

Project Code Highlight: Saving Scores to the Leaderboard

This code shows how my game sends score data to a leaderboard API. It uses async and await so the game can wait for the server response, fetch() to send the data, and JSON.stringify() to convert the score information into JSON before sending it.

async saveScore(score) {
    const response = await fetch(`${this.apiBase}/leaderboard`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            name:    this.playerName,
            score:   score,
            levelId: this.config.levelId
        })
    });
}
โ† Back to College Ready Blog