Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
SOFT Core
SOFT 260
Boost Board Game
Commits
0f90e119
Commit
0f90e119
authored
Nov 01, 2021
by
Brady James Garvin
Browse files
Added starter code for the homework on search.
parent
69f08637
Changes
15
Hide whitespace changes
Inline
Side-by-side
README.md
View file @
0f90e119
...
...
@@ -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.
boost-app/src/features/play/analysis.js
View file @
0f90e119
...
...
@@ -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
);
}
...
...
boost-app/src/features/play/analysis.test.js
View file @
0f90e119
...
...
@@ -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
'
],
}],
]);
});
...
...
boost-app/src/features/play/gameTreesSlice.js
View file @
0f90e119
...
...
@@ -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
{
...
...
boost-app/src/features/play/gameTreesSlice.test.js
View file @
0f90e119
...
...
@@ -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
'
:
{
...
...
boost-app/src/features/play/playerTypes.js
View file @
0f90e119
...
...
@@ -28,7 +28,7 @@ const PLAYER_DESCRIPTION_FORMATS = new Map([
}],
]);
function
formatUnknownPlayer
(
player
){
function
formatUnknownPlayer
(
player
)
{
return
[
'
Other
'
,
`
${
player
.
type
}
`
];
}
...
...
boost-engine/package.json
View file @
0f90e119
...
...
@@ -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"
,
...
...
boost-engine/src/controller/controller.js
View file @
0f90e119
...
...
@@ -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
'
:
...
...
boost-engine/src/controller/controller.test.js
View file @
0f90e119
...
...
@@ -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
'
,
()
=>
{
...
...
boost-engine/src/engine/bei.js
View file @
0f90e119
...
...
@@ -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
();
...
...
boost-engine/src/engine/bei.test.js
View file @
0f90e119
...
...
@@ -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
'
,
()
=>
{
...
...
boost-engine/src/engine/collections.js
0 → 100644
View file @
0f90e119
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
;
}
}
}
boost-engine/src/engine/engine.js
View file @
0f90e119
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
}
`
);
});
}
...
...
boost-engine/src/engine/findThreats.js
0 → 100644
View file @
0f90e119
/* 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
'
];
}
boost-game/src/generator.js
View file @
0f90e119
...
...
@@ -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
)}
}
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment