Public domain postcard of a pig (blurred background image) Public domain postcard of a pig

Greedy pig tech experiment

The goal for this post is mainly to set a precedent of doing weird little tech experiments, just so we’re all on the same page.

Also to kick the tyres on Hugo (the system I’m using to build this site) and to see how much of a hassle it is to get dynamic behaviour integrated into blog posts.

So the goal is to throw together one of the simplest games I can think of, this primary school push-your-luck game which I know as Greedy Pig.

There’s really not much to it gameplay-wise. You roll a die and then decide: you can bank it (spending some limited supply of banks) or you can push your luck and roll again. If you exceed some threshold of your unbanked points, you lose instantly. Otherwise when you’ve spent your last bank, that’s your score.

So, hella simple in Javascript.

  • a random number generator
  • three state trackers
    • current points
    • banked points
    • banks remaining,
  • two possible outcomes
  • three parameters
    • what kind of die are we rolling
    • what’s the bust threshold
    • how many banks

For fun let’s theme it around a weird creature trying to cram as much food in its mouth as it can before its head explodes. It’s only allowed to swallow a limited number of times due to… anxiety.

Vanilla mode

Rolling a d6, mouth can fit 12 points, 3 swallows.

Executing greedy.mjs

Tiny mouth mode

More swallows, but if you roll a 6 you’re toast.

Executing greedy.mjs

Behind the scenes

The game logic is pretty simple, the real interesting bit here I think is getting Hugo incorporating it properly.

I’ve achieved it with Hugo’s custom block rendering, I’ve got a file in the theme’s layouts/_default/_markup directory called render-codeblock-script.html with the following contents:

<div class="script-block">
  {{ if .Attributes.src }}
    {{ $script := .Attributes.src }}
    {{ $inner := .Inner }}
    <div class="execution-pane">
      Executing {{ $script }}
    </div>
    {{ with .Page.Resources.GetMatch $script }}
      {{ $target := printf "%s.bundle-%d.js" $script now.Unix }}
      {{ if hugo.IsProduction }}
        {{ $target = printf "%s.bundle.js" $script }}
      {{ end }}
      {{ $js := . | js.Build (dict "format" "esm" "targetPath" $target) }}
      <script type="text/javascript" type="module">
        (() => {
          const cs = document.currentScript;
          const pane = cs.parentElement.querySelector(".execution-pane");
          const resource = {{ $js.RelPermalink }};
          pane.innerHTML = `
            <span class="loading" data-src=${resource}>
              ♻️ Loading...
            </span>
          `;
          import({{ $js.RelPermalink }}).then((module) => {
            {{ $inner | safeJS }}
          });
        })();
      </script>
    {{ end }}
  {{ else }}
    <script type="text/javascript">
      {{ .Inner | safeJS }}
    </script>
  {{ end }}
</div>

It does a couple things:

  • loads a script file if provided, bundled into a module using Hugo’s resource builder
  • sets up a div to execute the script in and puts a handle to it in local scope
  • calls the script block’s contents once the module is loaded

Then in the content file for the post, I can put this inline with the markdown content…

```script { src=greedy.mjs }
module.default(pane, {
  maxMouth: 5,
  swallows: 12,
});
```

…and the custom code block will pick it up, load greedy.mjs from that post’s resources, and run it with the parameters I gave it.

It’s also got a little hack in it when it’s in dev mode to change the filename of the bundled script every time there’s a local change, which I added to fix an issue where the live reload wasn’t working during development.

Wrap up and take away

Yeah, this feels like a success! It’s importing some external modules (the controls and stuff are rendered with preact, if you’re curious) and building its own internal ones. Neato.

As an aside though: lord, Hugo templating is an odd system to work in! Just really weird syntax and ergonomics. It gets the job done well enough, but it really feels like it wasn’t designed for this. Even writing an if statement is an exercise in the arcane. I guess the point is to write stuff up once and then put it in a freezer and only touch it via content files, so it’s not a showstopper, but still.