This program creates a database of optimal moves for a specific boardgame. With this database you can run an AI that plays the boardgame perfectly. The boardgame-webserver project does this.
According to this Wikipedia article the boardgame can now be considered strongly solved.
The game is similar to chess. It is played by two players, one white and one black, on a board of 4 * 6 squares, where each player faces the short side. Each player owns four pieces: The rock, the paper, the scissors and the well. Before the game starts, each player arranges these pieces in any order on the four squares of the row closest to him (his "base row").
The game is started by the white player. Players take turns alternately until the game is over. To make a move, the player chooses one of his pieces and moves it onto one of the eight squares next to it. A piece can only move to a square that is empty or where an opponent's piece is standing that can be taken. Taken pieces are removed from the game. Each piece can take opponent's pieces according to this table:
|Taking Piece||Taken piece|
The game is won when a piece reaches the opponent's base row or when all opponent's pieces are taken.
For each position the database contains the number of moves it takes to win or lose it, assuming that both players play perfectly. Stalemate positions are not stored. A position consists of the squares where the eight pieces are standing. It is assumed to be the white player's turn. To cut storage need in half, only one of two positions is stored, where the positions are mirror-symmetric to the longer axis.
The program runs in iterations. The first iteration finds all positions that can be won in a single move. This is achieved by generating all positions where the white player has won and then undoing all moves that may have lead to it. Duplicates are eliminated.
The second iteration finds all positions that are lost in two moves. To do this, for each position of iteration 1 the board is turned and all possible moves of the white player leading to it are undone, resulting in candidates for iteration 2. A candidate is valid only if any move by the white player results in the black player being able to win in one move. In other words the white player has to lose no matter which move he chooses.
The third iteration finds all positions that can be won in three moves. For each position found by iteration 2 the board is turned and all of white's moves that may have lead to this position are undone. Duplicates are eliminated.
This pattern is repeated until an iteration does not find any results. For the boardgame described above this happens in iteration 66.
When all iterations are done, a compacted database in binary format is created that contains the positions and its number of turns in sorted order. This makes lookups in O(log(n)) time possible.
This approach is transferable to other boardgames where two players take turns alternately. In chess it is known as retrograde analysis.
I wrote this program in Python first using generators, but the performance was abysmal. With C i was able to write the program using macros, which unfold to a large amount of simple code. This is perfect for GCC to optimize.
On my Macbook Air it takes around 20 days of runtime to create the database. The earlier iterations take longer than the latter. The result is a binary file of 6.89 GB. Intermediate storage of around 50 GB is needed.
Iterations 1, 2 and 3 are skipped. They can be computed on the fly and take a ton of HD space.
To create the database, you need to run these steps:
Store the result:
cat moves.?.txt | sort -n > moves.iteration04.txt && rm moves.?.txt
Create SQLite db:
On the SQLite console enter:
PRAGMA journal_mode = OFF; PRAGMA locking_mode = EXCLUSIVE; PRAGMA synchronous = OFF; CREATE TABLE positions (position integer, move integer, count integer); .separator " " .import moves.iteration04.txt positions CREATE UNIQUE INDEX position_index ON positions (position ASC);
The following three steps have to be executed for each iteration from 5 to 65. Run them once, then replace each 5 with a 6, then with a 7, etc.
Store the result:
cat moves.?.txt | sort -n > moves.iteration05.txt && rm moves.?.txt
sqlite3 --separator " " moves.db '.import moves.iteration05.txt positions'
Merge all results:
sort -mn moves.iteration??.txt > moves.txt && rm moves.iteration??.txt
Replace SQLite DB with compacted DB:
cat moves.txt | python create_compact_db.py > moves.db && rm moves.txt
You can check your files after each iteration. They should have the following number of lines: