From 7b28a4c4f6a8edd67b508ad327eae8701f27e6c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=A4nner?= Date: Sun, 6 Jul 2025 19:49:34 +0200 Subject: [PATCH] check for initial food placement --- battlesnake/src/bin/seed-cracker.rs | 285 +++++++++++++++++++--------- 1 file changed, 191 insertions(+), 94 deletions(-) diff --git a/battlesnake/src/bin/seed-cracker.rs b/battlesnake/src/bin/seed-cracker.rs index 9bfee59..4220db7 100644 --- a/battlesnake/src/bin/seed-cracker.rs +++ b/battlesnake/src/bin/seed-cracker.rs @@ -240,7 +240,8 @@ struct PreparedGame { corners: [Option; 4], cardinals: [Option; 4], corner_first: bool, - food_spawns: Vec, + initial_food: Box<[(u8, u8)]>, + food_spawns: Box<[FoodSpawn]>, food_chance: u8, } @@ -258,107 +259,18 @@ enum FoodSpawn { impl PreparedGame { fn prepare(requests: &[Request]) -> Self { - debug!("Preparing initial board state"); let request = &requests[0]; - assert_eq!(request.turn, 0); - let mn = 1; - let md = (request.board.width - 1) / 2; - let mx = request.board.width - 2; - let corners = [ - Coord { x: mn, y: mn }, - Coord { x: mn, y: mx }, - Coord { x: mx, y: mn }, - Coord { x: mx, y: mx }, - ]; - let cardinals = [ - Coord { x: mn, y: md }, - Coord { x: md, y: mn }, - Coord { x: md, y: mx }, - Coord { x: mx, y: md }, - ]; - let corner_first = corners.contains(&request.board.snakes[0].head); - let corners = corners.map(|start| { - request - .board - .snakes - .iter() - .position(|snake| snake.head == start) - .map(|pos| (pos % 4).az()) - }); - let cardinals = cardinals.map(|start| { - request - .board - .snakes - .iter() - .position(|snake| snake.head == start) - .map(|pos| (pos % 4).az()) - }); + let (corner_first, corners, cardinals) = prepare_starts(request); - debug!("Preparing food placement"); - let food_chance = request.game.ruleset.settings.food_spawn_chance; - let food_min = request.game.ruleset.settings.minimum_food; + let initial_food = prepare_initial_food(request); - let food_spawns = requests - .windows(2) - .filter_map(|window| { - if let [previous, next] = window { - if previous.turn + 1 == next.turn { - Some((previous, next)) - } else { - None - } - } else { - error!("Windows didn't return 2 requests: {requests:#?}"); - None - } - }) - .map(|(previous, next)| { - let mut tmp = next.clone(); - tmp.board.food.clone_from(&previous.board.food); - let heads: Vec<_> = tmp.board.snakes.iter().map(|snake| snake.head).collect(); - tmp.board.food.retain(|coord| !heads.contains(coord)); - let free_spots = get_unoccupied_points(&tmp); - - if free_spots.is_empty() { - return FoodSpawn::Blocked; - } - - let diff: Vec<_> = next - .board - .food - .iter() - .filter(|next| !previous.board.food.contains(next)) - .collect(); - if diff.is_empty() { - // We didn't spawn any food. It was only eaten - return FoodSpawn::None; - } - - let selected = free_spots - .iter() - .position(|spot| spot == diff[0]) - .unwrap() - .az(); - let free_spots = free_spots.len().az(); - - if next.board.food.len() == usize::from(food_min) { - FoodSpawn::Forced { - selected, - free_spots, - } - } else { - FoodSpawn::Random { - selected, - free_spots, - } - } - }) - .collect(); + let (food_chance, food_spawns) = prepare_food_placement(requests, request); Self { corners, cardinals, corner_first, + initial_food, food_spawns, food_chance, } @@ -396,6 +308,18 @@ impl PreparedGame { return false; } + if log { + debug!("Checking Starting food"); + } + for &(selected, possibilities) in &self.initial_food { + if rand.int_n(usize::from(possibilities)).az::() != selected { + if log { + error!("Initial food spawn is wrong: expected {selected}"); + } + return false; + } + } + self.food_spawns.iter().enumerate().all(|(turn, spawn)| { if log { debug!("Checking turn {turn}"); @@ -444,6 +368,179 @@ impl PreparedGame { } } +fn prepare_food_placement(requests: &[Request], request: &Request) -> (u8, Box<[FoodSpawn]>) { + debug!("Preparing food placement"); + let food_chance = request.game.ruleset.settings.food_spawn_chance; + let food_min = request.game.ruleset.settings.minimum_food; + + let food_spawns = requests + .windows(2) + .filter_map(|window| { + if let [previous, next] = window { + if previous.turn + 1 == next.turn { + Some((previous, next)) + } else { + None + } + } else { + error!("Windows didn't return 2 requests: {requests:#?}"); + None + } + }) + .map(|(previous, next)| { + let mut tmp = next.clone(); + tmp.board.food.clone_from(&previous.board.food); + let heads: Vec<_> = tmp.board.snakes.iter().map(|snake| snake.head).collect(); + tmp.board.food.retain(|coord| !heads.contains(coord)); + let free_spots = get_unoccupied_points(&tmp); + + if free_spots.is_empty() { + return FoodSpawn::Blocked; + } + + let diff: Vec<_> = next + .board + .food + .iter() + .filter(|next| !previous.board.food.contains(next)) + .collect(); + if diff.is_empty() { + // We didn't spawn any food. It was only eaten + return FoodSpawn::None; + } + + let selected = free_spots + .iter() + .position(|spot| spot == diff[0]) + .unwrap() + .az(); + let free_spots = free_spots.len().az(); + + if next.board.food.len() == usize::from(food_min) { + FoodSpawn::Forced { + selected, + free_spots, + } + } else { + FoodSpawn::Random { + selected, + free_spots, + } + } + }) + .collect(); + (food_chance, food_spawns) +} + +fn prepare_initial_food(request: &Request) -> Box<[(u8, u8)]> { + debug!("Preparing initial food spawn"); + let center = Coord { + x: (request.board.width - 1) / 2, + y: (request.board.height) / 2, + }; + let initial_food = request + .board + .snakes + .iter() + .enumerate() + .map(|(i, snake)| { + let head = snake.head; + let possible_food_locations = [ + Coord { + x: head.x - 1, + y: head.y - 1, + }, + Coord { + x: head.x - 1, + y: head.y + 1, + }, + Coord { + x: head.x + 1, + y: head.y - 1, + }, + Coord { + x: head.x + 1, + y: head.y + 1, + }, + ]; + let available_food_locations: Vec<_> = possible_food_locations + .into_iter() + .filter(|food| { + // Don't place in the center + if *food == center { + return false; + } + + // Food must be further than snake from center on at least one axis + if !((food.x < head.x && head.x < center.x) + || (food.x > head.x && head.x > center.x) + || (food.y < head.y && head.y < center.y) + || (food.y > head.y && head.y > center.y)) + { + return false; + } + + // Don't spawn food in corners + if (food.x == 0 || food.x == (request.board.width - 1)) + && (food.y == 0 || food.y == (request.board.height - 1)) + { + return false; + } + + true + }) + .collect(); + + let possibilities = available_food_locations.len().az(); + let selected = available_food_locations + .iter() + .position(|food| *food == request.board.food[i]) + .unwrap() + .az(); + (selected, possibilities) + }) + .collect(); + initial_food +} + +fn prepare_starts(request: &Request) -> (bool, [Option; 4], [Option; 4]) { + debug!("Preparing initial board state"); + assert_eq!(request.turn, 0); + let mn = 1; + let md = (request.board.width - 1) / 2; + let mx = request.board.width - 2; + let corners = [ + Coord { x: mn, y: mn }, + Coord { x: mn, y: mx }, + Coord { x: mx, y: mn }, + Coord { x: mx, y: mx }, + ]; + let cardinals = [ + Coord { x: mn, y: md }, + Coord { x: md, y: mn }, + Coord { x: md, y: mx }, + Coord { x: mx, y: md }, + ]; + let corner_first = corners.contains(&request.board.snakes[0].head); + let corners = corners.map(|start| { + request + .board + .snakes + .iter() + .position(|snake| snake.head == start) + .map(|pos| (pos % 4).az()) + }); + let cardinals = cardinals.map(|start| { + request + .board + .snakes + .iter() + .position(|snake| snake.head == start) + .map(|pos| (pos % 4).az()) + }); + (corner_first, corners, cardinals) +} + #[must_use] //#[flame] fn get_rand(rand: &mut SeedRand, seed: i64, turn: i64) -> &mut SeedRand {