As a web developer, a part of my job is staying up-to-date with new technologies, to be able to offer my users the best possible web experience.

That’s why I decided to create some small games trying out the best our browsers have to offer in terms of rendering: HTML Tables.

First of all: I’m a backend developer; I did those experiments primarily to piss off my coworkers that live on the client-side of life.

Instead of getting angry, they asked me to write an article about it, ’cause you know: tables.

So, if you are a frontend developer, don’t be scared: take a deep breath, read this article slowly and you should be able to cope with it, okay?

Good.

The beginning: Maze & Minotaurs

The first experiment is quite an old one and it’s a simple random generated Maze-game:

You can find the GitHub repo here

Basically every time you refresh the page a maze is created; the goal is to reach the exit (the red cell with an X ) starting from the start cell (the blue one with an S ).

As it seemed quite boring to manually move around a maze, I decided to put the focus on having the game solve itself: after generating the maze, an invisible hero starts searching for the exit.

We can see the path he’s taking by following the yellow marked cells. Whenever the hero finds a dead end he backtracks its own steps (orange cells) to the last crossroad.

Once I was done, it hits me that there was no challenge, so I added an enemy: the Minotaur, which is marked by a grey cell with an M inside. The Minotaur will randomly walk around the maze and eat you if it touches you; as it now seemed too difficult, I also added the fact that the Minotaur sleeps for 10 steps every 40 steps the hero takes; while the Minotaur is asleep, the hero can pass through him (sleeping Minotaur is marked with a Z ).

How it’s done

The maze generation is done with a simple randomized depth-first search algorithm: we have a group of nodes, mapped as a matrix, and starting from one node we try to connect it to a random non-visited neighbour cell, if there are any available, and flag it as visited; if not we go back to the last visited cell with at least one non-visited neighbour cell and reiterate the logic until all nodes are visited exactly once.

As we can see, a table is the best way to print out our maze: every <td> can be a wall, a path, the start, the exit or the minotaur.

The thing about tables and HTML is that they provide us not only a neat looking maze, but also a model where we can store our data: we can literally remember only the coordinates of our hero and query the <td> ’s data-attributes, css classes and text values to know if we can go that way, if we already passed there, if we reached the exit or if the minotaur has killed us.

The maze solving algorithm is basically the same algorithm as the previous one, the only difference being that instead of creating a connection we follow a connection if it has not been already visited.

How do we decide if we already visited that path? Breadcrumbs, of course: at every step we mark the visited cell with a dot ( . ).

When we arrive at a crossroad, we randomly follow one of the non visited way; when we arrive at a dead end, we follow back our breadcrumbs until we find a crossroad, then go down a non-dotted path.

So, yeah, not much of a gameplay here: you load the page and look while the magic happens.

You can cheer, if you’re into that.

Let’s move on.

The challenge: do you even table?

When my frontend colleagues found out I did that, they had mixed feelings about it; something in between shock, horror and “is that jQuery?”-fueled rage.

(Yep, I use jQuery; I like your point of Vue, but I don’t understand why you should React that way)

But what they hated most was the fact that it was all done through a table instead of using a canvas.

Or, to put it more bluntly, they weren’t up to date with the Table API offered by the web nowadays (spoiler alert: they’ve been the same for quite some time!)

They said there were other technologies, best suited to do what I was trying to achieve, most of them open source.

They passed me links to free game engines, drawing libraries and tutorials from all around the internet.

They tried to make me go to GitHub, but I said, no, no, no.

I took it as a personal challenge to prove ’em wrong.

And I did. Oh, boy I did!

(no, I did not)

First Game: Snake

I started out with an old classic.

Here the repo

The game was perfect: it had a discreet grid and simple mechanics.

As I am lazy as hell, I decided to give it the simplest old school look I could think about.

Or, to put it another way, I didn’t want to add special effects, like the animation of the snake swallowing the bug, seen in Snake 2; we all know that’s what destroyed the franchise.

How it’s done

Once again: in this case too the table is the perfect way to both render and store data at the same time. The board is even simpler than the maze, as it has no obstacles and literally only 3 types of cell (free cell, bug and snake).

What we needed to remember is also quite simple: it literally is an array of coordinates, where the first object represents the head and the other ones are the tail, plus one variable that goes from 1 to 4, that stores the current direction. Other things were trivial ones, like score and speed.

