I’ve had a nagging feeling my whole career that computing has gone wrong somewhere.

I can’t put my finger on it exactly. I have plenty of day-to-day complaints — the bloat, the ads, the surveillance, the sheer amount of code, JavaScript. Each of them deserves a blog post on their own. But my bigger concern is that programs, even good ones, are well, just programs. We’ve got better at organising huge piles of code. Microsoft Office is millions and millions of lines of code — an unfathomable inhuman scale.

We have stricter type systems, smarter compilers, friendlier languages, better packaging. But underneath it’s still a way of getting to a heap of imperative steps in a general-purpose box, and the box doesn’t know what kind of thing it’s holding. As my kids would say, it’s all a bit mid.

It’s amazing that you can write a web site or a flight simulator or a compiler in the same language and the language can’t tell the difference between them. They all ship in the same box.

Then, a few years ago I came across Nile and Gezira — bits of work out of Alan Kay’s old Viewpoints Research Institute. Gezira is a 2D graphics renderer. Cairo, the C reference renderer it competes with, is tens of thousands of lines. Gezira is a few hundred, written in Nile, with its own stream-processing algebra invented just for the purpose. The whole thing isn’t a program describing how to draw shapes. Instead, it’s a small specific language inside which drawing and rendering shapes are the only things you can say. And the whole algebra/language fits on one page.

I loved the idea that you could find a small algebra fitted to a domain, and that ‘programs’ written in that algebra would be smaller and more honest than the equivalent in C or Python or whatever. Not faster necessarily. Just truer and more understandable. Tacit expression is a super-power. Constraints make the world smaller and more sharply focussed. The box is the same shape as the problem it’s solving. This was clearly the future of computing!

Then I went and forgot about it for a while, because I had to write some JavaScript.


Meanwhile, I started to play a lot of tabletop roleplaying games. At one stage I had three D&D campaigns going at the same time and a long-running Monster of the Week table I’ve never quite been able to put down. TTRPGs are a curious shape of thing. They’re rules-plus-prose. The rules tell you to roll dice and add a number and consult a table; the prose tells you what any of that means in the fiction.

What’s odd about TTRPG rules — to a programmer — is how informally they’re specified. A typical Powered by the Apocalypse move looks like this:

When you act under fire, roll +cool. On a 10+, you do it. On a 7–9, the MC offers you a worse outcome, a hard bargain, or an ugly choice. On a miss, the MC makes as hard a move as they like, and you mark experience.

Three quarters of the rules in the book look like this. There are hundreds of them. They interact. Designers playtest the combinations for years and tune them by gut, partly because the best way to assess them is to play and because there is nowhere else to tune them. But to me, with my programmer eyes, I see state machines and branches, mutation and events.

It’s plainly computational. There’s a distribution, a predicate, a branch, a track mutation, a prompt to a person, a piece of fiction to be emitted. But TTRPG designers don’t write code. They write prose, in a word processor, and they hold the algebra in their heads. The character sheet lives in a different file. The rulebook lives in a third. Balancing the whole thing means playing it for six months and hoping for the best.

Does this seem like a place where an algebra could live? Is there a TTRPG-shaped code box?


So I’ve started building a thing called Uruk - named after the ancient Mesopotamian city. It is, deep breath, yet another DSL. Or maybe an algebra. The bet is that a useful subset of tabletop games (call them uruk-friendly — PbtA, Forged in the Dark, OSR-likes, Cairn-likes, Ironsworn-likes, journaling games, most of the indie scene) can be described in about ten primitives, a couple of operations and a few types.

On top of that sits an author-friendly DSL that ‘compiles’ to these primitives. I’ll keep the surface DSL for another post, because I want this one to be about the algebra under it.

Here’s a complete PbtA-style move, written in the algebra (my actual implementation is in Elm, but I’m showing it in Haskell-ish style so it looks more academic):

defyDanger =
  Prompt (PickStat ["cool", "hard", "sharp"])  >>= λs.
  Roll   (2d6 + Stat s)                        >>= λn.
  Bind   "total" n                             >>= λ_.
  Branch (WorkspaceValue "total")
    [ ("hit",     MatchIntAtLeast 10, Note "You do it.")
    , ("partial", MatchIntAtLeast  7, Note "You do it, but the MC offers a complication.")
    , ("miss",    MatchIntAtLeast  0, Note "Mark XP. The MC makes a hard move.")
    ]

