CFG Game
Put your CS skills to good use and craft burgers.
This past weekend I made a game for Ludum Dare 43. Tools used: Aseprite, quicksilver. Inspired by Zachtronics.
It is written in Rust and compiled to WebAssembly. Checkout the source code.
How the game is implemented
The core of the game is a pretty standard LL(1) parser whose grammar is defined dynamically in game by player.
type RuleID = usize;
struct Grammar<T: Debug + Clone + PartialEq + Hash + Eq> {
start: String,
rules: Vec<Rule<T>>,
first_sets: Option<HashMap<String, HashSet<(Token<T>, Rule<T>)>>>,
}
struct Rule<T: Debug + Clone + PartialEq + Hash + Eq> {
name: String,
id: RuleID,
production: Vec<Token<T>>,
}
enum Token<T: Debug + Clone + PartialEq + Hash + Eq> {
Terminal(T),
Epsilon,
NonTerminal(String),
}
Note the rule has an id
field so the the parse result is traceable, i.e. which paths the parser took.
enum AbstractBurgerTree<T: Debug + Clone + PartialEq + Hash + Eq> {
NonTerm((RuleID, Vec<Box<AbstractBurgerTree<T>>>)),
Term(Token<T>),
/// errors:
IncompleteParse,
WrongToken,
Cyclic,
AdditionalTokens(Box<AbstractBurgerTree<T>>),
}
The parser errors are also valid AST elements.
Next, there are two helper functions that operates on the AST:
fn to_burger()
converts the parse tree, which may or may not include errors, back into a burger.
pub fn to_burger(&self) -> Burger {
let mut bg = Burger::new();
bg.toks = self.to_burger_aux();
bg
}
fn to_burger_aux(&self) -> Vec<Token<BurgerItem>> {
use self::AbstractBurgerTree::*;
let mut ret = vec![];
match &self {
Term(Token::Epsilon) => { }
Term(t) => { ret.push(t.clone()); }
NonTerm((_,t)) => {
for i in t.iter() {
ret.extend(i.to_burger_aux());
}
}
AdditionalTokens(i) => { ret.extend(i.to_burger_aux()); }
_ => (), // all the errors are ignored
}
ret
}
fn to_delta_seq()
converts an AST into an sequence of animations …
fn to_delta_seq(&self) -> Vec<AnimDelta> {
use self::AbstractBurgerTree::*;
let mut ret = vec![];
match self {
Term(Token::Epsilon) => {
ret.push(AnimDelta::Noop);
}
Term(Token::Terminal(_t)) => {
ret.push(AnimDelta::Incr);
ret.push(AnimDelta::StepAnim);
}
Term(Token::NonTerminal(_)) => panic!("Impossible"),
NonTerm(t) => {
ret.push(AnimDelta::Incr);
ret.push(AnimDelta::EnterPtr(t.0));
for i in &t.1 {
ret.extend(i.to_delta_seq());
}
ret.push(AnimDelta::ExitPtr(t.0));
}
IncompleteParse | WrongToken | Cyclic => {
ret.push(AnimDelta::PauseIndefinitely);
}
AdditionalTokens(i) => {
ret.extend(i.to_delta_seq());
ret.push(AnimDelta::PauseIndefinitely);
}
}
ret
}
which are then dispatched by the main game state.
Everything else(~1400 loc) is game UI which is pretty tedious to write. I’ve never written a game before so it took several refactors.