1use std::collections::HashMap;
38
39mod rla_types;
40pub use rla_types::*;
41use rla_types::{NStepBuffer, QEntry};
42
43#[derive(Debug)]
53pub struct ReinforcementLearningAgent {
54 config: AgentConfig,
56 state_actions: HashMap<RlState, Vec<RlAction>>,
58 q_table: HashMap<(RlState, RlAction), QEntry>,
60 eligibility: HashMap<(RlState, RlAction), f64>,
62 double_q_toggle: bool,
64 n_step_buf: NStepBuffer,
66 replay: ExperienceReplay,
68 stats: AgentStats,
70 total_visits: u64,
72 last_episode_delta: f64,
74}
75
76impl ReinforcementLearningAgent {
77 pub fn new(config: AgentConfig) -> Self {
81 let replay_cap = config.replay_capacity.max(1);
82 let n = match config.algorithm {
83 AlgorithmType::NStepTD(n) => n.max(1),
84 _ => 1,
85 };
86 Self {
87 replay: ExperienceReplay::new(replay_cap),
88 config,
89 state_actions: HashMap::new(),
90 q_table: HashMap::new(),
91 eligibility: HashMap::new(),
92 double_q_toggle: false,
93 n_step_buf: NStepBuffer::new(n),
94 stats: AgentStats {
95 episodes_run: 0,
96 total_steps: 0,
97 avg_reward: 0.0,
98 best_episode_reward: f64::NEG_INFINITY,
99 convergence_delta: 0.0,
100 },
101 total_visits: 0,
102 last_episode_delta: 0.0,
103 }
104 }
105
106 pub fn register_state(
115 &mut self,
116 state: RlState,
117 actions: Vec<RlAction>,
118 ) -> Result<(), RlAgentError> {
119 if actions.is_empty() {
120 return Err(RlAgentError::InvalidConfig(format!(
121 "state {:?} must have at least one action",
122 state.0
123 )));
124 }
125 let entry = self.state_actions.entry(state.clone()).or_default();
126 for a in actions {
127 self.q_table.entry((state.clone(), a.clone())).or_default();
129 if !entry.contains(&a) {
130 entry.push(a);
131 }
132 }
133 Ok(())
134 }
135
136 pub fn select_action(
143 &self,
144 state: &RlState,
145 rng_seed: &mut u64,
146 ) -> Result<RlAction, RlAgentError> {
147 let actions = self
148 .state_actions
149 .get(state)
150 .ok_or_else(|| RlAgentError::StateNotFound(state.clone()))?;
151
152 match &self.config.policy {
153 AgentPolicy::EpsilonGreedy { epsilon, .. } => {
154 let r = xorshift_f64(rng_seed);
155 if r < *epsilon {
156 Ok(self.random_action(actions, rng_seed))
157 } else {
158 Ok(self.greedy_action(state, actions))
159 }
160 }
161 AgentPolicy::Boltzmann { temperature } => {
162 Ok(self.boltzmann_action(state, actions, *temperature, rng_seed))
163 }
164 AgentPolicy::UCB { c } => Ok(self.ucb_action(state, actions, *c)),
165 AgentPolicy::Random => Ok(self.random_action(actions, rng_seed)),
166 }
167 }
168
169 pub fn best_action(&self, state: &RlState) -> Result<RlAction, RlAgentError> {
174 let actions = self
175 .state_actions
176 .get(state)
177 .ok_or_else(|| RlAgentError::StateNotFound(state.clone()))?;
178 Ok(self.greedy_action(state, actions))
179 }
180
181 pub fn value(&self, state: &RlState) -> f64 {
183 match self.state_actions.get(state) {
184 None => 0.0,
185 Some(actions) => actions
186 .iter()
187 .map(|a| self.q1(state, a))
188 .fold(f64::NEG_INFINITY, f64::max),
189 }
190 }
191
192 pub fn update(&mut self, transition: &Transition) -> Result<f64, RlAgentError> {
202 self.validate_transition(transition)?;
203
204 let delta = match self.config.algorithm.clone() {
205 AlgorithmType::QLearning => self.update_q_learning(transition),
206 AlgorithmType::Sarsa => self.update_sarsa(transition),
207 AlgorithmType::ExpectedSarsa => self.update_expected_sarsa(transition),
208 AlgorithmType::DoubleQLearning => self.update_double_q(transition),
209 AlgorithmType::NStepTD(_) => self.update_n_step(transition),
210 };
211
212 self.decay_eligibility();
214
215 if delta.abs() > self.last_episode_delta {
217 self.last_episode_delta = delta.abs();
218 }
219
220 self.total_visits += 1;
222 let entry = self
223 .q_table
224 .entry((transition.state.clone(), transition.action.clone()))
225 .or_default();
226 entry.visits += 1;
227
228 Ok(delta.abs())
229 }
230
231 pub fn run_episode(
241 &mut self,
242 transitions: Vec<Transition>,
243 _rng_seed: u64,
244 ) -> Result<EpisodeStats, RlAgentError> {
245 if transitions.is_empty() {
246 return Ok(EpisodeStats {
247 total_reward: 0.0,
248 steps: 0,
249 epsilon: self.current_epsilon(),
250 avg_q_value: 0.0,
251 });
252 }
253
254 self.last_episode_delta = 0.0;
255 self.eligibility.clear();
256
257 let mut total_reward = 0.0;
258 let mut q_sum = 0.0;
259 let mut q_count = 0usize;
260
261 for t in &transitions {
262 total_reward += t.reward;
264 let _ = self.update(t)?;
265 let q = self.q1(&t.state, &t.action);
266 q_sum += q;
267 q_count += 1;
268 }
269
270 let steps = transitions.len();
271 let eps = self.current_epsilon();
272
273 self.decay_epsilon();
275
276 let ema_alpha = 0.05_f64;
278 self.stats.avg_reward =
279 self.stats.avg_reward * (1.0 - ema_alpha) + total_reward * ema_alpha;
280 if total_reward > self.stats.best_episode_reward {
281 self.stats.best_episode_reward = total_reward;
282 }
283 self.stats.episodes_run += 1;
284 self.stats.total_steps += steps as u64;
285 self.stats.convergence_delta = self.last_episode_delta;
286
287 let avg_q = if q_count > 0 {
288 q_sum / q_count as f64
289 } else {
290 0.0
291 };
292
293 Ok(EpisodeStats {
294 total_reward,
295 steps,
296 epsilon: eps,
297 avg_q_value: avg_q,
298 })
299 }
300
301 pub fn decay_epsilon(&mut self) {
306 if let AgentPolicy::EpsilonGreedy {
307 ref mut epsilon,
308 decay,
309 min_epsilon,
310 } = self.config.policy
311 {
312 *epsilon = (*epsilon * decay).max(min_epsilon);
313 }
314 }
315
316 pub fn add_experience(&mut self, t: Transition) {
320 self.replay.push(t);
321 }
322
323 pub fn sample_experience(
329 &self,
330 n: usize,
331 rng_seed: u64,
332 ) -> Result<Vec<Transition>, RlAgentError> {
333 let buf_len = self.replay.len();
334 if buf_len < n {
335 return Err(RlAgentError::InsufficientExperience(buf_len));
336 }
337 let mut seed = rng_seed ^ 0xdead_beef_cafe_u64;
338 let mut out = Vec::with_capacity(n);
339 let mut indices: Vec<usize> = (0..buf_len).collect();
341 for i in 0..n {
342 let j = i + (xorshift64(&mut seed) as usize % (buf_len - i));
343 indices.swap(i, j);
344 out.push(self.replay.buffer[indices[i]].clone());
345 }
346 Ok(out)
347 }
348
349 pub fn stats(&self) -> AgentStats {
353 self.stats.clone()
354 }
355
356 fn q1(&self, state: &RlState, action: &RlAction) -> f64 {
362 self.q_table
363 .get(&(state.clone(), action.clone()))
364 .map_or(0.0, |e| e.q1)
365 }
366
367 fn q2(&self, state: &RlState, action: &RlAction) -> f64 {
369 self.q_table
370 .get(&(state.clone(), action.clone()))
371 .map_or(0.0, |e| e.q2)
372 }
373
374 fn max_q1(&self, state: &RlState) -> f64 {
376 self.state_actions
377 .get(state)
378 .map(|acts| {
379 acts.iter()
380 .map(|a| self.q1(state, a))
381 .fold(f64::NEG_INFINITY, f64::max)
382 })
383 .unwrap_or(0.0)
384 }
385
386 fn greedy_action(&self, state: &RlState, actions: &[RlAction]) -> RlAction {
388 actions
389 .iter()
390 .max_by(|a, b| {
391 self.q1(state, a)
392 .partial_cmp(&self.q1(state, b))
393 .unwrap_or(std::cmp::Ordering::Equal)
394 })
395 .cloned()
396 .unwrap_or_else(|| actions[0].clone())
397 }
398
399 fn random_action(&self, actions: &[RlAction], rng_seed: &mut u64) -> RlAction {
401 let idx = xorshift64(rng_seed) as usize % actions.len();
402 actions[idx].clone()
403 }
404
405 fn boltzmann_action(
407 &self,
408 state: &RlState,
409 actions: &[RlAction],
410 temperature: f64,
411 rng_seed: &mut u64,
412 ) -> RlAction {
413 if temperature <= 0.0 {
414 return self.greedy_action(state, actions);
415 }
416 let qs: Vec<f64> = actions.iter().map(|a| self.q1(state, a)).collect();
418 let max_q = qs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
419 let exps: Vec<f64> = qs
420 .iter()
421 .map(|&q| ((q - max_q) / temperature).exp())
422 .collect();
423 let sum: f64 = exps.iter().sum();
424 let r = xorshift_f64(rng_seed) * sum;
425 let mut cumulative = 0.0;
426 for (i, &e) in exps.iter().enumerate() {
427 cumulative += e;
428 if r <= cumulative {
429 return actions[i].clone();
430 }
431 }
432 actions[actions.len() - 1].clone()
433 }
434
435 fn ucb_action(&self, state: &RlState, actions: &[RlAction], c: f64) -> RlAction {
437 let ln_n = if self.total_visits > 0 {
438 (self.total_visits as f64).ln()
439 } else {
440 0.0
441 };
442 actions
443 .iter()
444 .max_by(|a, b| {
445 let visits_a = self
446 .q_table
447 .get(&(state.clone(), (*a).clone()))
448 .map_or(0, |e| e.visits);
449 let visits_b = self
450 .q_table
451 .get(&(state.clone(), (*b).clone()))
452 .map_or(0, |e| e.visits);
453 let ucb_a = self.q1(state, a) + c * (ln_n / (visits_a as f64 + 1.0)).sqrt();
454 let ucb_b = self.q1(state, b) + c * (ln_n / (visits_b as f64 + 1.0)).sqrt();
455 ucb_a
456 .partial_cmp(&ucb_b)
457 .unwrap_or(std::cmp::Ordering::Equal)
458 })
459 .cloned()
460 .unwrap_or_else(|| actions[0].clone())
461 }
462
463 fn expected_q(&self, state: &RlState, actions: &[RlAction]) -> f64 {
465 let n = actions.len() as f64;
466 let eps = match &self.config.policy {
467 AgentPolicy::EpsilonGreedy { epsilon, .. } => *epsilon,
468 _ => 0.0,
469 };
470 let best = self.greedy_action(state, actions);
471 let random_contrib: f64 = actions.iter().map(|a| self.q1(state, a)).sum::<f64>() / n;
472 let greedy_contrib = self.q1(state, &best);
473 eps * random_contrib + (1.0 - eps) * greedy_contrib
474 }
475
476 fn current_epsilon(&self) -> f64 {
478 if let AgentPolicy::EpsilonGreedy { epsilon, .. } = &self.config.policy {
479 *epsilon
480 } else {
481 0.0
482 }
483 }
484
485 fn validate_transition(&self, t: &Transition) -> Result<(), RlAgentError> {
487 let actions = self
488 .state_actions
489 .get(&t.state)
490 .ok_or_else(|| RlAgentError::StateNotFound(t.state.clone()))?;
491 if !actions.contains(&t.action) {
492 return Err(RlAgentError::ActionNotFound {
493 state: t.state.clone(),
494 action: t.action.clone(),
495 });
496 }
497 if !t.done && !self.state_actions.contains_key(&t.next_state) {
499 return Err(RlAgentError::StateNotFound(t.next_state.clone()));
500 }
501 Ok(())
502 }
503
504 fn update_q_learning(&mut self, t: &Transition) -> f64 {
508 let alpha = self.config.alpha;
509 let gamma = self.config.gamma;
510 let q_sa = self.q1(&t.state, &t.action);
511 let max_next = if t.done {
512 0.0
513 } else {
514 self.max_q1(&t.next_state)
515 };
516 let delta = t.reward + gamma * max_next - q_sa;
517 let entry = self
518 .q_table
519 .entry((t.state.clone(), t.action.clone()))
520 .or_default();
521 entry.q1 += alpha * delta;
522 delta
523 }
524
525 fn update_sarsa(&mut self, t: &Transition) -> f64 {
528 let alpha = self.config.alpha;
529 let gamma = self.config.gamma;
530 let q_sa = self.q1(&t.state, &t.action);
531 let q_next = if t.done {
533 0.0
534 } else {
535 let next_actions = self
536 .state_actions
537 .get(&t.next_state)
538 .cloned()
539 .unwrap_or_default();
540 if next_actions.is_empty() {
541 0.0
542 } else {
543 let next_a = self.greedy_action(&t.next_state, &next_actions);
544 self.q1(&t.next_state, &next_a)
545 }
546 };
547 let delta = t.reward + gamma * q_next - q_sa;
548 let lambda = self.config.lambda;
550 *self
551 .eligibility
552 .entry((t.state.clone(), t.action.clone()))
553 .or_insert(0.0) += 1.0;
554 let keys: Vec<(RlState, RlAction)> = self.eligibility.keys().cloned().collect();
556 for key in keys {
557 let e = *self.eligibility.get(&key).unwrap_or(&0.0);
558 let entry = self.q_table.entry(key.clone()).or_default();
559 entry.q1 += alpha * delta * e;
560 let e_ref = self.eligibility.entry(key).or_insert(0.0);
561 *e_ref *= gamma * lambda;
562 }
563 delta
564 }
565
566 fn update_expected_sarsa(&mut self, t: &Transition) -> f64 {
568 let alpha = self.config.alpha;
569 let gamma = self.config.gamma;
570 let q_sa = self.q1(&t.state, &t.action);
571 let expected_next = if t.done {
572 0.0
573 } else {
574 let next_actions = self
575 .state_actions
576 .get(&t.next_state)
577 .cloned()
578 .unwrap_or_default();
579 if next_actions.is_empty() {
580 0.0
581 } else {
582 self.expected_q(&t.next_state, &next_actions)
583 }
584 };
585 let delta = t.reward + gamma * expected_next - q_sa;
586 let entry = self
587 .q_table
588 .entry((t.state.clone(), t.action.clone()))
589 .or_default();
590 entry.q1 += alpha * delta;
591 delta
592 }
593
594 fn update_double_q(&mut self, t: &Transition) -> f64 {
596 let alpha = self.config.alpha;
597 let gamma = self.config.gamma;
598 self.double_q_toggle = !self.double_q_toggle;
599 let delta = if self.double_q_toggle {
600 let q1_sa = self.q1(&t.state, &t.action);
602 let max_next = if t.done {
603 0.0
604 } else {
605 let next_actions = self
607 .state_actions
608 .get(&t.next_state)
609 .cloned()
610 .unwrap_or_default();
611 if next_actions.is_empty() {
612 0.0
613 } else {
614 let best_a = self.greedy_action(&t.next_state, &next_actions);
615 self.q2(&t.next_state, &best_a)
616 }
617 };
618 let delta = t.reward + gamma * max_next - q1_sa;
619 let entry = self
620 .q_table
621 .entry((t.state.clone(), t.action.clone()))
622 .or_default();
623 entry.q1 += alpha * delta;
624 delta
625 } else {
626 let q2_sa = self.q2(&t.state, &t.action);
628 let max_next = if t.done {
629 0.0
630 } else {
631 let next_actions = self
633 .state_actions
634 .get(&t.next_state)
635 .cloned()
636 .unwrap_or_default();
637 if next_actions.is_empty() {
638 0.0
639 } else {
640 let best_a = self
641 .state_actions
642 .get(&t.next_state)
643 .and_then(|acts| {
644 acts.iter()
645 .max_by(|a, b| {
646 self.q2(&t.next_state, a)
647 .partial_cmp(&self.q2(&t.next_state, b))
648 .unwrap_or(std::cmp::Ordering::Equal)
649 })
650 .cloned()
651 })
652 .unwrap_or_else(|| next_actions[0].clone());
653 self.q1(&t.next_state, &best_a)
654 }
655 };
656 let delta = t.reward + gamma * max_next - q2_sa;
657 let entry = self
658 .q_table
659 .entry((t.state.clone(), t.action.clone()))
660 .or_default();
661 entry.q2 += alpha * delta;
662 delta
663 };
664 delta
665 }
666
667 fn update_n_step(&mut self, t: &Transition) -> f64 {
670 self.n_step_buf.transitions.push_back(t.clone());
671 if !self.n_step_buf.ready() {
672 return 0.0;
673 }
674 let oldest = match self.n_step_buf.transitions.pop_front() {
675 Some(o) => o,
676 None => return 0.0,
677 };
678 let alpha = self.config.alpha;
679 let gamma = self.config.gamma;
680 let q_sa = self.q1(&oldest.state, &oldest.action);
681 let tail = self
683 .n_step_buf
684 .transitions
685 .back()
686 .map(|last| {
687 if last.done {
688 0.0
689 } else {
690 self.max_q1(&last.next_state)
691 }
692 })
693 .unwrap_or(0.0);
694 let g = self.n_step_buf.n_step_return(gamma, tail);
695 let delta = g - q_sa;
696 let entry = self
697 .q_table
698 .entry((oldest.state.clone(), oldest.action.clone()))
699 .or_default();
700 entry.q1 += alpha * delta;
701 delta
702 }
703
704 fn decay_eligibility(&mut self) {
706 let gamma = self.config.gamma;
707 let lambda = self.config.lambda;
708 let factor = gamma * lambda;
709 if (factor - 0.0).abs() < f64::EPSILON {
710 self.eligibility.clear();
711 return;
712 }
713 for e in self.eligibility.values_mut() {
714 *e *= factor;
715 }
716 self.eligibility.retain(|_, e| e.abs() > 1e-10);
718 }
719}
720
721#[cfg(test)]
726mod tests {
727 use super::*;
728
729 fn s(name: &str) -> RlState {
732 RlState(name.to_string())
733 }
734
735 fn a(name: &str) -> RlAction {
736 RlAction(name.to_string())
737 }
738
739 fn two_state_agent(algo: AlgorithmType, policy: AgentPolicy) -> ReinforcementLearningAgent {
740 let config = AgentConfig {
741 algorithm: algo,
742 policy,
743 alpha: 0.5,
744 gamma: 0.9,
745 lambda: 0.8,
746 replay_capacity: 100,
747 batch_size: 8,
748 };
749 let mut agent = ReinforcementLearningAgent::new(config);
750 agent
751 .register_state(s("A"), vec![a("left"), a("right")])
752 .expect("test: should succeed");
753 agent
754 .register_state(s("B"), vec![a("left"), a("right")])
755 .expect("test: should succeed");
756 agent
757 }
758
759 fn simple_transition(done: bool) -> Transition {
760 Transition {
761 state: s("A"),
762 action: a("left"),
763 reward: 1.0,
764 next_state: s("B"),
765 done,
766 }
767 }
768
769 #[test]
772 fn test_register_state_basic() {
773 let mut agent = ReinforcementLearningAgent::new(AgentConfig::default());
774 agent
775 .register_state(s("s0"), vec![a("up"), a("down")])
776 .expect("test: should succeed");
777 assert!(agent.state_actions.contains_key(&s("s0")));
778 assert_eq!(agent.state_actions[&s("s0")].len(), 2);
779 }
780
781 #[test]
782 fn test_register_state_empty_actions_error() {
783 let mut agent = ReinforcementLearningAgent::new(AgentConfig::default());
784 let result = agent.register_state(s("s0"), vec![]);
785 assert!(matches!(result, Err(RlAgentError::InvalidConfig(_))));
786 }
787
788 #[test]
789 fn test_register_state_dedup_actions() {
790 let mut agent = ReinforcementLearningAgent::new(AgentConfig::default());
791 agent
792 .register_state(s("s0"), vec![a("up"), a("up"), a("down")])
793 .expect("test: should succeed");
794 assert_eq!(agent.state_actions[&s("s0")].len(), 2);
795 }
796
797 #[test]
798 fn test_register_state_multiple_calls_merge() {
799 let mut agent = ReinforcementLearningAgent::new(AgentConfig::default());
800 agent
801 .register_state(s("s0"), vec![a("up")])
802 .expect("test: should succeed");
803 agent
804 .register_state(s("s0"), vec![a("down")])
805 .expect("test: should succeed");
806 assert_eq!(agent.state_actions[&s("s0")].len(), 2);
807 }
808
809 #[test]
812 fn test_epsilon_greedy_high_epsilon_mostly_random() {
813 let policy = AgentPolicy::EpsilonGreedy {
814 epsilon: 1.0,
815 decay: 1.0,
816 min_epsilon: 1.0,
817 };
818 let agent = two_state_agent(AlgorithmType::QLearning, policy);
819 let mut seed = 42u64;
820 for _ in 0..20 {
822 let act = agent
823 .select_action(&s("A"), &mut seed)
824 .expect("test: should succeed");
825 assert!(act == a("left") || act == a("right"));
826 }
827 }
828
829 #[test]
830 fn test_epsilon_greedy_zero_epsilon_greedy() {
831 let policy = AgentPolicy::EpsilonGreedy {
832 epsilon: 0.0,
833 decay: 1.0,
834 min_epsilon: 0.0,
835 };
836 let mut agent = two_state_agent(AlgorithmType::QLearning, policy);
837 agent.q_table.entry((s("A"), a("right"))).or_default().q1 = 10.0;
839 let mut seed = 999u64;
840 let act = agent
841 .select_action(&s("A"), &mut seed)
842 .expect("test: should succeed");
843 assert_eq!(act, a("right"));
844 }
845
846 #[test]
847 fn test_epsilon_greedy_state_not_found() {
848 let policy = AgentPolicy::EpsilonGreedy {
849 epsilon: 0.1,
850 decay: 0.99,
851 min_epsilon: 0.01,
852 };
853 let agent = two_state_agent(AlgorithmType::QLearning, policy);
854 let mut seed = 1u64;
855 let result = agent.select_action(&s("UNKNOWN"), &mut seed);
856 assert!(matches!(result, Err(RlAgentError::StateNotFound(_))));
857 }
858
859 #[test]
862 fn test_boltzmann_returns_valid_action() {
863 let policy = AgentPolicy::Boltzmann { temperature: 1.0 };
864 let agent = two_state_agent(AlgorithmType::QLearning, policy);
865 let mut seed = 7u64;
866 for _ in 0..30 {
867 let act = agent
868 .select_action(&s("A"), &mut seed)
869 .expect("test: should succeed");
870 assert!(act == a("left") || act == a("right"));
871 }
872 }
873
874 #[test]
875 fn test_boltzmann_zero_temperature_greedy() {
876 let policy = AgentPolicy::Boltzmann { temperature: 0.0 };
877 let mut agent = two_state_agent(AlgorithmType::QLearning, policy);
878 agent.q_table.entry((s("A"), a("left"))).or_default().q1 = 5.0;
879 agent.q_table.entry((s("A"), a("right"))).or_default().q1 = -1.0;
880 let mut seed = 0u64;
881 let act = agent
882 .select_action(&s("A"), &mut seed)
883 .expect("test: should succeed");
884 assert_eq!(act, a("left"));
885 }
886
887 #[test]
888 fn test_boltzmann_high_temperature_distribution() {
889 let policy = AgentPolicy::Boltzmann {
891 temperature: 1000.0,
892 };
893 let agent = two_state_agent(AlgorithmType::QLearning, policy);
894 let mut seed = 13u64;
895 let mut left = 0u32;
896 let mut right = 0u32;
897 for _ in 0..200 {
898 match agent
899 .select_action(&s("A"), &mut seed)
900 .expect("test: should succeed")
901 {
902 x if x == a("left") => left += 1,
903 _ => right += 1,
904 }
905 }
906 assert!(left > 0);
908 assert!(right > 0);
909 }
910
911 #[test]
914 fn test_ucb_returns_valid_action() {
915 let policy = AgentPolicy::UCB { c: 1.0 };
916 let agent = two_state_agent(AlgorithmType::QLearning, policy);
917 let act = agent
918 .select_action(&s("A"), &mut 0u64)
919 .expect("test: should succeed");
920 assert!(act == a("left") || act == a("right"));
921 }
922
923 #[test]
924 fn test_ucb_with_many_visits() {
925 let policy = AgentPolicy::UCB { c: 0.5 };
926 let mut agent = two_state_agent(AlgorithmType::QLearning, policy);
927 agent.total_visits = 1000;
928 agent.q_table.entry((s("A"), a("right"))).or_default().q1 = 2.0;
929 agent
930 .q_table
931 .entry((s("A"), a("right")))
932 .or_default()
933 .visits = 500;
934 agent.q_table.entry((s("A"), a("left"))).or_default().visits = 500;
935 let act = agent
936 .select_action(&s("A"), &mut 0u64)
937 .expect("test: should succeed");
938 assert!(act == a("left") || act == a("right"));
939 }
940
941 #[test]
944 fn test_random_policy_all_actions_reachable() {
945 let agent = two_state_agent(AlgorithmType::QLearning, AgentPolicy::Random);
946 let mut seed = 17u64;
947 let mut seen_left = false;
948 let mut seen_right = false;
949 for _ in 0..100 {
950 match agent
951 .select_action(&s("A"), &mut seed)
952 .expect("test: should succeed")
953 {
954 x if x == a("left") => seen_left = true,
955 _ => seen_right = true,
956 }
957 }
958 assert!(seen_left && seen_right);
959 }
960
961 #[test]
964 fn test_qlearning_update_increases_q() {
965 let mut agent = two_state_agent(AlgorithmType::QLearning, AgentPolicy::Random);
966 let t = simple_transition(false);
967 let before = agent.q1(&s("A"), &a("left"));
968 let delta = agent.update(&t).expect("test: TD update should succeed");
969 let after = agent.q1(&s("A"), &a("left"));
970 assert!(delta >= 0.0);
971 assert!(after > before);
972 }
973
974 #[test]
975 fn test_qlearning_terminal_no_bootstrap() {
976 let mut agent = two_state_agent(AlgorithmType::QLearning, AgentPolicy::Random);
977 let t = simple_transition(true);
978 agent.update(&t).expect("test: TD update should succeed");
979 assert!((agent.q1(&s("A"), &a("left")) - 0.5).abs() < 1e-9);
981 }
982
983 #[test]
984 fn test_qlearning_converges_to_optimal() {
985 let mut agent = two_state_agent(AlgorithmType::QLearning, AgentPolicy::Random);
986 let t_right = Transition {
987 state: s("A"),
988 action: a("right"),
989 reward: 10.0,
990 next_state: s("B"),
991 done: true,
992 };
993 let t_left = Transition {
994 state: s("A"),
995 action: a("left"),
996 reward: 1.0,
997 next_state: s("B"),
998 done: true,
999 };
1000 for _ in 0..50 {
1001 agent
1002 .update(&t_right)
1003 .expect("test: TD update should succeed");
1004 agent
1005 .update(&t_left)
1006 .expect("test: TD update should succeed");
1007 }
1008 assert!(agent.q1(&s("A"), &a("right")) > agent.q1(&s("A"), &a("left")));
1009 assert_eq!(
1010 agent.best_action(&s("A")).expect("test: should succeed"),
1011 a("right")
1012 );
1013 }
1014
1015 #[test]
1018 fn test_sarsa_update_basic() {
1019 let mut agent = two_state_agent(AlgorithmType::Sarsa, AgentPolicy::Random);
1020 let t = simple_transition(false);
1021 let delta = agent.update(&t).expect("test: TD update should succeed");
1022 assert!(delta >= 0.0);
1023 }
1024
1025 #[test]
1026 fn test_sarsa_eligibility_traces_populated() {
1027 let mut agent = two_state_agent(AlgorithmType::Sarsa, AgentPolicy::Random);
1028 let t = simple_transition(false);
1029 agent.update(&t).expect("test: TD update should succeed");
1030 assert!(!agent.eligibility.is_empty());
1032 }
1033
1034 #[test]
1035 fn test_sarsa_terminal_state() {
1036 let mut agent = two_state_agent(AlgorithmType::Sarsa, AgentPolicy::Random);
1037 let t = simple_transition(true);
1038 agent.update(&t).expect("test: TD update should succeed");
1039 assert!(agent.q1(&s("A"), &a("left")) > 0.0);
1040 }
1041
1042 #[test]
1045 fn test_expected_sarsa_basic() {
1046 let policy = AgentPolicy::EpsilonGreedy {
1047 epsilon: 0.1,
1048 decay: 0.99,
1049 min_epsilon: 0.01,
1050 };
1051 let mut agent = two_state_agent(AlgorithmType::ExpectedSarsa, policy);
1052 let t = simple_transition(false);
1053 let delta = agent.update(&t).expect("test: TD update should succeed");
1054 assert!(delta >= 0.0);
1055 assert!(agent.q1(&s("A"), &a("left")) != 0.0);
1056 }
1057
1058 #[test]
1059 fn test_expected_sarsa_terminal() {
1060 let policy = AgentPolicy::EpsilonGreedy {
1061 epsilon: 0.1,
1062 decay: 0.99,
1063 min_epsilon: 0.01,
1064 };
1065 let mut agent = two_state_agent(AlgorithmType::ExpectedSarsa, policy);
1066 let t = simple_transition(true);
1067 agent.update(&t).expect("test: TD update should succeed");
1068 assert!((agent.q1(&s("A"), &a("left")) - 0.5).abs() < 1e-9);
1069 }
1070
1071 #[test]
1074 fn test_double_q_updates_alternating_tables() {
1075 let mut agent = two_state_agent(AlgorithmType::DoubleQLearning, AgentPolicy::Random);
1076 let t = simple_transition(false);
1077 agent.update(&t).expect("test: TD update should succeed"); let q1_after_1 = agent.q1(&s("A"), &a("left"));
1079 let q2_after_1 = agent.q2(&s("A"), &a("left"));
1080 agent.update(&t).expect("test: TD update should succeed"); let q2_after_2 = agent.q2(&s("A"), &a("left"));
1082 assert!((agent.q1(&s("A"), &a("left")) - q1_after_1).abs() < 1e-12);
1084 assert!(q2_after_2 != q2_after_1);
1086 }
1087
1088 #[test]
1089 fn test_double_q_terminal() {
1090 let mut agent = two_state_agent(AlgorithmType::DoubleQLearning, AgentPolicy::Random);
1091 let t = simple_transition(true);
1092 agent.update(&t).expect("test: TD update should succeed");
1093 assert!(agent.q1(&s("A"), &a("left")) != 0.0 || agent.q2(&s("A"), &a("left")) != 0.0);
1095 }
1096
1097 #[test]
1100 fn test_nstep_td_returns_zero_before_n_steps() {
1101 let config = AgentConfig {
1102 algorithm: AlgorithmType::NStepTD(3),
1103 policy: AgentPolicy::Random,
1104 alpha: 0.5,
1105 gamma: 0.9,
1106 lambda: 0.0,
1107 replay_capacity: 100,
1108 batch_size: 8,
1109 };
1110 let mut agent = ReinforcementLearningAgent::new(config);
1111 agent
1112 .register_state(s("A"), vec![a("left"), a("right")])
1113 .expect("test: should succeed");
1114 agent
1115 .register_state(s("B"), vec![a("left"), a("right")])
1116 .expect("test: should succeed");
1117
1118 let t = simple_transition(false);
1119 let d1 = agent.update(&t).expect("test: TD update should succeed");
1120 assert_eq!(d1, 0.0); let d2 = agent.update(&t).expect("test: TD update should succeed");
1122 assert_eq!(d2, 0.0); }
1124
1125 #[test]
1126 fn test_nstep_td_updates_after_n_steps() {
1127 let config = AgentConfig {
1128 algorithm: AlgorithmType::NStepTD(2),
1129 policy: AgentPolicy::Random,
1130 alpha: 0.5,
1131 gamma: 0.9,
1132 lambda: 0.0,
1133 replay_capacity: 100,
1134 batch_size: 8,
1135 };
1136 let mut agent = ReinforcementLearningAgent::new(config);
1137 agent
1138 .register_state(s("A"), vec![a("left"), a("right")])
1139 .expect("test: should succeed");
1140 agent
1141 .register_state(s("B"), vec![a("left"), a("right")])
1142 .expect("test: should succeed");
1143
1144 let t = simple_transition(false);
1145 agent.update(&t).expect("test: TD update should succeed"); let d3 = agent.update(&t).expect("test: TD update should succeed"); assert!(d3 >= 0.0);
1149 }
1150
1151 #[test]
1152 fn test_nstep_td_n1_equivalent_to_qlearning() {
1153 let config = AgentConfig {
1155 algorithm: AlgorithmType::NStepTD(1),
1156 policy: AgentPolicy::Random,
1157 alpha: 0.5,
1158 gamma: 0.9,
1159 lambda: 0.0,
1160 replay_capacity: 100,
1161 batch_size: 8,
1162 };
1163 let mut agent = ReinforcementLearningAgent::new(config);
1164 agent
1165 .register_state(s("A"), vec![a("left"), a("right")])
1166 .expect("test: should succeed");
1167 agent
1168 .register_state(s("B"), vec![a("left"), a("right")])
1169 .expect("test: should succeed");
1170 let t = simple_transition(true);
1171 let delta = agent.update(&t).expect("test: TD update should succeed");
1172 assert!(delta >= 0.0);
1174 }
1175
1176 #[test]
1179 fn test_run_episode_empty() {
1180 let mut agent = two_state_agent(AlgorithmType::QLearning, AgentPolicy::Random);
1181 let stats = agent
1182 .run_episode(vec![], 42)
1183 .expect("test: episode run should succeed");
1184 assert_eq!(stats.steps, 0);
1185 assert_eq!(stats.total_reward, 0.0);
1186 }
1187
1188 #[test]
1189 fn test_run_episode_single_transition() {
1190 let mut agent = two_state_agent(AlgorithmType::QLearning, AgentPolicy::Random);
1191 let t = simple_transition(false);
1192 let stats = agent
1193 .run_episode(vec![t], 1)
1194 .expect("test: episode run should succeed");
1195 assert_eq!(stats.steps, 1);
1196 assert_eq!(stats.total_reward, 1.0);
1197 }
1198
1199 #[test]
1200 fn test_run_episode_accumulates_reward() {
1201 let mut agent = two_state_agent(AlgorithmType::QLearning, AgentPolicy::Random);
1202 let transitions = vec![
1203 Transition {
1204 state: s("A"),
1205 action: a("left"),
1206 reward: 2.0,
1207 next_state: s("B"),
1208 done: false,
1209 },
1210 Transition {
1211 state: s("B"),
1212 action: a("right"),
1213 reward: 3.0,
1214 next_state: s("A"),
1215 done: true,
1216 },
1217 ];
1218 let stats = agent
1219 .run_episode(transitions, 0)
1220 .expect("test: episode run should succeed");
1221 assert_eq!(stats.steps, 2);
1222 assert!((stats.total_reward - 5.0).abs() < 1e-9);
1223 }
1224
1225 #[test]
1226 fn test_run_episode_updates_aggregate_stats() {
1227 let mut agent = two_state_agent(AlgorithmType::QLearning, AgentPolicy::Random);
1228 assert_eq!(agent.stats().episodes_run, 0);
1229 let t = simple_transition(false);
1230 agent
1231 .run_episode(vec![t], 0)
1232 .expect("test: episode run should succeed");
1233 assert_eq!(agent.stats().episodes_run, 1);
1234 assert_eq!(agent.stats().total_steps, 1);
1235 }
1236
1237 #[test]
1238 fn test_run_episode_tracks_best_reward() {
1239 let mut agent = two_state_agent(AlgorithmType::QLearning, AgentPolicy::Random);
1240 let t1 = Transition {
1241 state: s("A"),
1242 action: a("left"),
1243 reward: 5.0,
1244 next_state: s("B"),
1245 done: true,
1246 };
1247 let t2 = Transition {
1248 state: s("A"),
1249 action: a("left"),
1250 reward: 100.0,
1251 next_state: s("B"),
1252 done: true,
1253 };
1254 agent
1255 .run_episode(vec![t1], 0)
1256 .expect("test: episode run should succeed");
1257 agent
1258 .run_episode(vec![t2], 0)
1259 .expect("test: episode run should succeed");
1260 assert!((agent.stats().best_episode_reward - 100.0).abs() < 1e-9);
1261 }
1262
1263 #[test]
1264 fn test_run_episode_epsilon_in_stats() {
1265 let policy = AgentPolicy::EpsilonGreedy {
1266 epsilon: 0.5,
1267 decay: 0.9,
1268 min_epsilon: 0.01,
1269 };
1270 let mut agent = two_state_agent(AlgorithmType::QLearning, policy);
1271 let t = simple_transition(false);
1272 let stats = agent
1273 .run_episode(vec![t], 0)
1274 .expect("test: episode run should succeed");
1275 assert!((stats.epsilon - 0.5).abs() < 1e-9);
1277 assert!((agent.current_epsilon() - 0.45).abs() < 1e-9);
1279 }
1280
1281 #[test]
1284 fn test_best_action_returns_highest_q() {
1285 let mut agent = two_state_agent(AlgorithmType::QLearning, AgentPolicy::Random);
1286 agent.q_table.entry((s("A"), a("left"))).or_default().q1 = 1.0;
1287 agent.q_table.entry((s("A"), a("right"))).or_default().q1 = 5.0;
1288 assert_eq!(
1289 agent.best_action(&s("A")).expect("test: should succeed"),
1290 a("right")
1291 );
1292 }
1293
1294 #[test]
1295 fn test_best_action_unknown_state_error() {
1296 let agent = two_state_agent(AlgorithmType::QLearning, AgentPolicy::Random);
1297 assert!(matches!(
1298 agent.best_action(&s("Z")),
1299 Err(RlAgentError::StateNotFound(_))
1300 ));
1301 }
1302
1303 #[test]
1304 fn test_value_equals_max_q() {
1305 let mut agent = two_state_agent(AlgorithmType::QLearning, AgentPolicy::Random);
1306 agent.q_table.entry((s("A"), a("left"))).or_default().q1 = 2.0;
1307 agent.q_table.entry((s("A"), a("right"))).or_default().q1 = 7.0;
1308 assert!((agent.value(&s("A")) - 7.0).abs() < 1e-9);
1309 }
1310
1311 #[test]
1312 fn test_value_unregistered_state_zero() {
1313 let agent = two_state_agent(AlgorithmType::QLearning, AgentPolicy::Random);
1314 assert_eq!(agent.value(&s("UNKNOWN")), 0.0);
1315 }
1316
1317 #[test]
1320 fn test_decay_epsilon_reduces_epsilon() {
1321 let policy = AgentPolicy::EpsilonGreedy {
1322 epsilon: 1.0,
1323 decay: 0.5,
1324 min_epsilon: 0.0,
1325 };
1326 let mut agent = two_state_agent(AlgorithmType::QLearning, policy);
1327 agent.decay_epsilon();
1328 assert!((agent.current_epsilon() - 0.5).abs() < 1e-12);
1329 }
1330
1331 #[test]
1332 fn test_decay_epsilon_respects_min() {
1333 let policy = AgentPolicy::EpsilonGreedy {
1334 epsilon: 0.01,
1335 decay: 0.1,
1336 min_epsilon: 0.05,
1337 };
1338 let mut agent = two_state_agent(AlgorithmType::QLearning, policy);
1339 agent.decay_epsilon();
1340 assert!((agent.current_epsilon() - 0.05).abs() < 1e-12);
1341 }
1342
1343 #[test]
1344 fn test_decay_epsilon_noop_for_boltzmann() {
1345 let policy = AgentPolicy::Boltzmann { temperature: 2.0 };
1346 let mut agent = two_state_agent(AlgorithmType::QLearning, policy);
1347 agent.decay_epsilon(); assert_eq!(agent.current_epsilon(), 0.0);
1349 }
1350
1351 #[test]
1352 fn test_decay_epsilon_noop_for_random() {
1353 let mut agent = two_state_agent(AlgorithmType::QLearning, AgentPolicy::Random);
1354 agent.decay_epsilon();
1355 assert_eq!(agent.current_epsilon(), 0.0);
1356 }
1357
1358 #[test]
1361 fn test_add_experience_populates_buffer() {
1362 let mut agent = two_state_agent(AlgorithmType::QLearning, AgentPolicy::Random);
1363 assert_eq!(agent.replay.len(), 0);
1364 agent.add_experience(simple_transition(false));
1365 assert_eq!(agent.replay.len(), 1);
1366 }
1367
1368 #[test]
1369 fn test_add_experience_respects_capacity() {
1370 let config = AgentConfig {
1371 replay_capacity: 3,
1372 ..AgentConfig::default()
1373 };
1374 let mut agent = ReinforcementLearningAgent::new(config);
1375 agent
1376 .register_state(s("A"), vec![a("left"), a("right")])
1377 .expect("test: should succeed");
1378 agent
1379 .register_state(s("B"), vec![a("left"), a("right")])
1380 .expect("test: should succeed");
1381 for _ in 0..10 {
1382 agent.add_experience(simple_transition(false));
1383 }
1384 assert_eq!(agent.replay.len(), 3);
1385 }
1386
1387 #[test]
1388 fn test_sample_experience_correct_count() {
1389 let mut agent = two_state_agent(AlgorithmType::QLearning, AgentPolicy::Random);
1390 for _ in 0..20 {
1391 agent.add_experience(simple_transition(false));
1392 }
1393 let sample = agent
1394 .sample_experience(5, 42)
1395 .expect("test: experience sampling should succeed");
1396 assert_eq!(sample.len(), 5);
1397 }
1398
1399 #[test]
1400 fn test_sample_experience_insufficient_error() {
1401 let mut agent = two_state_agent(AlgorithmType::QLearning, AgentPolicy::Random);
1402 agent.add_experience(simple_transition(false)); let result = agent.sample_experience(5, 0);
1404 assert!(matches!(
1405 result,
1406 Err(RlAgentError::InsufficientExperience(1))
1407 ));
1408 }
1409
1410 #[test]
1411 fn test_sample_experience_empty_buffer_error() {
1412 let agent = two_state_agent(AlgorithmType::QLearning, AgentPolicy::Random);
1413 let result = agent.sample_experience(1, 0);
1414 assert!(matches!(
1415 result,
1416 Err(RlAgentError::InsufficientExperience(0))
1417 ));
1418 }
1419
1420 #[test]
1421 fn test_sample_experience_randomness_different_seeds() {
1422 let mut agent = two_state_agent(AlgorithmType::QLearning, AgentPolicy::Random);
1423 for i in 0..20u64 {
1424 agent.add_experience(Transition {
1425 state: s("A"),
1426 action: a("left"),
1427 reward: i as f64,
1428 next_state: s("B"),
1429 done: false,
1430 });
1431 }
1432 let s1: Vec<f64> = agent
1433 .sample_experience(5, 1)
1434 .expect("test: should succeed")
1435 .iter()
1436 .map(|t| t.reward)
1437 .collect();
1438 let s2: Vec<f64> = agent
1439 .sample_experience(5, 99999)
1440 .expect("test: should succeed")
1441 .iter()
1442 .map(|t| t.reward)
1443 .collect();
1444 let any_diff = s1.iter().zip(&s2).any(|(a, b)| (a - b).abs() > 1e-12);
1447 let all_same = s1 == s2;
1448 assert!(any_diff || !all_same || s1.len() == 1);
1449 }
1450
1451 #[test]
1454 fn test_stats_initial_values() {
1455 let agent = two_state_agent(AlgorithmType::QLearning, AgentPolicy::Random);
1456 let stats = agent.stats();
1457 assert_eq!(stats.episodes_run, 0);
1458 assert_eq!(stats.total_steps, 0);
1459 assert_eq!(stats.avg_reward, 0.0);
1460 }
1461
1462 #[test]
1463 fn test_stats_convergence_delta_updates() {
1464 let mut agent = two_state_agent(AlgorithmType::QLearning, AgentPolicy::Random);
1465 let t = simple_transition(false);
1466 agent
1467 .run_episode(vec![t], 0)
1468 .expect("test: episode run should succeed");
1469 assert!(agent.stats().convergence_delta >= 0.0);
1471 }
1472
1473 #[test]
1474 fn test_stats_avg_reward_ema() {
1475 let mut agent = two_state_agent(AlgorithmType::QLearning, AgentPolicy::Random);
1476 for _ in 0..50 {
1478 let t = Transition {
1479 state: s("A"),
1480 action: a("left"),
1481 reward: 10.0,
1482 next_state: s("B"),
1483 done: true,
1484 };
1485 agent
1486 .run_episode(vec![t], 0)
1487 .expect("test: episode run should succeed");
1488 }
1489 let avg = agent.stats().avg_reward;
1491 assert!(avg > 5.0, "avg_reward {avg} should be > 5");
1492 }
1493
1494 #[test]
1497 fn test_update_unknown_state_error() {
1498 let mut agent = ReinforcementLearningAgent::new(AgentConfig::default());
1499 let t = Transition {
1500 state: s("GHOST"),
1501 action: a("up"),
1502 reward: 0.0,
1503 next_state: s("GHOST2"),
1504 done: false,
1505 };
1506 assert!(matches!(
1507 agent.update(&t),
1508 Err(RlAgentError::StateNotFound(_))
1509 ));
1510 }
1511
1512 #[test]
1513 fn test_update_invalid_action_error() {
1514 let mut agent = ReinforcementLearningAgent::new(AgentConfig::default());
1515 agent
1516 .register_state(s("X"), vec![a("go")])
1517 .expect("test: should succeed");
1518 agent
1519 .register_state(s("Y"), vec![a("go")])
1520 .expect("test: should succeed");
1521 let t = Transition {
1522 state: s("X"),
1523 action: a("FORBIDDEN"),
1524 reward: 1.0,
1525 next_state: s("Y"),
1526 done: false,
1527 };
1528 assert!(matches!(
1529 agent.update(&t),
1530 Err(RlAgentError::ActionNotFound { .. })
1531 ));
1532 }
1533
1534 #[test]
1535 fn test_update_next_state_not_found_non_terminal() {
1536 let mut agent = ReinforcementLearningAgent::new(AgentConfig::default());
1537 agent
1538 .register_state(s("X"), vec![a("go")])
1539 .expect("test: should succeed");
1540 let t = Transition {
1541 state: s("X"),
1542 action: a("go"),
1543 reward: 1.0,
1544 next_state: s("UNREGISTERED"),
1545 done: false,
1546 };
1547 assert!(matches!(
1548 agent.update(&t),
1549 Err(RlAgentError::StateNotFound(_))
1550 ));
1551 }
1552
1553 #[test]
1554 fn test_update_terminal_next_state_unregistered_ok() {
1555 let mut agent = ReinforcementLearningAgent::new(AgentConfig::default());
1556 agent
1557 .register_state(s("X"), vec![a("go")])
1558 .expect("test: should succeed");
1559 let t = Transition {
1560 state: s("X"),
1561 action: a("go"),
1562 reward: 1.0,
1563 next_state: s("TERMINAL"),
1564 done: true,
1565 };
1566 assert!(agent.update(&t).is_ok());
1568 }
1569
1570 #[test]
1571 fn test_rlagent_error_display() {
1572 let e1 = RlAgentError::StateNotFound(s("X"));
1573 let e2 = RlAgentError::ActionNotFound {
1574 state: s("X"),
1575 action: a("go"),
1576 };
1577 let e3 = RlAgentError::InsufficientExperience(3);
1578 let e4 = RlAgentError::InvalidConfig("bad alpha".into());
1579 assert!(!e1.to_string().is_empty());
1580 assert!(!e2.to_string().is_empty());
1581 assert!(!e3.to_string().is_empty());
1582 assert!(!e4.to_string().is_empty());
1583 }
1584
1585 #[test]
1588 fn test_xorshift64_non_zero_output() {
1589 let mut s = 1u64;
1590 for _ in 0..100 {
1591 let v = xorshift64(&mut s);
1592 assert_ne!(v, 0);
1593 }
1594 }
1595
1596 #[test]
1597 fn test_xorshift_f64_range() {
1598 let mut s = 12345u64;
1599 for _ in 0..1000 {
1600 let v = xorshift_f64(&mut s);
1601 assert!((0.0..1.0).contains(&v));
1602 }
1603 }
1604
1605 #[test]
1608 fn test_ucb_zero_c_behaves_like_greedy() {
1609 let policy = AgentPolicy::UCB { c: 0.0 };
1610 let mut agent = two_state_agent(AlgorithmType::QLearning, policy);
1611 agent.q_table.entry((s("A"), a("left"))).or_default().q1 = 100.0;
1612 let act = agent
1613 .select_action(&s("A"), &mut 0u64)
1614 .expect("test: should succeed");
1615 assert_eq!(act, a("left"));
1616 }
1617
1618 #[test]
1621 fn test_experience_replay_is_empty() {
1622 let buf = ExperienceReplay::new(10);
1623 assert!(buf.is_empty());
1624 }
1625
1626 #[test]
1627 fn test_experience_replay_evicts_oldest() {
1628 let mut buf = ExperienceReplay::new(2);
1629 buf.push(Transition {
1630 state: s("A"),
1631 action: a("x"),
1632 reward: 1.0,
1633 next_state: s("B"),
1634 done: false,
1635 });
1636 buf.push(Transition {
1637 state: s("B"),
1638 action: a("y"),
1639 reward: 2.0,
1640 next_state: s("A"),
1641 done: false,
1642 });
1643 buf.push(Transition {
1644 state: s("A"),
1645 action: a("z"),
1646 reward: 3.0,
1647 next_state: s("B"),
1648 done: false,
1649 });
1650 assert_eq!(buf.len(), 2);
1651 let rewards: Vec<f64> = buf.buffer.iter().map(|t| t.reward).collect();
1653 assert!(!rewards.contains(&1.0));
1654 }
1655
1656 #[test]
1659 fn test_multi_episode_qlearning_improves() {
1660 let policy = AgentPolicy::EpsilonGreedy {
1661 epsilon: 0.3,
1662 decay: 0.99,
1663 min_epsilon: 0.01,
1664 };
1665 let mut agent = two_state_agent(AlgorithmType::QLearning, policy);
1666 let good = Transition {
1667 state: s("A"),
1668 action: a("right"),
1669 reward: 10.0,
1670 next_state: s("B"),
1671 done: true,
1672 };
1673 for _ in 0..100 {
1674 agent
1675 .run_episode(vec![good.clone()], 0)
1676 .expect("test: should succeed");
1677 }
1678 assert!(agent.q1(&s("A"), &a("right")) > 0.0);
1679 assert_eq!(
1680 agent.best_action(&s("A")).expect("test: should succeed"),
1681 a("right")
1682 );
1683 }
1684
1685 #[test]
1688 fn test_double_q_both_tables_nonzero_after_many_updates() {
1689 let mut agent = two_state_agent(AlgorithmType::DoubleQLearning, AgentPolicy::Random);
1690 let t = simple_transition(false);
1691 for _ in 0..20 {
1692 agent.update(&t).expect("test: TD update should succeed");
1693 }
1694 let q1 = agent.q1(&s("A"), &a("left"));
1695 let q2 = agent.q2(&s("A"), &a("left"));
1696 assert!(q1 != 0.0 || q2 != 0.0);
1697 }
1698}