Skip to content

Commit 20286c4

Browse files
fix(cfr): replace String validate errors with typed enums (#259)
`RoundActionConfig::validate`, `ConfigurableActionConfig::validate`, and `PreflopChartConfig::validate` all returned `Result<(), String>`, which `AgentConfig::validate` then absorbed into a catch-all `AgentConfigError::ValidationError(String)` variant — violating the "never bare String errors" rule in CLAUDE.md. Introduce `ConfigurableActionConfigError` and `PreflopChartConfigError` thiserror enums, wire them through `AgentConfigError` via `#[from]`, and replace the `ValidationError(String)` catch-all with typed variants (plus a dedicated `PreflopChartPresetUnavailable` variant for the preset-not- supported path). Add unit tests for each validation failure mode.
1 parent 74edd3b commit 20286c4

5 files changed

Lines changed: 161 additions & 38 deletions

File tree

src/arena/agent/config.rs

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,11 @@
9494
//! pub fn validate(&self) -> Result<(), AgentConfigError> {
9595
//! match self {
9696
//! AgentConfig::MyNewAgent { param1, param2 } => {
97-
//! // Validate parameters
98-
//! if *param1 < 0.0 {
99-
//! return Err(AgentConfigError::ValidationError(
100-
//! "param1 must be non-negative".to_string()
101-
//! ));
97+
//! // Validate parameters via a domain-specific error
98+
//! // variant; add a new variant to AgentConfigError if
99+
//! // none of the existing typed variants fits.
100+
//! if *param1 < 0.0 || *param1 > 1.0 {
101+
//! return Err(AgentConfigError::InvalidProbability(*param1));
102102
//! }
103103
//! }
104104
//! // ... other variants ...
@@ -153,8 +153,9 @@ use crate::arena::agent::{
153153
};
154154
use crate::arena::cfr::{
155155
ActionGenerator, BasicCFRActionGenerator, CFRAgentBuilder, CFRState, CfrDepthConfig,
156-
ConfigurableActionConfig, ConfigurableActionGenerator, PreflopChartActionConfig,
157-
PreflopChartActionGenerator, PreflopChartConfig, SimpleActionGenerator, TraversalSet,
156+
ConfigurableActionConfig, ConfigurableActionConfigError, ConfigurableActionGenerator,
157+
PreflopChartActionConfig, PreflopChartActionGenerator, PreflopChartConfig,
158+
PreflopChartConfigError, SimpleActionGenerator, TraversalSet,
158159
};
159160
use crate::arena::{Agent, GameState};
160161
use rand::SeedableRng;
@@ -295,12 +296,9 @@ impl PreflopChartConfigOption {
295296
/// Preset names are not currently supported - use inline configuration.
296297
pub fn resolve(&self) -> Result<PreflopChartConfig, AgentConfigError> {
297298
match self {
298-
PreflopChartConfigOption::Preset(name) => {
299-
Err(AgentConfigError::ValidationError(format!(
300-
"Preset charts are not available. Use inline configuration instead of preset '{}'. See examples/configs/preflop_6max_rfi.json for an example.",
301-
name
302-
)))
303-
}
299+
PreflopChartConfigOption::Preset(name) => Err(
300+
AgentConfigError::PreflopChartPresetUnavailable(name.clone()),
301+
),
304302
PreflopChartConfigOption::Inline(config) => Ok(config.clone()),
305303
}
306304
}
@@ -333,9 +331,21 @@ pub enum AgentConfigError {
333331
#[error("File I/O error: {0}")]
334332
IoError(#[from] std::io::Error),
335333

336-
/// Generic validation error
337-
#[error("Validation error: {0}")]
338-
ValidationError(String),
334+
/// Failed to validate a configurable action generator config.
335+
#[error("invalid configurable action config: {0}")]
336+
ConfigurableActionConfig(#[from] ConfigurableActionConfigError),
337+
338+
/// Failed to validate a preflop chart config.
339+
#[error("invalid preflop chart config: {0}")]
340+
PreflopChartConfig(#[from] PreflopChartConfigError),
341+
342+
/// A preflop chart preset name was supplied, but presets are not yet
343+
/// supported. Use an inline configuration instead.
344+
#[error(
345+
"preflop chart preset '{0}' is not available; use inline configuration instead. \
346+
See examples/configs/preflop_6max_rfi.json for an example"
347+
)]
348+
PreflopChartPresetUnavailable(String),
339349
}
340350

341351
impl AgentConfig {
@@ -369,15 +379,11 @@ impl AgentConfig {
369379
validate_probabilities(percent_call)?;
370380
}
371381
AgentConfig::CfrConfigurable { action_config, .. } => {
372-
action_config
373-
.validate()
374-
.map_err(AgentConfigError::ValidationError)?;
382+
action_config.validate()?;
375383
}
376384
AgentConfig::CfrPreflopChart { preflop_config, .. } => {
377385
let resolved = preflop_config.resolve()?;
378-
resolved
379-
.validate()
380-
.map_err(AgentConfigError::ValidationError)?;
386+
resolved.validate()?;
381387
}
382388
_ => {}
383389
}

src/arena/cfr/action_generator/configurable.rs

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,26 @@
11
use std::sync::Arc;
22

3+
use thiserror::Error;
4+
35
use crate::arena::{GameState, action::AgentAction, game_state::Round};
46

57
use super::super::{CFRState, TraversalState};
68
use super::ActionGenerator;
79

10+
/// Errors produced when validating a [`ConfigurableActionConfig`] or one of
11+
/// its nested [`RoundActionConfig`] entries.
12+
#[derive(Debug, Error, PartialEq)]
13+
pub enum ConfigurableActionConfigError {
14+
/// A `raise_mult` entry was less than 1.0 (raises must be at least the
15+
/// minimum raise size).
16+
#[error("raise_mult must be >= 1.0 (cannot raise less than min raise), got {0}")]
17+
RaiseMultBelowOne(f32),
18+
19+
/// A `pot_mult` entry was negative.
20+
#[error("pot_mult must be non-negative, got {0}")]
21+
PotMultNegative(f32),
22+
}
23+
824
/// Configuration for per-round bet sizing options.
925
#[derive(Debug, Clone)]
1026
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@@ -35,19 +51,16 @@ impl Default for RoundActionConfig {
3551
}
3652

3753
impl RoundActionConfig {
38-
/// Validate the round configuration
39-
pub fn validate(&self) -> Result<(), String> {
54+
/// Validate the round configuration.
55+
pub fn validate(&self) -> Result<(), ConfigurableActionConfigError> {
4056
for &mult in &self.raise_mult {
4157
if mult < 1.0 {
42-
return Err(format!(
43-
"raise_mult must be >= 1.0 (cannot raise less than min raise), got {}",
44-
mult
45-
));
58+
return Err(ConfigurableActionConfigError::RaiseMultBelowOne(mult));
4659
}
4760
}
4861
for &mult in &self.pot_mult {
4962
if mult < 0.0 {
50-
return Err(format!("pot_mult must be non-negative, got {}", mult));
63+
return Err(ConfigurableActionConfigError::PotMultNegative(mult));
5164
}
5265
}
5366
Ok(())
@@ -103,8 +116,8 @@ impl ConfigurableActionConfig {
103116
}
104117
}
105118

106-
/// Validate the configuration
107-
pub fn validate(&self) -> Result<(), String> {
119+
/// Validate the configuration.
120+
pub fn validate(&self) -> Result<(), ConfigurableActionConfigError> {
108121
self.default.validate()?;
109122
if let Some(ref cfg) = self.preflop {
110123
cfg.validate()?;
@@ -293,6 +306,45 @@ mod tests {
293306
}
294307
}
295308

309+
#[test]
310+
fn test_validate_raise_mult_below_one() {
311+
let cfg = RoundActionConfig {
312+
raise_mult: vec![0.5],
313+
..RoundActionConfig::default()
314+
};
315+
assert_eq!(
316+
cfg.validate().unwrap_err(),
317+
ConfigurableActionConfigError::RaiseMultBelowOne(0.5)
318+
);
319+
}
320+
321+
#[test]
322+
fn test_validate_pot_mult_negative() {
323+
let cfg = RoundActionConfig {
324+
pot_mult: vec![-0.1],
325+
..RoundActionConfig::default()
326+
};
327+
assert_eq!(
328+
cfg.validate().unwrap_err(),
329+
ConfigurableActionConfigError::PotMultNegative(-0.1)
330+
);
331+
}
332+
333+
#[test]
334+
fn test_validate_nested_round_override() {
335+
let cfg = ConfigurableActionConfig {
336+
preflop: Some(RoundActionConfig {
337+
raise_mult: vec![0.9],
338+
..RoundActionConfig::default()
339+
}),
340+
..ConfigurableActionConfig::default()
341+
};
342+
assert!(matches!(
343+
cfg.validate().unwrap_err(),
344+
ConfigurableActionConfigError::RaiseMultBelowOne(_)
345+
));
346+
}
347+
296348
#[test]
297349
fn test_configurable_gen_actions_basic() {
298350
let stacks = vec![500.0; 2];

src/arena/cfr/action_generator/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@ use crate::arena::{GameState, action::AgentAction};
1010
use super::{CFRState, TraversalState};
1111

1212
pub use basic::BasicCFRActionGenerator;
13-
pub use configurable::{ConfigurableActionConfig, ConfigurableActionGenerator, RoundActionConfig};
13+
pub use configurable::{
14+
ConfigurableActionConfig, ConfigurableActionConfigError, ConfigurableActionGenerator,
15+
RoundActionConfig,
16+
};
1417
pub use preflop_chart::{
1518
PreflopChartActionConfig, PreflopChartActionGenerator, PreflopChartConfig,
19+
PreflopChartConfigError,
1620
};
1721
pub use simple::SimpleActionGenerator;
1822

src/arena/cfr/action_generator/preflop_chart.rs

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use std::sync::Arc;
99

1010
use serde::{Deserialize, Serialize};
11+
use thiserror::Error;
1112
use tracing::event;
1213

1314
use crate::arena::GameState;
@@ -18,6 +19,22 @@ use crate::holdem::{PreflopActionType, PreflopChart, PreflopHand};
1819

1920
use super::{ActionGenerator, ConfigurableActionConfig, ConfigurableActionGenerator};
2021

22+
/// Errors produced when validating a [`PreflopChartConfig`].
23+
#[derive(Debug, Error, PartialEq)]
24+
pub enum PreflopChartConfigError {
25+
/// At least one preflop chart must be supplied.
26+
#[error("at least one preflop chart is required")]
27+
NoCharts,
28+
29+
/// `raise_size_bb` must be strictly positive.
30+
#[error("raise_size_bb must be positive, got {0}")]
31+
NonPositiveRaiseSizeBb(f32),
32+
33+
/// `three_bet_multiplier` must be strictly positive.
34+
#[error("three_bet_multiplier must be positive, got {0}")]
35+
NonPositiveThreeBetMultiplier(f32),
36+
}
37+
2138
fn default_raise_size_bb() -> f32 {
2239
2.5
2340
}
@@ -131,15 +148,19 @@ impl PreflopChartConfig {
131148
}
132149

133150
/// Validate the configuration.
134-
pub fn validate(&self) -> Result<(), String> {
151+
pub fn validate(&self) -> Result<(), PreflopChartConfigError> {
135152
if self.charts.is_empty() {
136-
return Err("At least one preflop chart is required".to_string());
153+
return Err(PreflopChartConfigError::NoCharts);
137154
}
138155
if self.raise_size_bb <= 0.0 {
139-
return Err("raise_size_bb must be positive".to_string());
156+
return Err(PreflopChartConfigError::NonPositiveRaiseSizeBb(
157+
self.raise_size_bb,
158+
));
140159
}
141160
if self.three_bet_multiplier <= 0.0 {
142-
return Err("three_bet_multiplier must be positive".to_string());
161+
return Err(PreflopChartConfigError::NonPositiveThreeBetMultiplier(
162+
self.three_bet_multiplier,
163+
));
143164
}
144165
Ok(())
145166
}
@@ -389,6 +410,45 @@ mod tests {
389410
use crate::core::Value;
390411
use crate::holdem::{PreflopChart, PreflopStrategy};
391412

413+
#[test]
414+
fn test_validate_no_charts() {
415+
let cfg = PreflopChartConfig {
416+
charts: vec![],
417+
raise_size_bb: 2.5,
418+
three_bet_multiplier: 3.0,
419+
};
420+
assert_eq!(
421+
cfg.validate().unwrap_err(),
422+
PreflopChartConfigError::NoCharts
423+
);
424+
}
425+
426+
#[test]
427+
fn test_validate_non_positive_raise_size() {
428+
let cfg = PreflopChartConfig {
429+
charts: vec![PreflopChart::new()],
430+
raise_size_bb: 0.0,
431+
three_bet_multiplier: 3.0,
432+
};
433+
assert_eq!(
434+
cfg.validate().unwrap_err(),
435+
PreflopChartConfigError::NonPositiveRaiseSizeBb(0.0)
436+
);
437+
}
438+
439+
#[test]
440+
fn test_validate_non_positive_three_bet_multiplier() {
441+
let cfg = PreflopChartConfig {
442+
charts: vec![PreflopChart::new()],
443+
raise_size_bb: 2.5,
444+
three_bet_multiplier: -1.0,
445+
};
446+
assert_eq!(
447+
cfg.validate().unwrap_err(),
448+
PreflopChartConfigError::NonPositiveThreeBetMultiplier(-1.0)
449+
);
450+
}
451+
392452
fn create_test_game_state() -> GameState {
393453
GameStateBuilder::new()
394454
.num_players_with_stack(2, 100.0)

src/arena/cfr/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,9 @@ mod traversal_state;
7171

7272
pub use action_generator::{
7373
ActionGenerator, BasicCFRActionGenerator, ConfigurableActionConfig,
74-
ConfigurableActionGenerator, PreflopChartActionConfig, PreflopChartActionGenerator,
75-
PreflopChartConfig, RoundActionConfig, SimpleActionGenerator,
74+
ConfigurableActionConfigError, ConfigurableActionGenerator, PreflopChartActionConfig,
75+
PreflopChartActionGenerator, PreflopChartConfig, PreflopChartConfigError, RoundActionConfig,
76+
SimpleActionGenerator,
7677
};
7778
pub use action_index_mapper::{
7879
ACTION_IDX_ALL_IN, ACTION_IDX_CALL, ACTION_IDX_FOLD, ACTION_IDX_RAISE_MAX,

0 commit comments

Comments
 (0)