Skip to content

Commit 4ce38a8

Browse files
committed
style: format reference-solver hooks and tests
1 parent d6ecea4 commit 4ce38a8

7 files changed

Lines changed: 155 additions & 42 deletions

File tree

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

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -115,28 +115,68 @@ async fn real_analysis_finds_a_win_without_mutating_history_and_reports_uncalibr
115115
}
116116

117117
#[tokio::test]
118-
async fn sudoku_reference_solver_plays_only_through_generic_analysis_and_moves() -> Result<(), ServerError> {
118+
async fn sudoku_reference_solver_plays_only_through_generic_analysis_and_moves(
119+
) -> Result<(), ServerError> {
119120
let directory = tempfile::tempdir()?;
120121
let app = fixture(&config(&directory)).await?;
121-
let (_, catalog) = call(&app.router, "GET", "/v1/games/sudoku/opponents", Value::Null, None).await?;
122-
assert!(catalog.as_array().ok_or("catalog")?.iter().any(|item| item["id"] == "reference"));
123-
let (_, mut state) = call(&app.router, "POST", "/v1/matches", json!({
124-
"game_id":"sudoku", "config":{"size":4}, "seed":42,
125-
"assists":{"allow_analysis":true}
126-
}), None).await?;
122+
let (_, catalog) = call(
123+
&app.router,
124+
"GET",
125+
"/v1/games/sudoku/opponents",
126+
Value::Null,
127+
None,
128+
)
129+
.await?;
130+
assert!(catalog
131+
.as_array()
132+
.ok_or("catalog")?
133+
.iter()
134+
.any(|item| item["id"] == "reference"));
135+
let (_, mut state) = call(
136+
&app.router,
137+
"POST",
138+
"/v1/matches",
139+
json!({
140+
"game_id":"sudoku", "config":{"size":4}, "seed":42,
141+
"assists":{"allow_analysis":true}
142+
}),
143+
None,
144+
)
145+
.await?;
127146
let id = state["match_id"].as_str().ok_or("id")?.to_owned();
128147
for _ in 0..16 {
129-
if state["terminated"] == true { break; }
130-
let (status, result) = call(&app.router, "POST", "/v1/analysis", json!({
131-
"game_id":"sudoku","from":{"match_id":id,"seat":0},
132-
"opponent":{"id":"reference"},"seed":7
133-
}), None).await?;
148+
if state["terminated"] == true {
149+
break;
150+
}
151+
let (status, result) = call(
152+
&app.router,
153+
"POST",
154+
"/v1/analysis",
155+
json!({
156+
"game_id":"sudoku","from":{"match_id":id,"seat":0},
157+
"opponent":{"id":"reference"},"seed":7
158+
}),
159+
None,
160+
)
161+
.await?;
134162
assert_eq!(status, StatusCode::OK, "{result}");
135-
assert!(result["advice"]["summary"].as_str().is_some_and(|s| !s.is_empty()));
136-
assert!(state["legal_actions"].as_array().ok_or("legal")?.contains(&result["best_moves"][0]));
137-
let (status, moved) = call(&app.router, "POST", &format!("/v1/matches/{id}/actions"), json!({
138-
"seat":0,"turn":state["turn"],"action":result["best_moves"][0]["json"]
139-
}), None).await?;
163+
assert!(result["advice"]["summary"]
164+
.as_str()
165+
.is_some_and(|s| !s.is_empty()));
166+
assert!(state["legal_actions"]
167+
.as_array()
168+
.ok_or("legal")?
169+
.contains(&result["best_moves"][0]));
170+
let (status, moved) = call(
171+
&app.router,
172+
"POST",
173+
&format!("/v1/matches/{id}/actions"),
174+
json!({
175+
"seat":0,"turn":state["turn"],"action":result["best_moves"][0]["json"]
176+
}),
177+
None,
178+
)
179+
.await?;
140180
assert_eq!(status, StatusCode::OK);
141181
state = moved["state"].clone();
142182
}

‎crates/core/gfa-core/src/dynamic.rs‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,11 +197,19 @@ impl<G: Game> DynGame for GameAdapter<G> {
197197
G::supports_reference_advice()
198198
}
199199

200-
fn reference_advice(&self, state: &Value, player: PlayerId) -> Result<Option<crate::Advice<LegalAction>>, GameError> {
200+
fn reference_advice(
201+
&self,
202+
state: &Value,
203+
player: PlayerId,
204+
) -> Result<Option<crate::Advice<LegalAction>>, GameError> {
201205
let state = Self::state(state)?;
202-
let Some(advice) = G::reference_advice(&state, player)? else { return Ok(None); };
206+
let Some(advice) = G::reference_advice(&state, player)? else {
207+
return Ok(None);
208+
};
203209
if !G::legal_actions(&state, player).contains(&advice.action) {
204-
return Err(GameError::illegal("Reference solver returned an illegal action"));
210+
return Err(GameError::illegal(
211+
"Reference solver returned an illegal action",
212+
));
205213
}
206214
Ok(Some(crate::Advice {
207215
action: LegalAction {

‎crates/domain/gfa-service/src/opponents.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ use gfa_api_types::{
55
};
66
use gfa_core::{DynGame, Information, LegalAction, Observation, TurnStructure, Viewer};
77
use gfa_opponents::{
8-
ActionChoice, Algorithm, Clock, Opponent, PlayerTurn, Random, SearchLimits, SearchOpponent, ReferenceOpponent,
8+
ActionChoice, Algorithm, Clock, Opponent, PlayerTurn, Random, ReferenceOpponent, SearchLimits,
9+
SearchOpponent,
910
};
1011
use serde_json::Value;
1112
use std::{future::Future, pin::Pin, sync::Arc};

‎crates/games/gfa-game-sudoku/src/engine.rs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,10 @@ impl Game for Sudoku {
111111
true
112112
}
113113

114-
fn reference_advice(state: &State, player: PlayerId) -> Result<Option<gfa_core::Advice<Action>>, GameError> {
114+
fn reference_advice(
115+
state: &State,
116+
player: PlayerId,
117+
) -> Result<Option<gfa_core::Advice<Action>>, GameError> {
115118
Self::validate_state(state)?;
116119
advice::recommend(state, player)
117120
}

‎crates/games/gfa-game-sudoku/src/engine/advice.rs‎

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,28 @@ use super::{outcome, puzzle_solution};
22
use crate::{hint, Action, Grid, State, Technique};
33
use gfa_core::{serde_json, Advice, AdviceInfo, ErrorCode, GameError, PlayerId};
44

5-
pub(super) fn recommend(state: &State, player: PlayerId) -> Result<Option<Advice<Action>>, GameError> {
5+
pub(super) fn recommend(
6+
state: &State,
7+
player: PlayerId,
8+
) -> Result<Option<Advice<Action>>, GameError> {
69
if player != 0 {
7-
return Err(GameError::new(ErrorCode::NotYourTurn, "Only the solver seat may request a hint", "Use seat 0."));
10+
return Err(GameError::new(
11+
ErrorCode::NotYourTurn,
12+
"Only the solver seat may request a hint",
13+
"Use seat 0.",
14+
));
815
}
916
if outcome(state).is_some() {
1017
return Ok(None);
1118
}
1219
let size = usize::from(state.config.size);
1320
let solution = puzzle_solution(state)?;
14-
if let Some(cell) = state.grid.iter().zip(solution).position(|(&digit, &correct)| digit != 0 && digit != correct) {
21+
if let Some(cell) = state
22+
.grid
23+
.iter()
24+
.zip(solution)
25+
.position(|(&digit, &correct)| digit != 0 && digit != correct)
26+
{
1527
let row = (cell / size + 1) as u8;
1628
let col = (cell % size + 1) as u8;
1729
return Ok(Some(Advice {
@@ -25,23 +37,38 @@ pub(super) fn recommend(state: &State, player: PlayerId) -> Result<Option<Advice
2537
}));
2638
}
2739
let grid = Grid::from_cells(state.config.size, state.grid.clone())?;
28-
let Some(steps) = hint(&grid)? else { return Ok(None); };
29-
let step = steps.last().ok_or_else(|| GameError::position("Hint has no steps"))?;
30-
let placement = step.placements.first().ok_or_else(|| GameError::position("Hint has no placement"))?;
40+
let Some(steps) = hint(&grid)? else {
41+
return Ok(None);
42+
};
43+
let step = steps
44+
.last()
45+
.ok_or_else(|| GameError::position("Hint has no steps"))?;
46+
let placement = step
47+
.placements
48+
.first()
49+
.ok_or_else(|| GameError::position("Hint has no placement"))?;
3150
let row = (placement.cell / size + 1) as u8;
3251
let col = (placement.cell % size + 1) as u8;
3352
let digit = placement.digit;
34-
let technique = serde_json::to_value(step.technique)?.as_str().ok_or_else(|| GameError::position("Hint technique is invalid"))?.to_owned();
53+
let technique = serde_json::to_value(step.technique)?
54+
.as_str()
55+
.ok_or_else(|| GameError::position("Hint technique is invalid"))?
56+
.to_owned();
3557
let is_guess = step.technique == Technique::Guess;
3658
let summary = if is_guess {
3759
format!("Search-backed guess: place {digit} at r{row}c{col}. No supported logical placement remains.")
3860
} else {
39-
format!("Use {} to place {digit} at r{row}c{col}.", technique.replace('_', " "))
61+
format!(
62+
"Use {} to place {digit} at r{row}c{col}.",
63+
technique.replace('_', " ")
64+
)
4065
};
4166
Ok(Some(Advice {
4267
action: Action::Place { row, col, digit },
4368
info: AdviceInfo {
44-
summary, technique, is_guess,
69+
summary,
70+
technique,
71+
is_guess,
4572
details: serde_json::json!({"steps":steps}),
4673
},
4774
}))

‎crates/games/gfa-game-sudoku/src/engine/tests.rs‎

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -242,20 +242,35 @@ fn untrusted_positions_configs_and_action_encodings_are_validated() -> TestResul
242242

243243
#[test]
244244
fn reference_hints_are_legal_explained_and_solve_all_supported_sizes() -> TestResult {
245-
for (size, difficulty) in [(4, crate::Difficulty::Easy), (6, crate::Difficulty::Medium), (9, crate::Difficulty::Expert)] {
246-
let mut state = Sudoku::new_initial_state(&Config { size, difficulty, ..Config::default() }, 42)?;
245+
for (size, difficulty) in [
246+
(4, crate::Difficulty::Easy),
247+
(6, crate::Difficulty::Medium),
248+
(9, crate::Difficulty::Expert),
249+
] {
250+
let mut state = Sudoku::new_initial_state(
251+
&Config {
252+
size,
253+
difficulty,
254+
..Config::default()
255+
},
256+
42,
257+
)?;
247258
let mut guessed = false;
248259
for _ in 0..100 {
249260
let before = serde_json::to_value(&state)?;
250-
let Some(advice) = Sudoku::reference_advice(&state, 0)? else { break; };
261+
let Some(advice) = Sudoku::reference_advice(&state, 0)? else {
262+
break;
263+
};
251264
assert_eq!(before, serde_json::to_value(&state)?);
252265
assert!(!advice.info.summary.is_empty());
253266
assert!(Sudoku::legal_actions(&state, 0).contains(&advice.action));
254267
guessed |= advice.info.is_guess;
255268
Sudoku::apply(&mut state, 0, &advice.action)?;
256269
}
257270
assert_eq!(Sudoku::returns(&state), [1.0], "size {size}");
258-
if difficulty == crate::Difficulty::Expert { assert!(guessed); }
271+
if difficulty == crate::Difficulty::Expert {
272+
assert!(guessed);
273+
}
259274
assert!(Sudoku::reference_advice(&state, 0)?.is_none());
260275
}
261276
Ok(())
@@ -266,7 +281,7 @@ fn reference_hints_identify_wrong_entries_without_exporting_the_answer_grid() ->
266281
let mut state = Sudoku::new_initial_state(&config(), 0)?;
267282
play(&mut state, "r1c3=5")?;
268283
let advice = Sudoku::reference_advice(&state, 0)?.ok_or("hint")?;
269-
assert_eq!(advice.action, Action::Erase { row:1, col:3 });
284+
assert_eq!(advice.action, Action::Erase { row: 1, col: 3 });
270285
assert_eq!(advice.info.technique, "solution_check");
271286
assert!(!serde_json::to_string(&advice.info)?.contains(SOLUTION));
272287
assert!(Sudoku::reference_advice(&state, 1).is_err());
@@ -277,7 +292,11 @@ fn reference_hints_identify_wrong_entries_without_exporting_the_answer_grid() ->
277292
let adapter = gfa_core::GameAdapter::<Sudoku>::default();
278293
use gfa_core::DynGame;
279294
assert!(adapter.supports_reference_advice());
280-
let advice = adapter.reference_advice(&serde_json::to_value(&state)?, 0)?.ok_or("dynamic hint")?;
281-
assert!(adapter.legal_actions(&serde_json::to_value(&state)?, 0)?.contains(&advice.action));
295+
let advice = adapter
296+
.reference_advice(&serde_json::to_value(&state)?, 0)?
297+
.ok_or("dynamic hint")?;
298+
assert!(adapter
299+
.legal_actions(&serde_json::to_value(&state)?, 0)?
300+
.contains(&advice.action));
282301
Ok(())
283302
}

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

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,30 @@ impl ReferenceOpponent {
2020
}
2121

2222
impl Opponent for ReferenceOpponent {
23-
fn choose_action(&self, turn: &PlayerTurn<'_>, limits: SearchLimits, _: &dyn Clock) -> Result<ActionChoice, GameError> {
23+
fn choose_action(
24+
&self,
25+
turn: &PlayerTurn<'_>,
26+
limits: SearchLimits,
27+
_: &dyn Clock,
28+
) -> Result<ActionChoice, GameError> {
2429
let limits = limits.validate()?;
25-
let state = self.game.state_from_observation(&self.config, turn.observation, Viewer::Player(turn.seat), limits.seed)?;
30+
let state = self.game.state_from_observation(
31+
&self.config,
32+
turn.observation,
33+
Viewer::Player(turn.seat),
34+
limits.seed,
35+
)?;
2636
if self.game.legal_actions(&state, turn.seat)? != turn.legal_actions {
2737
return Err(GameError::illegal("Observation and legal actions disagree"));
2838
}
29-
let advice = self.game.reference_advice(&state, turn.seat)?.ok_or_else(|| GameError::illegal("No reference action is available"))?;
39+
let advice = self
40+
.game
41+
.reference_advice(&state, turn.seat)?
42+
.ok_or_else(|| GameError::illegal("No reference action is available"))?;
3043
if !turn.legal_actions.contains(&advice.action) {
31-
return Err(GameError::illegal("Reference solver returned an illegal action"));
44+
return Err(GameError::illegal(
45+
"Reference solver returned an illegal action",
46+
));
3247
}
3348
let mut result = choice(advice.action, "reference");
3449
result.info.advice = Some(advice.info);

0 commit comments

Comments
 (0)