diff options
Diffstat (limited to 'docs/module')
-rw-r--r-- | docs/module/fuzzer.md | 58 | ||||
-rw-r--r-- | docs/module/guide.md | 45 | ||||
-rw-r--r-- | docs/module/library.md | 123 | ||||
-rw-r--r-- | docs/module/rules.md | 420 | ||||
-rw-r--r-- | docs/module/script.md | 155 |
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 + } + `) |