1use rand::{Rng, SeedableRng, rngs::StdRng};
7
8use super::{
9 snake::Snake,
10 types::{Cell, Direction, GameState, Position},
11};
12use crate::env::{Environment, SpaceInfo, SpaceType, StepInfo, StepResult};
13
14const DEFAULT_SEED: u64 = 0;
16
17#[derive(Debug, Clone)]
26pub struct SnakeEnvState {
27 pub width: i32,
29 pub height: i32,
31 pub snakes: Vec<super::snake::Snake>,
33 pub num_agents: usize,
35 pub food: Position,
37 pub episode: usize,
39 pub steps: usize,
41 pub max_steps: usize,
43 pub done: bool,
45 pub rng: StdRng,
47}
48
49#[derive(Debug, Clone)]
60pub struct SnakeEnv {
61 pub width: i32,
63 pub height: i32,
65 pub snakes: Vec<Snake>,
67 pub num_agents: usize,
69 pub food: Position,
71 pub episode: usize,
73 pub steps: usize,
75 pub max_steps: usize,
77 pub done: bool,
79 pub rng: StdRng,
82}
83
84impl SnakeEnv {
85 pub fn new_multi(width: i32, height: i32, num_agents: usize) -> Self {
87 Self::new_multi_with_seed(width, height, num_agents, DEFAULT_SEED)
88 }
89
90 pub fn new_multi_with_seed(width: i32, height: i32, num_agents: usize, seed: u64) -> Self {
95 let mut rng = StdRng::seed_from_u64(seed);
96
97 let mut snakes = Vec::new();
99 let positions = [
100 (width / 4, height / 4, Direction::Right), (3 * width / 4, height / 4, Direction::Left), (width / 4, 3 * height / 4, Direction::Right), (3 * width / 4, 3 * height / 4, Direction::Left), ];
105
106 for (i, &(x, y, dir)) in positions.iter().enumerate().take(num_agents.min(4)) {
107 let start_pos = Position::new(x, y);
108 snakes.push(Snake::new(i, start_pos, dir));
109 }
110
111 let food_pos = Position::new(rng.random_range(0..width), rng.random_range(0..height));
113
114 Self {
115 width,
116 height,
117 snakes,
118 num_agents,
119 food: food_pos,
120 episode: 0,
121 steps: 0,
122 max_steps: 400,
123 done: false,
124 rng,
125 }
126 }
127
128 pub fn new(width: i32, height: i32) -> Self {
130 Self::new_multi(width, height, 1)
131 }
132
133 fn toroidal_distance(&self, a: &Position, b: &Position) -> f32 {
135 let dx = (a.x - b.x).unsigned_abs() as f32;
136 let dy = (a.y - b.y).unsigned_abs() as f32;
137 let dx = dx.min(self.width as f32 - dx);
138 let dy = dy.min(self.height as f32 - dy);
139 (dx * dx + dy * dy).sqrt()
140 }
141
142 pub fn reset(&mut self) {
144 self.snakes.clear();
146 let positions = [
147 (self.width / 4, self.height / 4, Direction::Right),
148 (3 * self.width / 4, self.height / 4, Direction::Left),
149 (self.width / 4, 3 * self.height / 4, Direction::Right),
150 (3 * self.width / 4, 3 * self.height / 4, Direction::Left),
151 ];
152
153 for (i, &(x, y, dir)) in positions.iter().enumerate().take(self.num_agents.min(4)) {
154 let start_pos = Position::new(x, y);
155 self.snakes.push(Snake::new(i, start_pos, dir));
156 }
157
158 self.food = Position::new(
160 self.rng.random_range(0..self.width),
161 self.rng.random_range(0..self.height),
162 );
163
164 self.episode += 1;
165 self.steps = 0;
166 self.done = false;
167 }
168
169 pub fn step_multi(&mut self, actions: &[i64]) -> StepResult {
173 if self.done {
174 return StepResult {
175 observation: self.get_observation(),
176 reward: 0.0,
177 terminated: true,
178 truncated: false,
179 info: StepInfo::default(),
180 };
181 }
182
183 let mut prev_distances: Vec<f32> = Vec::new();
185 for snake in &self.snakes {
186 if snake.is_alive() {
187 prev_distances.push(self.toroidal_distance(&snake.head, &self.food));
188 } else {
189 prev_distances.push(f32::MAX); }
191 }
192
193 for (i, &action) in actions.iter().enumerate() {
195 if i < self.snakes.len() && self.snakes[i].is_alive() {
196 let new_direction = Direction::from_action(action);
197 self.snakes[i].change_direction(new_direction);
198 self.snakes[i].move_forward_wrap(self.width, self.height);
199 }
200 }
201
202 self.steps += 1;
203
204 let mut total_reward = 0.0;
205 let mut any_alive = false;
206
207 for i in 0..self.snakes.len() {
209 if !self.snakes[i].is_alive() {
210 continue;
211 }
212
213 if self.snakes[i].collides_with_self() {
217 self.snakes[i].alive = false;
218 total_reward -= 0.5; continue;
220 }
221
222 for j in 0..self.snakes.len() {
224 if i == j || !self.snakes[j].is_alive() {
225 continue;
226 }
227 if self.snakes[j].get_all_positions().contains(&self.snakes[i].head) {
229 self.snakes[i].alive = false;
230 total_reward -= 0.5; break;
232 }
233 }
234
235 if !self.snakes[i].is_alive() {
236 continue;
237 }
238
239 if self.snakes[i].eats_food(&self.food) {
241 total_reward += 1.0;
242 self.snakes[i].grow();
243
244 loop {
246 let x = self.rng.random_range(0..self.width);
247 let y = self.rng.random_range(0..self.height);
248 let new_food = Position::new(x, y);
249
250 let mut on_snake = false;
252 for snake in &self.snakes {
253 if snake.get_all_positions().contains(&new_food) {
254 on_snake = true;
255 break;
256 }
257 }
258
259 if !on_snake {
260 self.food = new_food;
261 break;
262 }
263 }
264 }
265
266 if self.snakes[i].is_alive() {
267 any_alive = true;
268 total_reward += 0.01;
271
272 if self.snakes[i].body.len() > 3 {
274 let length_bonus = 0.1 * ((self.snakes[i].body.len() - 3) as f32);
275 total_reward += length_bonus;
276 }
277
278 if i < prev_distances.len() {
280 let current_distance = self.toroidal_distance(&self.snakes[i].head, &self.food);
281 let distance_delta = prev_distances[i] - current_distance;
282 let distance_reward =
283 distance_delta / (self.width.max(self.height) as f32) * 2.0;
284 total_reward += distance_reward;
285 }
286 }
287 }
288
289 let terminated = !any_alive;
291 if terminated {
292 self.done = true;
293 }
294
295 let truncated = self.steps >= self.max_steps;
297 if truncated {
298 self.done = true;
299 }
300
301 StepResult {
302 observation: self.get_observation(),
303 reward: total_reward,
304 terminated,
305 truncated,
306 info: StepInfo::default(),
307 }
308 }
309
310 pub fn step_multi_agents(&mut self, actions: &[i64]) -> (Vec<f32>, bool, bool) {
313 if self.done {
314 return (vec![0.0; self.snakes.len()], true, false);
315 }
316
317 let mut prev_distances: Vec<f32> = Vec::new();
319 for snake in &self.snakes {
320 if snake.is_alive() {
321 prev_distances.push(self.toroidal_distance(&snake.head, &self.food));
322 } else {
323 prev_distances.push(f32::MAX); }
325 }
326
327 for (i, &action) in actions.iter().enumerate() {
329 if i < self.snakes.len() && self.snakes[i].is_alive() {
330 let new_direction = Direction::from_action(action);
331 self.snakes[i].change_direction(new_direction);
332 self.snakes[i].move_forward_wrap(self.width, self.height);
333 }
334 }
335
336 self.steps += 1;
337
338 let mut agent_rewards = vec![0.0; self.snakes.len()];
339 let mut any_alive = false;
340
341 for i in 0..self.snakes.len() {
343 if !self.snakes[i].is_alive() {
344 continue;
345 }
346
347 if self.snakes[i].collides_with_self() {
352 self.snakes[i].alive = false;
353 agent_rewards[i] -= 0.1; continue;
355 }
356
357 for j in 0..self.snakes.len() {
359 if i == j || !self.snakes[j].is_alive() {
360 continue;
361 }
362 if self.snakes[j].get_all_positions().contains(&self.snakes[i].head) {
364 self.snakes[i].alive = false;
365 agent_rewards[i] -= 0.1; break;
367 }
368 }
369
370 if !self.snakes[i].is_alive() {
371 continue;
372 }
373
374 if self.snakes[i].eats_food(&self.food) {
376 agent_rewards[i] += 1.0;
377 self.snakes[i].grow();
378
379 loop {
381 let x = self.rng.random_range(0..self.width);
382 let y = self.rng.random_range(0..self.height);
383 let new_food = Position::new(x, y);
384
385 let mut on_snake = false;
387 for snake in &self.snakes {
388 if snake.get_all_positions().contains(&new_food) {
389 on_snake = true;
390 break;
391 }
392 }
393
394 if !on_snake {
395 self.food = new_food;
396 break;
397 }
398 }
399 }
400
401 if self.snakes[i].is_alive() {
402 any_alive = true;
403 agent_rewards[i] += 0.01;
405
406 if self.snakes[i].body.len() > 3 {
408 let length_bonus = 1.0 * ((self.snakes[i].body.len() - 3) as f32);
409 agent_rewards[i] += length_bonus;
410 }
411
412 if i < prev_distances.len() {
414 let current_distance = self.toroidal_distance(&self.snakes[i].head, &self.food);
415 let distance_delta = prev_distances[i] - current_distance;
416 let distance_reward =
417 distance_delta / (self.width.max(self.height) as f32) * 2.0;
418 agent_rewards[i] += distance_reward;
419 }
420 }
421 }
422
423 let terminated = !any_alive;
425 if terminated {
426 self.done = true;
427 }
428
429 let truncated = self.steps >= self.max_steps;
431 if truncated {
432 self.done = true;
433 }
434
435 (agent_rewards, terminated, truncated)
436 }
437
438 pub fn step(&mut self, action: i64) -> StepResult {
440 self.step_multi(&[action])
441 }
442
443 pub fn get_grid_observation(&self, snake_id: usize) -> Vec<f32> {
454 if snake_id >= self.snakes.len() {
455 return vec![0.0; 5 * (self.width as usize) * (self.height as usize)];
457 }
458
459 let grid_size = (self.width as usize) * (self.height as usize);
460 let mut obs = vec![0.0; 5 * grid_size];
461
462 let own_snake = &self.snakes[snake_id];
463
464 for pos in &own_snake.body {
466 if pos.x >= 0 && pos.x < self.width && pos.y >= 0 && pos.y < self.height {
467 let idx = (pos.y as usize) * (self.width as usize) + (pos.x as usize);
468 obs[idx] = 1.0;
469 }
470 }
471
472 if own_snake.head.x >= 0
474 && own_snake.head.x < self.width
475 && own_snake.head.y >= 0
476 && own_snake.head.y < self.height
477 {
478 let head_idx =
479 (own_snake.head.y as usize) * (self.width as usize) + (own_snake.head.x as usize);
480 obs[grid_size + head_idx] = 1.0;
481 }
482
483 for (id, snake) in self.snakes.iter().enumerate() {
485 if id != snake_id {
486 for pos in &snake.body {
487 if pos.x >= 0 && pos.x < self.width && pos.y >= 0 && pos.y < self.height {
488 let idx = 2 * grid_size
489 + (pos.y as usize) * (self.width as usize)
490 + (pos.x as usize);
491 obs[idx] = 1.0;
492 }
493 }
494 }
495 }
496
497 if self.food.x >= 0
499 && self.food.x < self.width
500 && self.food.y >= 0
501 && self.food.y < self.height
502 {
503 let food_idx = 3 * grid_size
504 + (self.food.y as usize) * (self.width as usize)
505 + (self.food.x as usize);
506 obs[food_idx] = 1.0;
507 }
508
509 for x in 0..self.width as usize {
512 obs[4 * grid_size + x] = 1.0; obs[4 * grid_size + ((self.height as usize - 1) * (self.width as usize)) + x] = 1.0; }
515 for y in 0..self.height as usize {
517 obs[4 * grid_size + y * (self.width as usize)] = 1.0; obs[4 * grid_size + y * (self.width as usize) + (self.width as usize - 1)] = 1.0; }
520
521 obs
522 }
523
524 pub fn get_observation(&self) -> Vec<f32> {
526 if self.snakes.is_empty() {
527 return vec![0.0; 6];
528 }
529
530 let snake = &self.snakes[0];
531 let dx = (self.food.x - snake.head.x) as f32 / self.width as f32;
532 let dy = (self.food.y - snake.head.y) as f32 / self.height as f32;
533
534 let direction_onehot = match snake.direction {
535 Direction::Up => [1.0, 0.0, 0.0, 0.0],
536 Direction::Down => [0.0, 1.0, 0.0, 0.0],
537 Direction::Left => [0.0, 0.0, 1.0, 0.0],
538 Direction::Right => [0.0, 0.0, 0.0, 1.0],
539 };
540
541 vec![
542 dx,
543 dy,
544 direction_onehot[0],
545 direction_onehot[1],
546 direction_onehot[2],
547 direction_onehot[3],
548 ]
549 }
550
551 pub fn render(&self) -> GameState {
553 let mut grid = vec![vec![Cell::Empty; self.width as usize]; self.height as usize];
554
555 grid[self.food.y as usize][self.food.x as usize] = Cell::Food;
557
558 for snake in &self.snakes {
560 for (i, &pos) in snake.body.iter().enumerate() {
561 let cell = if i == 0 {
562 Cell::SnakeHead(snake.id)
563 } else {
564 Cell::SnakeBody(snake.id)
565 };
566 grid[pos.y as usize][pos.x as usize] = cell;
567 }
568 }
569
570 GameState {
571 grid,
572 scores: self.snakes.iter().map(|s| s.length as i32).collect(),
573 active_agents: self.snakes.iter().map(|s| s.is_alive()).collect(),
574 episode: self.episode,
575 steps: self.steps,
576 }
577 }
578}
579
580impl Environment for SnakeEnv {
581 type Action = i64;
582 type State = SnakeEnvState;
583
584 fn reset(&mut self) {
585 self.reset();
586 }
587
588 fn get_observation(&self) -> Vec<f32> {
589 self.get_observation()
590 }
591
592 fn step(&mut self, action: i64) -> StepResult {
593 self.step(action)
594 }
595
596 fn observation_space(&self) -> SpaceInfo {
597 SpaceInfo {
598 shape: vec![6], space_type: SpaceType::Box,
600 }
601 }
602
603 fn action_space(&self) -> SpaceInfo {
604 SpaceInfo {
605 shape: vec![4], space_type: SpaceType::Discrete(4),
607 }
608 }
609
610 fn render(&self) -> Vec<u8> {
611 vec![] }
613
614 fn close(&mut self) {
615 }
617
618 fn clone_state(&self) -> SnakeEnvState {
619 SnakeEnvState {
620 width: self.width,
621 height: self.height,
622 snakes: self.snakes.clone(),
623 num_agents: self.num_agents,
624 food: self.food,
625 episode: self.episode,
626 steps: self.steps,
627 max_steps: self.max_steps,
628 done: self.done,
629 rng: self.rng.clone(),
630 }
631 }
632
633 fn restore_state(&mut self, state: &SnakeEnvState) {
634 self.width = state.width;
635 self.height = state.height;
636 self.snakes = state.snakes.clone();
637 self.num_agents = state.num_agents;
638 self.food = state.food;
639 self.episode = state.episode;
640 self.steps = state.steps;
641 self.max_steps = state.max_steps;
642 self.done = state.done;
643 self.rng = state.rng.clone();
644 }
645}
646
647#[cfg(test)]
648mod tests {
649 use super::*;
650
651 #[test]
652 fn clone_restore_round_trips() {
653 let mut env = SnakeEnv::new(10, 10);
654 env.reset();
655
656 for i in 0..5 {
658 env.step((i % 4) as i64);
659 }
660 let snap = env.clone_state();
661
662 assert_eq!(env.steps, snap.steps);
664 assert_eq!(env.food, snap.food);
665 assert_eq!(env.snakes.len(), snap.snakes.len());
666
667 let r1 = env.step(0);
669 let post_food_1 = env.food;
670 let post_steps_1 = env.steps;
671
672 env.restore_state(&snap);
674 assert_eq!(env.steps, snap.steps);
675 assert_eq!(env.food, snap.food);
676
677 let r2 = env.step(0);
678
679 assert_eq!(r1.observation, r2.observation);
683 assert_eq!(r1.reward, r2.reward);
684 assert_eq!(r1.terminated, r2.terminated);
685 assert_eq!(r1.truncated, r2.truncated);
686 assert_eq!(env.food, post_food_1);
687 assert_eq!(env.steps, post_steps_1);
688 }
689
690 #[test]
696 fn clone_restore_round_trips_when_food_is_eaten() {
697 let mut env = SnakeEnv::new(10, 10);
698 env.reset();
699
700 let head = env.snakes[0].head;
704 let food_before = Position::new(head.x, head.y - 1);
705 env.food = food_before;
706
707 let snap = env.clone_state();
708
709 let r1 = env.step(0);
710 let food_after_1 = env.food;
711
712 env.restore_state(&snap);
713 let r2 = env.step(0);
714 let food_after_2 = env.food;
715
716 assert_ne!(food_after_1, food_before, "expected food to be eaten and respawned");
719
720 assert_eq!(food_after_1, food_after_2, "respawned food must match across restore");
724 assert_eq!(r1.observation, r2.observation);
725 assert_eq!(r1.reward, r2.reward);
726 }
727}