Skip to content

Commit ad8ddc0

Browse files
authored
Merge pull request #72 from CurtisTechSolutions/fix/search-truncated-leaves
fix(opponents): treat length-capped states as search leaves
2 parents a3c5364 + 7f064d0 commit ad8ddc0

4 files changed

Lines changed: 38 additions & 3 deletions

File tree

‎crates/apps/gfa-server/src/opponent_tests.rs‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,34 @@ fn players_reject_invalid_limits_and_inconsistent_turns() -> TestResult {
216216
);
217217
Ok(())
218218
}
219+
220+
#[test]
221+
fn search_treats_length_caps_as_leaves_without_changing_game_endings() -> TestResult {
222+
let game = game("chess")?;
223+
for max_plies in [1, 2] {
224+
let config = json!({"max_plies": max_plies});
225+
for algorithm in [Algorithm::Minimax, Algorithm::Mcts] {
226+
let player = SearchOpponent::new(game.clone(), config.clone(), algorithm)?;
227+
let mut state = game.initial_state(&config, 0)?;
228+
for _ in 0..max_plies {
229+
let budget = SearchLimits {
230+
nodes: 128,
231+
depth: 4,
232+
..limits()
233+
};
234+
let first = choose(game.as_ref(), &player, &state, budget, &Fixed)?;
235+
assert_eq!(
236+
first,
237+
choose(game.as_ref(), &player, &state, budget, &Fixed)?
238+
);
239+
assert!(first.info.nodes <= budget.nodes);
240+
let seat = game.current_players(&state)?[0];
241+
state = game.apply(&state, seat, &first.action.json)?.0;
242+
}
243+
assert!(game.is_truncated(&state)?);
244+
assert!(!game.is_terminal(&state)?);
245+
assert_eq!(game.returns(&state)?, vec![0.0, 0.0]);
246+
}
247+
}
248+
Ok(())
249+
}

‎crates/players/gfa-opponents/src/lib.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ impl Opponent for SearchOpponent {
113113
limits.seed,
114114
)?;
115115
if self.game.is_terminal(&state)?
116+
|| self.game.is_truncated(&state)?
116117
|| self.game.current_players(&state)? != [turn.seat]
117118
|| self.game.legal_actions(&state, turn.seat)? != turn.legal_actions
118119
{

‎crates/players/gfa-opponents/src/mcts.rs‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ struct Node {
1010
}
1111

1212
fn node(game: &dyn DynGame, state: Value, rng: &mut SeededRng) -> Result<Node, GameError> {
13-
let seat = if game.is_terminal(&state)? {
13+
let seat = if game.is_terminal(&state)? || game.is_truncated(&state)? {
1414
None
1515
} else {
1616
Some(actor(game, &state)?)
@@ -116,7 +116,10 @@ pub(super) fn choose(
116116
result.info.depth = result.info.depth.max(depth);
117117
// Rollout uses the same per-seat engine rules, with independent randomness.
118118
let mut rollout = tree[current].state.clone();
119-
while depth < budget.limits.depth && !game.is_terminal(&rollout)? {
119+
while depth < budget.limits.depth
120+
&& !game.is_terminal(&rollout)?
121+
&& !game.is_truncated(&rollout)?
122+
{
120123
if !budget.enter() {
121124
break;
122125
}

‎crates/players/gfa-opponents/src/minimax.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ fn search(
5252
if !budget.enter() {
5353
return Ok(None);
5454
}
55-
if depth == 0 || game.is_terminal(state)? {
55+
if depth == 0 || game.is_terminal(state)? || game.is_truncated(state)? {
5656
return Ok(Some((payoff(game, state, root)?, vec![])));
5757
}
5858
let seat = actor(game, state)?;

0 commit comments

Comments
 (0)