diff --git a/README.md b/README.md index f3f584956d2576d5155cbc6750303f4c49f08aba..92ccded02c4fb216a6eb12ec0a1ea0b4eb445ca2 100644 --- a/README.md +++ b/README.md @@ -1,702 +1,714 @@ -# 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