Skip to content

Instantly share code, notes, and snippets.

@straker
Last active February 24, 2024 15:01
  • Star 24 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save straker/769fb461e066147ea16ac2cb9463beae to your computer and use it in GitHub Desktop.
Basic Bomberman HTML and JavaScript Game

Basic Bomberman HTML and JavaScript Game

This is a basic implementation of the NES game Bomberman, but it's missing a few things intentionally and they're left as further exploration for the reader.

Further Exploration

  • Player death
    • The player should die when it is hit by an explosion from a bomb
  • Powerups
    • Add powerups that increase the bomb size and the number of bombs the player can place. The powerups should spawn randomly after a soft wall is blown up
  • 2nd Player
    • Add a 2nd player to the game in the opposite corner and add additional controls so they can be played by someone else

Important note: I will answer questions about the code but will not add more features or answer questions about adding more features. This series is meant to give a basic outline of the game but nothing more.

License

(CC0 1.0 Universal) You're free to use this game and code in any project, personal or commercial. There's no need to ask permission before using these. Giving attribution is not required, but appreciated.

Other Basic Games

Support

Basic HTML Games are made possible by users like you. When you become a Patron, you get access to behind the scenes development logs, the ability to vote on which games I work on next, and early access to the next Basic HTML Game.

Top Patrons

  • Karar Al-Remahy
  • UnbrandedTech
  • Innkeeper Games
  • Nezteb
