Writing Scripts

Contents

Scripts in CGSuite

In addition to recursive function definitions (see the previous section of this tutorial), Combinatorial Game Suite supports several scripting commands, including if/then/else statements and for loops.  CGSuite's language is modeled after other common scripting languages; its syntax and structure should be recognizable to anyone familiar with, say, Maple or Visual Basic.

Like all other commands, scripts may be entered directly onto the worksheet.  However, the entire script must be entered into a single worksheet field, and since scripts generally consist of long sequences of commands, this can be somewhat cumbersome.  Alternatively, a script may be composed in an external text editor (such as GNU Emacs) and run in CGSuite by selecting "Run Script ..." from the File menu.  The entire file containing the script is treated as if it were entered into a single worksheet field.  You may choose to direct the script's output either to a new worksheet or to a text file.

Using out to Display Output

As noted previously in this tutorial, multiple commands may be strung together in sequence using semicolons.  For example:

g := 3; g := g + 2; g + 5

This would output 10.  Notice that output is generated only for the last command in a sequence; it is suppressed for each intermediate command.  If the sequence ends with a semicolon, no output is generated at all.

If you want to display output for intermediate commands, you can append  >> out  to the end of an expression.  For example:

g := 3 >> out; g := g + 2 >> out; g + 5 >> out;

This would output 3, 5, and 10, each on a separate line.

if-then-else Statements

The general form of the if-then-else statement is:

if condition then statements [elif condition then statements] [else statements] fi

Here and throughout this section, an expression enclosed in brackets [ ] is optional and may be omitted.  statements can be any single command, or any sequence of commands separated by semicolons.  Any number (zero or more) of "elif condition then statements" clauses may be included.  CGSuite will run the statement(s) following the first condition that evaluates to true, and only those statements.  If none of the conditions is true, and an else clause is present, CGSuite will run its statement(s).  The following code tests how g compares with 0 and displays a useful message:

if g < 0 then "negative" >> out;
elif g > 0 then "positive" >> out;
elif g == 0 then "zero" >> out;
else "fuzzy" >> out;
fi

for Loops

The general form of the for statement is:

[for variable] [from expression] [to expression] [by expression] [while condition] [where condition] do statements od

As before, any bracketed expression is optional, and statements can be replaced by any command or sequence of commands.  Each of the expressions must be a canonical game (or some more complicated expression that evaluates to a canonical game), and the conditions must be boolean values (true or false).  CGSuite interprets the for statement as follows:

  1. Set the value of variable to match the from expression.
  2. If the value of variable is greater than or confused with the to expression, or if the while condition fails, then exit the loop immediately.
  3. If the where condition holds, perform the statement(s) between do and od.
  4. Increase the value of variable by the by expression, and repeat step 2.

If any of from, to, or by are omitted, then the default values are 1, infinity, and 1, respectively.  The default conditions are always true.

The following example simply prints out all even integers between 2 and 10:

for n from 2 to 10 by 2 do
    n >> out;
od

The same effect could be achieved using while:

n := 2;
while n <= 10 do
    n >> out;
    n := n + 2;
od

Finally, there is an alternate form of the for statement:

[for variable] [in expression] [while condition] [where condition] do statements od

In this form, the in expression should evaluate to a list of objects, and the statements(s) between do and od will be repeated once for each element of the list.  while and where have the same meanings as before.  The following example displays all good left options in 4x4 Domineering together with their canonical forms:

for g in SensibleLeftOptions(DomineeringRectangle(4,4)) do
    g >> out;
    C(g) >> out;
od

You can use the break command within a loop to terminate the loop immediately, and continue to continue the loop.

Either type of loop can be used in seq and tabulate expressions, for example:

seq(n.^ for n from 1 to 5) >> out;
tabulate(h, C(h) for h in LeftOptions(g)) >> out;

Procedures

A procedure is a user-defined function.  We met one type of procedure - recursive functions - in the previous section of this tutorial.  Procedures can also be designed to execute a sequence of commands, rather than simply evaluate one expression.  In such cases it's easiest to use the following alternate syntax for defining a procedure:

proc(variable-list) [local variable-list;] [option remember;] statements end

The following two examples define the same function f:

f(n_) := n + 1;
f := proc(n) n + 1 end;

There is a crucial difference between these two forms.  Procedures defined using f(n_) will store their values in a cache as they are computed, making recursive calculations vastly more efficient.  Procedures defined using proc(n) will not cache values by default, but you can override this behavior by inserting option remember; immediately before the body of the procedure.  Contrast the following two definitions:

slowfib := proc(n)
    if n == 0 or n == 1 then 1
    else slowfib(n-1) + slowfib(n-2) fi
end;

fastfib := proc(n) option remember;
    if n == 0 or n == 1 then 1
    else fastfib(n-1) + fastfib(n-2) fi
end;

Both calculate fibonacci numbers, but fastfib is substantially more efficient (linear time instead of exponential).

Procedures can also take several parameters.  Here's an example that computes the maximum of two integers:

max := proc(m,n)
    if m <= n then n else m fi
end;

Procedures can take any object as parameters, not just games.  Here's a more sophisticated example that takes a list of games (as described in the previous section) and returns the sum of all games in the list.

listsum := proc(L) local i, g;
    g := 0;
    for i from 1 to Length(L) do
        g := g + L[i];
    od;
    g
end;

For example, typing listsum([3*,vv,^*]) would return 3v.  Notice that in procedures involving multiple statements, the value returned by the procedure is equal to the value of the last statement in the sequence (unless that statement ends in a semicolon, in which case the procedure returns no value).  Also note the use of local i, g; in the procedure definition: This marks i and g as local variables for the procedure, so that calls to listsum will not affect the value of the global i and g.

You can also use return statements from within a procedure.  If  return expression  is encountered at any point during the procedure's execution, then it immediately terminates with value equal to the expression.

A third "shorthand" notation for procedures is sometimes useful.  The following definitions of f and max, equivalent to those given above, illustrate this notation:

f := n -> n + 1;
max := (m,n) -> if m <= n then n else m fi;

Procedures defined using -> will never cache values, so they should not be used for recursive calculations.

log and err

Recall that appending >> out to an expression simply outputs the value of the expression; for example:

"Hello" >> out;

Two similar constructions are useful.  The following:

expression >> log;

inserts the value of expression into the kernel log.  This is useful primarily when you want to track the progress of a calculation from within a script, without distorting your output file.  Finally,

expression >> err;

causes the entire script to terminate immediately and display the value of expression as an error message.  This is useful for handling error conditions within procedures.  Here's an improved version of the max procedure, which operates on any pair of games and displays an error message if they are incomparable:

max := proc(g,h)
    if g <= h then h
    elif h <= g then g
    else "The games are incomparable." >> err
    fi
end;

Example: Domineering

The usual way to construct game positions in the Worksheet is to pass a list of strings to the appropriate method.  For example:

Amazons("L...","R...")

While this is straightforward to enter directly, it's highly inconvenient for scripts that need to iterate over a class of game positions.  An alternative construction is provided for this purpose: A game position can be represented by a two-dimensional array of one-character strings.  (A two-dimensional array is just a list of lists.)  Here are three ways to construct the same Amazons position:

Amazons("L...","R...")
Amazons(["L...","R..."])
Amazons([["L",".",".","."],["R",".",".","."]])

The third construction is extremely awkward to type directly, but it's quite convenient for use in scripts.  The following simple example illustrates how it's used.  It creates a table showing the canonical forms of all 3x3 Domineering positions with just one square blocked.  Note the use of the methods CreateTable and AddTableRow to construct and populate a table with two columns.

table := CreateTable(2);
AddTableRow(table, "Position", "Value");

// Initialize the grid
for row from 1 to 3 do
  for col from 1 to 3 do
    grid[row][col] := ".";
od od;

for row from 1 to 3 do
  for col from 1 to 3 do
    grid[row][col] := "x"; // Block this square
    g := Domineering(grid);
    AddTableRow(table, g, C(g));
    grid[row][col] := "."; // Now unblock this square
od od;

table

Notice how the script iterates over the desired positions using the same grid for efficiency.

Several more sophisticated examples are included in the examples directory of the cgsuite distribution.  You can run them directly using the "Run Script ..." command from the File menu.

Exercises

1. Partizan End Nim.  Partizan end nim is played with n nim heaps arranged in linear order.  Left, on his turn, may remove any number of beans from the leftmost non-empty heap; right, from the rightmost.  We can represent partizan end nim positions as lists in cgsuite.  For example, Left's options from [2,3,1] are to [1,3,1] and [3,1]; Right's only option is to [2,3].  Write a procedure pendnim that takes a list as input and calculates the value of the corresponding position in partizan end nim.  (You might find it easiest to use the list-manipulation methods Remove and Clone.)  Then construct a table showing the values of [m,n] for 1 <= m,n <= 6.


Continue on to Loopy Games