Skip to content

Instantly share code, notes, and snippets.

@straker
Last active October 1, 2024 15:26
Show Gist options
  • Save straker/df855f22e57576c80d6126aa5609654e to your computer and use it in GitHub Desktop.
Save straker/df855f22e57576c80d6126aa5609654e to your computer and use it in GitHub Desktop.
Basic Block Dude HTML and JavaScript Game

Basic Block Dude HTML and JavaScript Game

This is a basic implementation of Block Dude, an old TI-83/4 Calculator game. It's missing a few things intentionally and they're left as further exploration for the reader.

Further Exploration

  • More levels
    • Add more levels and have the next level start once the last one is finished
  • Add Restart
    • If the player gets stuck they should be able to restart the current level
  • Mobile and touchscreen support
  • Level Skip
    • The original game had codes for each level that would allow a player to skip to that level

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 Block Dude HTML Game</title>
<meta charset="UTF-8">
<style>
html, body {
height: 100%;
margin: 0;
}
body {
background: #fafafa;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
canvas {
border: 1px solid white;
margin-bottom: 1rem;
}
</style>
</head>
<body>
<canvas width="384" height="256" id="game"></canvas>
<div>
<div><b>CONTROLS</b></div>
<div><b>Left / Right Arrow:</b> Move left / right</div>
<div><b>Down Arrow:</b> Pick up or drop block</div>
</div>
<script>
// since block dude and sokoban share similarities in look and play style
// we'll reuse a lot of the code from the basic sokoban game
// @see https://gist.github.com/straker/2fddb507d4bb6bec54ea2fdb022d020c
const canvas = document.getElementById('game');
const context = canvas.getContext('2d');
const grid = 32;
// create a new canvas and draw the 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 = 'white';
wallCtx.fillRect(1, 1, grid, grid);
wallCtx.fillStyle = 'black';
// 1st row brick
wallCtx.fillRect(0, 1, 21, 10);
wallCtx.fillRect(23, 1, 10, 10);
// 2nd row bricks
wallCtx.fillRect(0, 12, 10, 9);
wallCtx.fillRect(11, 12, 21, 9);
// 3rd row bricks
wallCtx.fillRect(0, 22, 21, 10);
wallCtx.fillRect(23, 22, 10, 10);
// the direction to move the player each frame. we'll use change in
// direction so "row: 1" means move down 1 row, "row: -1" means move
// up one row, etc.
let playerDir = { row: 0, col: 0 };
let playerPos = { row: 0, col: 0 }; // player position in the 2d array
let playerFacing = -1; // the direction the player is facing (1 for right, -1 for left)
let rAF = null; // keep track of the animation frame so we can cancel it
let carryingBlock = false; // if the player is carrying a block
let width = 0; // find the largest row and use that as the game width
// create a mapping of object types using the sok file format
const types = {
wall: '#',
player: '@',
block: '$',
goal: '.',
empty: ' '
};
// a level using the sok file format
const level1 = `
# ## ##
# #
## #
#. #
## #
# # $ #
# #$ $$@ #
##### #############
# $#
#####
`;
// keep track of what is in every cell of the game using a 2d array
const cells = [];
// use each line of the level as the row (remove empty lines)
level1.split('\n')
.filter(rowData => !!rowData)
.forEach((rowData, row) => {
cells[row] = [];
if (rowData.length > width) {
width = rowData.length;
}
// use each character of the level as the col
rowData.split('').forEach((colData, col) => {
cells[row][col] = colData;
if (colData === types.player) {
playerPos = { row, col };
}
});
});
// clamp a value between two values
function clamp(min, max, value) {
return Math.min(Math.max(min, value), max);
}
// move an entity from one cell to another
function move(startPos, endPos) {
const startCell = cells[startPos.row][startPos.col];
const endCell = cells[endPos.row][endPos.col];
const isPlayer = startCell === types.player;
// first remove then entity from its current cell
switch(startCell) {
// if the start cell is the player or a block (no goal)
// then leave empty
case types.player:
case types.block:
cells[startPos.row][startPos.col] = types.empty;
break;
}
// then move then entity into the new cell
switch(endCell) {
// if the end cell is empty, add the block or player
case types.empty:
cells[endPos.row][endPos.col] = isPlayer ? types.player : types.block;
break;
}
playerFacing = endPos.col - startPos.col;
// move the block along with the player
if (carryingBlock) {
cells[startPos.row - 1][startPos.col] = types.empty;
cells[endPos.row - 1][endPos.col] = types.block;
}
}
// game loop
function loop() {
rAF = requestAnimationFrame(loop);
context.clearRect(0,0,canvas.width,canvas.height);
// check to see if the player can move in the desired direction
let row = playerPos.row + playerDir.row;
const col = playerPos.col + playerDir.col;
const cell = cells[row][col];
switch(cell) {
// allow the player to move into empty or goal cells
case types.empty:
case types.goal:
// apply gravity
let rowBelow = row + 1 + playerDir.row;
let belowCell = cells[rowBelow][col];
while (belowCell === types.empty || belowCell == types.goal) {
row = rowBelow;
rowBelow = row + 1 + playerDir.row;
belowCell = cells[rowBelow][col];
}
move(playerPos, { row, col });
playerPos.row = row;
playerPos.col = col;
// end game
if (cell === types.goal) {
cancelAnimationFrame(rAF);
}
break;
// only allow the player to move on top of a block or wall
// if it is empty above
case types.block:
case types.wall:
const rowAbove = row - 1 + playerDir.row;
const nextCell = cells[rowAbove][col];
if (nextCell === types.empty || nextCell === types.goal) {
move(playerPos, { row: rowAbove, col });
playerPos.row = rowAbove;
playerPos.col = col;
}
break;
}
// reset player dir after checking move
playerDir = { row: 0, col: 0 };
// draw the board
context.strokeStyle = 'black';
context.fillStyle = 'black';
context.lineWidth = 2;
// center the view to the player but don't let the view go outside
// the game boundaries
const startRow = clamp(0, cells.length - 8, playerPos.row - 4);
const startCol = clamp(0, width - 12, playerPos.col - 6);
for (let row = startRow; row < cells.length; row++) {
for (let col = startCol; col < cells[row].length; col++) {
const cell = cells[row][col];
const drawRow = row - startRow;
const drawCol = col - startCol;
switch(cell) {
case types.wall:
context.drawImage(wallCanvas, drawCol * grid, drawRow * grid);
break;
case types.block:
context.strokeRect(drawCol * grid, drawRow * grid, grid, grid);
break;
case types.goal:
context.strokeRect((drawCol + 0.2) * grid, drawRow * grid, grid - 12, grid);
context.beginPath();
context.arc((drawCol + 0.7) * grid, (drawRow + 0.5) * grid, 2, 0, Math.PI * 2);
context.fill();
break;
case types.player:
context.beginPath();
// head
context.arc((drawCol + 0.5) * grid, (drawRow + 0.3) * grid, 7, 0, Math.PI * 2);
context.stroke();
// hat
const x = (drawCol + ( playerFacing < 0 ? 0.1 : 0.6)) * grid;
context.fillRect(x, (drawRow + 0.15) * grid, grid / 3, 2);
context.beginPath();
context.arc((drawCol + 0.5) * grid, (drawRow + 0.25) * grid, 7, 0, Math.PI, 1);
context.fill();
// body
context.fillRect((drawCol + 0.48) * grid, (drawRow + 0.4) * grid, 2, grid / 2.5 );
// arms
context.fillRect((drawCol + 0.3) * grid, (drawRow + 0.6) * grid, grid / 2.5, 2);
// legs
context.moveTo((drawCol + 0.5) * grid, (drawRow + 0.8) * grid);
context.lineTo((drawCol + 0.65) * grid, (drawRow + 1) * grid);
context.moveTo((drawCol + 0.5) * grid, (drawRow + 0.8) * grid);
context.lineTo((drawCol + 0.35) * grid, (drawRow + 1) * grid);
context.stroke();
}
}
}
}
// listen to keyboard events to move the player
document.addEventListener('keydown', function(e) {
playerDir = { row: 0, col: 0};
// left arrow key
if (e.which === 37) {
playerDir.col = -1;
}
// right arrow key
else if (e.which === 39) {
playerDir.col = 1;
}
// down arrow key
else if (e.which === 40) {
const nextCol = playerFacing + playerPos.col;
const nextCell = cells[playerPos.row][nextCol];
const cellAbove = cells[playerPos.row - 1][nextCol];
const cellBelow = cells[playerPos.row + 1][nextCol];
// pick up block only if there isn't a block on top of it
if (
!carryingBlock &&
nextCell === types.block &&
cellAbove === types.empty
) {
cells[playerPos.row][nextCol] = types.empty;
cells[playerPos.row - 1][playerPos.col] = types.block;
carryingBlock = true;
}
// put down block
else if (carryingBlock) {
let row = playerPos.row;
// drop block
if (nextCell === types.empty) {
// apply gravity
let rowBelow = row - 1;
let belowCell = cells[rowBelow][nextCol];
while (belowCell === types.empty) {
row = rowBelow;
rowBelow++;
belowCell = cells[rowBelow][nextCol];
}
}
// put block on top wall or block
if (
(nextCell === types.wall ||
nextCell === types.block) &&
cellAbove === types.empty
) {
row = row - 1;
}
cells[playerPos.row - 1][playerPos.col] = types.empty;
cells[row][nextCol] = types.block;
carryingBlock = false;
}
}
});
// start the game
requestAnimationFrame(loop);
</script>
</body>
</html>
@parkerguy
Copy link

there is just one level right

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