In the game loop, what we do is:

retrieve the cell where the head of the snake wants to go;

if that cell is out of boundaries (walls), or is not empty (the snake tail), it’s Game Over ;

; if that cell is the bug, increment the snake by one (at the end of the tail) and continue to go;

if that cell is empty, simply move.

The movement too is quite easy: as there are only2 cases, we literally have at most 2 cells to re-render at any given time, leaving all the others untouched:

the head of the snake : it changes at every step; it means that, based on the direction, we unshift a new head on the snake and render those coordinates; this give us the impression of the head moving forward;

: it changes at every step; it means that, based on the direction, we a new head on the snake and render those coordinates; this give us the impression of the head moving forward; the end of the snake’s tail: if we are eating the bug, we do nothing (the snake is one piece more long, thanks to the unshifting of the new head); if we are NOT eating the bug, we pop the last piece of the snake and render an empty cell at its coordinates.

The only other cell that needs to be re-rendered is the one holding a new bug, whenever we eat one.

The logic is summarized by this function:

var move = function() {

// snake is the snake-storing array

var head = snake[0];

// snakeDir is the current snake direction; this variable is

// updated via js keydown events bound to the window

var x = (snakeDir === 1) // right

? head.x + 1

: (snakeDir === 3) // left

? head.x - 1

: head.x;

var y = (snakeDir === 0) // up

? head.y - 1

: (snakeDir === 2) // down

? head.y + 1

: head.y;

var cellType = getCellType(x, y);

if (cellType !== 'empty' && cellType !== 'bug') {

gameOver();

return;

}

snake.unshift({

x: x,

y: y

});

draw(head.x, head.y, 'snake');

// if head is on bug, we don't remove the tail

if (cellType !== 'bug') {

var tail = snake.pop();

// draw without a third parameters draws an empty cell

draw(tail.x, tail.y);

} else {

if (speed > maxSpeed) {

speed -= 5;

}

points += 1;

updatePoints();

// get bug creates a new bug at random coordinates, if needed

getBug();

}

draw(snake[0].x, snake[0].y, 'snake');

};

That was it, simple as it is.

It gave me confidence, a sensation I do not recommend.

Second Game: Tetris

I decided to take a leap: Tetris was still big in Russia, so I thought it could be my ticket to wealth and fame.

I was wrong.

Here the repo

I used this wikia as a reference to rules, how to rotate the pieces and how to score points.

How it’s done

The approach was a bit different in Tetris: I used the table as a “rendering engine”, but not as a model.

I stored the board-status in a simple object; this only stores a “stable state” (by that I mean that no moving pieces are stored here).

The board-status object is a matrix where every element is 0 or 1 (I also store the color for cosmetic purposes, but it’s not important); this way both checking for completed rows and updating the board is trivial:

a completed row is simply a whole array of ones;

to remove a completed row we just need to do an array.splice at the correct index and to unshift a new empty row at the top.

As I said, the board-status object represents a stable state, which means that it only changes when a new piece is placed and not while it is moving; as this does not happens often through the game loop, we can afford to redraw the whole board whenever it happens (looping through the board state and drawing cell by cell).

The other important element is the piece currently falling down (called CurrPiece in the code): every time a new piece is added, the CurrPiece object is updated, with a new list of coordinates (the four parts of the tetromino), an initial position (in terms of rotation) and a type ( S , Z , T , I , O , L or J , which are the possible shapes).

This piece is actually the only element we need to update more often; as a tetromino has at most four parts, the worst case scenario is that we have to re-render eight cells: totally doable.

Rotations where mapped based on the tetromino’s type. Every type has basically this form:

I: {

name: 'I',

colour: "cyan",

form: [

[ -1, 0 ], [ 0, 0 ], [ 1, 0 ], [ 2, 0 ]

],

rotations: [

[ [ 2, -1 ], [ 1, 0 ], [ 0, 1 ], [ -1, 2 ] ],

[ [ 1, 2 ], [ 0, 1 ], [ -1, 0 ], [ -2, -1 ] ],

[ [ -2, 1 ], [ -1, 0 ], [ 0, -1 ], [ 1, -2 ] ],

[ [ -1, -2 ], [ 0, -1 ], [ 1, 0 ], [ 2, 1 ] ]

]

}

