1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
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.
|