Snake...in pure HTML⁉️ [no JS, no CSS, no images!!] 😱
Tags: html, php, webdev, programming
Published: 2024-03-08 06:06:56

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](#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:
- <kbd>ALT</kbd> + <kbd>I</kbd> for up,
- <kbd>ALT</kbd> + <kbd>J</kbd> for left,
- <kbd>ALT</kbd> + <kbd>K</kbd> for down,
- <kbd>ALT</kbd> + <kbd>L</kbd> for right,
- <kbd>ALT</kbd> + <kbd>O</kbd> to start a new game (once you lose).

On Mac it is <kbd>Control</kbd> + <kbd>Option</kbd> instead of <kbd>Alt</kbd> I believe!

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

<b>One last Warning:</b> 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
<b>Sadly this won't run in a codepen, so you will have to [play HTML snake on my site](https://grahamthe.dev/demos/snake/).</b>

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"`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#http-equiv) 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:

``` html
<meta http-equiv="refresh" content="[time];url=[url-to-redirect-to]">
```

**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.
```

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()`](https://www.php.net/manual/en/function.urlencode.php) and [`json_encode()`](https://www.php.net/manual/en/function.json-encode.php).

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:

```php
$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
    );
``` 

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

``` php
$url = urlencode(json_encode($state));
```

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
```

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 [`accesskey`s](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/accesskey) 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:** `accesskey`s 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.

```php

$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>

```

Now when you press <kbd>ALT</kbd> + <kbd>K</kbd> 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:

```php
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;
}

```

And another function to move the snake

```php
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;
}
```

and a loop to build the game board.
```php
 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 '⬜';
            }
        }
    }
```

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! 💗