feat(ai): project clan_index into PlayerView (clan-conditioning prerequisite)
The clan-conditioned learned policy needs the bound player's clan in its observation, but PlayerView exposed none. Add PlayerView.clan_index: the canonical 0..5 clan index (ai_personalities.json key order: ironhold, goldvein, blackhammer, deepforge, tinkersmith, runesmith; -1 = generalist), projected from PlayerState.clan_id via clan_to_index(). CLAN_ORDER is the shared contract the Python encoder (encoders.py::CLAN_ORDER) must match for the clan one-hot. serde default = -1 so old fixtures/saves deserialize as generalist. Encoder unchanged (doesn't read it yet), so learned_parity stays green. Verified on the DO fleet: mc-player-api 188/188 passed (new clan mapping test + learned_parity + full_game_transcript determinism). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0554ae7389
commit
bbf56c7ab7
2 changed files with 46 additions and 0 deletions
|
|
@ -142,6 +142,40 @@ pub fn project_view_with_vision(
|
|||
pending_events: PendingEventsView::default(),
|
||||
legal_actions,
|
||||
score,
|
||||
clan_index: own.map(|p| clan_to_index(&p.clan_id)).unwrap_or(-1),
|
||||
}
|
||||
}
|
||||
|
||||
/// Canonical clan order — the key order in `ai_personalities.json`. The
|
||||
/// clan-conditioned learned policy one-hots a clan by this index; the Python
|
||||
/// encoder (`encoders.py::CLAN_ORDER`) MUST share this exact order.
|
||||
pub const CLAN_ORDER: [&str; 6] = [
|
||||
"ironhold", "goldvein", "blackhammer", "deepforge", "tinkersmith", "runesmith",
|
||||
];
|
||||
|
||||
/// Map a `PlayerState.clan_id` string to its canonical `0..5` index, or `-1`
|
||||
/// for empty/unknown (generalist).
|
||||
pub fn clan_to_index(clan_id: &str) -> i32 {
|
||||
CLAN_ORDER
|
||||
.iter()
|
||||
.position(|&c| c == clan_id)
|
||||
.map_or(-1, |i| i as i32)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod clan_index_tests {
|
||||
use super::clan_to_index;
|
||||
|
||||
#[test]
|
||||
fn maps_clan_ids_to_canonical_indices() {
|
||||
assert_eq!(clan_to_index("ironhold"), 0);
|
||||
assert_eq!(clan_to_index("goldvein"), 1);
|
||||
assert_eq!(clan_to_index("blackhammer"), 2);
|
||||
assert_eq!(clan_to_index("deepforge"), 3);
|
||||
assert_eq!(clan_to_index("tinkersmith"), 4);
|
||||
assert_eq!(clan_to_index("runesmith"), 5);
|
||||
assert_eq!(clan_to_index(""), -1, "unset clan = generalist");
|
||||
assert_eq!(clan_to_index("nonexistent"), -1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -431,6 +431,17 @@ pub struct PlayerView {
|
|||
pub legal_actions: Vec<LegalActionEntry>,
|
||||
/// Score / standings.
|
||||
pub score: ScoreView,
|
||||
/// Bound player's clan as a canonical `0..5` index — the key order in
|
||||
/// `ai_personalities.json` (ironhold, goldvein, blackhammer, deepforge,
|
||||
/// tinkersmith, runesmith). `-1` = unset/unknown (generalist). The
|
||||
/// clan-conditioned learned policy one-hots this; the Python and Rust
|
||||
/// observation encoders share the same order.
|
||||
#[serde(default = "default_clan_index")]
|
||||
pub clan_index: i32,
|
||||
}
|
||||
|
||||
fn default_clan_index() -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
impl PlayerView {
|
||||
|
|
@ -455,6 +466,7 @@ impl PlayerView {
|
|||
pending_events: PendingEventsView::default(),
|
||||
legal_actions: Vec::new(),
|
||||
score: ScoreView::default(),
|
||||
clan_index: -1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue