summaryrefslogtreecommitdiff
path: root/docs/module/rules.md
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2025-04-24 12:48:51 +0200
committerTor Andersson <tor@ccxvii.net>2025-04-25 16:06:05 +0200
commited2361980b455d1825d811670f329cbcf5624927 (patch)
treea1e620490bf305635c3a0f22a6f695e22c14ff2f /docs/module/rules.md
parent71a98cbbe0657880cfb1d81962786188b009fc5c (diff)
downloadserver-ed2361980b455d1825d811670f329cbcf5624927.tar.gz
Move docs to Markdown and use markdown renderer in server.
Diffstat (limited to 'docs/module/rules.md')
-rw-r--r--docs/module/rules.md420
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.
+