<!DOCTYPE html>
<html>
<head>
<title>Basic Bomberman HTML Game</title>
<meta charset="UTF-8">
<style>
html, body {
height: 100%;
margin: 0;
}
body {
background: black;
display: flex;
align-items: center;
justify-content: center;
}
canvas {
background: forestgreen;
}
</style>
</head>
<body>
<canvas width="960" height="832" id="game"></canvas>
<script>
const canvas = document.getElementById('game');
const context = canvas.getContext('2d');
const grid = 64;
const numRows = 13;
const numCols = 15;
// create a new canvas and draw the soft wall image. then we can use this
// canvas to draw the images later on
const softWallCanvas = document.createElement('canvas');
const softWallCtx = softWallCanvas.getContext('2d');
softWallCanvas.width = softWallCanvas.height = grid;
softWallCtx.fillStyle = 'black';
softWallCtx.fillRect(0, 0, grid, grid);
softWallCtx.fillStyle = '#a9a9a9';
// 1st row brick
softWallCtx.fillRect(1, 1, grid - 2, 20);
// 2nd row bricks
softWallCtx.fillRect(0, 23, 20, 18);
softWallCtx.fillRect(22, 23, 42, 18);
// 3rd row bricks
softWallCtx.fillRect(0, 43, 42, 20);
softWallCtx.fillRect(44, 43, 20, 20);
// create a new canvas and draw the soft wall image. then we can use this
// canvas to draw the images later on
const wallCanvas = document.createElement('canvas');
const wallCtx = wallCanvas.getContext('2d');
wallCanvas.width = wallCanvas.height = grid;
wallCtx.fillStyle = 'black';
wallCtx.fillRect(0, 0, grid, grid);
wallCtx.fillStyle = 'white';
wallCtx.fillRect(0, 0, grid - 2, grid - 2);
wallCtx.fillStyle = '#a9a9a9';
wallCtx.fillRect(2, 2, grid - 4, grid - 4);
// create a mapping of object types
const types = {
wall: '▉',
softWall: 1,
bomb: 2
};
// keep track of all entities
let entities = [];
// keep track of what is in every cell of the game using a 2d array. the
// template is used to note where walls are and where soft walls cannot spawn.
// '▉' represents a wall
// 'x' represents a cell that cannot have a soft wall (player start zone)
let cells = [];
const template = [
['▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉'],
['▉','x','x', , , , , , , , , ,'x','x','▉'],
['▉','x','▉', ,'▉', ,'▉', ,'▉', ,'▉', ,'▉','x','▉'],
['▉','x', , , , , , , , , , , ,'x','▉'],
['▉', ,'▉', ,'▉', ,'▉', ,'▉', ,'▉', ,'▉', ,'▉'],
['▉', , , , , , , , , , , , , ,'▉'],
['▉', ,'▉', ,'▉', ,'▉', ,'▉', ,'▉', ,'▉', ,'▉'],
['▉', , , , , , , , , , , , , ,'▉'],
['▉', ,'▉', ,'▉', ,'▉', ,'▉', ,'▉', ,'▉', ,'▉'],
['▉','x', , , , , , , , , , , ,'x','▉'],
['▉','x','▉', ,'▉', ,'▉', ,'▉', ,'▉', ,'▉','x','▉'],
['▉','x','x', , , , , , , , , ,'x','x','▉'],
['▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉']
];
// populate the level with walls and soft walls
function generateLevel() {
cells = [];
for (let row = 0; row < numRows; row++) {
cells[row] = [];
for (let col = 0; col < numCols; col++) {
// 90% chance cells will contain a soft wall
if (!template[row][col] && Math.random() < 0.90) {
cells[row][col] = types.softWall;
}
else if (template[row][col] === types.wall) {
cells[row][col] = types.wall;
}
}
}
}
// blow up a bomb and its surrounding tiles
function blowUpBomb(bomb) {
// bomb has already exploded so don't blow up again
if (!bomb.alive) return;
bomb.alive = false;
// remove bomb from grid
cells[bomb.row][bomb.col] = null;
// explode bomb outward by size
const dirs = [{
// up
row: -1,
col: 0
}, {
// down
row: 1,
col: 0
}, {
// left
row: 0,
col: -1
}, {
// right
row: 0,
col: 1
}];
dirs.forEach((dir) => {
for (let i = 0; i < bomb.size; i++) {
const row = bomb.row + dir.row * i;
const col = bomb.col + dir.col * i;
const cell = cells[row][col];
// stop the explosion if it hit a wall
if (cell === types.wall) {
return;
}
// center of the explosion is the first iteration of the loop
entities.push(new Explosion(row, col, dir, i === 0 ? true : false));
cells[row][col] = null;
// bomb hit another bomb so blow that one up too
if (cell === types.bomb) {
// find the bomb that was hit by comparing positions
const nextBomb = entities.find((entity) => {
return (
entity.type === types.bomb &&
entity.row === row && entity.col === col
);
});
blowUpBomb(nextBomb);
}
// stop the explosion if hit anything
if (cell) {
return;
}
}
});
}
// bomb constructor function
function Bomb(row, col, size, owner) {
this.row = row;
this.col = col;
this.radius = grid * 0.4;
this.size = size; // the size of the explosion
this.owner = owner; // which player placed this bomb
this.alive = true;
this.type = types.bomb;
// bomb blows up after 3 seconds
this.timer = 3000;
// update the bomb each frame
this.update = function(dt) {
this.timer -= dt;
// blow up bomb if timer is done
if (this.timer <= 0) {
return blowUpBomb(this);
}
// change the size of the bomb every half second. we can determine the size
// by dividing by 500 (half a second) and taking the ceiling of the result.
// then we can check if the result is even or odd and change the size
const interval = Math.ceil(this.timer / 500);
if (interval % 2 === 0) {
this.radius = grid * 0.4;
}
else {
this.radius = grid * 0.5;
}
};
// render the bomb each frame
this.render = function() {
const x = (this.col + 0.5) * grid;
const y = (this.row + 0.5) * grid;
// draw bomb
context.fillStyle = 'black';
context.beginPath();
context.arc(x, y, this.radius, 0, 2 * Math.PI);
context.fill();
// draw bomb fuse moving up and down with the bomb size
const fuseY = (this.radius === grid * 0.5 ? grid * 0.15 : 0);
context.strokeStyle = 'white';
context.lineWidth = 5;
context.beginPath();
context.arc(
(this.col + 0.75) * grid,
(this.row + 0.25) * grid - fuseY,
10, Math.PI, -Math.PI / 2
);
context.stroke();
};
}
// explosion constructor function
function Explosion(row, col, dir, center) {
this.row = row;
this.col = col;
this.dir = dir;
this.alive = true;
// show explosion for 0.3 seconds
this.timer = 300;
// update the explosion each frame
this.update = function(dt) {
this.timer -= dt;
if (this.timer <=0) {
this.alive = false;
}
};
// render the explosion each frame
this.render = function() {
const x = this.col * grid;
const y = this.row * grid;
const horizontal = this.dir.col;
const vertical = this.dir.row;
// create a fire effect by stacking red, orange, and yellow on top of
// each other using progressively smaller rectangles
context.fillStyle = '#D72B16'; // red
context.fillRect(x, y, grid, grid);
context.fillStyle = '#F39642'; // orange
// determine how to draw based on if it's vertical or horizontal
// center draws both ways
if (center || horizontal) {
context.fillRect(x, y + 6, grid, grid - 12);
}
if (center || vertical) {
context.fillRect(x + 6, y, grid - 12, grid);
}
context.fillStyle = '#FFE5A8'; // yellow
if (center || horizontal) {
context.fillRect(x, y + 12, grid, grid - 24);
}
if (center || vertical) {
context.fillRect(x + 12, y, grid - 24, grid);
}
};
}
// player character (just a simple circle)
const player = {
row: 1,
col: 1,
numBombs: 1,
bombSize: 3,
radius: grid * 0.35,
render() {
const x = (this.col + 0.5) * grid;
const y = (this.row + 0.5) * grid;
context.save();
context.fillStyle = 'white';
context.beginPath();
context.arc(x, y, this.radius, 0, 2 * Math.PI);
context.fill();
}
}
// game loop
let last;
let dt;
function loop(timestamp) {
requestAnimationFrame(loop);
context.clearRect(0,0,canvas.width,canvas.height);
// calculate the time difference since the last update. requestAnimationFrame
// passes the current timestamp as a parameter to the loop
if (!last) {
last = timestamp;
}
dt = timestamp - last;
last = timestamp;
// update and render everything in the grid
for (let row = 0; row < numRows; row++) {
for (let col = 0; col < numCols; col++) {
switch(cells[row][col]) {
case types.wall:
context.drawImage(wallCanvas, col * grid, row * grid);
break;
case types.softWall:
context.drawImage(softWallCanvas, col * grid, row * grid);
break;
}
}
}
// update and render all entities
entities.forEach((entity) => {
entity.update(dt);
entity.render();
});
// remove dead entities
entities = entities.filter((entity) => entity.alive);
player.render();
}
// listen to keyboard events to move the snake
document.addEventListener('keydown', function(e) {
let row = player.row;
let col = player.col;
// left arrow key
if (e.which === 37) {
col--;
}
// up arrow key
else if (e.which === 38) {
row--;
}
// right arrow key
else if (e.which === 39) {
col++;
}
// down arrow key
else if (e.which === 40) {
row++;
}
// space key (bomb)
else if (
e.which === 32 && !cells[row][col] &&
// count the number of bombs the player has placed
entities.filter((entity) => {
return entity.type === types.bomb && entity.owner === player
}).length < player.numBombs
) {
// place bomb
const bomb = new Bomb(row, col, player.bombSize, player);
entities.push(bomb);
cells[row][col] = types.bomb;
}
// don't move the player if something is already at that position
if (!cells[row][col]) {
player.row = row;
player.col = col;
}
});
// start the game
generateLevel();
requestAnimationFrame(loop);
</script>
</body>
</html>
@straker
Copy link
Author

