Schemescape

Development log of a life-long coder

Building a browser-based app without JavaScript, part 2

In the last post, I brainstormed ideas for creating a real-time, interactive browser-based app without using JavaScript (or WebAssembly).

In this post, I'll describe a proof-of-concept that I created for Lisp Game Jam (Autumn 2023).

Note: the game server has been taken offline. Sorry!

Source code is here: https://github.com/jaredkrinke/cl-stuff/tree/main/foolander

Recap

The most difficult adjective to support without JavaScript is "real-time". In some cases, CSS itself might be enough (although I truly hope CSS is not bloated enough for implementing a snake game!).

If you don't need real-time feedback, you can probably get by with regular HTML forms, long-polling, or even meta refresh. But if you need immediate feedback for both user input and server state updates, avoiding JavaScript can be tricky.

Here's what I came up with:

I don't know if this is the best approach (in fact, I doubt it), but it is an approach that is "good enough" for my terrible game.

Architecture

The architecture for my game is pretty simple:

Examples

Table

<table><tbody>
<tr>
<td class="s29_0">&nbsp;</td> <!-- Row 29, column 0 -->
<td class="s29_1">&nbsp;</td> <!-- Row 29, column 1 -->
<td class="s29_2">&nbsp;</td> <!-- etc. -->
<td class="s29_3">&nbsp;</td>
...
</tr>
</tbody></table>

Game board visual updates

<style>.s9_12 { background-color: yellow }</style> <!-- Change row 9, col 12 to yellow -->
<style>.s6_12 { background-color: blue }</style>

Note: I should have used ids instead of classes (since they're unique elements). Also, I could have consolidated multiple same-frame updates into a single <style> tag.

"Overwriting" the last bit of HTML (the score)

.score { display: none }
.score:last-of-type { display: block }
<p class="score">Score: 0</p> <!-- Initial score -->
...
<p class="score">Score: 1</p> <!-- Later, this score is shown instead -->

Input frame

My game only uses a single button, but multiple buttons are easy to support using distinct forms with hidden inputs. In fact, I used multiple buttons until I decided to simplify at the very end.

Parent page:

<iframe src="controls?id=cbecixentpsc"></iframe>

Frame body:

<form action="action" method="post">
<!-- This id links input to an active instance of the game -->
<input type="hidden" name="id" cbecixentpsc="" value="cbecixentpsc">

<!-- This was for supporting multiple buttons -->
<input type="hidden" name="action" value="clockwise">

<!-- The actual submit button that is shown in-game -->
<input type="submit" value="↻" accesskey="e">
</form>

The id parameter is tracked on the server to link input to a particular HTTP request (which runs an entire round of the game).

Analysis

Give the game a try! If your latency isn't too high, then it's somewhat playable.

Of course, I wouldn't recommend this approach because it has a number of drawbacks:

But it does (mostly) work! And it gave me an excuse to mess around with Lisp and participate in a game jam.

Overall, I'm calling this a success since it answered my original question... even if it's not a technique I'd recommend.

Addendum

Achievement not unlocked

Unfortunately, I wasn't able to achieve one secret goal I had in mind: getting the game to run in a minimalist, no-JavaScript browser, like Dillo. Apparently Dillo doesn't support inline frames, so the control scheme doesn't really work (unless you have another window open to the frame). Additionally, Dillo didn't seem to like my "streaming CSS" approach--the entire game board just remained its initial blue color.

Oh well. At least I tried.

nginx

In order to support HTTPS using nginx, I had to tweak a couple of nginx settings. I'm noting them down here for reference:

proxy_http_version 1.1; # nginx default is 1.0, but chunking is a 1.1 feature
proxy_buffering off; # Necessary to ensure chunks are forwarded as soon as received
gzip off; # This is actually the default, chunking only worked with gzip disabled