Introducing Card Lang

When I was first starting the project that would become Solaria Tactics I knew that if you're going to make a card game, you better make the cards easy to design and tweak. There's going to be hundreds of cards each with special rules, conditions, actions and other crazy stuff I want to add. Designing all these cards and making sure they work as intended would be a big challenge then especially with a developer team of one. So the challenge was take a card description like "Deal 5 damage to all minions" and encode that into something the game engine can understand and execute when you play that card. I had heard games like Hearthstone have a custom editor tool for designing and setting up the cards which I briefly considered doing, but that seemed like a black hole of time to create and maintain. It then dawned on me that the problem I was trying to solve, "Create meaning out of text", is exactly what languages (both computer and written) are meant to do. Brilliant! I would create my own DSL which I could customize fairly easily and get all the power I need out of it. This idea also really appealed to the CS nerd inside of me since computer languages are such great examples of recursion. We'll be using a program to make a language which will then be used to make a program!

If you're not too familiar with computer languages, parsers, and grammars there are some good resources online that can help since there's a lot to learn that's outside the scope of this article. Crafting Interpreters is a great book that you can dive deep into the gritty nitty or jump to specific sections that we'll be talking about. The Super Tiny Compiler is another great learning exercise that walks you through creating a compiler (and parser, and tokenizer) from scratch in code.

For my purpose though, I will just need the front-end of the compiler which takes the raw text, breaks it up into tokens (the words of the language), then parses those words into an unambiguously interpreted sentence with the grammar of the language. This results in an AST data structure that I can use on the server to evaluate what needs to be done at various events in the game.

I started looking around for ways to create the language that I could iterate on quickly and found Jison which is derived from Bison but written in javascript which makes it perfect for running in node.js on the game server. I also found the excellent tool Jison Debugger which you can use to play with and follow along with the code examples in the rest of the article. I used this tool a ton when getting started with the language to quickly see how things parse.

The first thing to set up in the language are the lexing rules to create the "words" of the language which we can refer to in the grammar later. The lexing rules take regular expressions to match on and then return the name of the token (or nothing in the case of the whitespace ignore rule). We start with the rule to match all whitespace so our language can be whitespace insensitive, then we have the rule to match names of events that actions will be triggered on. Sprinkle in some syntax tokens which produce themselves, and then the rest are the boilerplate rules to match the end of file and then any other characters that fall through the cracks to be marked as INVALID parse errors.


%lex
%%

\s+ /* skip whitespace */

//Events are things to be reacted to, like when a minion or spell is played, or a minion dies
(playMinion|playSpell|death)
  return 'event'

//syntax
'('    return '('
')'    return ')'
'['    return '['
']'    return ']'
','    return ','
'{'    return '{'
'}'    return '}'

<>               return 'EOF'
.                     return 'INVALID'

/lex

Right below the lexing rules are where the grammar rules go to define how the "sentence" structure of the language works. To get off the ground we'll just create a grammar to support creating a list of events, with each event needing curly braces after as a placeholder for the actions to go later. Each of these grammar rules starts with the name of the rule (like eventList), followed by the colon and the first tokens or other grammar rules to match on (eventList and pEvent). After these rules is the block in curlies that defines what the parsed output object will be, with dollar signs to reference the matched elements either by their position like $1 or everything that matched with $$. Additional rules to match on are then tacked on with a pipe character and continue as before. As you might have noticed, creating lists is done though recursive matching where the eventList can match another eventList or a pEvent itself.


%ebnf

//This is the root expression that encapsulates the whole program
%start events

%% /* language grammar */

events
  : eventList EOF {return $1;}
;

eventList
  : eventList pEvent
     { $$ = $eventList; $$.push($pEvent); }
  | pEvent
     { $$ = [$pEvent]; }
;

/* events */
pEvent
  : event'{''}'
   { $$ = { event: $1 } }
 
;

And there you have the beginnings of a language! We can now parse playSpell{} to get


[
  { "event": "playSpell" }
]

As well as multiple events


playMinion{}
death{}

To get


[
  { "event": "playMinion" },
  { "event": "death" }
]

And here is the breakdown of how everything parsed courtesy of the Jison debugger

Screenshot-2017-12-12-10.59.33

Next up in the card language series we'll make the language actually do something useful and explore some more advanced grammar, actions, and how to start selecting specific pieces!

Back to home More blog posts

Comments

comments powered by Disqus