straker commented Feb 3, 2021

@wilsoncolin544 Is there a JavaScript error in the devtools? I can't think of a reason it wouldn't work off the top of my head.

@wilsoncolin544
Copy link

ok, thank you @straker I will check

@OzzyCantu
Copy link

@straker how do I replace the up, down, left, right keys with the WASD keys?

@straker
Copy link
Author

straker commented Apr 8, 2021

@OzzyCantu you can replace these key code numbers with the key code numbers of WASD. I like to use https://keycode.info/ to find the key code associated with a key

@Jackson595
Copy link

GREAT GAME!

@Jackson595
Copy link

How do you die?

@straker
Copy link
Author

straker commented Apr 26, 2021

@Jackson595 having the player die has ben intentionally left out.

@Jackson595
Copy link

thank you!

@aizigao
Copy link

aizigao commented Nov 23, 2022

nice!!

@rmkane
Copy link

rmkane commented Nov 17, 2023

I prefer to store the level layout as a multi-line string, and then parse it when I want to convert it into a matrix.

const level1 = `
▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉
▉xx         xx▉
▉x▉ ▉ ▉ ▉ ▉ ▉x▉
▉x           x▉
▉ ▉ ▉ ▉ ▉ ▉ ▉ ▉
▉             ▉
▉ ▉ ▉ ▉ ▉ ▉ ▉ ▉
▉             ▉
▉ ▉ ▉ ▉ ▉ ▉ ▉ ▉
▉x           x▉
▉x▉ ▉ ▉ ▉ ▉ ▉x▉
▉xx         xx▉
▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉
`;

const template = parseLevel(level1); // Convert it into the original 2D Matrix

function parseLevel(level) {
  return level
    .trim()
    .split("\n")
    .map((row) =>
      row
        .trim()
        .split("")
        .map((s) => (s !== " " ? s : null))
    );
}

@rmkane
Copy link

rmkane commented Nov 17, 2023

Also, avoid using UIEvent.which in favor of KeyboardEvent.code (more specific, based on keyboard layout) or KeyboardEvent.key (general, not based on keyboard layout).

The main difference between the two is that code will report the physical key on the keyboard, whereas key will only report the key type. For example, if you press the "Right Shift" key, the value of code will be ShiftRight, but the value of key will be simply Shift. If you need to treat the left and right Alt, Ctrl, or Shift keys as different actions then you should use code. If you do not care, just stick with the safer key.

For the alpha/number/arrow keys the value will typically be the same.

I noticed that Steven's later games i.e. Helicopter and Puzzle Bobble use code instead of which.

Here is a simple website for checking keyboard event properties: https://www.toptal.com/developers/keycode

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment