Skip to content

Commit db4c277

Browse files
authored
Merge pull request #41 from CurtisTechSolutions/refactor/shared-player-contracts
refactor(core): share player contracts with external adapters
2 parents 6bd68c5 + a4c0064 commit db4c277

3 files changed

Lines changed: 119 additions & 105 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
mod dynamic;
33
mod error;
44
mod game;
5+
mod player;
56
mod rng;
67
mod types;
78

89
pub use dynamic::{DynGame, GameAdapter, GameRegistry};
910
pub use error::{ErrorCode, GameError};
1011
pub use game::Game;
12+
pub use player::{ActionChoice, ChoiceInfo, Clock, Opponent, PlayerTurn, SearchLimits};
1113
pub use rng::SeededRng;
1214
/// Serialization and schema derives for game plugins, without extra direct dependencies.
1315
pub use schemars;

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

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
//! Observation-only player contracts shared by in-process and external adapters.
2+
use crate::{ErrorCode, GameError, LegalAction, Observation, PlayerId};
3+
use serde::{Deserialize, Serialize};
4+
5+
/// Monotonic time supplied by the host, independently of game state.
6+
pub trait Clock: Send + Sync {
7+
/// Milliseconds since an arbitrary fixed origin.
8+
fn now_ms(&self) -> u64;
9+
}
10+
11+
/// All information supplied to a player for one decision.
12+
pub struct PlayerTurn<'a> {
13+
/// Acting seat.
14+
pub seat: PlayerId,
15+
/// Seat-scoped observation; never a complete live match state.
16+
pub observation: &'a Observation,
17+
/// Canonical legal actions for this seat.
18+
pub legal_actions: &'a [LegalAction],
19+
}
20+
21+
/// Hard search limits. Fixed seed and node budgets reproduce decisions.
22+
#[derive(Clone, Copy, Debug)]
23+
pub struct SearchLimits {
24+
/// Maximum visited nodes, including rollout steps.
25+
pub nodes: u64,
26+
/// Maximum minimax depth and MCTS tree/rollout length.
27+
pub depth: u8,
28+
/// Wall clock time allowed, checked before each node.
29+
pub time_ms: u64,
30+
/// Independent planning RNG seed.
31+
pub seed: u64,
32+
}
33+
34+
impl SearchLimits {
35+
/// Provisional resource ladder. Strength calibration is reported separately.
36+
pub fn for_level(level: u8, seed: u64) -> Result<Self, GameError> {
37+
if !(1..=10).contains(&level) {
38+
return Err(invalid("Level must be 1..10"));
39+
}
40+
Ok(Self {
41+
nodes: 128_u64 << level,
42+
depth: level,
43+
time_ms: 5_000,
44+
seed,
45+
})
46+
}
47+
48+
/// Reject zero or excessive resource budgets before scheduling work.
49+
pub fn validate(self) -> Result<Self, GameError> {
50+
if self.nodes == 0
51+
|| self.nodes > 1_000_000
52+
|| self.depth == 0
53+
|| self.depth > 64
54+
|| self.time_ms == 0
55+
|| self.time_ms > 60_000
56+
{
57+
return Err(invalid(
58+
"Search requires 1..1000000 nodes, 1..64 depth and 1..60000 ms",
59+
));
60+
}
61+
Ok(self)
62+
}
63+
}
64+
65+
/// Search diagnostics accompanying a chosen move.
66+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
67+
68+
pub struct ChoiceInfo {
69+
/// Optional engine-provided solving explanation.
70+
#[serde(default, skip_serializing_if = "Option::is_none")]
71+
pub advice: Option<crate::AdviceInfo>,
72+
/// Algorithm identifier.
73+
pub algorithm: String,
74+
/// Number of visited nodes.
75+
pub nodes: u64,
76+
/// Deepest completed minimax iteration or deepest MCTS path.
77+
pub depth: u8,
78+
/// Root player's expected return, absent without completed search.
79+
pub evaluation: Option<f64>,
80+
/// Canonical principal variation, beginning with the chosen move.
81+
pub principal_variation: Vec<String>,
82+
/// Whether node or time limits interrupted searching.
83+
pub budget_exhausted: bool,
84+
}
85+
86+
/// One legal move and optional explanatory diagnostics.
87+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
88+
89+
pub struct ActionChoice {
90+
/// Canonical action selected from the supplied legal list.
91+
pub action: LegalAction,
92+
/// Search diagnostics.
93+
pub info: ChoiceInfo,
94+
}
95+
96+
/// A player that receives only its observation, legal actions and search clock.
97+
pub trait Opponent: Send + Sync {
98+
/// Choose one legal action without modifying the match.
99+
fn choose_action(
100+
&self,
101+
turn: &PlayerTurn<'_>,
102+
limits: SearchLimits,
103+
clock: &dyn Clock,
104+
) -> Result<ActionChoice, GameError>;
105+
}
106+
107+
fn invalid(message: &str) -> GameError {
108+
GameError::new(
109+
ErrorCode::InvalidConfig,
110+
message,
111+
"Use a supported opponent and bounded limits.",
112+
)
113+
}

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

