Why write an entire game (including Graphics) in a single, hand-coded JavaScript file?

My new game, Rightwaves, is out now! It’s a tribute to the great classic R-Type.

A pixel-art spaceship battles aliens

The entire implementation, including all the graphics, is contained within a single ~5000-line JavaScript file. Why?

This is a terrible idea

Let me start by saying I do not recommend writing code this way. It’s a terrible idea, and some aspects of the development process have been severely hampered by doing this.

A pixellated spaceship dodges bullets and heads for a narrow passageway

Complexity

One of my goals for Smolpxl is to strip coding back to the simplest possible thing it can be.

Over the last 40 years, programming has changed a huge amount: the number and variety of the tools we have to work with has increased beyond what I can take in, and almost all of these things are utterly brilliant.

I can’t imagine writing a game like Eat Apples Quick using the line-orientated editor I used on the ZX Spectrum. I thoroughly enjoyed the Rust code completion and documentation I get from running rust-language-server inside NeoVim. (I’ve even heard there are editors that work inside a windowed environment, but I am not brave enough to try one.)

A pacman-like game

I can’t imagine writing the simple physics engine for Spring in assembly language, or trying to concentrate on that while I had to unravel complex graphics rendering conventions and timings. Instead, I was able to write code that looked a bit like the maths it represented, and allow the browser to handle the graphics card.

A spring with a cheery face bounces off a platform

But, along with all these powerful tools like high-level programming languages, fully cross-platform user interface components, and libraries that make writing a game loop simple, comes a new problem: complexity.

We must choose which tools, libraries and paradigms we’re going to use, and to make that choice we need to understand them.

The Smolpxl library makes some of these choices for you, by enforcing an Elm-style model/update/view split, providing a pixellated canvas of known size, and handling a game loop with fixed framerate. This is particularly suited to writing little retro-style games, similar to what I used to write in AMOS Basic on my Amiga.

I want to make everything simple, but I want our game to work on people’s computers. The only platform we can realistically choose is the browser: the exact same code works almost everywhere.

None of this explains why I would write a game inside a single file of source code, but hopefully it begins to demonstrate where I am coming from.

Simplicity

Another goal for Smolpxl is to teach programming, and a huge barrier I see to getting started is the build process.

No build

You can write Smolpxl games in Rust and compile them to WASM (which is how Eat Apples Quick is done), and you could also package the JavaScript with webpack or similar, but I want the first-class way of using Smolpxl to involve no build at all: I want you to be able to copy in a standard index.html and smolpxl.js and then write your code in game.js and have it just work when you double-click on index.html.

A goal for Rightwaves is to demonstrate that you can write a full game in Smolpxl, without stepping outside that simple story: just double-click on index.html.

It’s all inside game.js

So, Rightwaves is a single* JavaScript file containing the code, the level descriptions, and, probably most unusually, the graphics.

* Note: I cheated – the “action-replay” data is in a separate file – it was much bigger than the source code, and would have made it too hard to deal with. If I wanted to stay pure, I would have had to remove the default action replay from the game, but I just liked it too much.

Level design

Many times while I was writing Rightwaves, I wished I had written a level editor. In fact, I am often quoted as saying

“A game without a level editor is only half a game.”
— Andy Balaam, quite often

But instead, the level design is code like this:

