Looking for feedback: Choice-based IF in Markdown and Lua

Demo & a hurried tutorial draft: Brocatel | Brocatel

I’ve been working on this authoring system for a while, and I would like to hear your opinions on it.

Basically,

  1. The system runtime is written in Lua, making it runnable on most platforms, including browsers. (And the web tutorial is interactive.)
  2. Most grammar from Markdown is preserved, providing a bit of mark-up ability.
  3. Headings and links are used naturally to represent anchors and jumps. (Hopefully most Markdown IDEs support link anchor auto-complete.)
    # header
    A loop.
    [](#header)
    
  4. Lists are used as choices.
    - Go north
      No. It's really raining cats and dogs out there.
    - `RECUR` Go west
      The walls of this small room were clearly once lined with hooks, though now only one remains. The exit is a door to the east.
    
  5. Inline code snippets and code blocks in Markdown are used for conditional texts and Lua scripting.
    `condition` Conditional text
    
  6. The compiler runs macros (built-in or user-provided) that manipulates the Markdown structure. Currently a switch statement, a loop and the show-once choices are based on this.
    :::loop
    - Loops over and over.
    
    Same as the following:
    # loop
    Loops over and over.
    [](#loop)
    
  7. Value interpolation is borrowed from MDX.
    Score: {score}
    

(The tutorial has more examples (interactive).)

It seems it is getting a bit technical… Anyway, by using Markdown and Lua, which are both mature and have a rather large ecosystem around them, we can quite easily come up with something that works, whether it is syntax highlighting or auto-complete support.
I am looking to implement a web frontend for this so that one can publish to the web more easily. And maybe I will borrow more features from Ink. But before that, I would really like to hear your thoughts on it. Any feedback will be appreciated. Thank you!

7 Likes

This is really, really cool. Love that it runs in the browser. Very streamlined syntax. Love it!

Any plans for a parser with it?

3 Likes

Thank you! I am not that familiar with parser games, and I don’t think I know them well enough to actually write a system for them.
After reading some articles online and having a go with Inform, it seems to me that a parser-based system can get separated into two parts: a user-input parsing part and a choice system associating actions to rooms, items or any other objects.
The parsing part is rather complex (but it certainly depends how smart we want the parser to be) and does not translate well. Since this system uses gettext as a translation framework, which probably won’t work with parsers. So I’m afraid I won’t be able to integrate a parser.
However, the latter part sounds quite interesting. Object-based actions can be quite useful for choice-based games too and it seems doable - we might ship a room macro:

:::room `foyer`
- [to]
  - [](#cloakroom) West
  - [](#bar) South
- [description]
  You are standing in a spacious hall.
- [objects]
  - [](#yourself) Examine yourself

It will take some time to implement (and probably require several unimplemented features), but I will definitely put this on my to-do list!

2 Likes

Interesting.

The question was just more of a curiosity, but everything you’ve said is spot on. I’m not too familiar with all the major authoring systems, but having a dedicated :::room macro might be useful and unique among the landscape of choice-based authoring. Maybe others can chime in on this observation.

Maybe :::room might be better served with a more generic name though, as it doesn’t necessary take you to physical places, but plot/conversation points as well… or am I misunderstanding? :::scene maybe is a better term?

Just thinking out loud. What you’ve accomplished thus far is pretty amazing. :slight_smile:

1 Like

Yes, I agree on that. I, too, prefer parsers. So I can’t judge your program. But you made a good choice with Markdown and Lua. Such a pity that so few reactions were made here (from fans of choice based dev systems.)

2 Likes

Great ideas.

You need some kind of subroutine. What if;

# foo
foo didn't automatically drop through to bar
# bar
we're in bar.

And instead you have to always jump,

# foo
foo didn't automatically drop through to bar
![](bar)
# bar
we're in bar.

Most of your COD example appears to work like this anyhow.

Then, if there’s no jump a return is performed.

So;

## xme
You examine yourself.
`examined_self = true`
:::if`has_cloak`
- You are wearing a handsome cloak, of velvet trimmed with satin, and slightly splattered with raindrops. Its blackness is so deep that it almost seems to suck light from the room.
- You aren't carrying anything.

## foyer_options
1. Examine yourself.
   [](xme)
2. Go north.
   [](leave)
3. Go west.
   [](cloakroom)
4. `not has_cloak` Go south.
   [](bar_light)
5. `has_cloak` Go south.
   [](bar_dark)

[](foyer_options)

And you’d also put Examine yourself in other choices too.

1 Like

I would definitely recommend that you focus on making great choice-based system, rather than trying to add parser features on the side.

5 Likes

Thanks for the suggestion! I have been thinking about it for a while but am still wondering how to do it right. It feels a bit weird to fit non-linear stories into some fixed call-and-return structures. (But it turns out implementing it in the runtime was not too much of a trouble: a prototype here).
Maybe setting up some restrictions (forbidding jumping out of a subroutine or jumping into one (instead of calling)) is better?

(Ink seems to forbid doing so in their “tunnels”, and they have an interesting “return to somewhere else” grammar, which indeed sound useful for non-linear stories.)

1 Like

I have a system which is based on a concept I call “flow”. I find this idea quite suitable for IF and non-linear construction. My suggestion is based on the same idea as a flow system.

Where you have markdown “header”, i call these “terms”. Terms flow to other terms and so on. When a flow has nowhere to go, it flows back (ie return).

I must explain something that makes it work well;

When you think in terms of flows going to other flows (ie authoring), you do not think about it in programmer terms like “goto” and “call”. In fact, it’s important in a flow system, these are the same thing.

How are they the same?

This may or may not be possible in your implementation, but the way it works for me is “jumps” to other terms are “calls” (ie can return) unless the destination is already on the call stack. In which case it performs a jump and unroll to that place.

Example, standard flow (in my flow system):

FOO
This is some text that calls BAR, and comes back.

BAR
a thing called bar

emits:

This is some text that calls a thing called bar, and comes back.

But consider (jump/call flow)

CHOOSEDRINK?
What do you want to drink?
* Coffee
NONE
* Tea
NONE
* Hot chocolate
NONE
* Soda
Coming right up!
MAIN

NONE
Sorry, we're out of that! Try something else. CHOOSEDRINK

So CHOOSEDRINK creates a choice set. Most flow to NONE, which prints a message and flows back to CHOOSEDRINK. This won’t be a “call” because we’re already inside CHOOSEDRINK so it will be a jump.

Choosing “Soda” breaks out of the loop by jumping to MAIN, which is somewhere up on the call stack already.

Maybe you could do the same jump/call trick with your links to headers?

Idea 2: Objects

People have suggested you consider parsers or at least a world model;

I got to the same point and decided my terms could also be objects. In principle, your headers could also be objects. And this would be another good reason why execution does not automatically flow off the end, because objects do not flow the same way.

Example object:

COFFEE@ THING
* name
some coffee
* x it
It's still a bit hot.
* drink it
You take a sip. Very nice.

Choices are replaced by “semantic reactions” but the syntax is the same. I used the same syntax for everything; text generation FOO and BAR, Choices CHOOSEDRINK? and objects COFFEE@. The type is indicated by the last character with plain terms the default.

3 Likes

I’m not a programmer so I hope I can give you some insight anyway.
Most of the parts in the architecture section were a bit too technical for me but I think that was not meant for beginners?

I managed to get as far as making choices because that was extremely easy.
I might have struggled so much with the idents and coding because I was on mobile and my eyes are quite bad.
The syntax is pretty simple but I have no Lua knowledge so learning Brocatel might be more intuitive for someone who knows a bit more Lua.
(I’m happy I have italics down in Markdown, ok.)

I could set a budget as in the example but then got a bit lost with links and had, unfortunately, no idea how to make headings, though I am no further to having working if conditions than before.

Still, I think I did well enough with it. I only learned today, after all.
I had to work for it (it’s so hot here my brain is mush) but it was fun.

Sometimes a tutorial section didn’t work for me, as in it showed no text: once, the Recur part and then the loop, I think.
The further text links maybe?
I refreshed to try out something else so I can’t look.

All in all, I am looking forward to where this goes. (Especially once I have a bit more brainpower. :sweat_smile:)

Edit because I forgot to add this: A text only documentation would be better readable for me personally, but probably a lot of work and leave out the type and test it option, which I liked. Could still be a possibility for later when it’s further along.

3 Likes

Thank you! It is so nice of you to detail this, and it really made me rethink my previous (arbitrarily made) design decisions.

The flow system The current implementation
Labels Going to a label is made explicit Only function labels require explicit jumps
goto Not supported Jumps without modifying call stack
call Flow to that label Pushes a stack frame
return Flow back to somewhere upstream Pops a stack frame
Recursion Not applicable (I can’t think of recursive stories anyway)
For users call & return unified! goto / call / return each differs.

I tried to summarize the unique features of your flow system, and unifying call and return (and goto) is really appealing to me. I do think it can ease the learning curve for users with no prior programming experience. But if I am to migrate to a flow system, maybe adding an explicit return statement can be better? For example, when multiple dialogues flow to the same side story, that side story will not be able to call to return,
and this can be inconvenient for stories with a bit of nested choices.

CHOOSEDRINK?
What do you want to drink?
* Coffee
NONE
* Tea
NONE
* Hot chocolate
NONE
* Soda
Coming right up!
// The flow has nowhere to go, it flows back.

NONE
Sorry, we're out of that! Try something else. CHOOSEDRINK
// It can be a trouble if we want to just return here, instead of asking the users to choose again.
// (But in this example it actually works since it is a tail-call.)
// So probably a RETURN FROM CHOOSEDRINK? statement can be useful.

I am still thinking about it and it can take a while before I start implementing things. Anyway, thanks for the new perspective!

1 Like

Thank you for sharing your experiences!

The architecture section is intended for people looking to modifying the whole system, but not for normal users. (I should probably put a note somewhere.)

I’ve also tried editing those code snippets on mobile and I have to admit that the experience was terrible (with my input method somehow fighting with the editor over auto-completion). And I do think I need to rework both the tutorial and the editor widget soon. (A text-only tutorial indeed improves readability. Maybe I will go with a hybrid approach by using a pop-up editor if I am to rewrite the document.)

As to the Markdown syntax and indentation, instead of the current code-centric editor, I am considering using a WYSIWYG editor one instead, which hides all the indentation problems and may provide better visual aids. (Also with a WYSIWYG editor we can finally have sans or serif fonts.) I will give Milkdown a go once I finish implementing some other features.

Thanks again!

2 Likes

Firstly, your summary is correct. Call/goto/return are all the same. And there is no recursion in a flow system. This last point is not so much of a sacrifice because for recursion to be useful, you would need some kind of local variables. Then you’re getting into a programming headache.

Secondly, you are correct there is no need for MAIN in the “Soda” choice. Without MAIN, the flow will return to whomever called CHOOSEDRINK.

I’m not quite sure what you mean when you talk of a “explicit return”. Can you give an example.

Let me try; Two dialogues that flow to the same place.

DIAG1?
What do you want to do?
* go shopping
You go shopping and buy a new tie. 
* go fishing
You go fishing but you don't catch anything.
* go on a quest for the holy grail
HOLYGRAIL


DIAG2?
What quest are you to go on?
* quest for the magic lantern
QUESTLANTERN
* quest for the holy grail
HOLYGRAIL
* quest to rescue the princess
QUESTPRINCESS


HOLYGRAIL
Great! Let's go... HG1
// will return to either DIAG1 or DIAG2 and then return to whoever called them.

HG1?
* look in the cave
It's not there!
* check the magic bag
It's not there!
* ask the wizard for help
Sorry, he's gone shopping.
2 Likes

What I had in mind was something from the Intercept (written in Ink). (I actually remember reading some nested choices, but I will just use this as an example.) In a simpler form:

SECTION: missing_reel
CHOICES:
* The stolen component...
  // The story continues to `The reel...`.
* Shrug
  RETURN
  // The story returns to wherever it is called.

The reel went missing from the Bombe this afternoon...
CHOICES:
* Panic
  RUN CODE: lower(forceful)
* Calculate
  RUN CODE: raise(evasive)
* Deny
  RUN CODE: raise(forceful)
RETURN

The flow system differs with Ink (or the current Markdown-based system) in this, since it seems to require labeling all choice nodes (and some other nodes maybe) if I understand it correctly.
And one can get the example working in the flow system by adding more labels:

SECTION: missing_reel
CHOICES:
* The stolen component...
  // The story continues to `The reel...`.
  GOTO TEXT
* Shrug
  // No need for `RETURN`
  // The story returns to wherever it is called.

TEXT:
The reel went missing from the Bombe this afternoon...
GOTO SECOND_CHOICE

SECOND_CHOICE
CHOICES:
* Panic
  RUN CODE: lower(forceful)
* Calculate
  RUN CODE: raise(evasive)
* Deny
  RUN CODE: raise(forceful)
// No need for `RETURN`

I assume with enough labels, the flow system can achieve anything that Ink is capable of. But personally speaking, it tires me a bit and is not that Markdown-ish, where you usually get lengthy dialogues and choices nested in choices. A random example with nested choices:

* Order pizza...
  * With pineapple on it.
    SET waiter_angry = true
    // Return immediately
* Other choices...
  * Other options...
// Common dialogue
Coming right up!

(I believe one can get this working in the flow system though, by adding more labels.)

1 Like

Thank you for this reply. It’s most interesting.

My flow system is called Strand, and it’s true that you will often need additional labels (i call terms) compared to Ink. This is because in Strand:

  • You cannot have multiple sets of choices within the same term flow.
  • You cannot nest choices, instead you flow to another term (and return).

However, i don’t think this is such a drawback as it first appears. I’ll explain why in a bit.

The flow system is definitely capable of anything in Ink.

But the main advantage with Strand flow is that flows return. Or, in general, that call/goto/return are the same. This also allow terms to be “dropped” inline into text that expand like macros (because they return). This avoids all the messy Ink-like “variable text” syntax.

The reason why you have nested choices in Ink is to fix the problem that you cannot have a choice return, so you have to nest it. What happens when two choices want to invoke the same subchoice? You cannot factor it as there is no automatic return. Instead you might have to test a conditional to know where to divert.

Let me write your example a bit different. You can avoid additional labels like this:

MISSING_REEL
* The stolen component...
  The reel went missing from the Bombe this afternoon... SECOND_CHOICE
* Shrug
  // The story returns to wherever it is called.

SECOND_CHOICE
* Panic
  ..
* Calculate
  ..
* Deny
  ...

I put the text inline with the choice and branch to SECOND_CHOICE

But really;

You would probably structure it more like your layout deliberately with extra labels. Why?

This is my explanation of why extra labels are useful;

Examples look great when the text is short. In a real game, the text is much longer and there is a lot more of it. When this happens your workspace is dominated by content and you cannot easily visually scan the flow logic anymore.

I am routinely taking out chunks of content and refactoring them into separate terms, so that i can more clearly see the flow.

Furthermore, as covered in the ink manual, you wind up labelling gathers and options anyhow with a (bracket) syntax.

Here’s an example from the Ink manual:

=== meet_guard ===
The guard frowns at you.

* 	(greet) [Greet him]
	'Greetings.'
*	(get_out) 'Get out of my way[.'],' you tell the guard.

- 	'Hmm,' replies the guard.

*	{greet} 	'Having a nice day?' // only if you greeted him

* 	'Hmm?'[] you reply.

*	{get_out} [Shove him aside] 	 // only if you threatened him
	You shove him sharply. He stares in reply, and draws his sword!
	-> fight_guard 			// this route diverts out of the weave

-	'Mff,' the guard replies, and then offers you a paper bag. 'Toffee?'

And here’s how it would look in Strand flow;

MEET_GUARD
The guard frowns at you. MG1
'Hmm,' replies the guard. MG2
'Mff,' the guard replies, and then offers you a paper bag. 'Toffee?'

MG1?
* Greet him
GREET
* Get out of my way
GETOUT

GREET
'Greetings.'

GETOUT
'Get out of my way,' you tell the guard.

MG2?
*?GREET	Having a nice day?
* Hmm?
'Hmm?' you reply.
*?GETOUT Shove him aside
You shove him sharply. He stares in reply, and draws his sword! FIGHT_GUARD

So more labels indeed. But these replace the Ink ones that were necessary there to make the conditionals. I personally prefer the Strand flow version as the content is more factored and easy to edit outside of the flow logic. You can be sure that these lines will probably be longer in a real game. And more options.

To cover your last example with Pizza…

CAFE
What now?
ORDER
WAITERREPPLY

// waiter reply is conditional on angry
WAITERREPPLY=
*?WAITERANGRY Ok then
* Coming right up!

ORDER?
* order pizza
ORDER_PIZZA
* something else
You order the daily special.

ORDER_PIZZA?
* With pineapple on it?
WAITERANGRY
* plain

// empty term for conditional
WAITERANGRY

Yes, there are more labels. but they replace variables too and the flow is cleaner.

1 Like