Snake...in pure HTML⁉️ [no JS, no CSS, no images!!] 😱

GrahamTheDev - Mar 8 - - Dev Community

They say that some people just like chaos.

Hi there 👋🏼, I am Graham "loves chaos" TheDev, and this time I am back with another silly internet experiment (and you can skip straight to the game if you want).

It all started so innocently, "can I write a snake game?".

But, as always, the little devil on my shoulder whispered "make it harder"...so I thought "no JavaScript, do it in pure CSS".

Yet again, he chirped up "pffft, still too easy and you have done too much CSS stuff lately, do it in raw, unstyled HTML".

I turned to my other shoulder to hear what the angel thought, hoping for something more sensible.

Then I remembered that the angel is never there for me...

So here it is, snake, in pure HTML (with a little PHP trickery to power it).

That's right!

  • No JavaScript
  • No Images
  • No CSS
  • No Cookies

However, I want to be clear (as I do not want to be accused of clickbait), I am rendering this HTML using PHP.

Although it is possible to do it in pure HTML using just files, with no backend language, it would require 640,345,228,142,352,307,046,244,325,015,114,448,670,890,662,773,914,918,117,331,955,996,440,709,549,671,345,290,477,020,322,434,911,210,797,593,280,795,101,545,372,667,251,627,877,890,009,349,763,765,710,326,350,331,533,965,349,868,386,831,339,352,024,373,788,157,786,791,506,311,858,702,618,270,169,819,740,062,983,025,308,591,298,346,162,272,304,558,339,520,759,611,505,302,236,086,810,433,297,255,194,852,674,432,232,438,669,948,422,404,232,599,805,551,610,635,942,376,961,399,231,917,134,063,858,996,537,970,147,827,206,606,320,217,379,472,010,321,356,624,613,809,077,942,304,597,360,699,567,595,836,096,158,715,129,913,822,286,578,579,549,361,617,654,480,453,222,007,825,818,400,848,436,415,591,229,454,275,384,803,558,374,518,022,675,900,061,399,560,145,595,206,127,211,192,918,105,032,491,008,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000 files.

So please excuse the "shortcut" of using PHP generate the next page (and for those of you who spend too much time on tech Twitter, please excuse my use of a "dead" language too! 😱🤣).

Anyway, that is way too much preamble, I know why you are here, you want to see it in action!

Play HTML snake (with caveats!)

Works on Chrome on Desktop PC...is too fast to play on Firefox and any browser on iOS for me...you will see why later in this article.

So basically, play it in Chrome on desktop!

The keys are:

  • ALT + I for up,
  • ALT + J for left,
  • ALT + K for down,
  • ALT + L for right,
  • ALT + O to start a new game (once you lose).

On Mac it is Control + Option instead of Alt I believe!

If you want to know why the weird keys and not WASD, unfortunately ALT + D is already used in Chrome on Windows, so I had to pick "safe" keys.

One last Warning: One of the tricks we use to make this work will flood your browser history with a lot of URLs...you have been warned!

The game

Sadly this won't run in a codepen, so you will have to play HTML snake on my site.

When you have finished playing, pop back to see what tricks I used to make this work (and share your top score in the comments too!).

Problem 1: Getting a game tick

For a game you generally need to have a "game tick". Each tick is when an action occurs or you compute a new game state and then render the new game state.

But that raises our first issue, how can we possibly have a page automatically update without JavaScript?

Well, to do this in HTML is actually quite simple, we just use <meta http-equiv="refresh" set to a low value.

So we start out at 0.35 seconds refresh time and then speed that up to 0.2 seconds refresh time as your score climbs.

What "meta refresh" allows us to do is to instruct a browser that once it has loaded the HTML for the page, wait for X seconds and then load another URL.

By setting a low refresh time and then changing the URL we redirect to in each refresh (more on that in a second), we have a way of having the page change all on its own, even if you don't press any buttons!

Here is a quick example of what the format looks like:

<meta http-equiv="refresh" content="[time];url=[url-to-redirect-to]">
Enter fullscreen mode Exit fullscreen mode

Side note: This is where I mentioned earlier that it doesn't work on other browsers. They do not except partial second refresh times and so the refresh is instant, making the game too fast to play.

But meta refresh alone isn't enough to make the game work, we need some way of saving the game state and communicating changes in the snakes direction to the server.

For that we use another straight-forward trick: URL encoded GET parameters.

Problem 2: Managing game state

Because we can't use POST requests or anything like that, we need another mechanism for managing game state between the browser and the server.

At first I was managing the state with several GET parameters, so the URL looked like this:

url?playing=1&x=2&y=6&foodx=3&foody=6&dir=left.
Enter fullscreen mode Exit fullscreen mode

This worked fine right up until the point I needed to store multiple points for the snake (x,y coordinates for each square it occupies).

While I did get it working with some hacky x,y coordinates list and parsing (like snake=1,1,2,1 with the snake being at x=1,y=1 and x=2,y=1) that was messy.

So instead we turn to our good friends: urlencode() and json_encode().

Used together I can take an array (or in this case a multi-dimensional array), convert it to JSON and then convert it to valid characters for a URL.

Let me explain:

Storing complex data in the URL

Here is a sample of the data I use for game state:

$state = array(
        'width' => $width,
        'height' => $height,
        'snake' => array(array('x' => 5, 'y' => 5)),
        'food' => array('x' => 7, 'y' => 7),
        'direction' => 'right',
        'score' => 0, 
        'state' => true
    );
Enter fullscreen mode Exit fullscreen mode

To store that data in the URL we can use the following:

$url = urlencode(json_encode($state));
Enter fullscreen mode Exit fullscreen mode

By JSON encoding our array and then replacing invalid characters with URL friendly ones, that gives us a our state in a URL friendly (although not human friendly!) way:

