Building Dragon Survival
In this lesson you are going to build a basic game named "Dragon Survival" it takes inspiration from Adventure by Warren Robinett which he designed in 1979, ancient news in the gaming industry; however, the same rules still apply! Don't forget to grab the Basic Game Desgin Companion Doc
Open a blank text file. Copy each code block below in order. Paste them sequentially into your file and save it as index.html. When you reach the end, you will have a fully playable 8-bit style browser game.
1. The Foundation
Every page needs a skeleton. We start with the HTML declaration, the head for our CSS styling, and the body to hold our game canvas and control buttons. We keep the graphics pixelated for a proper retro aesthetic.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Analog Rebellion Engine</title>
<style>
body { background-color: #000; color: #39ff14; font-family: 'Courier New', Courier, monospace; text-shadow: 0 0 5px #39ff14; text-align: center; }
canvas { background-color: rgba(0, 20, 0, 0.8); border: 2px solid #39ff14; image-rendering: pixelated; margin: 20px auto; display: block; box-shadow: 0 0 10px #39ff14; max-width: 600px; width: 100%; }
.d-pad { display: grid; grid-template-columns: repeat(3, 60px); gap: 5px; justify-content: center; margin-bottom: 20px; }
button { background: #000; color: #39ff14; border: 2px solid #39ff14; cursor: pointer; font-weight: bold; width: 60px; height: 60px; touch-action: none; }
button:hover { background: #39ff14; color: #000; text-shadow: none; }
.empty { visibility: hidden; }
.action-btn { width: 90px; height: 90px; border-radius: 50%; }
.controls-wrapper { display: flex; justify-content: center; gap: 40px; align-items: center; }
</style>
</head>
<body>
<h1>DRAGON SURVIVAL</h1>
<canvas id="gameCanvas" width="600" height="400"></canvas>
<div class="controls-wrapper">
<div class="d-pad">
<div class="empty"></div><button id="upBtn">UP</button><div class="empty"></div>
<button id="leftBtn">LFT</button><button id="downBtn">DWN</button><button id="rightBtn">RGT</button>
</div>
<button class="action-btn" id="actBtn">ACTION</button>
</div>
2. The Actors and Input
We do not need massive 3D engines to create tension. You just need collision detection and a solid loop. Every entity is a simple JavaScript object defined by X and Y coordinates. We map our keyboard and touch inputs here so the player can move.
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let gameLoopId;
let gameStarted = false;
const keys = { ArrowUp: false, ArrowDown: false, ArrowLeft: false, ArrowRight: false };
let player = { x: 50, y: 180, w: 20, h: 20, speed: 4, color: '#39ff14', hasSword: false };
let sword = { x: 150, y: 185, w: 25, h: 5, color: '#fff' };
let dragon = { x: 450, y: 150, w: 40, h: 40, color: 'red', speed: 2, dy: 2, alive: true, respawnTime: 0 };
let fire = { x: 450, y: 165, w: 30, h: 10, color: 'orange', speed: 6, active: true };
let chest = { x: 540, y: 180, w: 30, h: 30, color: 'gold' };
let wallTop = { x: 500, y: 0, w: 20, h: 160, color: 'gray' };
let wallBottom = { x: 500, y: 240, w: 20, h: 160, color: 'gray' };
let door = { x: 500, y: 160, w: 20, h: 80, color: 'brown' };
let npcs = [];
let survivors = 3;
const bindBtn = (id, key) => {
const btn = document.getElementById(id);
btn.addEventListener('pointerdown', () => keys[key] = true);
btn.addEventListener('pointerup', () => keys[key] = false);
btn.addEventListener('pointerleave', () => keys[key] = false);
};
bindBtn('upBtn', 'ArrowUp'); bindBtn('downBtn', 'ArrowDown');
bindBtn('leftBtn', 'ArrowLeft'); bindBtn('rightBtn', 'ArrowRight');
document.getElementById('actBtn').addEventListener('pointerdown', toggleAction);
window.addEventListener('keydown', e => {
if (keys.hasOwnProperty(e.code)) { keys[e.code] = true; e.preventDefault(); }
if (e.code === 'Space') { toggleAction(); e.preventDefault(); }
});
window.addEventListener('keyup', e => {
if (keys.hasOwnProperty(e.code)) keys[e.code] = false;
});
3. The Rules (Collision and Reset)
We use Axis-Aligned Bounding Box (AABB) math for collisions. We simply check if two squares overlap. If they do, the game executes a specific rule. We also need functions to restart the board and handle the player picking up the weapon.
function toggleAction() {
if (!gameStarted) { gameStarted = true; return; }
if (player.hasSword) { player.hasSword = false; }
else if (checkCollision(player, sword)) { player.hasSword = true; }
}
function checkCollision(r1, r2) {
return r1.x < r2.x + r2.w && r1.x + r1.w > r2.x && r1.y < r2.y + r2.h && r1.h + r1.y > r2.y;
}
function resetGame() {
player.x = 50; player.y = 180; player.hasSword = false;
sword.x = 150; sword.y = 185;
dragon.alive = true; dragon.x = 450; dragon.y = 150;
fire.x = dragon.x; fire.y = dragon.y + 15; fire.active = true;
npcs = [
{ x: 100, y: 50, w: 15, h: 15, color: 'blue', alive: true, dx: 2, dy: 2 },
{ x: 100, y: 200, w: 15, h: 15, color: 'blue', alive: true, dx: -2, dy: 2 },
{ x: 200, y: 300, w: 15, h: 15, color: 'blue', alive: true, dx: 2, dy: -2 }
];
survivors = 3;
gameStarted = false;
}
function triggerEnd(msg) {
cancelAnimationFrame(gameLoopId);
if (confirm(msg)) {
resetGame();
update();
}
}
4. The Logic (Walls, Doors, and Survival)
What if there were a wall behind the dragon so you couldn't just run up and grab the treasure? What if there were a door? Make the door open if there is no dragon on the screen. We track the player's old position before moving. If they hit a solid wall, or if they hit the door while the dragon lives, we snap them right back. The path is blocked.
Now consider this: what if there were 3 NPC characters running randomly around the screen but staying away from the dragon? What if they die if dragon fire hits them and a survivor counter ticks down? If it hits 0 then you lose. We handle this by looping through an array of villagers. We reverse their direction if they cross the halfway point of the screen to keep them away from the beast, and we add a small random jitter to their movement to simulate panic. This final block puts all of that logic together, draws everything to the canvas, and closes out your HTML file.
function update() {
if (!gameStarted) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#39ff14'; ctx.font = 'bold 24px Courier New'; ctx.textAlign = 'center';
ctx.fillText('PRESS ACTION TO START', canvas.width / 2, canvas.height / 2);
gameLoopId = requestAnimationFrame(update);
return;
}
let prevX = player.x; let prevY = player.y;
if (keys.ArrowUp) player.y -= player.speed;
if (keys.ArrowDown) player.y += player.speed;
if (keys.ArrowLeft) player.x -= player.speed;
if (keys.ArrowRight) player.x += player.speed;
if (player.x < 0) player.x = 0;
if (player.y < 0) player.y = 0;
if (player.x + player.w > canvas.width) player.x = canvas.width - player.w;
if (player.y + player.h > canvas.height) player.y = canvas.height - player.h;
if (checkCollision(player, wallTop) || checkCollision(player, wallBottom)) {
player.x = prevX; player.y = prevY;
}
if (dragon.alive && checkCollision(player, door)) {
player.x = prevX; player.y = prevY;
}
if (player.hasSword) {
sword.x = player.x + player.w;
sword.y = player.y + (player.h / 2) - (sword.h / 2);
}
if (dragon.alive) {
dragon.y += dragon.dy;
if (dragon.y < 0 || dragon.y + dragon.h > canvas.height) dragon.dy *= -1;
fire.x -= fire.speed;
if (fire.x < 0) { fire.x = dragon.x; fire.y = dragon.y + 15; }
if (player.hasSword && checkCollision(sword, dragon)) {
dragon.alive = false; fire.active = false; dragon.respawnTime = Date.now() + 5000;
} else if (checkCollision(player, dragon)) {
triggerEnd("Game Over! The dragon got you."); return;
}
if (fire.active && checkCollision(player, fire)) {
triggerEnd("Game Over! You burned."); return;
}
} else if (Date.now() > dragon.respawnTime) {
dragon.alive = true; dragon.y = 150; fire.x = dragon.x; fire.y = dragon.y + 15; fire.active = true;
}
if (checkCollision(player, chest)) {
triggerEnd("You win! You claimed the gold."); return;
}
let allDead = false;
npcs.forEach(npc => {
if (!npc.alive) return;
npc.x += npc.dx; npc.y += npc.dy;
if (npc.x < 0 || npc.x + npc.w > 480) npc.dx *= -1;
if (npc.y < 0 || npc.y + npc.h > canvas.height) npc.dy *= -1;
if (Math.random() < 0.02) npc.dx *= -1;
if (Math.random() < 0.02) npc.dy *= -1;
if (dragon.alive && fire.active && checkCollision(npc, fire)) {
npc.alive = false;
survivors--;
if (survivors <= 0) allDead = true;
}
});
if (allDead) {
triggerEnd("All villagers died. Game Over!");
return;
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = chest.color; ctx.fillRect(chest.x, chest.y, chest.w, chest.h);
ctx.fillStyle = wallTop.color; ctx.fillRect(wallTop.x, wallTop.y, wallTop.w, wallTop.h);
ctx.fillStyle = wallBottom.color; ctx.fillRect(wallBottom.x, wallBottom.y, wallBottom.w, wallBottom.h);
if (dragon.alive) {
ctx.fillStyle = door.color; ctx.fillRect(door.x, door.y, door.w, door.h);
ctx.fillStyle = dragon.color; ctx.fillRect(dragon.x, dragon.y, dragon.w, dragon.h);
if (fire.active) { ctx.fillStyle = fire.color; ctx.fillRect(fire.x, fire.y, fire.w, fire.h); }
}
npcs.forEach(npc => {
if (npc.alive) {
ctx.fillStyle = npc.color; ctx.fillRect(npc.x, npc.y, npc.w, npc.h);
}
});
ctx.fillStyle = '#fff'; ctx.font = '16px Courier New'; ctx.textAlign = 'left';
ctx.fillText('SURVIVORS: ' + survivors, 10, 20);
ctx.fillStyle = sword.color; ctx.fillRect(sword.x, sword.y, sword.w, sword.h);
ctx.fillStyle = player.color; ctx.fillRect(player.x, player.y, player.w, player.h);
gameLoopId = requestAnimationFrame(update);
}
resetGame();
update();
</script>
</body>
</html>
The Final Playable Game
Save your file and run it, or test the live engine right here. Defend the villagers, grab the sword, slay the dragon to open the door, and secure the treasure.
5. The Bug Hunt
What happens if you are on mobile and hit Action instead of OK in the alert popup? What if you hit Cancel in the Alert popup? The game is frozen! That is called a BUG!
Bugs are inevitable. They are the mechanical friction in your engine. Hunting them down requires applying the Minimum Input Maximum Gain philosophy in reverse. You start at the maximum failure and trace it back to the minimum input.
Methodology for Exterminating Bugs
- Reproduce the Error: You cannot fix what you cannot consistently break. Trigger the freeze on purpose to see exactly how it behaves.
- Trace the Logic: Read your code like the machine does. When a player dies, the game calls the
triggerEnd(msg) function. That function immediately stops the animation frame. Then it uses an if statement. If you hit OK, the condition is true. The game resets and updates.
- Isolate the Failure: Look at what happens if the
if statement is false because you hit Cancel. Absolutely nothing. The code just ends. The animation frame is already canceled, so the engine flatlines. The loop is permanently broken.
- Apply the Fix: We need to handle the alternate reality. We add an
else condition. If the player hits cancel or taps away, we still need to reset the variables and restart the engine loop so the "PRESS ACTION TO START" screen appears.
The Code Fix
Find the triggerEnd(msg) function in your script and replace it with this updated block.
function triggerEnd(msg) {
cancelAnimationFrame(gameLoopId);
if (confirm(msg)) {
// Player hit OK
resetGame();
update();
} else {
// Player hit Cancel or clicked away
resetGame();
update();
}
}
Save your file and run it again. Hit cancel when you die. The loop is now unbreakable. Your engine is solid.