Snake: DOM’inating a Classic?

A Cute Snake

Pushing Limits == Fun Problems

I’ve been wanting to get more exposure to front-end web development. Although I’ve technically been a full stack developer for the last decade, the majority of my work has focused on the back-end. When I work in the front-end I’m often still isolated from the raw HTML5/CSS3 by various frameworks. When not isolated by a framework the code sometimes just doesn’t feel as intuitive and instinctive as I’d like. So I’ve tried to come up with a task that could serve as front-end experience while also being fun.

What I’ve decided on is making a web based game of Snake. I already have fond childhood memories of tinkering with the QBasic code for a snake style game, Nibbles. So this is guaranteed to be fun. I searched to see what others have done in this regard, and found that the majority of web based snake games are done with Flash or HTML5 canvas. There are some done in the DOM, but all the ones I tried seemed to break if I changed the screen size or deviated too far from desktop resolutions. That got me thinking: I wonder if I could make one purely in the DOM, with raw HTML5, CSS3, and vanilla JavaScript? One with my own twist on it. Could I make mine succeed at being more responsive? The type of media queries and layout necessary for that is exactly the kind of thing I want more experience with.

While I think canvas is better suited to this kind of application, I’m doing this for the training, and doing it in the DOM will likely push some limits. I think that pushing the limits will mean problems, the kind of problems that will be good for training and that will be fun to solve.

Respect to UI Designers

I was expecting to encounter interesting problems to solve, but I had anticipated they would be mostly in code. Convincing CSS3 to lay things out the way I want, dealing with browser quirks and differences, etc. There were certainly many of those type of problems, but there were also lot of purely UI design issues, far more than I had counted on.

I had decided I wanted to support resolutions as small as the iPhone 5’s 320×568, and to be playable completely on a mobile touchscreen. I wanted it to adapt and work fine on a desktop with a keyboard too. This is one of my first design mockups:

An early design mockup for the UI of the snake game

This design felt suitable, but as it came to fruition I soon realized there were serious problems. The controls area was too cramped. The arrow keys didn’t work very well, it was hard to use them without looking away from the play area, and it was far too easy to accidentally pause or hit the other buttons. The heading to the play grid was too cramped as well. It did work fine for basic statistics, but showing things like whether the game was paused, letting the player know they’ve died, those kinds of things would too easily be missed.

It ended up taking several days of playing around and many iterations before I came up with a solution that felt satisfying:

  • I got rid of the settings button entirely. I’d planned to allow the player to tweak their colours, speed, sounds, etc. But I decided that was overcomplicated for a game that is primarily a training exercise, and getting rid of it saved valuable screen space.
  • I got rid of the help button too. The instructions are so simple that it wasn’t really worth having a button dedicated to it. I decided I’d design the overall feel to be that of a retro handheld game console. The play grid would then act like it was the screen area, and the instructions would show there when the game is first loaded. That saved more screen space while simultaneously creating a retro feel.
  • I combined the play/pause and reset buttons into a single ‘~’ button, what I’d call the ‘snake’ button, and moved it into the title area. Pressing the button would do the play/pause, and holding it for a few seconds would do the reset. Making it just say ‘~’ takes up less space; I could use the instructions area to convey how it worked. And combining multiple actions on a single button like this is exactly how old handheld games worked, so it would also fit with the theme of it being a retro handheld console. Moving it to the title area ensured no accidental triggering when the player was simply trying to move the snake.
  • I moved the more significant messages I had wanted to display in the play grid heading to be large semi-transparent messages in the center of the play area. That approach caught my attention much better. It also gave me a perfect excuse to play more with CSS popup dialogs and opacity. Win-win.
  • I found the movement controls to be the most difficult design challenge. I thought the first design for the movement keys would work now that they had the entire controls area dedicated to them. But it was still too hard to quickly change the snakes direction. The controls were still clumsy and required me to look away from the play area. I tried removing them entirely, opting instead to have the player touch the play grid directly. The snake would then follow the touched point. That works great when the snake is in the middle of the play area, but not at all when it gets really close to the edge. I tried having the player swipe on a virtual touchpad. That works great on an emulator, but on a real device the swiping can get too close to the bottom and then the player will end up accidentally navigating to a different app. I finally settled on a virtual pad broken into four quadrants. Touch upper/lower directs the snake north/south when it is horizontal, and left/right directs the snake east/west when it is vertical. Since both directions are overlapped I get twice the screen space. The same touch point acts on both horizontal and vertical movements, so if the player anticipates one move ahead they can achieve rapid snake movement, almost as quick as the keyboard. It is little unintuitive at first, but I found it didn’t take long to get the hang of it.

And with that I finally had a design that worked:

Professional UI designers certainly have my respect. This exercise challenged me with far more UI design problems than I’d anticipated, several of them being trickier than I’d expected too.

