ec2-3-144-42-196.us-east-2.compute.amazonaws.com | ToothyWiki | ToothyGDL | RecentChanges | Login | Webcomic
Introduction
A game logic definition consists of:
- A set of declarations for all required game state.
- A set of (conditional, ordered list of text strings and integer expressions, set of state assignments)
The runtime interpreter does the following:
- Initialise the game state
- Repeat
- Clear set of permitted actions
- Evaluate all conditionals in the world. For all those that are true,
- Evaluate the integer expressions/text strings to create an action label
- Add (a mapping from this label to the set of state assignments) to the set of permitted actions
- Allow the user to select one of the available actions
- Execute the attached state assignments for the selected action
The display section describes a set of page elements (currently TABLE cells, but this is an implementation detail which doesn't greatly matter), possibly with content/css, each of which has zero or more mappings between it and action labels, and may have content/formatting predicated on whether any of the mapped labels curently exist in the set of actions the player is permitted to take (the predicates are reevaluated whenever the user performs an action).
This should be sufficient for most board/card games, and for some puzzles such as Solitaire.
Notes
- Operator precedence is currently as follows (operators grouped together have equal precedence): - (^, &, |, %), (*, /), (+, -), (==, !=, <, >, <=, >=), (and, or) - thanks to PeterTaylor
- If the runtime engine tries to resolve the symbol "random" and that symbol has not been otherwise defined, the engine will return a random integer x in the range 0 <= x < 2 if resolving the symbol as an integer, and 0 <= x < index if resolving as an array. This functionality is not enabled at initialisation time, and attempting to use it then will produce a compiler error.
- The for loops do not behave quite as they do in languages like C/C++; it's more like PostScript?'s repeat operation. In particular, the range is cached on entry and not reevaluated, so assignments to range variables in the loop body will not affect the number of iterations; and also the loop variable is local within the loop body and recreated for each iteration, so assignments to it do not affect the progress of the loop. These properties, together with the fact that there is no other way to cause code to loop, should ensure that each code block always terminates (though it might take a long time to do so); implying that the language is *not* Turing-complete, unless you consider the user to be part of the system.
- If a game complains about version mismatches, try refreshing the page first - your browser may be caching an old version of the engine files. Only recompile if that fails.
- The engine can save and load the state of the game to the user's cookie. To trigger this ability, define an integer called _save_slot. Whenever its value is zero or greater, the save option will appear, and the load option will also appear if cookie data exists under the right name; different cookie parameter names will be generated for different values. This allows the game writer to supply the player with multiple game slots if they so choose, and also to control times during the game during which the player can/cannot save. The cookie name depends on the set of global variable names, so the engine does not attempt to load saved states if the game code expects to be able to resolve a different set of globals to those stored in the state.
Game Logic Syntax
C-style comments are supported throughout the code (C-style escapes are *not*, however; use HTML entities instead.)
logic block contains a list of statements
each statement is a variable declaration or a game rule declaration
A variable may be declared as an array or an integer; each variable must be fully initialised on declaration. The syntax for declarations is
optional-qualifier int identifier { integer expression }
optional-qualifier array identifier { comma-separated list of integer expressions }
optional-qualifier may or may not be present. Currently, it can only be "local". A local variable is not stored in saved games or links or transmitted via chat; its contents is preserved over game loads.
A game rule declaration is either a for loop containing a list of game rule declarations, or a game rule
A for loop has the syntax
for identifier ( minimum .. maximum ) { list of game rule declarations }
It evaluates once for each integer in the range minimum to maximum inclusive, creating an integer variable identifier, valid within the loop body, that has the correct value for each pass. minimum and maximum are integer expressions.
A game rule has the syntax
when { boolean expression } allow { list of semicolon-terminated assignments } label { list of string expressions }
Whenever the boolean expression is true, any integer expressions within the label part are evaluated using the world state at the time, all elements are concatenated and a binding between the resulting string and the assignment block created. If multiple rules bind to the same string, all allowed actions will take place when the user selects that string.
A string expression is either a text string enclosed in double quotes, or an integer expression (which is evaluated and converted to a string)
Lists of assignments in the allow block may contain for and if statements, which may contain further lists of assignments.
An assignment has the syntax
lvalue assignment-operator integer expression
where an lvalue is either the name of an integer variable, or the name of an array followed by an integer expression in square brackets to give the particular element; assignment-operator may be =, += or -=.
The supported integer operators are
+ - / * %
with their usual meanings; also, brackets and the bitwise logical operators ^, & and |. Pre/post-increment and decrement (++, --) and unary plus / minus are also permitted. Boolean operators are and, or and !.
Integer expressions in variable declarations are evaluated immediately; referencing variables that have not yet been defined in a variable declaration body produces a compiler error.
NB. operator precedence is not implemented at this time; use brackets to set explicit precedence.
NB. the for() loops execute once, at initialisation time, to generate the full ruleset; the local context is remembered, and each rule is then reevaluated whenever the user takes an action. Therefore, the integer expressions used for the min/max values should not rely on global state being anything other than what you set it to initially.
Display code syntax:
The display block contains a list of statements. If the display block produces no display elements during initialisation, the debug menu will be enabled automatically.
Each statement is either a stringtable declaration or a display element imperative. The syntax for stringtable declarations is
stringtable identifier { comma-separated list of whitespace-separated lists of string expressions }
A display element imperative is either a for loop containing display element imperatives (syntax/caveats as for logic), an if statement (evaluated at initialisation time, like the loops) or a display element declaration.
A display element declaration declares either a newline element, a box or a sprite.
The syntax for a newline element is
newline
The syntax for a box is
box { list of formatting commands }
The box formatting is reset and list of formatting commands reevaluated every time the set of allowed actions changes. If multiple instances of the same command are encountered while evaluating box formatting, the last one is the one that takes effect.
A formatting command is one of the following:
width { css }
height { css }
border { css }
margin { css }
padding { css }
background { css }
position { list of position-related css }
content { list of string expressions, which may include string table lookups }
bind_action { list of string expressions, which may include string table lookups }
while_active { list of formatting commands }
..or a for or if statement containing a list of formatting commands.
position likewise appends CSS; the block contains a semicolon-separated list of any or all of:
- position type - absolute, relative, fixed.. etc.
- left: (integer expression) css length unit
- top: (integer expression) css length unit
width, height, border and background append the appropriate CSS to the element's STYLE property. css may contain lengths, colours and border styles. Lengths may contain integer expressions, and colours may also contain integer expressions if given in the rgb(a,b,c) form.
content replaces the current content of the box with the result of evaluating and concatenating all the supplied expressions.
bind_action evaluates/concatenates all supplied string expressions to generate a label. If the label exists in the set of allowed actions that resulted from evaluating game logic against current state, that action's assignment block is bound to the box's onclick property. If multiple labels get bound in this way, all their actions will be executed on click, in the order in which they were bound.
while_active executes the formatting commands supplied if and only if the box has at least one bound action that was allowed at the time while_active was encountered (that is, you can have several bind_actions, then a while_active that acts based on those, then some more binds, then another while_active that decides based on the lot, etc..).
The syntax for a sprite is
sprite { list of formatting commands }
where the formatting commands are the same as for a box, except that mouse handlers will not attach to sprites, and bind_action/while_active are forbidden.
Sprites are maintained in a separate DOM block from boxes, improving performance if they animate regularly; they are currently implemented using DIVs.
The keyword repeatedly is also allowed in the display section. The syntax is
repeatedly { list of string expressions }
where list of string expressions is defined as for label. Several times per second (currently trying for ~10-15fps), the list of string expressions is resolved and any actions bound to the resulting text performed. All sprites are redisplayed, and any boxes for which the style/content has changed are also redisplayed. Notice that MSIE will fail to register clicks within 250ms (often more) of a box being redisplayed, so try to arrange things so you don't need to do that during a game.
Optimisation tips
Premature optimisation is the root of all evil. Write the game first, optimise once everything works. That said,
- JS string operations are slow. If you can get away without content for your display elements, do.
- If using repeatedly, try to arrange your interface so you can make all elements that will change often into sprites.
.
.
(todo: more here)
Notes on Implementation
The minilanguage is "compiled" to a set of operations which are executed on a virtual stack machine.
The compiler contains an emulator for the stack machine, allowing it to precompute constant expressions. It does not emulate some of the opcodes, notably the display-related ones.
The runtime engine performs a small amount of dynamic optimisation on some of the operations. The functions for some operations, when executed, do some work, capture the result in a closure, and replace the call to them within the caller to a call of the closure, so that the work does not have to be repeated.
If a repeatedly{} section is present, and debug mode is not active, all UI events are queued in a FIFO and processed inside the timer handler. If no such section is present, the timer is not started and UI events are handled directly in onclick(). Any suggestions for improvements to the way the timer is implemented are welcome.