Runelang: A Bind Rune Generator

Continuing with my Runelang in the Browser series, I had the idea to automatically generate runes. So basically reversing the parsing step, rather than to take what I’ve written and make it look good, to write something that Runelang can parse–and still look good.

In a nutshell, I want to write a series of functions that can recursively call one another to render runes:

  • generate_bind_rune
    • n times generate_bind_rune_arm
      • m times generate bars, circles, and other decrations
      • add a fork at the end

Generate Bind Rune

I’ll follow up with a few other styles, but for this first one, here’s what we have:

function generate_rune() {
  stdlib()
  block("rune {")
  choose(generate_bind_rune)
  end_block("}")
}

function generate_bind_rune() {
  block("stroke(weight: 5) scale(0.75) {")
  block("radial(offset: 0) [")
  {
    for (let i = 0; i < random_int(2, 4) * 2; i++) {
      generate_bind_rune_arm()
    }
  }

  end_block("]")
  end_block("}")
}

function generate_bind_rune_arm() {
  block("stack [")
  line("line")
  block("linear(scale: 0.25, min: 0.5) [")
  {
    for (let i = 0; i < random_int(2, 5); i++) {
      choose(
        // cross bar
        8,
        () => line("bar"),
        // half circle in
        2,
        () => line("group { translate(y: -0.25) scale(0.5) arc(-1/4, 1/4) }"),
        // half circle out
        2,
        () => line("group { translate(y: 0.25) scale(0.5) arc(1/4, -1/4) }"),
        // full circle
        1,
        () => line('scale(0.25) fill("none") circle'),
        // two dots
        1,
        () => {
          block('fill("black") stack [')
          line("{ translate(-0.5) scale(0.1) circle }")
          line("{ translate(0.5) scale(0.1) circle }")
          end_block("]")
        }
      )
    }
    line(`fork(${random_int(3, 5)})`)
  }
  end_block("]")
  end_block("]")
}

// main
generate_rune()

Fairly straight forward to start with, if you assume that all of those helper functions do what you expect them to do:

  • block: starts an indented block (can be nested)
  • end_block: ends the indentation of a previous block
  • line: outputs a properly indented line of text
  • choose: random choose and output one of many options, with optional weights
  • random_float and random_int: wrap random to generate random numbers with given bounds

So let’s go through each of those.

Indentation levels: block and end_block

const INDENTATION_STRING = "  "
let INDENTATION_LEVEL = 0

function block(text) {
  line(text)
  INDENTATION_LEVEL++
}

function end_block(text) {
  INDENTATION_LEVEL--
  line(text)
}

All this does is keep track of a global (I know) indentation level, incrementing it when we see a block and decrementing it when we see an end_block. You do have to make sure to do that in the proper order, otherwise you’ll end up with the end block as part of the block (Python style) rather than as being at the level of the opening.

Single lines: line

Next, rendering single lines to the right level:

function line(text) {
  console.log(INDENTATION_STRING.repeat(INDENTATION_LEVEL) + text)
}

Not much there.

Weighty choices: choose


function choose(...options) {
  let weights = []
  let total_options = 1

  for (let i = 0; i < options.length; i++) {
    if (Number.isInteger(options[i])) {
      weights.push([options[i], options[i + 1]])
      total_options += options[i]
      i += 1
    } else {
      weights.push([1, options[i]])
      total_options += 1
    }
  }

  let value = random_int(0, total_options - 1)
  for (let [weight, option] of weights) {
    if (value <= weight) {
      if (option instanceof Function) {
        return option()
      } else {
        return option
      }
    } else {
      value -= weight
    }
  }
  console.error("out of range choice)")
}

This is an interesting function, in large part because the arguments are so dynamic. You can specify any number of children to randomly choose between, along with optional weights. So you could have:

// Randomly choose between a, b, and c
choose('a', 'b', 'c')

// Randomly choose between a, b, and c with each twice as likely as the next
choose(4, 'a', 2, 'b', 'c')

// Randomly choose a function with hello() twice as likely
function hello() {}
function world() {}
choose(2, hello, world)

It’s a bit persnickity if you want to randomly choose numeric values. Technically you can, but you always have to specify the weight. I think it’s probably best if you don’t though.

Random numbers

And finally, wrappers for random numbers:

function random_float(min, max) {
  min ||= 0.0
  max ||= 1.0

  return Math.random() * (max - min) + min
}

function random_int(min, max) {
  if (!max) {
    max = min
    min = 0
  }

  return Math.floor(Math.random() * (1 + max - min)) + min
}

Demo

Demo time!

Output

Source

Log (most recent messages first):