The non self-explanatory attributes are:

form : represents the initial coordinates at which the element must appear (they are modifier of the center cell in the top most row, where new pieces appear);

: represents the initial coordinates at which the element must appear (they are modifier of the center cell in the top most row, where new pieces appear); rotations: is an ordered list of modifiers; every element of the list modifies it’s own specific tetromino’s part (so order is important).

All those elements are in fact modifiers of x and y coordinates.

Moving the current piece was a bit trickier.

First of all, the movement was not determined only by key-pressing: a piece would stroll down on its own and we could only control left-right movement, rotation and speed up the falling.

Other than that, our “hero” (the moving piece) spanned through multiple cells: checking for out-of-boundaries situations was simple enough, but other checks (like a valid rotation) weren’t as simple.

I ended up using a minimal “double-buffer”-like approach on the CurrPiece object coordinates, which means that in the game loop I do the following:

look for user action: left , right , rotation , speed-up ;

, , , ; create a copy of the CurrPiece parts coordinates;

parts coordinates; update the copy’s coordinates, moving them left, right and/or down; also, if a rotation is needed, apply the current rotation modifiers;

check if the copy’s updated coordinates are valid ones; validity simply means that they are still inside the boundaries and don’t overlap other elements;

if they are valid, re-render the current piece and save the new coordinates;

if they are not valid, we reset coordinates as they were.

After that, I check if a movement down is required (the down movement doesn’t happen at every iteration, as I can move left, right and rotate a piece in between down movements):

if the piece can move down, we increase by one the CurrPiece ’s y coordinates and re-render them;

’s coordinates and re-render them; if the piece cannot move down, it’s set: we update the board status and ask for a new piece.

Again, the most important part of the whole game is this function:

move: function() {

// `p` is an alias of CurrPiece

if (p.isNewPiece()) {

return;

}

// drawListCell redraw a list of coords; the second parameter

// is the css class to apply to the cell; if no class is passed,

// they are rendered as empty cells

Board.drawListCell(p.coords);

var newCoords = [];

var canMove = true;

for (var i in p.coords) {

// horizontalMovement and verticalMovement are set depending

// on pressed keys and loop iterations; not every loop iteration

// move the piece down

if (

(p.horizontalMovement == -1 && !p.canMoveLeft())

|| (p.horizontalMovement == 1 && !p.canMoveRight())

) {

p.horizontalMovement = 0;

}

var newCoord = {

x: (p.coords[i].x + p.horizontalMovement)|0,

y: (p.coords[i].y + p.verticalMovement)|0

};

// rotationMovement is set depending on pressed keys

if (p.rotationMovement) {

var rotation = p.currType.rotations[p.rotation][i];

newCoord.x = newCoord.x + rotation[0];

newCoord.y = newCoord.y + rotation[1];

}

if (p.outOfBoundaries(newCoord.x, newCoord.y)

|| !Board.isEmpty(newCoord.x, newCoord.y)) {

canMove = false;

break;

}

newCoords.push(newCoord);

} // if we can move, we update CurrPiece coords with the newCoords

if (canMove) {

p.coords = newCoords;

Board.drawListCell(p.coords, p.currType.name);

if (p.rotationMovement) {

p.rotation = (p.rotation + 1) % 4;

p.rotationMovement = false;

}

// if not, we look if we can still move down

} else if (p.canMoveDown()) {

for (var i in p.coords) {

p.coords[i].y = (p.coords[i].y + p.verticalMovement)|0;

}

Board.drawListCell(p.coords, p.currType.name);

p.rotationMovement = false;

// otherwise, CurrPiece has found its new home; we save the

// new Board status, check if some rows are now completed and

// ask for a new CurrPiece

} else {

Board.setListStatus(Object.assign(p.coords), 1, p.currType.name);

Board.checkRows();

Board.redraw();

p.newPiece();

}

}

Other parts of the game are purely cosmetic, as the next-piece block, the scoring hud, etc.

Third Game: Sokoban

Sokoban is another grid-based game which dynamics fit very well inside the table’s world.

Here the repo

Sokoban is an old puzzle game where you play as a warehouse worker that needs to push boxes on the marked spots on the ground.

How it’s done

