Hello again! I return during this week of winter solstice to tell you about my experience participating in the Langjam Gamejam. I planned to use Raku, partially so that you could have an advent blogpost to read today, but also because Raku's builtin support for grammars ensure that I would not get stuck when writing my parser.
I did a few things to ensure that I would be able to complete the game jam. The first was to be realistic about what I could achieve; with seven days of time, I could realistically expect to produce about three weekends worth of working code. This is only going to be a few hundred lines, maybe one or two files total, and prioritizing basic functionality over any fancy features.
I decided to make an idle game. There's no rule against deciding that ahead of time. I also decided to use Raku. That's allowed too. I read the source of a couple different idle games too, mostly to get ideas of what not to do. In particular, big thanks to Idle Game Maker by Orteil, Antimatter Dimensions by Ivar K., and Trimps by the Trimp authors for publishing reasonably-readable source code. All three of these have some notion of programmability, although I ended up doing something quite different.
The first place to start when making a game is with the scenario that the player will be asked to experience. Games are fundamentally about roleplay, and roles only exist within scenarios. Fortunately, I didn't have to come up with any of this. Instead, I asked my friend Remi to come up with something interesting. We spent a couple hours adapting an idea for a Zelda-style fishing minigame, with the novel twist that the game would mechanically be an idle game; instead of putting in the athletic effort to retrieve the fish, the player manages a fishing business and delegates the work to employees.
It's important to allow a genuine brainstorm at the beginning of the process. Yes-and reasoning is essential for developing concepts. At the same time, it was important that we not plan for work that we couldn't schedule. Remi committed to drawing a few icons and I committed to carrying out the core of the gamejam objectives:
- Design and implement a programming language
- Design and implement a game using that language
The first place to start when making a language is with the objects of that language. I don't mean object-oriented design but the idea that the language expresses some sort of manipulation of reality. What are we manipulating? How do we manipulate them?
At a high level, idle and incremental games are about resource management. They can also include capacity planning. Thus, I decided that resources are the main objects of study. I also needed some way for the player to interact with the game world, and clickable buttons are a traditional way to express idle games. For each button, I'd thought to have a corresponding action in the game, which I'd also express in language.
I also needed to figure out what substrate I'm going to use. I mean, of course I'm using Raku, but how deeply do I want to embed the game? At one end, I could imagine compiling the game into a single blob which runs wholly in the end user's windowing system or Web browser, so that there's nothing of Raku in the end product. At the other end, I could imagine shallowly embedding the game by writing some Raku subroutines and having the game developer write in ordinary Raku. I initially decided to go with the shallowest embedding that would still allow me to use Raku's syntax for arithmetic: a Raku sublanguage, or Raku slang. Technically, a slang is its own programming language, or so I was prepared to argue.
One open question concerned the passage of time. A resource evolves in time, perhaps growing or shrinking; it also has some invariants in time, particularly identity. What did I actually want to store internally? All hand-coded games devolve into a soup of objects cross-referenced by string-keyed maps, or at least that was the case for a half-dozen idle games that I've looked at. Maybe we can organize all of that into one big string-keyed map?
Another question is how the game will be experienced. I'd assumed that it should be possible to put up some simple HTTP server and run it locally. My notes are unclear, but I think that this is around the first time that I took a serious look at Humming-Bird.
Let's actually write some code. I started by writing a slang that abused the
Raku metamodel. I was inspired by
OO::Actors, which
introduces an actor keyword, as well as the implementation of the standard
grammar keyword. I can just introduce my own resource and action
keywords which manage some subexpressions, including Raku arithmetic, and
that'll be my language. To prototype this, I first wrote out a file which I
want to be able to load, and then I wrote the parser which handles it. Here's
a snippet from that first prototype:
resource fishmonger {
flavor-text "employee with knives and a booming voice for telling stories";
eats seafood by 0.017 per second;
eats bux by 15 per second;
converts from seafood into bux by 75 per second;
}
action hire-fishmonger {
flavor-text "employ a fishmonger to sell seafood";
costs 10 bux;
pays 1 fishmonger;
}
Several features are very important here. One big deal is that flavor
text is inalienable from the
resources and actions. I was very conflicted about this. Languages like Idle
Game Maker are basically enlightened CSS and HTML; they are extremely
concerned with presentation details rather than getting to the essential
mechanics and handling time. At the same time, Remi and I are both big fans of
flavor text both for its immersive value as well as for its ability to create
a memorable experience. Another important idea here is that costs and pays
are two distinct attributes in concrete syntax, even though they're going to
be implemented as the same underlying sort of amount-and-currency pair.
This syntax is a little heavy. I was imagining that this would be a sort of Ruby or Tcl DSL where each command takes a row of arguments, some of which are literal tokens, and imperatively builds the corresponding resources and actions.
At this point, the scenario is complete. The game will have a few natural resources, like plankton, fish, and sharks; a conversion from fish to seafood; employees like fishers, fishmongers, and white mages; and an enhancement that white mages apply to fishers. There is no objective; it's a population-dynamics sandbox.
After a day of trying to understand Raku's metamodel, I concluded that a slang
is the wrong layer of integration. I really wanted to run an input file
through a parser in order to build a small nest of Resource and Action
objects in memory, set up an HTTP server displaying them, and repeatedly take
one tick per second, integrating changes over all of those objects.
This was a gumption trap for me; I completely lost motivation for a few hours. In those times, it is essential to allow one's emotions to flow in order to move past them, and also essential to rest in order to restore energy. After recovering a bit, I cleaned up my repository and thought about what I should do next. I might as well write a proper grammar. What should that language look like? I agonized over this for a few minutes, went through the possibilities of fixity and bracketing, and eventually decided that a nice little S-expression language would work for my needs. This did mean that I would need to internalize arithmetic, but I also knew one of the standard cheats of game development: it's okay to not implement arithmetic operations which aren't actually used. Consider the following snippet:
(resource plankton 1e15
"little crunchy floaty things"
(growth 0.004 /s)
(view water-color (if (< .count 1e20) "clear" "cloudy"))
(view concentration (str (/ .count 2e14)))
)
(action look-at-water-color
"gaze at the ocean"
(enables view plankton water-color)
)
(action measure-plankton
"buy a plankton meter and put it in the water"
(enables view plankton concentration)
(costs 10 bux)
)This is from my prototype. The only arithmetic that's required is in the
views, which format internal numbers about resources into strings. For those,
I have a mini-language which allows the user to specify any arithmetic they
want, as long as it's either division or less-than comparisons. The formatting
language is strongly typed; the parser won't allow a non-Boolean operation as
an if conditional, for example.
Some other design decisions stand out. Flavor text is now required. Resources have starting counts, which are also required. Rates always end with "/s", an abbreviation for "per second" that is supposed to easily distinguish them from non-rates.
Gumption management requires not just succeeding, but having a feeling of understanding and competence. I probably could have started on the parser that night, but instead I walked to the bar and speedran Zelda 3, doing any% No Major Glitches and finishing in about two hours and change. The parser can wait until tomorrow.
Parsing an S-expression is really easy, especially when the list of special
forms (the words that can legally follow an opening parenthesis) is short. For
each special form, we have a rule that parses each of the required components
in order, followed by an optional zero-or-more collection of
modifiers/attributes/members/components/accessories. The resource special
form from Wednesday is parsed with a rule like:
rule thing:sym<resource> { '(resource' <id> <n> <s> <rmod>* ')' }The parser bottoms out on some very simple tokens. For parsing numbers, we
parse a subset of what Raku accepts and then use .Rat or .Num methods to
convert those strings to live values by reusing Raku's parser. I may not have
been able to reuse arithmetic but I was certainly able to reuse the numerals!
token id { <[-a..zA..Z]>+ }
token s { '"' <-["]>+ '"' }
token n { <[0..9]>+ ('.' <[0..9]>+)? ('e' <[0..9]>+)? }As I wired up the parser, I also set up a Humming-Bird application. I'm a fan
of Ruby's Sinatra and Python's Flask, so it seemed like Humming-Bird would be
a good fit for me. It doesn't come with a preferred HTML-emitting library, so
I tried a few options. I started with
HTML::Tag, which I had added to
the project on either Tuesday or Wednesday, but after a few minutes of
practical usage, it became totally unusable due to syntactic zones of ceremony
(Subramaniam,
Seemann,
myself): making a fresh HTML tag requires many
source characters. I ended up using
HTML::Functional, which
is much lighter-weight but occasionally allows me to misuse Nil as a string.
I'm hacking out two roles. I'm presenting them here in their final versions;
initially they didn't take any parameters, which was too restrictive. One role
is for rendering HTML and the other role is for evolving with each tick. The
%context is all of the resources and actions, and the $resource is the
resource currently being acted upon. This sort of late-bound approach is
technically too flexible for what I'm building, but I don't feel like
restricting it.
role Render { method html(%context, $resource) { ... } }
role Evolve { method tick(%context, $resource) {;} }I committed, pushed, and asked Remi for feedback.
Remi approves! They'll make a few cute little icons for some resources. At this point, I stopped and reflected upon what I'd made so far. Pastafarians typically take Fridays off, and I'm not about to work when I could rest instead. What works? What doesn't work? Where should I spend the rest of my time? What should I have for dinner?
The parser works. The Resource and Action objects operate as nodes of an
AST. Exporting the AST as HTML with Render.html() works. Traversing the AST
for a tick with Evolve.tick() also works. The Nix environment, which I
haven't mentioned yet, also works; I'm using
direnv to configure the environment and
install Raku packages.
Arithmetic operations don't work yet. Actions don't actually act on anything. Remi's artwork isn't visible and I haven't split out the CSS yet.
I should spend the rest of my time getting the core mechanics of rendering and evolution to work properly. Easy to say; harder to do.
For dinner, I'll have noodles of some sort, because it's Friday. I end up having spaghetti and meatballs in red sauce.
Before dinner, I go to the bar and speedran Mario 1. I play for about 90 minutes but I don't finish a single run. On 8-3, the penultimate level, I am repeatedly defeated by a gauntlet of tough enemies.
Humming-Bird got in my way a little; it blocks by default and the documentation doesn't explain how to fix it. After reading the relevant code, I had to change this line:
listen(8080);To have a non-blocking annotation:
listen(8080, :no-block);I also explore how to perform ticks in the background. I do find
Supply.interval, but
that will let the interpreter exit. Instead, I end up with the following hack:
while 1 { sleep 1; $venture.tick };As I wired up operations and fixing display bugs, I became increasingly stressed as my CSS changes aren't being applied. By doing some testing, I discovered that the Humming-Bird convenience helper for attaching static directories is not working. I had initially written:
static('/static', 'static');But this doesn't work, or it had worked on Friday but not on Saturday, or I had somehow mistyped "static", or any of a dozen other impossible considerations. I knew that I can't get distracted by this, and I finished up all of the rest of the functionality; the game works, but it's not styled and Remi's artwork isn't visible.
That's the end of the game jam. I produced a language and a game. However, the game doesn't display properly and I wouldn't consider it to be playable. What a frustration.
I'm not done yet! I want to ensure that the release version has Remi's artwork displayed. First, I hand-wrote the static routes; these do correctly route the CSS and images.
$router.get('/static/style.css', -> $req, $resp {
$resp.file('static/style.css');
});
$router.get('/static/icons/:icon', -> $req, $resp {
my $name = $req.param('icon');
$name.contains('..') ?? $resp.status(400) !! $resp.file('static/icons/' ~ $name);
});I found a few holes in our scenario. For example, there's no way to see how
many Bux the user has. By default, resource amounts are hidden both to keep
the UI uncluttered and to provide a sense of mystery; however, for Bux or
seafood, we want to give the user precise numbers. Our existing syntax can
fully accommodate this! The view is enabled by an action which doesn't have
any line items (costs or pays) and it prints the .count variable as-is.
(resource bux 1000
"wireless cash"
(view cash-on-hand (str .count))
)
(action check-balance
"become aware of our earnings"
(enables view bux cash-on-hand)
)The idea of a declarative idle-game maker is reasonable and it was only a week's effort to prototype a basic interpreter which simulates a simple scenario. I think that the biggest time sinks were trying to make a slang instead of a deeper language with a parser, fighting with Humming-Bird, and generally trying to keep code clean. On that last point: I found that cleaning up my code was necessary to let bugs and mistakes become visible.
Have a happy winter solstice and Holiday!