summaryrefslogtreecommitdiff
path: root/docs/module
diff options
context:
space:
mode:
Diffstat (limited to 'docs/module')
-rw-r--r--docs/module/fuzzer.md58
-rw-r--r--docs/module/guide.md45
-rw-r--r--docs/module/library.md123
-rw-r--r--docs/module/rules.md420
-rw-r--r--docs/module/script.md155
5 files changed, 801 insertions, 0 deletions
diff --git a/docs/module/fuzzer.md b/docs/module/fuzzer.md
new file mode 100644
index 0000000..d69b992
--- /dev/null
+++ b/docs/module/fuzzer.md
@@ -0,0 +1,58 @@
+# Fuzzing the Troops!
+
+We use [Jazzer.js](https://github.com/CodeIntelligenceTesting/jazzer.js/)
+as a coverage-guided fuzzer for automatic testing of module rules.
+
+## What is fuzzing?
+
+Fuzzing or fuzz testing is an automated software testing technique that
+involves providing invalid, unexpected, or random data as inputs to a computer
+program. With the fuzzer you can test the rules for any RTT module. It will
+play random moves and check for unexpected errors.
+
+The fuzzer can detect the following types of errors:
+
+* Any crashes in the rules.js module.
+* Dead-end game states where no other actions are available (besides "undo").
+* A game taking an excessive number of steps. This could indicate infinite loops and other logical flaws in the rules.
+
+Work files are written to the "fuzzer" directory.
+
+## Running
+
+Start the fuzzer:
+
+ bash tools/fuzz.sh title [ jazzer options... ]
+
+This will run jazzer until you stop it or it has found too many errors.
+
+To keep an eye on the crashes, you can watch the fuzzer/log-title.txt file:
+
+ tail -f fuzzer/log-title.txt
+
+Each fuzzed title gets its own "fuzzer/corpus-title" sub-directory.
+The corpus helps the fuzzer find interesting game states in future runs.
+
+To create a code coverage report pass the `--cov` option to fuzz.sh.
+
+## Debug
+
+When the fuzzer finds a crash, it saves the game state and replay log to a JSON file.
+You can import the crashed game state like so:
+
+ node tools/import-game.js fuzzer/dump-title-*.json
+
+The imported games don't have snapshots. You can recreate them with the patch-game tool.
+
+ node tools/patch-game.js game_id
+
+## Avoidance
+
+If your rules have actions or rules you don't want to test, guard the code
+or action generation by checking if globalThis.RTT_FUZZER is true.
+
+ if (globalThis.RTT_FUZZER) {
+ // this code only runs in the fuzzer!
+ } else {
+ // this code never runs in the fuzzer!
+ }
diff --git a/docs/module/guide.md b/docs/module/guide.md
new file mode 100644
index 0000000..ada195f
--- /dev/null
+++ b/docs/module/guide.md
@@ -0,0 +1,45 @@
+# Module Implementation Guide
+
+## 0. Secure the rights.
+
+All games on RTT are published with the right holders written express permission.
+It's better to secure the rights before spending a lot of time on an implementation,
+in case the publisher says no.
+If you have a game in mind that you want to host on RTT, approach Tor to talk
+about whether a license may be available.
+
+## 1. Create a git project.
+
+TODO
+
+## 2. Setup the required files.
+
+TODO
+
+## 3. Import and prepare the art assets.
+
+Ask Tor to do this for you!
+
+## 4. Board representation.
+
+Design the data representation for the game board state.
+
+Implement enough of the rules to create and setup the initial board game state.
+
+Implement enough of the client to display the initial board game state.
+
+The client doesn't need to be fancy, all we need at this point is to be able to
+present the spaces and pieces at their approximate locations to be able to interact with them.
+Fine tuning the look & feel comes much later.
+
+## 5. Implement the sequence of play and core rules.
+
+Data representation accessors and helper functions.
+
+Main sequence of play.
+
+## 6. Implement events and special cases.
+
+All the hard work.
+
+## 7. Profit!
diff --git a/docs/module/library.md b/docs/module/library.md
new file mode 100644
index 0000000..134fae8
--- /dev/null
+++ b/docs/module/library.md
@@ -0,0 +1,123 @@
+# Utility Library
+
+The common framework.js file defines many useful functions.
+
+Some of these are optimized versions of common Javascript functions,
+that are simpler and faster because they do less.
+
+For example array_delete is much faster than Array.splice() because all it does
+is remove an item from the array. Splice also creates a new array with the deleted items
+and returns it; and since we usually don't need that it's better to use the simpler function
+defined here.
+
+Likewise, array_delete_item is faster than using array.filter.
+
+## Object functions
+
+ function object_copy(original)
+ Make a deep copy of an object without cycles.
+
+
+## Array functions
+
+ function array_delete(array, index)
+ Delete the item at index.
+
+ function array_delete_item(array, item)
+ Find and delete the first instance of the item.
+
+ function array_insert(array, index, item)
+ Insert item at the index.
+
+ function array_delete_pair(array, index)
+ Delete two items at the index.
+
+ function array_insert_pair(array, index, a, b)
+ Insert two items a and b at the index.
+
+## Set functions
+
+Sets can be represented as a sorted array.
+To use an array as a set this way, we provide these functions.
+
+ function set_clear(set)
+ Delete every entry in the set.
+
+ function set_has(set, item)
+ Check if item exists in the set.
+
+ function set_add(set, item)
+ Add an item to the set (if it doesn't alerady exist).
+
+ function set_delete(set, item)
+ Delete an item from the set (if it exists).
+
+ function set_toggle(set, item)
+ Toggle the presence of an item in the set.
+
+## Map functions
+
+Maps (or key/value dictionaries) can also be represented as an array of pairs
+sorted on the key value.
+
+ function map_clear(map)
+ Delete every entry in the map.
+
+ function map_has(map, key)
+ Check if the map has a value associated with the key.
+
+ function map_get(map, key, missing)
+ Return the value associated with key;
+ or missing if the key is not present.
+
+ function map_set(map, key, value)
+ Set the value for the key.
+
+ function map_delete(map, key)
+ Delete an entry.
+
+ function map_for_each(map, fun)
+ Iterate over each entry calling fun(key, value)
+
+## Group By
+
+The Object.groupBy function in standard Javascript is implemented here for both
+Objects and our "arrays as map" representation.
+
+ function object_group_by(items, callback)
+ function map_group_by(items, callback)
+
+## Game functions
+
+These functions affect the game state.
+
+ function log(s)
+
+View functions:
+
+ function prompt(s)
+ function button(action, enabled = true)
+ function action(action, argument)
+
+State transitions:
+
+ function call_or_goto(pred, name, env)
+ function call(name, env)
+ function goto(name, env)
+ function end(result)
+
+Ending a game:
+
+ function finish(result, message)
+
+## Undo stack handling
+
+ function clear_undo()
+ function push_undo()
+ function pop_undo()
+
+## Random number generator
+
+ function random(range)
+ function shuffle(list)
+
diff --git a/docs/module/rules.md b/docs/module/rules.md
new file mode 100644
index 0000000..a2135f1
--- /dev/null
+++ b/docs/module/rules.md
@@ -0,0 +1,420 @@
+# Rules Framework
+
+The rules.js file contains the logic of the game!
+
+This is exposed to the system via a handful of exported properties and functions.
+
+All of the board state is represented as a plain JS object that is passed to
+and returned from these functions.
+
+See the [module overview](/docs/overview/module) for the details.
+
+## Copy & Paste
+
+In order to simplify rules creation, there is a shared library of code that
+does some of the work and provides a structured approach to handling control
+flow through the game.
+
+A copy of this code can be found in the server repository in
+[public/common/framework.js](https://git.rally-the-troops.com/common/server/tree/public/common/framework.js)
+
+This framework.js file provides implementations of all of the necessary exports,
+a game state management system, random number generator, undo handling, and
+several other useful functions.
+
+Include a copy of this file at the end of rules.js to get started!
+
+> Note: We can't "require" it as a module because it needs access to the rules.js specific scope;
+> so unfortunately you'll just have to include a copy of it.
+
+## Prolog
+
+The framework uses several global variables that must be defined near the top of the file.
+
+
+ var G, L, R, V
+
+You also need to define the states and procedures tables.
+
+ const S = {}
+ const P = {}
+
+The rules need a list of player roles. The index of each role in the array determines their numeric value.
+
+ const ROLES = [ "White", "Black" ]
+
+ // mnemonics for use in the rules
+ const R_WHITE = 0
+ const R_BLACK = 1
+
+If there are multiple scenarios, define them in a list:
+
+ const SCENARIOS = [
+ "Wars of the Roses",
+ "Kingmaker",
+ "Richard III",
+ ]
+
+## Globals
+
+The framework uses four main global variables.
+
+### R - who are you?
+
+Whenever the system executes code to handle user actions or populate the user
+view object, the R global is set to the index of the corresponding user. You
+should rarely need to use this outside of on_view, but when multiple users are
+active simultaneously, use R to distinguish between them in the action handlers.
+
+### G - the (global) game state
+
+This object contains the full representation of the game.
+There are several properties here that are special!
+
+ G.L // local scope (aliased as L below)
+ G.seed // random number generator state
+ G.log // game log text
+ G.undo // undo stack
+ G.active // currently active role (or roles)
+
+Add to G any data you need to represent the game board state.
+
+The G.L, G.seed, G.log, and G.undo properties are automatically managed; you should never need to touch these.
+
+The G.active is used to indicate whose turn it is to take an action.
+It can be set to a single role index, or an array of multiple roles.
+
+ G.active = R_WHITE
+ G.active = [ R_WHITE, R_BLACK ]
+
+### L - the (local) game state
+
+There is a local scope for storing data that is only used by a particular state.
+This local scope has reserved properties ("P", "I", and "L") that you must not touch!
+These properties are used to track the execution of scripts and where to go next.
+
+### V - the view object
+
+The view object that is being generated for the client is held in V during
+the execution of the on_view hook and the state prompt functions (see below).
+
+## Setup
+
+You must provide the on_setup function to setup the game with the initial board state.
+
+At the end of the function you must transition to the first game state by invoking call.
+
+ function on_setup(scenario, options) {
+ G.active = R_WHITE
+ G.pieces = [ ... ]
+ call("main")
+ }
+
+## View
+
+The client needs a view object to display the game state. We can't send the
+full game object to the client, because that would reveal information that
+should be hidden to some or all players. Use the on_view function to populate
+the V object with the data that should be presented to the player.
+
+Use the R object to distinguish who the function is being called for.
+
+ function on_view() {
+ V.pieces = G.pieces
+ if (R === R_BRITAIN)
+ V.hand = G.hand[R_BRITAIN]
+ if (R === R_FRANCE)
+ V.hand = G.hand[R_FRANCE]
+ }
+
+## The Flow Chart
+
+---
+
+Consider the rules logic as a state machine, or a flow chart.
+
+At each "box" it pauses and waits for the active player to take an action. Once
+an action is taken, the game proceeds along the action "arrow", changing what
+needs to be changed (like moving a piece) along the way, before stopping at the
+next "box".
+
+These "boxes" are game states, and the "arrows" are transitions between states.
+
+In simple games that's all there is to it, but in more complicated games you
+sometimes want to share logic at different points in the sequence of play (like
+common handling of taking casualties whether it's from a battle or winter
+attrition).
+
+In order to support this, we can recursively "nest" the states.
+
+---
+
+The game is defined by a mix of states, scripts, and functions.
+
+The basic game flow consists of a set of "procedures" which are
+interrupted by "states" that prompt the players to perform actions.
+
+The game stops at states, prompting the user for input.
+When an action is chosen, the transition to another state can happen
+in a few different ways:
+
+End the current state and go back to the previous state or procedure that called this one.
+
+Call another state or procedure.
+
+Goto another state or procedure (combination of calling and ending).
+
+The game holds a stack of states (and their environments).
+Each state and procedure runs in its own scope (accessed via the L namespace).
+There's also a global scope for the main game data (via the G namespace).
+
+---
+
+## States
+
+The "states" where we wait for user input are kept in the S table.
+
+Each state is an object that provides several functions.
+
+ S.place_piece = {
+ prompt() {
+ prompt("Select a piece to move.")
+ for (var s = 0; s < 64; ++s)
+ if (G.board[s] === 0)
+ action("space", s)
+ button("pass")
+ },
+ space(s) {
+ log("Placed piece at " + s)
+ G.board[s] = 1
+ end()
+ },
+ pass() {
+ log("Passed")
+ end()
+ },
+ }
+
+### S.state.prompt()
+
+The prompt function is called for each active player to generate a list of
+valid actions.
+
+To show a text prompt to the user:
+
+ prompt("Do something!")
+
+To generate a valid action use one of the following functions:
+
+ function action(name, value)
+
+To generate a push button action:
+
+ function button(name)
+ function button(name, enabled)
+
+To show an enabled/disabled push button, use a boolean in the second argument:
+
+ button("pass", can_player_pass())
+
+It's sometimes helpful to define simple wrapper functions to save on typing and
+reduce the risk of introducing typos.
+
+ function action_space(s) {
+ action("space", s)
+ }
+
+> Note: The on_view function should NEVER change any game state!
+
+### S.state.action()
+
+When a player chooses a valid action, the function with the action name is
+invoked!
+
+Use the action handler function to perform the necessary changes to the game
+state, and transition to the next state using one of the state transition
+functions "call", "goto", or "end".
+
+To add entries to the game log:
+
+ log(ROLES[R] + " did something!")
+
+Calling log with no arguments inserts a blank line:
+
+ log()
+
+### S.state._begin() and _resume() and _end()
+
+These functions are invoked when the state is first entered, when control returns
+to the state (from a nested state), and when the state is departed.
+
+You can use this to do some house-keeping or initialize the L scope.
+
+ S.state.remove_3_pieces = {
+ _begin() {
+ L.count = 3
+ },
+ prompt() {
+ ...
+ },
+ piece(p) {
+ remove_piece(p)
+ if (--L.count === 0)
+ end()
+ }
+ }
+
+
+## State transitions
+
+When transitioning to another state in an action handler, it must be the last thing you do!
+
+> You cannot sequence multiple invocations to "call" in a normal function!
+
+See "procedures" below for a way to string together multiple states.
+
+### call - enter another state
+
+To recursively go to another state, use the call() function.
+
+This will transfer control to the named state or procedure, and once that has
+finished, control will come back to the current state.
+
+The second argument (if present) can be an object with the initial scope.
+The L scope for the new state is initialized with this data.
+
+ call("remove_pieces", { count: 3 })
+
+### end - return from whence we came
+
+Calling end() will return control to the calling state/procedure.
+
+If you pass an argument to end, that will be available to the caller as `L.$`.
+
+### goto - exit this state to go to the next
+
+The goto() function is like call() and end() combined. We exit the current state and jump to the next.
+Use this to transition to another state when you don't need to return to the current state afterwards.
+
+## Procedures
+
+Sometimes state transitions can become complicated.
+
+In order to make the code to deal with them easier, you can define procedures in the "P" table,
+
+Procedures defined by the "script" function are executed by the framework.
+They can sequence together states and other procedures.
+
+Calling a state will enter that state, and execution of the caller will resume
+where it left off when the called state ends. You can also recursively call other procedures.
+
+ P.hammer_of_the_scots = script (`
+ for G.year in 1 to 7 {
+ call deal_cards
+ for G.round in 1 to 5 {
+ set G.active [ R_ENGLAND, R_SCOTLAND ]
+ call choose_cards
+ call reveal_cards
+ set G.active G.p1
+ call movement_phase
+ set G.active G.p2
+ call movement_phase
+ set G.active G.p1
+ call combat_phase
+ }
+ call winter_phase
+ }
+ `)
+
+See [script syntax](script) for the commands available in this simple scripting language.
+
+### Plain function procedures
+
+Procedures can also be plain Javascript functions! There is normally no reason
+to use a plain procedure over simply calling a function, but if you want them
+to be part of a larger script sequence this can make it easier.
+
+Note that a plain function procedure must transition somewhere else before it
+returns, either via "goto" or "end".
+
+It's also a neat way to define events; to dispatch based on a string.
+
+ S.strategy_phase = {
+ ...
+ play_event(c) {
+ goto(data.events[c].event)
+ },
+ }
+
+ P.the_war_ends_in_1781 = function () {
+ G.war_ends = 1781
+ end()
+ }
+
+ P.major_campaign = script (`
+ call activate_general
+ call activate_general
+ call activate_general
+ `)
+
+
+## Ending the game
+
+To signal the termination of a game, call the finish function.
+
+ function finish(result, message)
+
+The result must be either the index of the role that won, the string "Draw",
+or any other string to indicate that nobody won.
+
+The message will both be logged and used as the text prompt.
+
+ finish(R_WHITE, "White has won!")
+ finish("Draw", "It's a tie!")
+ finish("None", "Nobody won.")
+
+Calling finish will abort the current scripts and/or states.
+
+## Random number generator
+
+There is a pseudo-random number generator included in the framework.
+
+> Do NOT use Math.random!
+
+Games must be reproducible for the replay and debugging to work, so
+the system initializes the PRNG with a random seed on setup. The random
+number generator state is stored in G.seed.
+
+To generate a new random number between 0 and range:
+
+ function random(range)
+
+To shuffle an array (for example a deck of cards):
+
+ function shuffle(list)
+
+## Undo
+
+Undo is handled by taking snapshots of the game state. Generating the undo
+action and handling it is taken care of by the framework. You only need to
+create and clear the checkpoints at suitable times.
+
+Call push_undo at the top of each action you want to be able to undo.
+
+Don't forget to call clear_undo whenever hidden information is revealed or any
+random number is generated.
+
+ function roll_die() {
+ clear_undo()
+ return random(6) + 1
+ }
+
+Whenever the active player changes, the undo stack is automatically cleared.
+
+## Miscellaneous utility functions
+
+The framework also includes a library of useful functions to work
+with sorted sets, maps, etc.
+
+See the [utility library](library) for how to use these.
+
diff --git a/docs/module/script.md b/docs/module/script.md
new file mode 100644
index 0000000..52d1eb5
--- /dev/null
+++ b/docs/module/script.md
@@ -0,0 +1,155 @@
+# Script Syntax
+
+The script function compiles a simple scripting language to a list of
+instructions that are executed by the framework.
+The argument to the script function is a plain text string,
+so use multiline backtick string quotes.
+
+The script language syntax is very similar to Tcl and is based on space
+separated list of "words".
+
+A word can be any sequential string of non-whitespace characters.
+Words can also be "quoted", ( parenthesized ), { bracketed } or [ braced ].
+Any whitespace characters (including line breaks) are permitted within these quoted strings.
+Parenthesises, brackets, and braces must be balanced!
+
+Each line of words is parsed as a command.
+The first word defines a command, and the remaining words are arguments to that command.
+
+Control flow commands can take "blocks" as arguments. These words are parsed
+recursively to form further lists of commands.
+
+Some commands take "expressions" as arguments.
+These words are included verbatim as Javascript snippets.
+
+## Eval
+
+To run any snippet of Javascript, you can include it verbatim with the eval command.
+
+ eval <expr>
+
+Example:
+
+ eval {
+ do_stuff()
+ }
+
+## Variables
+
+The set, incr, and decr commands set, increment, and decrement a variable.
+This can be done with the eval command, but using these commands is shorter.
+
+ set <lhs> <expr>
+ incr <lhs>
+ decr <lhs>
+
+Example:
+
+ set G.active P_ROME
+ set G.active (1 - G.active)
+ incr G.turn
+ decr L.count
+
+## Log
+
+A shorter way to print a log message to the game log:
+
+ log <expr>
+
+ log "Hello, world!"
+
+## State transitions
+
+Use call to invoke another state or procedure (with an optional environment scope).
+
+ call <name> <env>
+ call <name>
+
+Use goto to jump to another state or procedure without coming back (a tail call).
+
+ goto <name> <env>
+ goto <name>
+
+Use return to exit the current procedure.
+This is equivalent to the end function used in states and function procedures.
+
+ return <expr>
+ return
+
+Examples:
+
+ call movement { mp: 4 }
+ goto combat_phase
+ return L.who
+
+
+## Loops
+
+Loops come in three flavors:
+
+ while <expr> <block>
+ for <lhs> in <expr> <block>
+ for <lhs> in <expr> to <expr> <block>
+
+A while loop has full control:
+
+ set G.turn 1
+ while (G.turn <= 3) {
+ call turn
+ incr G.turn
+ }
+
+Iterating over a range with for is easiest:
+
+ for G.turn in 1 to 3 {
+ call turn
+ }
+
+Note that the list expression in a for statement is re-evaluated each iteration!
+
+ for G.turn in [1, 2, 3] {
+ call turn
+ }
+
+## Branches
+
+The if-else command is used for branching code.
+
+ if <expr> <block> else <block>
+ if <expr> <block>
+
+Example:
+
+ if (G.month < 12) {
+ call normal_turn
+ } else {
+ call winter_turn
+ }
+
+## Return
+
+Use return (or the end() function in states and function procedures) to
+pass information up the call chain.
+
+ S.greeting = {
+ prompt() {
+ button("hello")
+ button("goodbye")
+ },
+ hello() {
+ end("hello")
+ },
+ goodbye() {
+ end("goodbye")
+ },
+ }
+
+ P.example = script (`
+ call greeting
+ if (L.$ === "hello") {
+ goto hello_world
+ }
+ if (L.$ === "goodbye") {
+ goto goodbye_cruel_world
+ }
+ `)