Why We Use Frameworks

There were also the kinds of issues I’d expected to find. A couple really reminded me of why frameworks are so nice to have:

One was in regards to CSS viewport units. I’d found that they worked great to get various parts of the UI to size accordingly based on different resolutions. I used the mobile emulator in Chrome and eventually reached a point where things were sizing nicely across all different sizes. Then I tried it on mobile Chrome on a real Galaxy S22. It was busted! Turns out viewport units don’t take into account the top navigation bar on many mobile devices. It does in the emulator, and on paper it seems like it should work, but the real devices do it differently. Some searching on Google confirmed this was a commonly known issue. My solution was to use JavaScript to detect the viewport, then setting custom viewport variables and using those in place of the CSS ones.

I ran into another issue when I tested on Safari. It was an embarrassing disaster, all the controls were on top of each other, it looked terrible. The reason was that I was relying on an OnLoad event to set the custom viewport units. That fires reliably in Chrome, but on Safari the page is loaded before reaching the code that was binding my function to the load event, and thus it doesn’t fire at all. The developer needs to check the readyState property on the document to determine if the document is already loaded. I’ve been bit by this before! I’ve just been isolated by frameworks for so long that I’d forgotten this was a thing.

I’m really glad I went through this exercise. I think it is important to know these underlying fundamentals. But it also reminded me of how nice it is to have a framework taking care of these shenanigans.

Problems using the DOM

I was expecting problems using the DOM for a game. That’s why I did it this way, I knew they would be fun problems that would lead to learning. I was able to find workarounds for most of them. One that I couldn’t solve is audio on mobile iOS devices. On such devices audio preloading is ignored, and currently all browsers on iOS use WebKit. Therefore the game acts quirky with its sound effects on mobile iOS. It will start out without any sounds at all and then they’ll sort of pop in and start working over time. At least it remains playable. But this is exactly the kind of reason why the DOM isn’t a good choice for this style of application. Unless of course your goal is training like it was for me!

Level Design

One thing that was nice about being in the DOM is that it was practically trivial to hack together a crude level editor. I used an OnMouseOver tied to left click to paint on the grid, and right click to erase. The same with OnClick for precise tinkering. A quick and dirty form element held the output data, and could also load in the same data for tweaking pre-existing levels. I just added temporary manual styling when I wanted to more easily align to the center or relative to other parts of the drawing. It wasn’t pretty, but it made level editing a cinch:

Level editor for DOM snake

Because it was so easy to add levels, I decided to turn my exercise into a legit game. I created 50 levels of various difficulty and themes, and I personally found it pretty fun to play. I even added a fun little ending if someone beats all the levels! 🙂

The JavaScript

I have mixed feelings about the JavaScript. I would hope that other developers would at least give me credit for coming up with some separation of concerns. I divided the logic into three main areas:

  • ViewOutput: Handles everything related to screen output
  • ViewInput: Handles all the input events from the browser
  • GameController: Handles the game logic

I wanted the game controller to, at least theoretically, be able to function in another environment. I wanted to keep all the HTML5/CSS3 and browser specifics isolated to the input and output areas. For a training exercise I’m pretty happy with it. It seems to run really well, and I found it pretty easy to understand. I think it is at least better than outright spaghetti code.

That said, I’m really just using a couple static classes to act as containers to separate these three main areas of logic. The game code absolutely does not adhere to SOLID design principles, there are no automated tests, there is just a lot left to be desired. It’s been a long time since I’ve done any serious level of JavaScript and unfortunately I think that shows. I think I’ve learned that I need to revisit my design pattern fundamentals too.

Responsive Design Isn’t Just UI

I’ve heard more than once that code without automated tests is simply code that is untested. That manual testing is obsolete and useless. I don’t think that’s true. I found some things in my manual testing that I really don’t think would have been found otherwise.

One of those findings was how the game felt on different resolutions. I’d read that it is best to design for mobile first then enhance for the desktop. That’s what I did, but I found that when I play tested on desktop that the snake was moving far too slow. It was boring. Yet if I sped things up to feel good on desktop, the mobile experience ended up feeling too fast and unfair. My solution was to make the difficulty itself responsive. I’m using the resolution to scale the speed of the snake. The scaling is subtle, but it really makes a huge difference to the feel.

I’ve also added a sort of responsiveness tied to the player skill level. This idea occurred to me when I saw my own mother play test a level. I want her to be able to beat this game too. So every time the player dies, the speed will scale back a bit. A tiny bit. It’s subtle, but it ensures a player of any skill level can win after a bit of practice.

The Results of DOM’inating Snake

If you’re interested in seeing the source code I’ve posted it on my GitHub. If you’d like to play the game itself, I’ve posted it here.

Leave a Reply

Your email address will not be published. Required fields are marked *