I admit this is uglier than the original prose, with its lambdas and its monad-bind. The point of having it in this shape is that it is now a thing the computer can think about. Not just print, not just store — reason about.

Because resolution is a pure expression — distributions, predicates, outcome ladders — there are some things you get more or less for free once you’ve committed to it:

  • the exact probability of each roll with modifiers
  • the rulebook chapter for this move
  • analytic tools like looking for dominated builds or unintended game breakages
  • monte carloing runs to test rule changes
  • automatic integration with VTTs
  • and so on…

You can see that as ’executing’ the algebra with different interpreters — one that makes a rulebook, one that makes a character builder and so on. Or you can see the algebra as just a formal verification tool of the system. I’ve come to see these different things as projections of the source through different lenses.

I am well aware that no-one really wants this! The people I’m building this for — game designers — mostly do not want a programming language. They want a word processor and a table of contents and a printout that looks like a book.

And, I can’t imagine that there would be any killer features in a marketing sense. But I want it as a playground. I want to experiment with the idea of DSL algebras and it’s a chance for me to scratch that Nile itch.

The algebra so far may well be wrong. I don’t know yet if it’s too narrow to express games usefully. I might find it needs to be too broad to mean anything. But it feels good to be doing something about the nagging feeling that everything is broken. Even if it works a little bit, it’s another thing to add to a future “Alan Kay was right” blog post.

Do you want to see the algebra so far? Of course you do. Here it is, again in Haskell-style so that we can pretend that this is serious research and not just slightly deranged monadic malarkey.

URUK 0.9

Core types

    TrackRef     = name : String                       persistent named value
    FactValue    = BoolFact Bool | IntFact  | StringFact String
    Fact         = (name : String, value : FactValue)
    Prose        = String                              markdown
    Queryable    = TrackValue TrackRef
                 | WorkspaceValue String                transient roll workspace
    Question     = PickStat [String] | YesNoMaybe
    Choice       = (label : String, body : Effect ())
    Case         = (label : String, match : Match, body : Effect ())

Effects

    Effect a =  Pure        a
             |  SetTrack    TrackRef (  )            (Effect a)
             |  Assert      Fact                        (Effect a)
             |  Query       Queryable           (FactValue  Effect a)
             |  Prompt      Question            (FactValue  Effect a)
             |  Choose       [Choice]            ([Choice]  Effect a)
             |  Roll        DiceExpr                    (  Effect a)
             |  Note        Prose                       (Effect a)
             |  Bind        String FactValue            (Effect a)
             |  Branch      Queryable [Case]            (Effect a)

    (>>=) : Effect a  (a  Effect b)  Effect b
            Pure a    >>= f  =  f a
            Op   k   >>= f  =  Op   (λx. k x >>= f)

Resolution side

    DiceExpr =  Dice (count : , faces : )   |   Const 
             |  Stat String  |  ConditionMeter String  |  WorkspaceStat String
             |  Sum [DiceExpr]   |   Highest [DiceExpr]
             |  KeepHighest  DiceExpr   |   KeepLowest  DiceExpr

    Predicate =  (LT | GT | EQ | GE | LE)  DiceExpr      per-die test
              |  Bound (key : String,
                        alts : [(label : String, Predicate)])

    Match =  MatchEquals String   |   MatchIntEquals 
          |  MatchIntAtLeast     |   MatchIntRange (lo, hi : )

    Resolution =
         Versus  ( primary, opposed : DiceExpr,
                   ladder : [(label : String, minBeats : )] )
       | SumVs   ( DiceExpr,
                   [(label : String, minimum : )] )
       | Pool    ( dice : DiceExpr, predicate : Predicate,
                   ladder : [(label : String, minSuccesses : )] )

Rules

    Move       = ( name, trigger : String,
                   body : Effect (),
                   outcomes : Maybe ( resolution : Resolution,
                                      branches  : [Case] ),
                   stats, meters : [String] )

    Reaction   = ( name : String, trigger : Trigger, body : Effect () )

    Trigger    = Threshold (TrackRef, Comparator, )
    Comparator = Equals | GreaterEqual | 

That’s the lot. It fits on one page — which is the point. If any of this resonates I would genuinely love to hear from you on Mastodon at @beng@mastodon.social.