Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions crates/phase-ai/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,11 @@ pub struct PolicyPenalties {
/// clock progress below that.
#[serde(default = "default_poison_clock_pressure")]
pub poison_clock_pressure: f64,
/// CR 205.2a: card-equivalent weight for advancing graveyard card-type
/// diversity toward a delirium/descend threshold. Strong band when the
/// action supplies the last missing type.
#[serde(default = "default_graveyard_types_progress")]
pub graveyard_types_progress: f64,
}

impl Default for PolicyPenalties {
Expand Down Expand Up @@ -533,10 +538,18 @@ impl Default for PolicyPenalties {
cycling_needed_land_penalty: default_cycling_needed_land_penalty(),
loop_shortcut_winning_declare_bonus: default_loop_shortcut_winning_declare_bonus(),
poison_clock_pressure: default_poison_clock_pressure(),
graveyard_types_progress: default_graveyard_types_progress(),
}
}
}

/// CR 205.2a. Shared by `Default` and `#[serde(default)]` so a tuning artifact
/// written before this field existed still deserializes (`ai_tune` reads the
/// `policy_penalties` section directly into this struct).
fn default_graveyard_types_progress() -> f64 {
2.5
}

fn default_wasted_cast_penalty() -> f64 {
-8.0
}
Expand Down Expand Up @@ -742,6 +755,11 @@ pub const UNTUNED_POLICY_PENALTY_FIELDS: &[(&str, &str)] = &[
load-bearing for correctness, not taste. Promote to ACTIVE only with a \
paired-seed ai-gate calibration.",
),
(
"graveyard_types_progress",
"CR 205.2a delirium-threshold progress weight — awaiting a paired-seed \
ai-gate calibration before promotion to ACTIVE.",
),
(
"artifact_cost_payoff_bonus",
"new ArtifactSynergyPolicy knob; awaiting a paired-seed ai-gate calibration before joining the CMA-ES vector",
Expand Down Expand Up @@ -1543,6 +1561,37 @@ mod tests {
);
}

/// Artifact compatibility: `ai_tune` deserializes a persisted
/// `policy_penalties` section straight into `PolicyPenalties`
/// (`bin/ai_tune.rs`, `TuneGroup::Penalties`), so an artifact written
/// before `graveyard_types_progress` existed must still load — with its own
/// tuned values intact and the new field filled from the shared default.
#[test]
fn policy_penalties_load_pre_graveyard_types_artifact() {
let mut artifact = serde_json::to_value(PolicyPenalties::default()).unwrap();
let object = artifact.as_object_mut().expect("serializes as object");
object
.remove("graveyard_types_progress")
.expect("field must be present before removal");
// A value CMA-ES could plausibly have tuned, to prove the round-trip
// reads the artifact rather than silently falling back to Default.
object.insert("wasted_cast_penalty".into(), serde_json::json!(-3.5));

let loaded: PolicyPenalties = serde_json::from_value(artifact)
.expect("a pre-graveyard_types_progress artifact must still deserialize");
assert_eq!(loaded.wasted_cast_penalty, -3.5, "tuned value preserved");
assert_eq!(
loaded.graveyard_types_progress,
default_graveyard_types_progress(),
"absent field must fall back to the shared default"
);
assert_eq!(
PolicyPenalties::default().graveyard_types_progress,
default_graveyard_types_progress(),
"Default and serde must share one source of truth"
);
}

#[test]
fn every_policy_penalty_is_tuning_registered_or_explicitly_untuned() {
let value = serde_json::to_value(PolicyPenalties::default()).unwrap();
Expand Down
Loading
Loading