fix(dqn): Bug #2 — epsilon_greedy_action samples 4-branch factored space (Task 2.5)

Stale 0..5 range from pre-2026-04-08 9-level code. Only test paths call
this cold-path fallback, but the stale range was a latent foot-gun and
would mislead anyone reading the code (per feedback_trust_code_not_docs).

Fix: sample dir ∈ [0,3), mag ∈ [0,3), ord ∈ [0,3), urg ∈ [0,3) and encode
as `dir*27 + mag*9 + ord*3 + urg`, matching MEMORY.md 4-branch DQN
architecture (81 factored actions).

Per Track 4 E1 triage tech-debt flag.
This commit is contained in:
jgrusewski
2026-04-22 15:51:18 +02:00
parent ef165e8961
commit 2a7005f29a

View File

@@ -164,8 +164,18 @@ impl DQNTrainer {
use rand::rngs::StdRng;
let epsilon = self.get_epsilon().await? as f32;
let mut rng = StdRng::seed_from_u64(0xAC7_DEF0);
if rng.gen::<f32>() < epsilon { Ok(rng.gen_range(0..5)) }
else {
if rng.gen::<f32>() < epsilon {
// Task 2.5 Bug #2: sample 4-branch factored action space
// (dir × mag × ord × urg = 3×3×3×3 = 81), matching MEMORY.md
// DQN architecture (updated 2026-04-08). The legacy 0..5 range
// predated the 4-branch refactor and was a latent foot-gun.
// Encoding: action = dir*27 + mag*9 + ord*3 + urg.
let dir = rng.gen_range(0..3usize);
let mag = rng.gen_range(0..3usize);
let ord = rng.gen_range(0..3usize);
let urg = rng.gen_range(0..3usize);
Ok(dir * 27 + mag * 9 + ord * 3 + urg)
} else {
let agent = self.agent.read().await;
let indices = agent.batch_greedy_actions(state)?;
let best_action = *indices.first().ok_or_else(|| anyhow::anyhow!("argmax on empty Q-values"))? as usize;