diff options
Diffstat (limited to 'docs/module/rules.md')
-rw-r--r-- | docs/module/rules.md | 420 |
1 files changed, 420 insertions, 0 deletions
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. + |