%7B%22width%22%3A20%2C%22height%22%3A20%2C%22snake%22%3A%5B%7B%22x%22%3A19%2C%22y%22%3A5%7D%5D%2C%22food%22%3A%7B%22x%22%3A4%2C%22y%22%3A11%7D%2C%22direction%22%3A%22right%22%2C%22score%22%3A0%2C%22state%22%3Afalse%7D
Enter fullscreen mode Exit fullscreen mode

And now we have a mechanism to pass the game state down to the browser and back up to the server.

max URL lengths

Those of you who know your stuff will know a gotchya here. There is a maximum URL length!

In Chrome that is 2083 characters.

If you play the game long enough you will actually hit that character limit, as to store our x,y position pairs we use over 10 characters each time.

But this is a silly demo and so I will just say: let me know what error happens if you make your snake long enough!

Oh and in the real world, you shouldn't JSON encode parameters in the URL, let's leave it at that!

We have state and game ticks, now what?

That is it!

Well, almost.

We need to communicate key presses to the server.

Problem 3: Changing the snake direction

This was the final problem (and why we ended up with the game state in the URL), we need to communicate a key press to the server to change the snake direction.

Problem 3a: button presses

Before we can communicate key presses to the server we need some way to actually capture them.

Remember, we have no JS to capture key presses.

We also can't use <button> elements as those require JS to work.

So all we have left is the humble anchor element (<a>).

But getting someone to click on anchors would make the game hard to play.

Luckily there is something called accesskeys built into HTML.

They allow us to assign a character to an anchor and then these can be accessed via a shortcut (ALT + the character in Chrome on Windows).

This gave us our mechanism for allowing keyboard controls, we just needed 4 links (anchors) with different directions as the URLs and then assign an accesskey to each of them.

Important Note: accesskeys should be used sparingly, if you pick keys that are used by assistive technology (AT) users then it may interfere.

Problem 3b: direction

Now that we had a way to press keys, and a way to communicate that key press to the server, we need a way to manage presses so they update the snake direction.

Luckily we already had a direction property in our state object we pass via the URL.

So all we had to do was create 4 different URLs, one for each direction. Then we add these to links and we are done.


$encodedState = urlencode(json_encode($state)); 

<a href="index.php?state=<?php echo $encodedState; ?>&direction=up" accesskey="i">up (ALT + I)</a><br/>

<a href="index.php?state=<?php echo $encodedState; ?>&direction=left" accesskey="j">left (ALT + J)</a>

<a href="index.php?state=<?php echo $encodedState; ?>&direction=right"  accesskey="l">right (ALT + L)</a>

<a href="index.php?state=<?php echo $encodedState; ?>&direction=down"  accesskey="k">down (ALT + K)</a>

Enter fullscreen mode Exit fullscreen mode

Now when you press ALT + K for example, the 4th link is clicked and we send the current state + new direction to the server!

Now all is left it is to take that information and compute the next game state.

Game logic

Finally, the last part of the puzzle is a bit of game logic.

For example when generating the food position we need to check it isn't on a tile the snake already occupies, so we have this function:

function generateFoodPosition($width, $height, $snake) {
  do {
    $food = array(
      'x' => rand(0, $width - 1), 
      'y' => rand(0, $height - 1));
  } while (
    in_array($food, $snake)
  );

  return $food;
}

Enter fullscreen mode Exit fullscreen mode

And another function to move the snake

function moveSnake($state) {
    $newHead = array('x' => $state['snake'][0]['x'], 'y' => $state['snake'][0]['y']);

    // Update snake's head position based on direction
    switch ($state['direction']) {
        case 'up':
            $newHead['y']--;
            break;
        case 'down':
            $newHead['y']++;
            break;
        case 'left':
            $newHead['x']--;
            break;
        case 'right':
            $newHead['x']++;
            break;
    }

    // Check if snake has collided with the wall or itself
    if ($newHead['x'] < 0 || $newHead['x'] >= $state['width'] || $newHead['y'] < 0 || $newHead['y'] >= $state['height'] || in_array($newHead, array_slice($state['snake'], 1))) {
        $state['state'] = false;
        return $state; // Game over
    }

    // Check if snake has eaten the food
    if ($newHead['x'] == $state['food']['x'] && $newHead['y'] == $state['food']['y']) {
        $state['score'] += 10;
        // Generate new food position
        $state['food'] = generateFoodPosition($state['width'], $state['height'], $state['snake']);
    } else {
        // Remove tail segment
        array_pop($state['snake']);
    }

    // Move snake
    array_unshift($state['snake'], $newHead);
    return $state;
}
Enter fullscreen mode Exit fullscreen mode

and a loop to build the game board.

 for ($y = 0; $y < 20; $y++) {
        echo "\r\n"; 
        for ($x = 0; $x < 20; $x++) {
            if ($x == $state['food']['x'] && $y == $state['food']['y']) {
                echo '🍎';
            } elseif (in_array(array('x' => $x, 'y' => $y), $state['snake'])) {
                echo '🟩';
            }else{
                echo '⬜';
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode

But I am not going to cover those in any detail as those are things that you can easily look up (and find much cleaner ways to do), find (better) code other people have written and adapt to your needs.

That's a wrap

So there you have it, we built a game using meta refresh, access keys and a hack to encode complex data in a URL.

Will these things be useful in your day to day? No, probably not.

Will they perhaps save your ass in a weird edge case, got to get this finished, can afford to use a hack to ship a product situation? Possibly.

What? You weren't expecting a useful tutorial from me were you? You should know better by now.

But, with that being said, if you did enjoy this article or by some miracle you learned something new, do drop me a comment below, it really means a lot!

Have a wonderful weekend everyone! 💗

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .