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

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.