This page shows coding practices from the rubric, including single responsibility, data-driven design, state management, multiplayer logic, collision systems, scoring, projectile mechanics, testing, deployment, and software development lifecycle practices.
| Learning Objective | Project Evidence Required | Assessment Method |
|---|---|---|
| Single Responsibility Principle | Section 1 explains how separate methods handle lasers, HUD, collisions, coins, and updates. | Code review of method organization. |
| Data-Driven Design | Section 2 explains configuration objects, sprite data, arrays, and level settings. | Review object literals and level setup. |
| State Management | Section 3 explains how the game tracks mode, battle ending, health, score, coins, and transitions. | Review state variables and update-loop logic. |
| Control Structures | Section 4 explains conditionals and nested logic for multiplayer mode, collisions, and boundaries. | Review if, else if, and compound checks. |
| Input / Output | Section 5 explains keyboard controls, HUD updates, localStorage, and code runner output. | Gameplay demo and portfolio review. |
| Software Development Lifecycle | Section 6 explains build, test, fix, integrate, deploy, and improvement steps. | Review testing notes, deployed portfolio page, and project improvements. |
This project connects to the software development lifecycle because it was not built all at once. The game went through planning, coding, testing, debugging, integration, and improvement. Each new feature, such as multiplayer mode, character selection, coins, scoring, localStorage, and leaderboard behavior, had to be added, tested, and adjusted so it worked with the rest of the game.
The project is a JavaScript-based boss battle game where the player controls Ishan Jha and fights Peppa Pig characters in different levels. The code handles movement, attacks, lasers, health, collision detection, HUD updates, win and lose screens, localStorage, character selection, multiplayer mode, and a leaderboard system.
The game is organized around a level class called PeppaBattleLevelBase, which manages the main gameplay
systems for each fight. This makes the project more advanced than a simple one-screen game because it keeps track
of many systems at the same time, such as player health, enemy health, attack cooldowns, API calls, and visual updates.
class PeppaBattleLevelBase { constructor(gameEnv, config) { this.gameEnv = gameEnv; this.config = config; this.playerMaxHealth = config.playerHealth ?? 5; this.playerHealth = this.playerMaxHealth; this.playerDamageCooldownMs = config.playerDamageCooldownMs ?? 650; this.attackCooldownMs = config.attackCooldownMs ?? 450; this.laserSpeed = config.laserSpeed ?? 14; this.apiBase = config.apiBase || null; this.playerName = config.playerName || 'Ishan'; this.levelScore = 0; this.leaderboard = []; } }
This code shows how the game stores important information at the start of the level. If no custom player health
is provided, the game automatically uses 5. If no player name is given, it defaults to
"Ishan". This makes the code more flexible and reusable.
The single responsibility principle means each method should have one main job. My project shows this because the game separates lasers, collisions, coins, scoring, HUD updates, and level setup into different systems instead of putting all of the logic into one huge method.
spawnLaserStraight() creates laser objects.areColliding() checks if two hitboxes overlap.updateHud() updates the player-facing message and stats.saveScore(), loadLeaderboard(), and renderLeaderboard() handle leaderboard behavior.spawnLaserStraight(fromX, fromY, direction, isPlayerLaser) { const [vx, vy] = this.directionToVelocity(direction); this.lasers.push({ x: fromX, y: fromY, vx, vy, isPlayerLaser, life: 60, maxLife: 60 }); }
This proves single responsibility because this method only creates a laser object. It does not also calculate the final score, update the leaderboard, or control character selection.
Data-driven design means values in objects and arrays control how the game behaves. My project uses a
config object to set player health, cooldowns, laser speed, API base, player name, coin values,
and other level settings.
constructor(gameEnv, config) { this.gameEnv = gameEnv; this.config = config; this.playerMaxHealth = config.playerHealth ?? 5; this.playerDamageCooldownMs = config.playerDamageCooldownMs ?? 650; this.attackCooldownMs = config.attackCooldownMs ?? 450; this.laserSpeed = config.laserSpeed ?? 14; }
The project also uses data objects for character choices. Each selected character is stored as an object with an image, name, and accent color.
const charData = {
image: char.image,
name: char.name,
accentColor: accentColor
};
This proves data-driven design because the game changes based on configuration data, character data, sprite data, and stored browser values instead of only using hard-coded behavior.
State management means tracking values that change while the game runs. My game stores and updates state for the game mode, player health, Player 2 health, coin count, final score, leaderboard, battle ending, selected characters, and active lasers.
this.gameMode = config.gameMode ?? localStorage.getItem('peppaGameMode') ?? 'singlePlayer'; this.playerHealth = this.playerMaxHealth; this.coinCount = 0; this.levelScore = 0; this.leaderboard = [];
Coins are a clear example of state changing during gameplay. When a coin is collected, the coin object changes, the coin count increases, and the HUD updates.
if (distance <= collectRadius) { coin.collected = true; this.coinCount += 1; this.updateHud(`Coin collected! +${this.coinValue} score`); }
This proves state management because the game stores important values and updates them when the player moves, attacks, collects coins, takes damage, or finishes the level.
Control structures are the if, else, and comparison checks that decide what the
program should do. My project uses control structures for multiplayer mode, collision checks, laser damage,
character loading, and keeping players inside the arena.
if (this.gameMode === 'twoPlayer') { this.classes.push({ class: Player, data: sprite_data_player2 }); } else { this.classes.push({ class: PeppaBossEnemy, data: sprite_data_enemy }); }
This logic changes the entire level depending on the game mode. In two-player mode, the game adds Player 2. In single-player mode, the game adds the boss enemy.
if (player && player.height) { if (player.position.y < floorY) { player.position.y = floorY; player.velocity.y = 0; } if (player.position.y + player.height > height) { player.position.y = height - player.height; player.velocity.y = 0; } }
This proves control structures because the game uses conditions to make decisions about mode, movement, collisions, boundaries, and damage.
Input is how the player gives information to the game, and output is how the game gives information back. My project uses keyboard input, character selection input, browser storage, HUD messages, laser colors, score updates, and leaderboard output.
WASD to move and Space to fire.Enter to fire.localStorage saves character choices and the selected game mode.updateHud() outputs messages like coin collection and laser hits.localStorage.setItem('peppaPlayer1CharImage', char.image); localStorage.setItem('peppaPlayer1CharName', char.name); this.updateHud(`Coin collected! +${this.coinValue} score`);
This proves input/output because the game receives player actions and saved choices, then shows visible feedback through the HUD, gameplay objects, score, and leaderboard.
The software development lifecycle is shown because the game was improved over multiple versions. The project started with a boss battle structure, then added multiplayer mode, character selection, coins, scoring, localStorage, laser hitboxes, and leaderboard behavior.
This proves the SDLC requirement because the page shows the project as a complete development process, not just a final code dump.
The updated code supports both singlePlayer and twoPlayer mode.
The mode can come from the level config, from localStorage, or default to single-player
if nothing was saved.
Player 1 uses WASD to move and Space to fire a cyan laser.
Player 2 uses the arrow keys to move and Enter to fire a green laser.
this.gameMode = config.gameMode ?? localStorage.getItem('peppaGameMode') ?? 'singlePlayer';
The level also creates different game objects depending on the mode. In twoPlayer mode,
it adds Player 2. In single-player mode, it adds the boss enemy.
if (this.gameMode === 'twoPlayer') { this.classes.push({ class: Player, data: sprite_data_player2 }); } else { this.classes.push({ class: PeppaBossEnemy, data: sprite_data_enemy }); }
The HUD changes too. In two-player mode, it becomes wider, shows Player 2 health, and displays both control schemes.
The game also supports character swapping. Before the battle starts, players can choose character images and names. Those choices are saved so the battle level can load the correct player sprites.
// Read character selections saved by the welcome/character-select screen const p1CharImage = localStorage.getItem('peppaPlayer1CharImage') || 'ishan-jha.png'; const p1CharName = localStorage.getItem('peppaPlayer1CharName') || 'Ishan'; const p2CharImage = localStorage.getItem('peppaPlayer2CharImage') || 'ishan-jha.png'; const p2CharName = localStorage.getItem('peppaPlayer2CharName') || 'Player 2'; const p1Image = this.gameMode === 'twoPlayer' ? p1CharImage : 'ishan-jha.png'; const p1Name = this.gameMode === 'twoPlayer' ? p1CharName : 'Ishan';
This code checks saved character data. If no character was saved, the game uses safe default values
like ishan-jha.png and Ishan.
selectCharacter(char, playerNum, accentColor) {
const charData = {
image: char.image,
name: char.name,
accentColor: accentColor
};
if (playerNum === 1) {
this.p1Character = charData;
localStorage.setItem('peppaPlayer1CharImage', char.image);
localStorage.setItem('peppaPlayer1CharName', char.name);
} else {
this.p2Character = charData;
localStorage.setItem('peppaPlayer2CharImage', char.image);
localStorage.setItem('peppaPlayer2CharName', char.name);
}
this.updateStartButton();
}
This is a strong example of data persistence because the game stores each player's character name and image in the browser, then uses those values later during gameplay.
The collision system checks whether laser hitboxes overlap with players or the boss. The updated laser objects
also store a source, so the game knows whether the laser came from Player 1, Player 2, or the boss.
if (this.gameMode === 'twoPlayer') { if (L.source === 'player1' && player2) { // Player 1 laser can hit Player 2 this.player2Health = Math.max(0, this.player2Health - 1); } else if (L.source === 'player2' && player) { // Player 2 laser can hit Player 1 this.playerHealth = Math.max(0, this.playerHealth - 1); } }
In two-player mode, Player 1 and Player 2 can damage each other with lasers.
The game uses cooldown timers like lastPlayerHitAt and lastPlayer2HitAt
so damage does not happen too quickly.
areColliding(a, b) { if (!a || !b) return false; return ( a.position.x < b.position.x + b.width && a.position.x + a.width > b.position.x && a.position.y < b.position.y + b.height && a.position.y + a.height > b.position.y ); }
This method checks whether two rectangles overlap. It is used for physical collision checks like the player touching the boss in single-player mode.
if (player && player.height) { if (player.position.y < floorY) { player.position.y = floorY; player.velocity.y = 0; } if (player.position.y + player.height > height) { player.position.y = height - player.height; player.velocity.y = 0; } }
The floor barrier logic keeps players and bosses inside the visible arena. This prevents characters from floating too high, falling out of bounds, or leaving the playable area.
The updated code adds collectible coins. Coins spawn randomly inside the playable arena, avoid spawning too close to the player or enemy, and increase the final score when collected.
this.coins = []; this.coinCount = 0; this.coinValue = config.coinValue ?? 25; this.coinRadius = config.coinRadius ?? 14; this.coinTotal = config.coinTotal ?? 8;
These values control how many coins appear, how large they are, and how many points each coin is worth.
if (distance <= collectRadius) { coin.collected = true; this.coinCount += 1; this.updateHud(`Coin collected! +${this.coinValue} score`); }
When the player is close enough to a coin, the coin becomes collected, the coin count increases, and the HUD shows immediate feedback.
const score = (this.playerHealth * 100) + (this.coinCount * this.coinValue); this.levelScore = score; this.saveScore(score) .then(() => this.loadLeaderboard()) .then(() => this.renderLeaderboard());
The final score combines remaining health with the coin bonus. After calculating it, the game saves the score, reloads the leaderboard, and re-renders the leaderboard on the HUD.
Improvement from the original version: The game now rewards exploration and survival instead of only counting boss defeat. Coins make the arena more interactive.
spawnLaserStraight(fromX, fromY, direction, isPlayerLaser) { const [vx, vy] = this.directionToVelocity(direction); this.lasers.push({ x: fromX, y: fromY, vx, vy, isPlayerLaser, life: 60, maxLife: 60 }); }
One of the most specific gameplay systems in the code is the laser system. When the player attacks, the game creates a new laser object with x and y position, velocity, type, and lifetime. The projectile actually stores its own position and speed values and updates over time.
if (L.isPlayerLaser && boss && !boss.isDefeated) { if ( hitLeft < boss.position.x + boss.width && hitLeft + hitW > boss.position.x && hitTop < boss.position.y + boss.height && hitTop + hitH > boss.position.y ) { boss.takeDamage(1); this.updateHud(`Laser hit! ${this.config.enemyName} took damage.`); this.lasers.splice(i, 1); } }
This collision code checks whether a player laser overlaps the boss hitbox. If it does, the boss takes damage and the laser is removed. The updated version also uses laser sources so Player 1 lasers can be cyan, Player 2 lasers can be green, and boss lasers can be red. This makes the gameplay easier to understand visually.