Commit 0f90e119 authored by Brady James Garvin's avatar Brady James Garvin
Browse files

Added starter code for the homework on search.

parent 69f08637
......@@ -553,6 +553,12 @@ methods:
Higher static evaluation scores are more favorable for that player. A score
equal to `game.victory` means that the player to move next has already won.
* `position.getCircleEvaluation(color)` is the contribution to the static
evaluation for `color` from that player's progress on a dragon circle. It
ignores all other factors, even other player's progress on dragon circles.
A score equal to `game.victory` means that the player has won by completing
a dragon circle.
* `position.inBookBonus` is the adjustment to make to a static evaluation when
a player is in book (see `--ai-in-book` above). This value is provided
separately because the opening book, if any, is the engine's responsibility,
......@@ -636,15 +642,18 @@ provide the following methods:
whenever the engine sends a description of itself (see the documentation
for the `id` BEI message below).
* `moveHandler` will be called with three arguments whenever the engine
* `moveHandler` will be called with four arguments whenever the engine
makes a move (see the documentation for the `move` BEI message below):
* the engine's score for the position in decitempi,
* the chosen move in algebraic notation,
* a list of the moves the engine considers best, including the chosen
move, also in algebraic notation, and
* the engine's score for the position in decitempi.
* a list of moves that the engine considers to be the opponent's
threats, also in algebraic notation.
* `controller.setStrength(strength)` sets the engine's strength as close as
possible to `strength` on a scale where `100` is the strength for giving a
......@@ -662,9 +671,10 @@ provide the following methods:
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.
* `go(wantThreats)` 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. If `wantThreats` is true, the engine will
also report the opponent's threats it has identified.
* `stop(wantMove)` tells the engine stop thinking early. If `wantMove` is
`true`, the controller will still call `moveHandler` with the best move the
......@@ -749,7 +759,9 @@ The following messages may be sent by a controller to an engine:
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
moves that it was able to find (or one move, a pass, if no real moves are
possible because the game has already been won or lost). The `newgame`,
possible because the game has already been won or lost). Alternatively, `go
wantThreats` has the same effect, except that it asks the engine to also
respond with any opponent's threats it has identified. The `newgame`,
`setline`, `ponder`, `go`, and `stop` messages all stop any previous
thinking and trigger a `move` message, even if the engine was not done
considering the position.
......@@ -797,6 +809,8 @@ The following messages may be sent by an engine to a controller:
should not assume that this move will actually be made; if it is, the engine
will receive an appropriate `setline` message. If no moves are possible
because the game has already been won or lost, the engine must send a single
placeholder move: a pass.
placeholder move: a pass. Alternatively, the message may take the form
`move [score] [move] [move] … | [threat] [threat] …` if the engine is
providing a list of opponent's threats that is has identified.
* `log [text]` is sent to report a log message from the engine.
......@@ -28,7 +28,7 @@ function suggest(treeName, depth, game, position, taboo, controller, moveHandler
controller.current.setDepth(depth);
controller.current.setLine(position, taboo);
controller.current.setMoveHandler(moveHandler);
controller.current.go();
controller.current.go(true);
return () => {
if (controller.current !== undefined) {
controller.current.stop();
......@@ -66,12 +66,13 @@ export function Analysis(props) {
}
const nextDepth = getNextDepth(position.analysisDepth, props.analysisDepth);
if (nextDepth !== undefined && nextDepth <= props.analysisDepth) {
const moveHandler = (score, move, moves) => dispatch(setAnalysis({
const moveHandler = (score, move, moves, threats) => dispatch(setAnalysis({
treeName,
positionIdentity: position.identity,
analysisDepth: nextDepth,
advantage: position.ply % game.playerCount === 0 ? score : -score,
suggestions: moves,
threats,
}));
return suggest(treeName, nextDepth, game, position, taboo, controller, moveHandler);
}
......
......@@ -132,7 +132,7 @@ describe('the Analysis component', () => {
[position, taboo],
]);
expect(controller.go.mock.calls).toEqual([
[],
[true],
]);
});
test('advances the analysis depth', () => {
......@@ -190,7 +190,7 @@ describe('the Analysis component', () => {
},
], 16);
const handler = controller.setMoveHandler.mock.calls[0][0];
handler(99, 'mno', ['mno', 'pqr']);
handler(99, 'mno', ['mno', 'pqr'], ['stu']);
expect(setAnalysis.mock.calls).toEqual([
[{
treeName: 'def',
......@@ -198,6 +198,7 @@ describe('the Analysis component', () => {
analysisDepth: 3,
advantage: 99,
suggestions: ['mno', 'pqr'],
threats: ['stu'],
}],
]);
});
......@@ -213,7 +214,7 @@ describe('the Analysis component', () => {
},
], 17);
const handler = controller.setMoveHandler.mock.calls[0][0];
handler(99, 'mno', ['mno', 'pqr']);
handler(99, 'mno', ['mno', 'pqr'], ['stu']);
expect(setAnalysis.mock.calls).toEqual([
[{
treeName: 'def',
......@@ -221,6 +222,7 @@ describe('the Analysis component', () => {
analysisDepth: 3,
advantage: -99,
suggestions: ['mno', 'pqr'],
threats: ['stu'],
}],
]);
});
......
......@@ -86,6 +86,7 @@ function encodePosition(game, eternal, parentIdentity, ply, position, pieces) {
analysisDepth: undefined,
advantage: undefined,
suggestions: [],
threats: [],
mainLineMove: undefined,
mostRecentMove: undefined,
children: {},
......@@ -372,6 +373,7 @@ const gameTreesSlice = createSlice({
analysisDepth,
advantage,
suggestions,
threats,
} = action.payload;
const tree = gameTrees[treeName];
if (tree === undefined || (positionIdentity !== undefined && positionIdentity !== tree.currentPositionIdentity)) {
......@@ -381,6 +383,7 @@ const gameTreesSlice = createSlice({
positionEncoding.analysisDepth = analysisDepth;
positionEncoding.advantage = advantage;
positionEncoding.suggestions = suggestions;
positionEncoding.threats = threats;
},
makeMove: (gameTrees, action) => {
const {
......
......@@ -120,6 +120,7 @@ function dummyEncoding(identity, parent = undefined, move = undefined, signature
analysisDepth: 3,
advantage: 99,
suggestions: ['y0y0', 'z0z0'],
threats: [],
mainLineMove: undefined,
mostRecentMove: undefined,
children: {},
......@@ -138,6 +139,7 @@ function bareEncoding(identity, ply, position, pieces, parentIdentity = null) {
analysisDepth: undefined,
advantage: undefined,
suggestions: [],
threats: [],
mainLineMove: undefined,
mostRecentMove: undefined,
children: {},
......@@ -1854,6 +1856,7 @@ describe('setAnalysis', () => {
analysisDepth: 3,
advantage: 99,
suggestions: ['y0y0', 'z0z0'],
threats: [],
};
const expected = {
...child,
......@@ -1900,6 +1903,7 @@ describe('setAnalysis', () => {
analysisDepth: 3,
advantage: 99,
suggestions: ['y0y0', 'z0z0'],
threats: [],
};
const prestate = {
'def': {
......
......@@ -28,7 +28,7 @@ const PLAYER_DESCRIPTION_FORMATS = new Map([
}],
]);
function formatUnknownPlayer(player){
function formatUnknownPlayer(player) {
return ['Other', `${player.type}`];
}
......
......@@ -10,6 +10,7 @@
"generate:version": "genversion --es6 --semi src/version.js",
"generate:game:demo": "generate-boost-game --demo > ./src/games/demo.js",
"generate:game:two-player": "generate-boost-game > ./src/games/twoPlayer.js",
"generate:game:threats": "generate-boost-game > ./src/games/threats.js",
"generate": "run-p generate:**",
"prelint": "run-s generate",
"lint:js": "eslint --max-warnings 0 ./src",
......
......@@ -71,7 +71,7 @@ export default class Controller extends Communicator {
this.moveHandler = moveHandler;
}
go() {
go(wantThreats = false) {
this._schedule(async() => {
if (!this._thinking) {
if (!await this.playingCheck()) {
......@@ -79,7 +79,11 @@ export default class Controller extends Communicator {
}
this._thinking = true;
this._movesWanted.push(true);
this._sendMessage('go');
if (wantThreats) {
this._sendMessage('go', 'wantThreats');
} else {
this._sendMessage('go');
}
await this._response();
}
});
......@@ -120,7 +124,12 @@ export default class Controller extends Communicator {
if (this._movesWanted.shift()) {
this._thinking = this._movesWanted.length > 0;
const [score, ...moves] = values;
this.moveHandler(Number(score), moves[0], moves);
const splitIndex = moves.findIndex((move) => move === '|');
if (splitIndex < 0) {
this.moveHandler(Number(score), moves[0], moves);
} else {
this.moveHandler(Number(score), moves[0], moves.slice(0, splitIndex), moves.slice(splitIndex + 1));
}
}
break;
case 'log':
......
......@@ -136,6 +136,49 @@ describe('the controller\'s automatic behaviors', () => {
['isready'],
]);
});
test('requests threats', async() => {
const controller = controllerWithMocks('abc', 'def', 'ghi');
controller.go(true);
controller.stop();
await callsTo(controller.worker.postMessage);
expect(controller.worker.postMessage.mock.calls).toEqual([
['bei def'],
['isready'],
]);
controller.receiveMessage('protocol 2.0.0');
controller.receiveMessage('beiok');
controller.receiveMessage('readyok');
await callsTo(controller.worker.postMessage);
expect(controller.worker.postMessage.mock.calls).toEqual([
['bei def'],
['isready'],
['newgame ghi'],
['isready'],
]);
controller.receiveMessage('known');
controller.receiveMessage('readyok');
await callsTo(controller.worker.postMessage);
expect(controller.worker.postMessage.mock.calls).toEqual([
['bei def'],
['isready'],
['newgame ghi'],
['isready'],
['go wantThreats'],
['isready'],
]);
controller.receiveMessage('readyok');
await callsTo(controller.worker.postMessage);
expect(controller.worker.postMessage.mock.calls).toEqual([
['bei def'],
['isready'],
['newgame ghi'],
['isready'],
['go wantThreats'],
['isready'],
['stop'],
['isready'],
]);
});
});
describe('the controller\'s communication checks', () => {
......
......@@ -13,8 +13,12 @@ export default class Engine extends Communicator {
this.thinking = false;
}
_move(score, moves) {
this._sendMessage('move', score, ...moves);
_move(score, moves, threats) {
if (threats !== undefined) {
this._sendMessage('move', score, ...moves, '|', ...threats);
} else {
this._sendMessage('move', score, ...moves);
}
this.thinking = false;
this.unblock();
}
......@@ -55,7 +59,7 @@ export default class Engine extends Communicator {
this.onStop();
}
onGo() {
onGo(wantThreats) {
this.onStop();
this.thinking = true;
}
......@@ -100,7 +104,7 @@ export default class Engine extends Communicator {
this.onPonder(Number(values[0]));
break;
case 'go':
this.onGo();
this.onGo(values[0] === 'wantThreats');
break;
case 'stop':
this.onStop();
......
......@@ -177,7 +177,23 @@ describe('the engine', () => {
engine.receiveMessage('setline ghi');
engine.receiveMessage('go');
expect(engine.onGo.mock.calls).toEqual([
[],
[false],
]);
});
test('understands requests for threats', () => {
const position = {
signature: 'abc',
};
const engine = engineWithMocks();
engine.receiveMessage('bei');
GAME_LIBRARY.get.mockReturnValue({
deserializePosition: jest.fn().mockName('deserializePosition').mockReturnValue(position),
});
engine.receiveMessage('newgame def');
engine.receiveMessage('setline ghi');
engine.receiveMessage('go wantThreats');
expect(engine.onGo.mock.calls).toEqual([
[true],
]);
});
test('stops searching', () => {
......
export class PriorityQueue {
constructor() {
this._vertices = [];
}
get size() {
return this._vertices.length;
}
insert(element, measure) {
let index = this._vertices.length;
while (index > 0) {
const parentIndex = Math.floor((index - 1) / 2);
const parent = this._vertices[parentIndex];
if (parent.measure < measure) {
break;
}
this._vertices[index] = parent;
index = parentIndex;
}
this._vertices[index] = {
element,
measure,
};
}
remove() {
console.assert(this._vertices.length > 0, 'Cannot remove an element from an empty priority queue');
const result = this._vertices[0].element;
const vertex = this._vertices[this._vertices.length - 1];
for (let index = 0; ;) {
this._vertices[index] = vertex;
let swapIndex = index;
for (const candidateIndex of [2 * index + 1, 2 * index + 2]) {
if (candidateIndex < this._vertices.length - 1 &&
this._vertices[candidateIndex].measure < this._vertices[swapIndex].measure) {
swapIndex = candidateIndex;
}
}
if (swapIndex === index) {
this._vertices[index] = vertex;
this._vertices.pop();
return result;
}
this._vertices[index] = this._vertices[swapIndex];
index = swapIndex;
}
}
}
import { version } from '../version.js';
import Engine from './bei.js';
import { findThreats } from './findThreats.js';
// Throughout, a depth of zero actually represents running the engine in its
// teaching-game mode, where it plays as if it expects the opponent to help it.
......@@ -38,6 +39,7 @@ const STRENGTH_DEPTH_PAIRS = [
[1879, 4.5],
[1942, 4.75],
[2000, 5.0],
[2750, 8.0],
];
/* eslint-enable no-magic-numbers */
......@@ -164,7 +166,7 @@ export default class PruningEngine extends Engine {
return moves;
}
async _go() {
async _go(wantThreats) {
if (!this.position.live) {
this._move(
this.position.getStaticEvaluation(0),
......@@ -194,12 +196,16 @@ export default class PruningEngine extends Engine {
bestMoves.push(...candidate.bestMoves);
}
}
this._move(bestScore, this._tiebreakByInfall(bestMoves));
this._move(
bestScore,
this._tiebreakByInfall(bestMoves),
wantThreats ? findThreats(this.game.identifier, this.position) : undefined,
);
}
onGo() {
super.onGo();
this._go().catch((exception) => {
onGo(wantThreats) {
super.onGo(wantThreats);
this._go(wantThreats).catch((exception) => {
this._sendMessage('log', `Exception in engine while choosing move: ${exception}`);
});
}
......
/* eslint-disable no-unused-vars -- remove this comment once findDragonSuggestions is implemented */
import { PriorityQueue } from './collections.js';
import TWO_PLAYER_GAME_FOR_THREAT_ANALYSIS from '../games/threats.js';
const THREAT_ANALYSIS_GAME_LIBRARY = new Map([
TWO_PLAYER_GAME_FOR_THREAT_ANALYSIS,
].map((game) => [game.identifier, game]));
const SEARCH_BUDGET = 8192;
const DRAGON_CIRCLE_SIZE = 4;
export function findThreats(gameIdentifier, position) {
return ['d4f6'];
}
......@@ -416,8 +416,6 @@ class Side {
const viableTowers = this.towers & ${encoding.constructionSites}n & dragons.nearTo4 & ~all.promotionPoints;
if (viableTowers & dragons.boostsBy4) {
this._won = true;
// The circle evaluation in this case should be immaterial in light of \`this._won\`, but safest is to keep it
// high in case any code looks anyway.
this.circleEvaluation = ${tuning.aiVictory};
} else if (viableTowers & dragons.boostsBy3) {
this.circleEvaluation = ${3 * tuning.aiCircleDragon};
......@@ -852,6 +850,10 @@ class Position {
return new Position(newDragons, newSides);
}
getCircleEvaluation(color) {
return this.sides[color].getDragonCircleEvaluation(this.dragons, this.all);
}
_staticallyEvaluate() {
${indent(generateStaticEvaluation(encoding, playerCount, tuning), 4)}
}
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment