Skip to content
Snippets Groups Projects
Commit 5a0bcc22 authored by Ethan Fox's avatar Ethan Fox
Browse files

Update README.md to include topological order

parent 6758abc1
Branches master
No related tags found
No related merge requests found
# Boost Board Game Application (Fall 2020 Version)
A progressive web app (PWA) for learning and playing the board game Boost.
Boost is a turn-based abstract strategy board game like checkers, chess,
Xiangqi, or Arimaa. It was designed to be new and interesting for humans to
play while still admitting a simple AI and supporting various homework
assignments on algorithms and data structures in the SOFT 260 course at UNL.
# Quick Start
Recursively clone this repository and `cd` into the root folder:
```
$ git clone --recursive git@git.unl.edu:soft-core/soft-260/boost-board-game.git
$ cd boost-board-game
```
(If you forget `--recursive` when cloning, you can `cd` into your clone and run
`git submodule update --init --recursive` instead.)
Install dependencies for each of the projects in order:
```
$ (cd eslint-config; npm install)
$ (cd boost-game; npm install)
$ (cd boost-puzzles; npm install)
$ (cd boost-engine; npm install)
$ (cd boost-app; npm install)
```
If you get the error "'@unlsoft/eslint-config@1.0.0' is not in the npm registry"
during the third, fourth, or fifth steps, you can work around that bug in npm by
using these commands instead:
```
$ (cd boost-puzzles; mv package-lock.json backup.json; npm install; cp backup.json package-lock.json; npm install; mv backup.json package-lock.json)
$ (cd boost-engine; mv package-lock.json backup.json; npm install; cp backup.json package-lock.json; npm install; mv backup.json package-lock.json)
$ (cd boost-app; mv package-lock.json backup.json; npm install; cp backup.json package-lock.json; npm install; mv backup.json package-lock.json)
```
Generate the game and engine code:
```
$ (cd boost-puzzles; npm run generate)
$ (cd boost-engine; npm run build)
$ (cd boost-app; npm run generate)
```
Optionally run the test suites to make sure everything so far is okay:
```
$ (cd boost-game; npm run test:js -- --watchAll=false)
$ (cd boost-engine; npm run test:js -- --watchAll=false)
$ (cd boost-app; npm run test:js -- --watchAll=false)
```
And then serve the application locally:
```
$ (cd boost-app; npm start)
```
Once the app is running, click on the "Rules" button to read the rules of the
game.
# Development Workflow
The project includes a VSCode workspace named `boost-board-game.code-workspace`.
Most development tasks can be completed from within VSCode, though command-line
development is also possible. The subsections below describe how to perform
common development tasks.
## Code Generation
As described later in the architecture overview, some projects depend on code
generated by other projects. Normally this code generation happens
automatically when you lint, test, or otherwise run the app, but if you need to
force an update (for example, if you change the game or engine code while the
app is still running), you can use project's `generate` script. From VSCode,
run the project's `generate` script from the "NPM SCRIPTS" tray. Or, on the
command line:
```
$ (cd …; npm run generate)
```
## Linting
VSCode should automatically detect each project's ESLint configuration and
dependencies and run them on the fly as you edit the code, though you may have
to tell VSCode to trust the ESLint instance the first time you edit a project.
Alternatively, you can also manually lint. From VSCode, run the project's
`lint` script from the "NPM SCRIPTS" tray. Or, on the command line:
```
$ (cd …; npm run lint)
```
## Unit Testing
The unit test suites are set up to run in "watch" mode, which means that you can
start the test script once, and it will rerun the test suite every time you save
a code change. From VSCode, run the project's `test` script from the "NPM
SCRIPTS" tray. Or, on the command line:
```
$ (cd …; npm run test)
```
If you want to run the tests only once and not watch for code changes, you can
pass the `--watchAll=false` option to the underlying script:
```
$ (cd …; npm run test:js -- --watchAll=false)
```
Likewise, if you want to only run tests whose names match some text, you can
pass the standard `-t` option:
```
$ (cd …; npm run test:js -- -t '[some test-name text]')
```
Options can be combined:
```
$ (cd …; npm run test:js -- --watchAll=false -t '[some test-name text]')
```
## System Testing
The main app is set up to run in "watch" mode, which means that you can start it
once, and it will refresh the page in your browser every time you save a code
change. From VSCode, run the project's `start` script from the "NPM SCRIPTS"
tray. Or, on the command line:
```
$ (cd boost-app; npm run start)
```
These automatic refreshes will *not* clear any persisted Redux state. Because
the app uses the local storage engine from `redux-persist` for persistence, you
can clear your data during development by running `localStorage.clear()` in the
developer console and then manually refreshing the page.
## Deployment
The main app runs entirely client-side, so it can be deployed as a `build`
folder to be placed on any hosting platform. From VSCode, run the project's
`build` script from the "NPM SCRIPTS" tray. Or, on the command line:
```
$ (cd boost-app; npm run build)
```
At the end of the command's output you should see a link to further deployment
instructions.
# Architecture Overview
The code for the Boost board game application is organized as follows:
* The project `@unlsoft/eslint-config` contains the ESLint configuration for
the coding style used across the other projects. Per `create-react-app`
convention, in a development build of the main app, a separate, weaker
coding style also warns at runtime about likely bugs.
* The project `@unlsoft/boost-game` is responsible for representing positions
as bit boards and for features that rely on bit-board operations to run
acceptably fast, primarily move generation and static evaluation. Because
so much of the bit-board logic can be precomputed and unrolled,
`@unlsoft/boost-game` does not implement game logic itself, but is actually
a parameterized code generator that writes game logic into separate
JavaScript files.
* The project `@unlsoft/boost-engine` contains the game-playing engine, the
application's AI. Like other abstract strategy board game engines, this
engine is designed to run in a separate thread or process from any UI, and
it communicates with a controller using a protocol called BEI (see the
protocol documentation further below). The engine has a library of games
that it knows how to play, and its build system automatically generates the
source code for each of these games using `@unlsoft/boost-game`.
* The project `@unlsoft/boost-app` is a `create-react-app` PWA that provides
the application's game controller and UI. Like `@unlsoft/boost-engine` it
relies on a game library generated by `@unlsoft/boost-game`, and it also
includes `@unlsoft/boost-engine` as a suite of web workers via symlink.
# Generating `Game` Objects with `boost-game`
When `@unlsoft/boost-game` is installed as a development dependency, it provides
a command `generate-boost-game` that writes a JavaScript implementation of a
Boost game to standard out. The exact code produced depends on a number of
command-line options, described below. You can also run `generate-boost-game
--help` to see a list of all of the above options, their short descriptions, and
their default values.
## Command-Line Options Affecting Game Rules
There are six main command-line options that can be passed to
`generate-boost-game` to control what kind of game is generated:
* `--board-width [number]` sets the width of the game board in points. If
omitted, the default width of nine points is used.
* `--board-height [number]` sets the height of the game board in points. If
omitted, the default height of nine points is used.
* `--player-count [number]` sets the number of players (and therefore the
number of non-dragon piece colors). If omitted, the default of two players
is used.
* `--starting-population-limit [number]` sets the maximum number of starting
pawns given to each player in a standard board setup. (It is only a maximum
because the game code may give players fewer starting pieces when the
board's perimeter is too crowded.) If omitted, the default limit of eight
pawns is used.
* `--tower-limit [number]` sets the maximum number of towers that each player
may build. (This limit only affects when construction moves are available
to a player; it does not preclude other code from placing extra towers on
the board.) If omitted, the default limit of two towers is used.
* `--demo` overrides the usual rules to allows players to move their pieces
even when they are defeated. This can be useful both in testing and
tutorials for keeping the number of pieces on the board down.
Based on these six options, the game will be assigned a unique string of the
form `boost-[width]-[height]-[players]-[population]-[towers]` or of the form
`boost-[width]-[height]-[players]-[population]-[towers]-demo`, which is called
its **game identifier**. For example, the game identifier for a standard Boost
game is `boost-9-9-2-8-2`.
## Command-Line Options Affecting the AI
Other command-line options are also available to tweak the weights in the static
evaluation function, which is the function used by `@unlsoft/boost-engine` to
estimate how favorable a position is for each player. The defaults values, used
when these options are omitted, were chosen by human intuition and are given in
decitempi (tenths of an extra turn). However, these defaults have not been
empirically validated yet. The command-line options for static evaluation
weights are as follows:
* `--ai-pawn [number]` sets the estimated value of a pawn. The default value
is 200 decitempi (20 extra turns).
* `--ai-knight [number]` sets the estimated value of a knight when a player
has all of their towers. The default value is 220 decitempi (a pawn plus
two extra turns).
* `--ai-endgame-knight [number]` sets the estimated value of a knight when a
player does not have all of their towers. The game also computes an
appropriate fixed penalty for a player not having all of their towers so
that the AI is not incentivized to sacrifice its towers to make its knights
more valuable. The default value is 350 decitempi (a pawn plus 15 extra
turns).
* `--ai-knight-activity [number]` sets the estimated value of moving a knight
one point closer to the closest non-friendly piece (i.e., a dragon or a
foe). The default value is 5 decitempi (an extra half turn).
* `--ai-zero-activity-distance [number]` sets the distance, in points, that
must lie between a knight and the closest non-friendly piece for that knight
to have an activity score of zero. As a knight moves closer to the
non-friendly piece, its activity score becomes positive (at the rate given
by `--ai-knight-activity`), and as it moves farther away, its activity score
turns negative (at the same rate). The main use of this parameter is to
control the relative value of pawns and inactive knights; because an
inactive knight prevents a player from promoting a better-positioned pawn,
the knight can actually be a liability. The default value is four points
(so a knight eight points from the nearest non-friendly piece is estimated
to be worth the same as a pawn).
* `--ai-construction-site [number]` sets the estimated value of a completed
construction site (a circle of four friendly pieces around an empty point).
Partial construction sites are awarded partial points: 1/16 value for a
construction site with one piece in place, 4/16 value for a construction
site with two pieces in place, and 9/16 value for a construction site with
three pieces in place. The number of construction sites that the AI can
score for each side is limited by the number of additional towers that the
player can usefully build, though that limit may be adjusted by
`--ai-extra-opening-sites`. Additionally, construction sites are
ineligible for scoring if they are not within four points of four friendly
mobile pieces. The default value is 80 decitempi (8 turns).
* `--ai-extra-opening-sites [number]` sets the number of extra construction
site candidates the AI can consider for a side if that side is in an
opening-like position (i.e., has the starting population and no towers). In
some cases, like in a standard game, a lie to the engine like
`--ai-extra-opening-sites 2` can help low-depth search set up better for a
second tower even though that the construction is beyond its horizon. The
default value is 0 extra candidates (no deception).
* `--ai-tower [number]` sets the estimated value of a tower when a player has
at least four mobile pieces. The default value is 125 decitempi (12.5 extra
turns).
* `--ai-endgame-tower [number]` sets the estimated value of a tower when a
player does not have at least four mobile pieces. The game also computes an
appropriate fixed penalty for a player not having four mobile pieces so that
the AI is not incentivized to sacrifice its pawns and knights to make its
towers more valuable. The default value is 700 decitempi (70 extra turns).
* `--ai-dragon-proximity [number]` sets the estimated value of moving a dragon
one point closer to the closest friendly tower when there are at least four
dragons on the board. The default value is 10 decitempi (an extra turn).
* `--ai-circle-dragon [number]` sets the estimated additional value of moving
a dragon next to a friendly tower when there are at least four dragons on
the board. The default value is 100 decitempi (ten extra turns).
* `--ai-crowd-member [number]` sets the estimated additional value of a piece
that has at least three other friendly pieces at most four points away. The
default value is 1 decitempo (one tenth of an extra turn).
* `--ai-defeat [number]` sets the estimated value of defeating an opponent.
The default value is 1,000,000 decitempi (100,000 extra turns).
## Typical Usage
By convention, one usually redirects the output of `generate-boost-game` to
JavaScript files in a `…/src/games` folder and then writes code like
```
import …_GAME from './games/….js';
import …_GAME from './games/….js';
const GAME_LIBRARY = new Map([
…_GAME,
…_GAME,
].map((game) => [game.identifier, game]));
export default GAME_LIBRARY;
```
in `…/src/gameLibrary.js` so that other code can import the game library and
look up games by their identifiers.
# Using `Game` Objects from `@unlsoft/boost-game`
`Game` objects from a game library provide the following fields and methods:
## Constants from Game Generation
These constants are numbers set at code generation time, as described earlier:
* `game.identifier` is the identifier for the game, as described in the
previous section.
* `game.boardWidth` is the width, in points, of the board on which the game is
played.
* `game.boardHeight` is the height, in points, of the board on which the game is
played.
* `game.playerCount` is the number of players in the game (and therefore the
number of non-dragon piece colors needed to play the game).
* `game.populationLimit` is the maximum number of starting pawns given to each
player in a standard board setup.
* `game.towerLimit` is the maximum number of towers that each player may
build. (This limit only affects when construction moves are available to a
player; it does not preclude other code from placing extra towers on the
board.)
## Piece Types
These constants represent the different piece types, which are returned by
`position.getColorAndPieceType` and passed to `position.modified` as described
later:
* `game.dragon` is a constant value used to represent a dragon.
* `game.pawn` is a constant value used to represent a pawn.
* `game.knight` is a constant value used to represent a knight.
* `game.tower` is a constant value used to represent a tower.
## Algebraic Notation
The following fields and helper methods are useful for encoding and decoding
algebraic notation for files, ranks, points, and moves:
* `game.prettifyFile(file)` takes a file as a zero-based 𝑥 coordinate and
returns the corresponding letter in algebraic notation.
* `game.unprettifyFile(file)` takes a file as a letter in algebraic notation
and returns the corresponding zero-based 𝑥 coordinate.
* `game.prettifyRank(rank)` takes a rank as a zero-based 𝑦 coordinate and
returns the corresponding digits in algebraic notation.
* `game.unprettifyRank(rank)` takes a rank as a string of digits in algebraic
notation and returns the corresponding zero-based 𝑦 coordinate.
* `game.noPoint` is the constant string representing no point at all in
algebraic notation.
* `game.prettifyPoint(file, rank)` takes a point as zero-based 𝑥 and 𝑦
coordinates and returns the corresponding algebraic notation. If either
coordinate is `undefined`, it returns `game.noPoint`.
* `game.unprettifyPoint(point)` takes a point in algebraic notation and
returns the corresponding zero-based 𝑥 and 𝑦 coordinates in an `Array`. If
the point is `game.noPoint`, both coordinates will be `undefined`.
* `game.pass` is the constant string representing a pass in algebraic
notation.
* `game.joinPoints(fromPoint, toPoint)` takes two points in algebraic notation
and returns the algebraic notation for a move from `fromPoint` to `toPoint`.
For constructions and promotions, one point may be omitted or set to
`game.noPoint`, or the two points may be set equal to each other. (For
consistency with `game.splitMove` and the main app's UI, the preferred
practice is to give the same point for both arguments.) For passes, both
points should be omitted or set to `game.noPoint`. (For consistency with
`game.splitMove`, the preferred practice is to pass `game.noPoint` for both
arguments.)
* `game.splitMove(move)` takes a move in algebraic notation and returns the
algebraic notation for its "from" and "to" points in an `Array`. For
constructions and promotions, the "from" point will be the same as the "to"
point. For passes, both points will be `game.noPoint`.
* `prettifyMove(fromFile, fromRank, toFile, toRank)` takes a move as
zero-based 𝑥 and 𝑦 coordinates for its "from" and "to" points and returns
the algebraic notation for the move. Coordinates may be omitted or
`undefined` for special moves in the same places where `game.joinPoints`
would take `game.noPoint`.
* `unprettifyMove(move)` takes a move in algebraic notation and returns the
corresponding zero-based 𝑥 and 𝑦 coordinates for its "from" and "to" points.
Coordinates will be repeated for constructions and promotions, and will be
`undefined` for passes.
## Positions
The following fields and helper method are useful for obtaining `Position`
objects corresponding to the game:
* `game.blankPosition` is a position containing no pieces.
* `game.startingPosition` is the game's standard starting position without any
dragons.
* `game.deserializePosition(serialization)` deserializes a position previously
encoded as a string with `position.serialization`, which is described later.
# Using `Position` Objects from `@unlsoft/boost-game`
`Position` objects corresponding to a game provide the following fields and
methods:
## Encodings
* `position.signature` is the position encoded as a single `BigInt`. This
encoding is meant to be used as a reasonably fast hash-table key. (The
position itself cannot be a hash-table key because ES6 does not support
hash-by-value natively.)
* `position.nextSignature` is equivalent to `position.nextTurn.signature` (see
the description of `position.nextTurn` in the "Moves" subsection further
below), but runs faster because it does not actually advance the turn. It
is mostly useful for checking candidate moves for repetitions.
* `position.serialization` is the position serialized as a string. A position
can later be deserialized with `game.deserializePosition` as described
earlier.
## Pieces
* `position.getColorAndPieceType(x, y)` returns the color and piece type of
the piece at the zero-based coordinates `x` and `y` as a two-element
`Array`. Colors are represented by the number of turns until the
corresponding player will play (e.g., `0` for the player whose turn it is
and `1` for the other player in a two-player game), and piece types are
represented with the constants `game.dragon`, `game.pawn`, `game.knight`,
and `game.tower` described earlier. Dragon's colors are always `undefined`.
If there is no piece at the given coordinates, both the color and piece type
will be `undefined`.
* `position.modified(x, y, color, pieceType)` returns a new position
(`Position` objects are immutable) where the piece at the zero-based
coordinates `x` and `y` has been replaced with a piece of the given color
and piece type. As above, the color should be given as the number of turns
until the corresponding player moves next (e.g., `0` for the player whose
turn it is and `1` for the other player in a two-player game), and piece
types should be given as one of the constants `game.dragon`, `game.pawn`,
`game.knight`, or `game.tower`, which are also described earlier. As
special cases, `color` should be `undefined` if `pieceType` is
`game.dragon`, and a point can be cleared by passing `undefined` for both
`color` and `pieceType`.
## Static Evaluation
* `position.getStaticEvaluation(color)` is the static evaluation of the
position from the perspective of the player who is to play in `color` turns
(hence, the possible values for `color` are the same as the values returned
by `position.getColorAndPieceType` or passed to `position.modified`).
Higher static evaluation scores are more favorable for that player. The
score `Infinity` means that the player to move next has already won, while
the score `-Infinity` means that some other player has won. The static
evaluation is finite in all other situations, though it may still be
extremely positive or negative (such as when the player has been defeated,
but other players are still actively competing).
* `position.live` is `true` if the game is still ongoing, `false` if any
player has won.
## Moves
* `position.children` is the list of positions that the current player can
move to, not considering the repetition rule. Note that it is the same
player's turn in the children positions as in the parent position; a turn is
not considered complete until the code uses `position.nextTurn`.
* `position.getMoveTo(child)` returns the algebraic notation for the move from
`position` to `child`.
* `position.getChildByMove(move)` returns the child reached by playing the
move given in algebraic notation.
* `position.nextTurn` is the same as `position`, except that in
`position.nextTurn` it is the next player's turn. So, for example,
`position.getChildByMove('a1a3').nextTurn` would be the position after the
current player played the move `a1a3`.
# Using `Controller` Objects from `@unlsoft/boost-engine`
When `@unlsoft/boost-game` is installed as a development dependency, it provides
two minified files, `…/node_modules/@unlsoft/boost-engine/dist/engine.js` and
`…/node_modules/@unlsoft/boost-engine/dist/engineThread.js` that implement the
engine's web workers. These files should be included as-is in any app that
wants to use the engine; for a `create-react-app` app, the easiest approach is
to symlink them to the `public` folder.
For talking to these web workers, the default export from
`@unlsoft/boost-engine` is a `Controller` class, where `Controller` objects
provide the following methods:
* `new Controller(engineURL, engineThreadURL, gameIdentifier, propertyHandler,
moveHandler)` creates a controller for an engine whose web workers source
code is located at the given URLs and that will play the game identified by
`gameIdentifier`. Two callbacks must be provided:
* `propertyHandler` will be called with a property name and a value
whenever the engine sends a description of itself; see the documentation
for the `id` BEI message below.
* `moveHandler` will be called with a move in algebraic notation whenever
the engine makes a move.
* `controller.setStrength(strength)` sets the engine's strength as close as
possible to `strength` on a scale where `0` is the strength of a theoretical
player that can never win, `1500` is the strength of the average experienced
human player, and `2500` is the strength of a grandmaster.
* `controller.setLine(position, taboo)` sends a position and an array of
preceding positions (which are taboo under the repetition rule) to the
engine.
* `go()` tells the engine to choose a move to play in the last sent position.
The engine's reply will be sent to the `moveHandler` callback once the
engine is done thinking.
* `stop(wantMove)` tells the engine stop thinking early. If `wantMove` is
`true`, the controller will still call `moveHandler` with the best move the
engine found so far; otherwise that move will be discarded.
Under the hood, the `Controller` constructor creates associated web workers, and
its other methods communicate with these web workers using BEI, a protocol
described in the next section.
# The Boost Engine Interface (BEI) Protocol
BEI is a text-based protocol for communication between a controller (a program
that wants to incorporate a Boost-playing AI) and an engine (a Boost-playing
AI). It is based on and simplified from the Arimaa Engine Interface (AEI),
which in turn is based on the Universal Chess Interface (UCI).
BEI is built on asynchronous bidirectional communication of short strings called
**messages** between the controller and engine. For example, a controller might
send messages as lines of text to an engine's standard input and read messages
from the engine's standard output, or the controller and engine might exchange
BEI messages as string payloads in web worker messages.
(`@unlsoft/boost-engine` uses the latter approach.)
Generally the engine should not process later messages until it has finished
processing earlier ones. The two exceptions are pondering (speculatively
searching ahead during an opponent's turn) and thinking (deciding what move to
make during the engine's turn), long-running operations that should effectively
happen "in the background" and not prevent the engine from responding to other
communication.
## Controller-to-Engine Messages
The following messages may be sent by a controller to an engine:
* `bei [value] [value] …` is sent to initiate communication and optionally to
provide engine-specific startup arguments. (It must be possible to
configure an engine to take no startup arguments so that the engine can be
used with an implementation-agnostic controller, but an engine may still opt
to take arguments in other settings.) The engine will reply with a
`protocol` message, possibly some `id` messages, and then `beiok`. The
controller must not send any other communication until it has confirmed that
it is using a compatible protocol version and has received a `beiok`.
* `isready` is sent to ping the engine and ensure that the engine has finished
processing any previous messages. The engine will reply with `readyok`.
* `newgame [identifier]` is sent to start a new game with the given game
identifier. The engine will reply with `known` if it can play that game,
`unknown` if it cannot.
* `setoption [option] [value]` is sent to set the named engine option to the
given value. Currently the only supported option name is `strength`, which
should be followed by a rating on a scale where `0` is the strength of a
theoretical player that can never win, `1500` is the strength of the average
experienced human player, and `2500` is the strength of a grandmaster. If
the engine strength is never set, the engine should default to its strongest
setting; if an engine cannot play at the requested strength, it should set
its strength as close as possible.
* `setline [serialization] [tabooSerialization] [tabooSerialization] …` is
sent to tell the engine about a new board position, which is given by the
first serialization, and all of the previous board positions that affect the
repetition rule, which are give by the following serializations. The
serialization format is the same format as used by `position.serialization`
from `@unlsoft/boost-game`.
* `ponder [turns]` is sent to tell the engine that it will be playing after
the given number of turns and that it may ponder in the background. The
`newgame`, `setline`, `go`, and `stop` messages all stop pondering.
* `go` is sent to tell the engine that it should start thinking in the
background about what move to play in the current position. When it is done
thinking, the engine will respond with `move` message reporting the best
move that it was able to find. The `newgame`, `setline`, `ponder`, and
`stop` messages all stop thinking immediately and trigger a `move` message,
even if the engine was not done considering the position.
* `stop` is sent to tell the engine that it should stop any pondering or
thinking. If the engine is thinking, this will prompt it to reply with a
`move` message.
* `quit` is sent to tell the engine that no further messages will be sent and
that it may exit.
## Engine-to-Controller Messages
The following messages may be sent by an engine to a controller:
* `protocol [version]` is sent as the first message in response to a `bei`
message to tell the controller what version of BEI the engine uses. The
version described here is `1.0.0`.
* `id [property] [value]` is sent in response to a `bei` message to describe
the engine to the controller. Each property should only be sent once.
Three property names are supported:
* The value for the `name` property is the name of the engine.
* The value for the `author` property is the name of the engine's author
(or the names of the engine's authors if there are more than one).
* The value for the `version` property is the version number of the
engine.
* `beiok` is sent to indicate the end of responses to a `bei` message.
* `readyok` is sent to reply to an `isready` message.
* `known` is sent to reply to a `newgame` message when the engine is able to
load the specified game from its game library.
* `unknown` is sent to reply to a `newgame` message when the engine is unable
to load the specified game from its game library.
* `move [move]` is sent to report the best move the engine was able to find
whenever the engine stops thinking. The engine should not assume that this
move will actually be made; if the move is mode, it will receive an
appropriate `setline` message.
* `log [text]` is sent to report a log message from the engine.
# Footrace Generator Design
The footrace generator necessitated the use of a dynamic programming algorithm rather than a greedy algorithm. If a greedy were used, it would
value the most movement north but would place the piece in situations that require more moves to reach the end.
`findStartingSituationAndFastestMoveSequence(position)` is the main dynamic programming algorithm.
The code uses `ONE_LANE_GAME` and `TWO_LANE_GAME` to generate the puzzles. The algorithm is only used for the `ONE_LANE_GAME` to generate a path.
The `ONE_LANE_GAME` is then mirrored to make the `TWO_LANE_GAME`.
When the dynamic programming algorithm is run, the board contains pawns and towers.
Each vertex of the DAG is a `Situation`. These vertices are added to the DAG using `selectBetterBackpointer()`.
Each edge of the DAG is a move. The algorithm finds the move leading to a `Situation` in the `predecessor` index of `backpointersTable`.
\ No newline at end of file
# Boost Board Game Application (Fall 2020 Version)
A progressive web app (PWA) for learning and playing the board game Boost.
Boost is a turn-based abstract strategy board game like checkers, chess,
Xiangqi, or Arimaa. It was designed to be new and interesting for humans to
play while still admitting a simple AI and supporting various homework
assignments on algorithms and data structures in the SOFT 260 course at UNL.
# Quick Start
Recursively clone this repository and `cd` into the root folder:
```
$ git clone --recursive git@git.unl.edu:soft-core/soft-260/boost-board-game.git
$ cd boost-board-game
```
(If you forget `--recursive` when cloning, you can `cd` into your clone and run
`git submodule update --init --recursive` instead.)
Install dependencies for each of the projects in order:
```
$ (cd eslint-config; npm install)
$ (cd boost-game; npm install)
$ (cd boost-puzzles; npm install)
$ (cd boost-engine; npm install)
$ (cd boost-app; npm install)
```
If you get the error "'@unlsoft/eslint-config@1.0.0' is not in the npm registry"
during the third, fourth, or fifth steps, you can work around that bug in npm by
using these commands instead:
```
$ (cd boost-puzzles; mv package-lock.json backup.json; npm install; cp backup.json package-lock.json; npm install; mv backup.json package-lock.json)
$ (cd boost-engine; mv package-lock.json backup.json; npm install; cp backup.json package-lock.json; npm install; mv backup.json package-lock.json)
$ (cd boost-app; mv package-lock.json backup.json; npm install; cp backup.json package-lock.json; npm install; mv backup.json package-lock.json)
```
Generate the game and engine code:
```
$ (cd boost-puzzles; npm run generate)
$ (cd boost-engine; npm run build)
$ (cd boost-app; npm run generate)
```
Optionally run the test suites to make sure everything so far is okay:
```
$ (cd boost-game; npm run test:js -- --watchAll=false)
$ (cd boost-engine; npm run test:js -- --watchAll=false)
$ (cd boost-app; npm run test:js -- --watchAll=false)
```
And then serve the application locally:
```
$ (cd boost-app; npm start)
```
Once the app is running, click on the "Rules" button to read the rules of the
game.
# Development Workflow
The project includes a VSCode workspace named `boost-board-game.code-workspace`.
Most development tasks can be completed from within VSCode, though command-line
development is also possible. The subsections below describe how to perform
common development tasks.
## Code Generation
As described later in the architecture overview, some projects depend on code
generated by other projects. Normally this code generation happens
automatically when you lint, test, or otherwise run the app, but if you need to
force an update (for example, if you change the game or engine code while the
app is still running), you can use project's `generate` script. From VSCode,
run the project's `generate` script from the "NPM SCRIPTS" tray. Or, on the
command line:
```
$ (cd …; npm run generate)
```
## Linting
VSCode should automatically detect each project's ESLint configuration and
dependencies and run them on the fly as you edit the code, though you may have
to tell VSCode to trust the ESLint instance the first time you edit a project.
Alternatively, you can also manually lint. From VSCode, run the project's
`lint` script from the "NPM SCRIPTS" tray. Or, on the command line:
```
$ (cd …; npm run lint)
```
## Unit Testing
The unit test suites are set up to run in "watch" mode, which means that you can
start the test script once, and it will rerun the test suite every time you save
a code change. From VSCode, run the project's `test` script from the "NPM
SCRIPTS" tray. Or, on the command line:
```
$ (cd …; npm run test)
```
If you want to run the tests only once and not watch for code changes, you can
pass the `--watchAll=false` option to the underlying script:
```
$ (cd …; npm run test:js -- --watchAll=false)
```
Likewise, if you want to only run tests whose names match some text, you can
pass the standard `-t` option:
```
$ (cd …; npm run test:js -- -t '[some test-name text]')
```
Options can be combined:
```
$ (cd …; npm run test:js -- --watchAll=false -t '[some test-name text]')
```
## System Testing
The main app is set up to run in "watch" mode, which means that you can start it
once, and it will refresh the page in your browser every time you save a code
change. From VSCode, run the project's `start` script from the "NPM SCRIPTS"
tray. Or, on the command line:
```
$ (cd boost-app; npm run start)
```
These automatic refreshes will *not* clear any persisted Redux state. Because
the app uses the local storage engine from `redux-persist` for persistence, you
can clear your data during development by running `localStorage.clear()` in the
developer console and then manually refreshing the page.
## Deployment
The main app runs entirely client-side, so it can be deployed as a `build`
folder to be placed on any hosting platform. From VSCode, run the project's
`build` script from the "NPM SCRIPTS" tray. Or, on the command line:
```
$ (cd boost-app; npm run build)
```
At the end of the command's output you should see a link to further deployment
instructions.
# Architecture Overview
The code for the Boost board game application is organized as follows:
* The project `@unlsoft/eslint-config` contains the ESLint configuration for
the coding style used across the other projects. Per `create-react-app`
convention, in a development build of the main app, a separate, weaker
coding style also warns at runtime about likely bugs.
* The project `@unlsoft/boost-game` is responsible for representing positions
as bit boards and for features that rely on bit-board operations to run
acceptably fast, primarily move generation and static evaluation. Because
so much of the bit-board logic can be precomputed and unrolled,
`@unlsoft/boost-game` does not implement game logic itself, but is actually
a parameterized code generator that writes game logic into separate
JavaScript files.
* The project `@unlsoft/boost-engine` contains the game-playing engine, the
application's AI. Like other abstract strategy board game engines, this
engine is designed to run in a separate thread or process from any UI, and
it communicates with a controller using a protocol called BEI (see the
protocol documentation further below). The engine has a library of games
that it knows how to play, and its build system automatically generates the
source code for each of these games using `@unlsoft/boost-game`.
* The project `@unlsoft/boost-app` is a `create-react-app` PWA that provides
the application's game controller and UI. Like `@unlsoft/boost-engine` it
relies on a game library generated by `@unlsoft/boost-game`, and it also
includes `@unlsoft/boost-engine` as a suite of web workers via symlink.
# Generating `Game` Objects with `boost-game`
When `@unlsoft/boost-game` is installed as a development dependency, it provides
a command `generate-boost-game` that writes a JavaScript implementation of a
Boost game to standard out. The exact code produced depends on a number of
command-line options, described below. You can also run `generate-boost-game
--help` to see a list of all of the above options, their short descriptions, and
their default values.
## Command-Line Options Affecting Game Rules
There are six main command-line options that can be passed to
`generate-boost-game` to control what kind of game is generated:
* `--board-width [number]` sets the width of the game board in points. If
omitted, the default width of nine points is used.
* `--board-height [number]` sets the height of the game board in points. If
omitted, the default height of nine points is used.
* `--player-count [number]` sets the number of players (and therefore the
number of non-dragon piece colors). If omitted, the default of two players
is used.
* `--starting-population-limit [number]` sets the maximum number of starting
pawns given to each player in a standard board setup. (It is only a maximum
because the game code may give players fewer starting pieces when the
board's perimeter is too crowded.) If omitted, the default limit of eight
pawns is used.
* `--tower-limit [number]` sets the maximum number of towers that each player
may build. (This limit only affects when construction moves are available
to a player; it does not preclude other code from placing extra towers on
the board.) If omitted, the default limit of two towers is used.
* `--demo` overrides the usual rules to allows players to move their pieces
even when they are defeated. This can be useful both in testing and
tutorials for keeping the number of pieces on the board down.
Based on these six options, the game will be assigned a unique string of the
form `boost-[width]-[height]-[players]-[population]-[towers]` or of the form
`boost-[width]-[height]-[players]-[population]-[towers]-demo`, which is called
its **game identifier**. For example, the game identifier for a standard Boost
game is `boost-9-9-2-8-2`.
## Command-Line Options Affecting the AI
Other command-line options are also available to tweak the weights in the static
evaluation function, which is the function used by `@unlsoft/boost-engine` to
estimate how favorable a position is for each player. The defaults values, used
when these options are omitted, were chosen by human intuition and are given in
decitempi (tenths of an extra turn). However, these defaults have not been
empirically validated yet. The command-line options for static evaluation
weights are as follows:
* `--ai-pawn [number]` sets the estimated value of a pawn. The default value
is 200 decitempi (20 extra turns).
* `--ai-knight [number]` sets the estimated value of a knight when a player
has all of their towers. The default value is 220 decitempi (a pawn plus
two extra turns).
* `--ai-endgame-knight [number]` sets the estimated value of a knight when a
player does not have all of their towers. The game also computes an
appropriate fixed penalty for a player not having all of their towers so
that the AI is not incentivized to sacrifice its towers to make its knights
more valuable. The default value is 350 decitempi (a pawn plus 15 extra
turns).
* `--ai-knight-activity [number]` sets the estimated value of moving a knight
one point closer to the closest non-friendly piece (i.e., a dragon or a
foe). The default value is 5 decitempi (an extra half turn).
* `--ai-zero-activity-distance [number]` sets the distance, in points, that
must lie between a knight and the closest non-friendly piece for that knight
to have an activity score of zero. As a knight moves closer to the
non-friendly piece, its activity score becomes positive (at the rate given
by `--ai-knight-activity`), and as it moves farther away, its activity score
turns negative (at the same rate). The main use of this parameter is to
control the relative value of pawns and inactive knights; because an
inactive knight prevents a player from promoting a better-positioned pawn,
the knight can actually be a liability. The default value is four points
(so a knight eight points from the nearest non-friendly piece is estimated
to be worth the same as a pawn).
* `--ai-construction-site [number]` sets the estimated value of a completed
construction site (a circle of four friendly pieces around an empty point).
Partial construction sites are awarded partial points: 1/16 value for a
construction site with one piece in place, 4/16 value for a construction
site with two pieces in place, and 9/16 value for a construction site with
three pieces in place. The number of construction sites that the AI can
score for each side is limited by the number of additional towers that the
player can usefully build, though that limit may be adjusted by
`--ai-extra-opening-sites`. Additionally, construction sites are
ineligible for scoring if they are not within four points of four friendly
mobile pieces. The default value is 80 decitempi (8 turns).
* `--ai-extra-opening-sites [number]` sets the number of extra construction
site candidates the AI can consider for a side if that side is in an
opening-like position (i.e., has the starting population and no towers). In
some cases, like in a standard game, a lie to the engine like
`--ai-extra-opening-sites 2` can help low-depth search set up better for a
second tower even though that the construction is beyond its horizon. The
default value is 0 extra candidates (no deception).
* `--ai-tower [number]` sets the estimated value of a tower when a player has
at least four mobile pieces. The default value is 125 decitempi (12.5 extra
turns).
* `--ai-endgame-tower [number]` sets the estimated value of a tower when a
player does not have at least four mobile pieces. The game also computes an
appropriate fixed penalty for a player not having four mobile pieces so that
the AI is not incentivized to sacrifice its pawns and knights to make its
towers more valuable. The default value is 700 decitempi (70 extra turns).
* `--ai-dragon-proximity [number]` sets the estimated value of moving a dragon
one point closer to the closest friendly tower when there are at least four
dragons on the board. The default value is 10 decitempi (an extra turn).
* `--ai-circle-dragon [number]` sets the estimated additional value of moving
a dragon next to a friendly tower when there are at least four dragons on
the board. The default value is 100 decitempi (ten extra turns).
* `--ai-crowd-member [number]` sets the estimated additional value of a piece
that has at least three other friendly pieces at most four points away. The
default value is 1 decitempo (one tenth of an extra turn).
* `--ai-defeat [number]` sets the estimated value of defeating an opponent.
The default value is 1,000,000 decitempi (100,000 extra turns).
## Typical Usage
By convention, one usually redirects the output of `generate-boost-game` to
JavaScript files in a `…/src/games` folder and then writes code like
```
import …_GAME from './games/….js';
import …_GAME from './games/….js';
const GAME_LIBRARY = new Map([
…_GAME,
…_GAME,
].map((game) => [game.identifier, game]));
export default GAME_LIBRARY;
```
in `…/src/gameLibrary.js` so that other code can import the game library and
look up games by their identifiers.
# Using `Game` Objects from `@unlsoft/boost-game`
`Game` objects from a game library provide the following fields and methods:
## Constants from Game Generation
These constants are numbers set at code generation time, as described earlier:
* `game.identifier` is the identifier for the game, as described in the
previous section.
* `game.boardWidth` is the width, in points, of the board on which the game is
played.
* `game.boardHeight` is the height, in points, of the board on which the game is
played.
* `game.playerCount` is the number of players in the game (and therefore the
number of non-dragon piece colors needed to play the game).
* `game.populationLimit` is the maximum number of starting pawns given to each
player in a standard board setup.
* `game.towerLimit` is the maximum number of towers that each player may
build. (This limit only affects when construction moves are available to a
player; it does not preclude other code from placing extra towers on the
board.)
## Piece Types
These constants represent the different piece types, which are returned by
`position.getColorAndPieceType` and passed to `position.modified` as described
later:
* `game.dragon` is a constant value used to represent a dragon.
* `game.pawn` is a constant value used to represent a pawn.
* `game.knight` is a constant value used to represent a knight.
* `game.tower` is a constant value used to represent a tower.
## Algebraic Notation
The following fields and helper methods are useful for encoding and decoding
algebraic notation for files, ranks, points, and moves:
* `game.prettifyFile(file)` takes a file as a zero-based 𝑥 coordinate and
returns the corresponding letter in algebraic notation.
* `game.unprettifyFile(file)` takes a file as a letter in algebraic notation
and returns the corresponding zero-based 𝑥 coordinate.
* `game.prettifyRank(rank)` takes a rank as a zero-based 𝑦 coordinate and
returns the corresponding digits in algebraic notation.
* `game.unprettifyRank(rank)` takes a rank as a string of digits in algebraic
notation and returns the corresponding zero-based 𝑦 coordinate.
* `game.noPoint` is the constant string representing no point at all in
algebraic notation.
* `game.prettifyPoint(file, rank)` takes a point as zero-based 𝑥 and 𝑦
coordinates and returns the corresponding algebraic notation. If either
coordinate is `undefined`, it returns `game.noPoint`.
* `game.unprettifyPoint(point)` takes a point in algebraic notation and
returns the corresponding zero-based 𝑥 and 𝑦 coordinates in an `Array`. If
the point is `game.noPoint`, both coordinates will be `undefined`.
* `game.pass` is the constant string representing a pass in algebraic
notation.
* `game.joinPoints(fromPoint, toPoint)` takes two points in algebraic notation
and returns the algebraic notation for a move from `fromPoint` to `toPoint`.
For constructions and promotions, one point may be omitted or set to
`game.noPoint`, or the two points may be set equal to each other. (For
consistency with `game.splitMove` and the main app's UI, the preferred
practice is to give the same point for both arguments.) For passes, both
points should be omitted or set to `game.noPoint`. (For consistency with
`game.splitMove`, the preferred practice is to pass `game.noPoint` for both
arguments.)
* `game.splitMove(move)` takes a move in algebraic notation and returns the
algebraic notation for its "from" and "to" points in an `Array`. For
constructions and promotions, the "from" point will be the same as the "to"
point. For passes, both points will be `game.noPoint`.
* `prettifyMove(fromFile, fromRank, toFile, toRank)` takes a move as
zero-based 𝑥 and 𝑦 coordinates for its "from" and "to" points and returns
the algebraic notation for the move. Coordinates may be omitted or
`undefined` for special moves in the same places where `game.joinPoints`
would take `game.noPoint`.
* `unprettifyMove(move)` takes a move in algebraic notation and returns the
corresponding zero-based 𝑥 and 𝑦 coordinates for its "from" and "to" points.
Coordinates will be repeated for constructions and promotions, and will be
`undefined` for passes.
## Positions
The following fields and helper method are useful for obtaining `Position`
objects corresponding to the game:
* `game.blankPosition` is a position containing no pieces.
* `game.startingPosition` is the game's standard starting position without any
dragons.
* `game.deserializePosition(serialization)` deserializes a position previously
encoded as a string with `position.serialization`, which is described later.
# Using `Position` Objects from `@unlsoft/boost-game`
`Position` objects corresponding to a game provide the following fields and
methods:
## Encodings
* `position.signature` is the position encoded as a single `BigInt`. This
encoding is meant to be used as a reasonably fast hash-table key. (The
position itself cannot be a hash-table key because ES6 does not support
hash-by-value natively.)
* `position.nextSignature` is equivalent to `position.nextTurn.signature` (see
the description of `position.nextTurn` in the "Moves" subsection further
below), but runs faster because it does not actually advance the turn. It
is mostly useful for checking candidate moves for repetitions.
* `position.serialization` is the position serialized as a string. A position
can later be deserialized with `game.deserializePosition` as described
earlier.
## Pieces
* `position.getColorAndPieceType(x, y)` returns the color and piece type of
the piece at the zero-based coordinates `x` and `y` as a two-element
`Array`. Colors are represented by the number of turns until the
corresponding player will play (e.g., `0` for the player whose turn it is
and `1` for the other player in a two-player game), and piece types are
represented with the constants `game.dragon`, `game.pawn`, `game.knight`,
and `game.tower` described earlier. Dragon's colors are always `undefined`.
If there is no piece at the given coordinates, both the color and piece type
will be `undefined`.
* `position.modified(x, y, color, pieceType)` returns a new position
(`Position` objects are immutable) where the piece at the zero-based
coordinates `x` and `y` has been replaced with a piece of the given color
and piece type. As above, the color should be given as the number of turns
until the corresponding player moves next (e.g., `0` for the player whose
turn it is and `1` for the other player in a two-player game), and piece
types should be given as one of the constants `game.dragon`, `game.pawn`,
`game.knight`, or `game.tower`, which are also described earlier. As
special cases, `color` should be `undefined` if `pieceType` is
`game.dragon`, and a point can be cleared by passing `undefined` for both
`color` and `pieceType`.
## Static Evaluation
* `position.getStaticEvaluation(color)` is the static evaluation of the
position from the perspective of the player who is to play in `color` turns
(hence, the possible values for `color` are the same as the values returned
by `position.getColorAndPieceType` or passed to `position.modified`).
Higher static evaluation scores are more favorable for that player. The
score `Infinity` means that the player to move next has already won, while
the score `-Infinity` means that some other player has won. The static
evaluation is finite in all other situations, though it may still be
extremely positive or negative (such as when the player has been defeated,
but other players are still actively competing).
* `position.live` is `true` if the game is still ongoing, `false` if any
player has won.
## Moves
* `position.children` is the list of positions that the current player can
move to, not considering the repetition rule. Note that it is the same
player's turn in the children positions as in the parent position; a turn is
not considered complete until the code uses `position.nextTurn`.
* `position.getMoveTo(child)` returns the algebraic notation for the move from
`position` to `child`.
* `position.getChildByMove(move)` returns the child reached by playing the
move given in algebraic notation.
* `position.nextTurn` is the same as `position`, except that in
`position.nextTurn` it is the next player's turn. So, for example,
`position.getChildByMove('a1a3').nextTurn` would be the position after the
current player played the move `a1a3`.
# Using `Controller` Objects from `@unlsoft/boost-engine`
When `@unlsoft/boost-game` is installed as a development dependency, it provides
two minified files, `…/node_modules/@unlsoft/boost-engine/dist/engine.js` and
`…/node_modules/@unlsoft/boost-engine/dist/engineThread.js` that implement the
engine's web workers. These files should be included as-is in any app that
wants to use the engine; for a `create-react-app` app, the easiest approach is
to symlink them to the `public` folder.
For talking to these web workers, the default export from
`@unlsoft/boost-engine` is a `Controller` class, where `Controller` objects
provide the following methods:
* `new Controller(engineURL, engineThreadURL, gameIdentifier, propertyHandler,
moveHandler)` creates a controller for an engine whose web workers source
code is located at the given URLs and that will play the game identified by
`gameIdentifier`. Two callbacks must be provided:
* `propertyHandler` will be called with a property name and a value
whenever the engine sends a description of itself; see the documentation
for the `id` BEI message below.
* `moveHandler` will be called with a move in algebraic notation whenever
the engine makes a move.
* `controller.setStrength(strength)` sets the engine's strength as close as
possible to `strength` on a scale where `0` is the strength of a theoretical
player that can never win, `1500` is the strength of the average experienced
human player, and `2500` is the strength of a grandmaster.
* `controller.setLine(position, taboo)` sends a position and an array of
preceding positions (which are taboo under the repetition rule) to the
engine.
* `go()` tells the engine to choose a move to play in the last sent position.
The engine's reply will be sent to the `moveHandler` callback once the
engine is done thinking.
* `stop(wantMove)` tells the engine stop thinking early. If `wantMove` is
`true`, the controller will still call `moveHandler` with the best move the
engine found so far; otherwise that move will be discarded.
Under the hood, the `Controller` constructor creates associated web workers, and
its other methods communicate with these web workers using BEI, a protocol
described in the next section.
# The Boost Engine Interface (BEI) Protocol
BEI is a text-based protocol for communication between a controller (a program
that wants to incorporate a Boost-playing AI) and an engine (a Boost-playing
AI). It is based on and simplified from the Arimaa Engine Interface (AEI),
which in turn is based on the Universal Chess Interface (UCI).
BEI is built on asynchronous bidirectional communication of short strings called
**messages** between the controller and engine. For example, a controller might
send messages as lines of text to an engine's standard input and read messages
from the engine's standard output, or the controller and engine might exchange
BEI messages as string payloads in web worker messages.
(`@unlsoft/boost-engine` uses the latter approach.)
Generally the engine should not process later messages until it has finished
processing earlier ones. The two exceptions are pondering (speculatively
searching ahead during an opponent's turn) and thinking (deciding what move to
make during the engine's turn), long-running operations that should effectively
happen "in the background" and not prevent the engine from responding to other
communication.
## Controller-to-Engine Messages
The following messages may be sent by a controller to an engine:
* `bei [value] [value] …` is sent to initiate communication and optionally to
provide engine-specific startup arguments. (It must be possible to
configure an engine to take no startup arguments so that the engine can be
used with an implementation-agnostic controller, but an engine may still opt
to take arguments in other settings.) The engine will reply with a
`protocol` message, possibly some `id` messages, and then `beiok`. The
controller must not send any other communication until it has confirmed that
it is using a compatible protocol version and has received a `beiok`.
* `isready` is sent to ping the engine and ensure that the engine has finished
processing any previous messages. The engine will reply with `readyok`.
* `newgame [identifier]` is sent to start a new game with the given game
identifier. The engine will reply with `known` if it can play that game,
`unknown` if it cannot.
* `setoption [option] [value]` is sent to set the named engine option to the
given value. Currently the only supported option name is `strength`, which
should be followed by a rating on a scale where `0` is the strength of a
theoretical player that can never win, `1500` is the strength of the average
experienced human player, and `2500` is the strength of a grandmaster. If
the engine strength is never set, the engine should default to its strongest
setting; if an engine cannot play at the requested strength, it should set
its strength as close as possible.
* `setline [serialization] [tabooSerialization] [tabooSerialization] …` is
sent to tell the engine about a new board position, which is given by the
first serialization, and all of the previous board positions that affect the
repetition rule, which are give by the following serializations. The
serialization format is the same format as used by `position.serialization`
from `@unlsoft/boost-game`.
* `ponder [turns]` is sent to tell the engine that it will be playing after
the given number of turns and that it may ponder in the background. The
`newgame`, `setline`, `go`, and `stop` messages all stop pondering.
* `go` is sent to tell the engine that it should start thinking in the
background about what move to play in the current position. When it is done
thinking, the engine will respond with `move` message reporting the best
move that it was able to find. The `newgame`, `setline`, `ponder`, and
`stop` messages all stop thinking immediately and trigger a `move` message,
even if the engine was not done considering the position.
* `stop` is sent to tell the engine that it should stop any pondering or
thinking. If the engine is thinking, this will prompt it to reply with a
`move` message.
* `quit` is sent to tell the engine that no further messages will be sent and
that it may exit.
## Engine-to-Controller Messages
The following messages may be sent by an engine to a controller:
* `protocol [version]` is sent as the first message in response to a `bei`
message to tell the controller what version of BEI the engine uses. The
version described here is `1.0.0`.
* `id [property] [value]` is sent in response to a `bei` message to describe
the engine to the controller. Each property should only be sent once.
Three property names are supported:
* The value for the `name` property is the name of the engine.
* The value for the `author` property is the name of the engine's author
(or the names of the engine's authors if there are more than one).
* The value for the `version` property is the version number of the
engine.
* `beiok` is sent to indicate the end of responses to a `bei` message.
* `readyok` is sent to reply to an `isready` message.
* `known` is sent to reply to a `newgame` message when the engine is able to
load the specified game from its game library.
* `unknown` is sent to reply to a `newgame` message when the engine is unable
to load the specified game from its game library.
* `move [move]` is sent to report the best move the engine was able to find
whenever the engine stops thinking. The engine should not assume that this
move will actually be made; if the move is mode, it will receive an
appropriate `setline` message.
* `log [text]` is sent to report a log message from the engine.
# Footrace Generator Design
The design for the footrace generator is as follows:
* The footrace generator necessitated the use of a dynamic programming algorithm
rather than a greedy algorithm. If a greedy were used, it would value the most
movement north but would place the piece in situations that require more moves
to reach the end.
* `findStartingSituationAndFastestMoveSequence(position)` is the main dynamic
programming algorithm.
* The code uses `ONE_LANE_GAME` and `TWO_LANE_GAME` to generate the puzzles.
The algorithm is only used for the `ONE_LANE_GAME` to generate a path.
The `ONE_LANE_GAME` is then mirrored to make the `TWO_LANE_GAME`.
* When the dynamic programming algorithm is run, the board contains pawns and towers.
* Each vertex of the DAG is a `Situation`. These vertices are added to the DAG using
`selectBetterBackpointer()`.
* Each edge of the DAG is a move. The algorithm finds the move leading to a `Situation`
in the `predecessor` index of `backpointersTable`.
* The topological order is determined by comparing the y values of two `Situation` objects.
If there is a tie, the x values are compared.
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment