So, you want to add a game to Gamesman Classic! Well, you've come to the right place. This document provides an in-depth explanation of how a game, such as Abrobad, can be integrated into the system.
You can learn more about the game here.
This guide is divided into three main sections:
Before we begin, one should be familiar with Classic's hashing scheme, which can be read here. Furthermore, you should have fully implemented Homefun 1.
Needless to say, you should have GamesmanClassic downloaded to your device for the entire 'solver' portion, and need GamesmanUWAPI and GamesmanUni for the AutoGUI portion. You can find all the repos here.
Piece and Stone may be used interchangeably.
Slot refers to one of the 12 places a piece can be placed or moved to.
To start, we'll go over how Tic-Tac-Toe is implemented in GamesmanClassic. Afterwards, we'll look into a more complicated example that I ended up solving, Abrobad.
Tic-Tac-Toe (TTT) is a classic two-player, zero-sum game played on a 3×3 grid. Two players alternate marking the cells with their respective symbols (X and O). The game can end in either:
Before implementing *Tic-Tac-Toe* in GamesmanClassic, it’s crucial to understand its mechanics and design efficient data structures and algorithms to solve it. Below, we detail the key components of its implementation as found in `mttt.c`.
In `mttt.c`, each board position is represented as an integer in the range [0, 3⁹ - 1], where each of the 9 cells in the 3×3 grid can be in one of three states:
Blank
→ 0o
→ 1x
→ 2This base-3 encoding offers several significant advantages:
The array `g3Array` contains the powers of 3, where each element represents 3position for each cell in the grid. Since our board state is encoded as a base-3 integer, we can use this array to convert between the two representations. We'll precompute this array on the very top of the file.
int g3Array[] = { 1, 3, 9, 27, 81, 243, 729, 2187, 6561 };
Two functions handle conversions between the integer representation and an array of cell states:
PositionToBlankOX(POSITION thePos, BlankOX *theBlankOX)
: Converts the integer `thePos` into a 9-element array `theBlankOX` by extracting base-3 digits, mapping them to `Blank`, `o`, or `x`.BlankOXToPosition(BlankOX *theBlankOX)
: Combines a 9-element `theBlankOX` array into a single base-3 integer using `g3Array`.These conversions are critical for manipulating the board state efficiently.
*Tic-Tac-Toe* exhibits symmetries (rotations and reflections), which `mttt.c` leverages to reduce the number of unique positions stored. This is optional and controlled by:
/**
* If kSupportsSymmetries is TRUE, we reduce memory usage by canonicalizing.
*/
BOOLEAN kSupportsSymmetries = TRUE;
The function `GetCanonicalPosition(POSITION position)` computes the smallest integer representation among all symmetric equivalents by applying 8 symmetry operations (4 rotations and 4 flipped rotations), defined in `gSymmetryMatrix`.
Several data structures are used:
BlankOX
: An enum with values `Blank`, `o`, `x` to represent cell states.gPosition
: A global struct containing:
BlankOX board[BOARDSIZE]
: The current board state.BlankOX nextPiece
: The piece to be placed next (`x` or `o`).int piecesPlaced
: The number of pieces on the board.g3Array
: An array of powers of 3 for position encoding.gSymmetryMatrix[NUMSYMMETRIES][BOARDSIZE]
: A 2D array mapping each cell to its position under each symmetry operation.GenerateMoves
, DoMove
, and Primitive
Similarly to HF1, we'll need to define three core functions: GenerateMoves()
, DoMove()
, and Primitive()
As a refresher, here's the high-level idea of how these functions work:
MOVELIST GenerateMove*(POSITION position)
POSITION DoMove(POSITION position, MOVE move)
VALUE Primitive(POSITION position)
The GenerateMoves(POSITION position)
function generates a linked list of all possible moves from a given position. In Tic-Tac-Toe, a move is placing a piece in an empty cell:
BlankOX
array (if not using gPosition
).Blank
cell to the move list using CreateMovelistNode
.MOVELIST *GenerateMoves(POSITION position) {
int index;
MOVELIST *moves = NULL;
if (!gUseGPS)
PositionToBlankOX(position, gPosition.board);
for (index = 0; index < BOARDSIZE; ++index)
if (gPosition.board[index] == Blank)
moves = CreateMovelistNode(index, moves);
return moves;
}
Moves are represented as integers from 0 to 8, corresponding to the 9 cells.
The DoMove(POSITION position, MOVE move)
function applies a move to the current position and returns the new position. It supports two modes:
gPosition.board[move]
with gPosition.nextPiece
.gPosition.nextPiece
between x
and o
.gPosition.piecesPlaced
.BlankOX
array.WhoseTurn
.o
, 2 for x
) multiplied by the appropriate power of 3 from g3Array
.POSITION DoMove(POSITION position, MOVE move) {
if (gUseGPS) {
gPosition.board[move] = gPosition.nextPiece;
gPosition.nextPiece = gPosition.nextPiece == x ? o : x;
++gPosition.piecesPlaced;
return BlankOXToPosition(gPosition.board);
} else {
BlankOX board[BOARDSIZE];
PositionToBlankOX(position, board);
return position + g3Array[move] * (int) WhoseTurn(board);
}
}
An UndoMove(MOVE move)
function is also provided to revert moves when using gUseGPS
.
The Primitive(POSITION position)
function determines if a position is terminal:
ThreeInARow
across all rows, columns, and diagonals.
lose
(standard game) or win
(misère game) for the player to move, as the previous player won.Blank
cells) using AllFilledIn
or gPosition.piecesPlaced
.
tie
.undecided
.VALUE Primitive(POSITION position) {
if (!gUseGPS)
PositionToBlankOX(position, gPosition.board);
if (ThreeInARow(gPosition.board, 0, 1, 2) ||
ThreeInARow(gPosition.board, 3, 4, 5) ||
ThreeInARow(gPosition.board, 6, 7, 8) ||
ThreeInARow(gPosition.board, 0, 3, 6) ||
ThreeInARow(gPosition.board, 1, 4, 7) ||
ThreeInARow(gPosition.board, 2, 5, 8) ||
ThreeInARow(gPosition.board, 0, 4, 8) ||
ThreeInARow(gPosition.board, 2, 4, 6))
return gStandardGame ? lose : win;
else if ((gUseGPS && (gPosition.piecesPlaced == BOARDSIZE)) ||
((!gUseGPS) && AllFilledIn(gPosition.board)))
return tie;
else
return undecided;
}
The helper function ThreeInARow(BlankOX *theBlankOX, int a, int b, int c)
checks if three cells contain the same non-Blank
value.