Lines changed: 4 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,16 @@ mod mcts;
33
mod minimax;
44
mod reference;
55
pub use reference::ReferenceOpponent;
6+
// Re-export the original paths for downstream source compatibility.
7+
pub use gfa_core::{ActionChoice, ChoiceInfo, Clock, Opponent, PlayerTurn, SearchLimits};
68

7-
use gfa_core::serde::{Deserialize, Serialize};
89
use gfa_core::serde_json::Value;
910
use gfa_core::{
10-
DynGame, ErrorCode, GameError, Information, LegalAction, Observation, PlayerId, SeededRng,
11-
TurnStructure, Viewer,
11+
DynGame, ErrorCode, GameError, Information, LegalAction, PlayerId, SeededRng, TurnStructure,
12+
Viewer,
1213
};
1314
use std::{sync::Arc, time::Instant};
1415

15-
/// Monotonic time supplied by the host, independently of game state.
16-
pub trait Clock: Send + Sync {
17-
/// Milliseconds since an arbitrary fixed origin.
18-
fn now_ms(&self) -> u64;
19-
}
20-
2116
/// System monotonic clock for interactive play.
2217
pub struct SystemClock(Instant);
2318

@@ -33,102 +28,6 @@ impl Clock for SystemClock {
3328
}
3429
}
3530

36-
/// All information supplied to a player for one decision.
37-
pub struct PlayerTurn<'a> {
38-
/// Acting seat.
39-
pub seat: PlayerId,
40-
/// Seat-scoped observation; never a complete live match state.
41-
pub observation: &'a Observation,
42-
/// Canonical legal actions for this seat.
43-
pub legal_actions: &'a [LegalAction],
44-
}
45-
46-
/// Hard search limits. Fixed seed and node budgets reproduce decisions.
47-
#[derive(Clone, Copy, Debug)]
48-
pub struct SearchLimits {
49-
/// Maximum visited nodes, including rollout steps.
50-
pub nodes: u64,
51-
/// Maximum minimax depth and MCTS tree/rollout length.
52-
pub depth: u8,
53-
/// Wall clock time allowed, checked before each node.
54-
pub time_ms: u64,
55-
/// Independent planning RNG seed.
56-
pub seed: u64,
57-
}
58-
59-
impl SearchLimits {
60-
/// Provisional resource ladder. Strength calibration is reported separately.
61-
pub fn for_level(level: u8, seed: u64) -> Result<Self, GameError> {
62-
if !(1..=10).contains(&level) {
63-
return Err(invalid("Level must be 1..10"));
64-
}
65-
Ok(Self {
66-
nodes: 128_u64 << level,
67-
depth: level,
68-
time_ms: 5_000,
69-
seed,
70-
})
71-
}
72-
73-
/// Reject zero or excessive resource budgets before scheduling work.
74-
pub fn validate(self) -> Result<Self, GameError> {
75-
if self.nodes == 0
76-
|| self.nodes > 1_000_000
77-
|| self.depth == 0
78-
|| self.depth > 64
79-
|| self.time_ms == 0
80-
|| self.time_ms > 60_000
81-
{
82-
return Err(invalid(
83-
"Search requires 1..1000000 nodes, 1..64 depth and 1..60000 ms",
84-
));
85-
}
86-
Ok(self)
87-
}
88-
}
89-
90-
/// Search diagnostics accompanying a chosen move.
91-
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
92-
#[serde(crate = "gfa_core::serde")]
93-
pub struct ChoiceInfo {
94-
/// Optional engine-provided solving explanation.
95-
#[serde(default, skip_serializing_if = "Option::is_none")]
96-
pub advice: Option<gfa_core::AdviceInfo>,
97-
/// Algorithm identifier.
98-
pub algorithm: String,
99-
/// Number of visited nodes.
100-
pub nodes: u64,
101-
/// Deepest completed minimax iteration or deepest MCTS path.
102-
pub depth: u8,
103-
/// Root player's expected return, absent without completed search.
104-
pub evaluation: Option<f64>,
105-
/// Canonical principal variation, beginning with the chosen move.
106-
pub principal_variation: Vec<String>,
107-
/// Whether node or time limits interrupted searching.
108-
pub budget_exhausted: bool,
109-
}
110-
111-
/// One legal move and optional explanatory diagnostics.
112-
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
113-
#[serde(crate = "gfa_core::serde")]
114-
pub struct ActionChoice {
115-
/// Canonical action selected from the supplied legal list.
116-
pub action: LegalAction,
117-
/// Search diagnostics.
118-
pub info: ChoiceInfo,
119-
}
120-
121-
/// A player that receives only its observation, legal actions and search clock.
122-
pub trait Opponent: Send + Sync {
123-
/// Choose one legal action without modifying the match.
124-
fn choose_action(
125-
&self,
126-
turn: &PlayerTurn<'_>,
127-
limits: SearchLimits,
128-
clock: &dyn Clock,
129-
) -> Result<ActionChoice, GameError>;
130-
}
131-
13231
/// Uniform, reproducible random baseline for any game.
13332
pub struct Random;
13433

0 commit comments

Comments
 (0)