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. |
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.
Space lets Player 1 attack.Enter lets Player 2 attack in two-player mode.event.preventDefault() prevents the browser from scrolling or doing unwanted default actions.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.
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.
textContent updates health and message text.document.getElementById() finds HUD elements on the page.document.createElement() creates leaderboard entries dynamically.appendChild() adds new output to the page.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.
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.
fetch(url) sends the web request.response.ok checks whether the API request succeeded.previewUrl and plays it with the browser Audio API.POST request to send score data.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.
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 marks a method that can wait for asynchronous work.await fetch(url) waits for the API response.await response.json() waits for JSON parsing..then() chains leaderboard save, load, and render steps.try/catch handles async errors safely.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.
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.
response.json() converts an API response into JavaScript data.JSON.stringify() converts score objects into text for storage or API sending.JSON.parse() converts stored text back into JavaScript objects.data.results, trackName, and previewUrl.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.
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.
peppaGameMode stores single-player or two-player mode.peppaPlayerName stores the name used for the leaderboard.peppaPlayer1CharImage and peppaPlayer2CharImage store character choices.peppa-leaderboard-levelId stores local score data for each level.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.
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.
This follows the same Web API flow used in class:
fetch() to request data from a web server.response.json().trackName, artistName, and previewUrl.
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.
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.
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(); }
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".
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.
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.
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.
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.
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 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.
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.
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 }) }); }
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.
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.
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.
playerHpEl displays the player's current health.enemyHpEl displays the enemy or boss health.messageEl shows feedback such as hits, wins, losses, and score updates.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.
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.
Experiment with localStorage and JSON handling. This mimics saving game settings and leaderboard data.
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.
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 }) }); }