const LEVELS = [
    {
        scenery: [
            { x:   0, y: 80, image: "machinery-20x16-01"},
            { x:  20, y: 88, image: "machinery-20x08-01"},
// ... etc.
        ],
        width: 1526,
        aliens: [
            newRedFlat(130, 20),
            newRedFlat(145, 25),
// ... etc.

The full code is at gitlab.com/smolpxl/rightwaves/-/blob/main/public/game.js#L3478.

It’s not a lot of fun to edit, but on the other hand, every time you make a change a simple refresh in the browser lets you see what it really looks like. No build process; no waiting.

Graphics

I drew the graphics for Rightwaves using GIMP, exported the images as PNG files, and converted them to text using a little Rust utility I wrote.

When I wanted to change an image, it was a nightmare, and I would recommend this approach to no-one whatsoever.

The reason why Smolpxl supports creating images in the source code is so you can hand-craft them right there, using an ASCII-art style.

Here’s the spaceship: An ASCII-art spaceship

and here is the code for it:

const IMAGES = {
    "ship": {
        pixels: [
            "..www.......",
            "dddddww.bbb.",
            "rllldddcccwb",
            "dddhlllccccb",
            "rddddddaccb.",
            "..aaa......."
        ],
        key: {
            "w": [255, 255, 255],
            "d": [88, 88, 88],
            "b": [77, 111, 249],
            "r": [141, 0, 0],
            "l": [126, 125, 125],
            "h": [192, 192, 192],
            "c": [3, 157, 157],
            "a": [42, 42, 42]
        }
    },
// ... lots, and I do mean lots, more here for the other graphics ...

The full code is at gitlab.com/smolpxl/rightwaves/-/blob/main/public/game.js#L41.

Note that I painstakingly coloured that code in for you, for this blog post. In the code there is no such luxury!

Openness

All the Smolpxl games are Free/Open Source software, of course. By avoiding a build, anyone can see the source code, just as I wrote, it in their browser. Anyone (including me) can debug problems without any extra steps. Putting everything in one file makes it easier to find the code (but probably makes it harder to understand).

Optimisation

Rightwaves loads really fast. On my machine, the first time you visit the page, it is playing within 1.4 seconds, after downloading 5 files at a total of 149kB (gzipped). If we exclude the included action replay, it is much smaller.

All of this, with no build process.

Maybe it wasn’t such a bad idea after all.

This is a bad idea

Don’t try this.

For fun

Except, of course, if you like fun. I tried this because it was fun. I would heartily recommend trying things because they might be fun.

Also, if you like fun, try playing the games on Smolpxl games, or write your own!

A puzzle game, a cross-the-road game, a tunnel game, a snake game, a Heli game, and a game-of-life thingy

Matrix is the only (chat) game in town

On my phone and computer I use WhatsApp, Signal, Slack, Keybase, Discord, IRC, XMPP/Jabber and Element/Matrix. In addition, I occasionally use the messaging features of Mastodon, Twitter and even LinkedIn. I’ve never used Telegram, Line, WeChat, Session, Wire or Status chat, but they exist too, along with many others.

It would be better if I could chat with people using the app I prefer, rather than the one I am forced to use.

Of course, the only useful chat app is the one your friends and family are on, so it’s pointless to debate the finer points in each service’s favour, but here I go anyway.

Only Matrix is:

The importance of decentralisation has been re-emphasised for me this week after the freenode IRC debacle. A single controlling entity, even when it is currently benign (as some people believe Signal is) is not a guarantee that things will stay this way. Thank goodness you can connect your usual IRC network to libera.chat: imagine what would happen to Signal users if they realised someone unscrupulous had acquired control.

Matrix does not solve all our problems. Notably:

  • Its security is probably not good enough for people threatened by powerful interests – at the moment it’s quite easy to see who’s talking to whom, and when.
  • Not all clients support end-to-end encryption, and not all turn it on by default (but the most-used ones do).

Despite these limitations, Matrix is the only chat network that is even attempting to provide what users need, and it seems to be doing a pretty good job of it.

I think we should work together to address its weaknesses, and adopt it wherever we can.

So, I recommend Matrix (specifically element.io) for group and individual chat.

Suspending the computer using Kupfer

I have recently started using Kupfer again as my application launcher in Ubuntu MATE, and I found it lacked the ability to suspend the computer.

Here is the plugin I wrote to support this.

To install it, quit Kupfer, create a directory in your home dir called .local/share/kupfer/plugins, and create this file suspend.py inside:

__kupfer_name__ = _("Power management")
__kupfer_sources__ = ("PowerManagementItemsSource", )
__description__ = _("Actions to suspend the computer")
__version__ = "2021-05-05"
__author__ = "Andy Balaam "


from kupfer.plugin import session_support as support


class Suspend (support.CommandLeaf):
    def __init__(self, commands):
        support.CommandLeaf.__init__(self, commands, "Suspend")
    def get_description(self):
        return _("Suspend the computer")
    def get_icon_name(self):
        return "system-suspend"


class PowerManagementItemsSource (support.CommonSource):
	def __init__(self):
		support.CommonSource.__init__(self, _("Power management"))
	def get_items(self):
		return (Suspend((["systemctl", "suspend"],)),)

# Copyright 2021 Andy Balaam, released under the MIT license.

Now restart Kupfer, go to Preferences, Plugins, and tick “Power management”.

You should now see a “Suspend” item if you search for it in the Kupfer interface.

Inspired by: Mate Session Management – Kupfer Plugin.

Reference docs: Kupfer Plugin API

Uploading to PeerTube from the command line

PeerTube’s API documentation gives an example of how to upload a video, but it is missing a couple of important aspects, most notably how to provide multiple tags use form-encoded input, so my more complete script is below. Use it like this:

# First, make sure jq is installed
echo "myusername" > username
echo "mypassword" > password
./upload "video_file.mp4"

Downsides:

  1. Your username and password are visible via ps to users on the same machine (tips to avoid this are welcome)
  2. I can’t work out how to include newlines in the video description (again, tips welcome)

You will need to edit the script to provide your own PeerTube server URL, channel ID (a number), video description, tags etc. Output and errors from the script will be placed in curl-out.txt. Read the API docs to see what numbers you need to use for category, license etc.

Here is upload:

#!/bin/bash

set -e
set -u

USERNAME="$(cat username)"
PASSWORD="$(cat password)"
FILE_PATH="$1"
CHANNEL_ID=MY_CHANNEL_ID_EG_1234
NAME="${FILE_PATH%.*}"
NAME="${NAME#*/}"

API_PATH="https://MY_PEERTUBE_SERVER_URL/api/v1"
## AUTH
client_id=$(curl -s "${API_PATH}/oauth-clients/local" | jq -r ".client_id")
client_secret=$(curl -s "${API_PATH}/oauth-clients/local" | jq -r ".client_secret")
token=$(curl -s "${API_PATH}/users/token" \
  --data client_id="${client_id}" \
  --data client_secret="${client_secret}" \
  --data grant_type=password \
  --data response_type=code \
  --data username="${USERNAME}" \
  --data password="${PASSWORD}" \
  | jq -r ".access_token")

echo "Uploading ${FILE_PATH}"
curl "${API_PATH}/videos/upload" \
  -H "Authorization: Bearer ${token}" \
  --output curl-out.txt \
  --max-time 6000 \
  --form videofile=@"${FILE_PATH}" \
  --form channelId=${CHANNEL_ID} \
  --form name="$NAME" \
  --form category=15 \
  --form licence=7 \
  --form description="MY_VIDEO_DESCRIPTION" \
  --form language=en \
  --form privacy=1 \
  --form tags="TAG1" \
  --form tags="TAG2" \
  --form tags="TAG3" \
  --form tags="TAG4"