HTML Tables: the bleeding edge of Web Game Development

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: TetrisI 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 repoI used this wikia as a reference to rules, how to rotate the pieces and how to score points.

How it’s doneThe 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);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;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;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: SokobanSokoban is another grid-based game which dynamics fit very well inside the table’s world.

Here the repoSokoban 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 doneOnce 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;the destination cell is a box and the cell after the box is empty: 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 directiongame.

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 CycleSome 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 repoAs 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 doneThis 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 100×100 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-timeConclusionsAs 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 tableThose 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!NOTE: This article is an ironic one; I literally used table in these games to make fun of some friends at work.

.

. More details

Leave a Reply