Once again, the table here is used as a pure rendering tool.

A difference between Sokoban and the previous games is the presence of game levels.

I found online this open source implementation by GitHub user Leo Liu which had this comprehensive list of levels; a quick parsing of them, imported as a string, and we were good to go.

Once I had the levels, it was actually pretty easy.

In Sokoban all interactions come from user input, so I ditched the whole game loop. All the logic is actually triggered by pressing one of the four directions.

Moving was also easy, as there are only three possible cases:

the destination cell is empty : I move the hero;

: I move the hero; the destination cell is a box and the cell after the box is empty : I move both the box and the hero;

: I move both the box and the hero; otherwise: the move cannot be done.

Rendering was quite easy too, as there were at most 3 cell to update (the hero current cell, the hero destination cell and the eventual box destination cell).

After every successful move I check if all the target cells have a box on them, in which case the level is finished and I simply show a link to the next level.

Again, the logic is basically handled by a couple of functions:

// <x, y> is the hero destination cell

// <x2, y2> is the eventual box destination cell

// dir is used only to change the hero sprite direction

game.move = function(x, y, x2, y2, dir) {

let src = game.hero.x + '-' + game.hero.y;

let dest = x + '-' + y;

let pushDest = x2 + '-' + y2;

// canMove check if the <x, y> cell is empty

if (game.canMove(x, y)) {

game.updateHero(src, dest, dir);

// canPush check if the <x, y> cell is is a box and

// <x2, y2> is empty

} else if (game.canPush(x, y, x2, y2)) {

game.moveCell(dest, pushDest);

game.updateHero(src, dest, dir);

game.incPushes();

}

// check if the level is completed; Controller.off simply remove

// event handlers so the hero doesn't go around

if (game.check()) {

Controller.off('up');

Controller.off('down');

Controller.off('right');

Controller.off('left');

setTimeout(function() {

game.win();

}, 100);

}

};

Fourth Game: Tron Light Cycle

Some of you may remember the Tron Light Cycle game, where you go around in your motorcycle leaving behind walls of light, trying to cut off opponents; basically a multiplayer Snake.

Here the repo

As I wanted it to be multiplayer, I implemented a very raw room system through WebSocket.

But I also wanted to be able to test it myself, as I have no friends, so I implemented a couple of very dumb AIs to compete against.

Last but not least, I decided to use goats instead of motorcycles, because of reasons, and named the game Cabrón.

How it’s done

This one is a little more trickier to explain, so I think it’s better to go look directly at the repo, if you are interested.

I will just talk about the main problems I had to face.

The table here is once again used as a pure rendering tool, as all the logic runs server side and send through WebSocket every new state update.

The movement is no different than snake, but we have up to four snakes to consider.

The controller is basically a series of events that sends to the server the new user direction (or if the user choose to use one of his turbo ).

The AI was added at a later time, as testing was becoming quite difficult.

For the AI, I found this study done by Jean-Baptiste Boin, a Ph.D. Candidate at Stanford’s University.

The article explains both simple and advanced approaches; as my goal was just to be able to test the game by myself, I decided to implement only the simple ones (the so called random , runner and hunter ); plus a floodfill -like one that tries to avoid being trapped.

The good part is that adding the AI was very simple: I already needed to loop through players server side when sending the board updated state, so all I had to do was to hook up there and add my AI logic when looping to all players flagged as AI-driven (it also meant to keep the AI logic as light as possible, but that wasn’t a problem as I implemented very dumb ones).

The bad part is that when you use a 100x100 table with small square cells and you start adding random box-shadow glow animation to every cell, things start to get glitchy.

Last, I added a simple monitoring page to see the list of rooms and players connected.

This was quite useful to debug room creation, join and deletion in near-real-time

Conclusions

As you can see, HTML tables are perfectly able to handle all kind of games.

They aren’t the best option when you want animation from one cell to the other or when you need to move a veeery big table (as you can see by yourself in this GuitarHero done in table I tried to do, where the song and the notes aren’t exactly synced), but still they work great in all other implementation.

Failed GuitarHero done in table

Those are just basic examples.

All games can obviously be expanded; for example you could add a simple scoreboard with name and score of the players; to do things like that, as they are trivial, I usually use directly a canvas.

So: spread the love of tables and stay tuned for other cell-driven games!