From 263b0642f4d7295c95d3e992f16e6b285cc9c56b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=A4nner?= Date: Tue, 24 Jun 2025 20:07:25 +0200 Subject: [PATCH] calculate less Speed up validation by precalculating some stuff and toing a fast return wherever possible --- battlesnake/src/bin/seed-cracker.rs | 381 +++++++++++++++++++++++----- 1 file changed, 313 insertions(+), 68 deletions(-) diff --git a/battlesnake/src/bin/seed-cracker.rs b/battlesnake/src/bin/seed-cracker.rs index 8b9ce83..5f431e7 100644 --- a/battlesnake/src/bin/seed-cracker.rs +++ b/battlesnake/src/bin/seed-cracker.rs @@ -1,12 +1,12 @@ -use std::{fs::File, path::PathBuf, sync::LazyLock}; +use std::{fs::File, path::PathBuf, sync::LazyLock, time::Instant}; use az::{Az, WrappingAs}; use battlesnake::types::{Coord, Direction, wire::Request}; use bytemuck::cast_slice; use clap::Parser; -use flamer::flame; +//use flamer::flame; use hashbrown::HashMap; -use log::{debug, error, info, trace, warn}; +use log::{debug, error, info, trace}; use memmap2::{Mmap, MmapOptions}; type DynError = Box; @@ -23,7 +23,7 @@ struct Args { file: PathBuf, } -#[flame] +//#[flame] fn main() -> Result<(), DynError> { env_logger::init(); @@ -39,38 +39,58 @@ fn main() -> Result<(), DynError> { let _ = *CHAIN; let mut rand = SeedRand::new(0); + let prepared = PreparedGame::prepare(&requests); if let Some(seed) = args.seed { - if validate(&mut rand, &requests, seed, true) { + info!("Validating using old method"); + let start = Instant::now(); + let valid = validate(&mut rand, &requests, seed, true); + let elapsed = start.elapsed(); + info!("Validation took {elapsed:?}"); + if valid { + println!("{seed} is the correct seed for this game"); + } else { + println!("{seed} is not the correct seed for this game"); + } + info!("Validating using prepared method"); + let start = Instant::now(); + let valid = prepared.validate(&mut rand, seed, true); + let elapsed = start.elapsed(); + info!("Validation took {elapsed:?}"); + if valid { println!("{seed} is the correct seed for this game"); } else { println!("{seed} is not the correct seed for this game"); } } else { + info!("Trying every seed from 1 to {}", i32::MAX); + let start = Instant::now(); for seed in 1..i32::MAX { - if seed == 100 { - break; - } if seed % 1_000_000 == 0 { debug!("Checking {seed:>10}"); } - if validate(&mut rand, &requests, i64::from(seed), false) { + if prepared.validate(&mut rand, i64::from(seed), false) { + let elapsed = start.elapsed(); + info!( + "Trying {seed} seeds took {elapsed:?} ({:?} / seed)", + elapsed / seed.az() + ); println!("The correct seed is {seed}"); break; } } } - flame::dump_html(File::create("flamegraph.html")?)?; + //flame::dump_html(File::create("flamegraph.html")?)?; Ok(()) } #[allow(clippy::too_many_lines)] -#[flame] +//#[flame] fn validate(input_rand: &mut SeedRand, requests: &[Request], seed: i64, log: bool) -> bool { if log { - info!("Checking starting positions"); + debug!("Checking starting positions"); } let request = &requests[0]; let rand = get_rand(input_rand, seed, 0); @@ -90,12 +110,8 @@ fn validate(input_rand: &mut SeedRand, requests: &[Request], seed: i64, log: boo Coord { x: mx, y: md }, ]; - rand.shuffle(corner_points.len(), |i, j| { - corner_points.swap(i, j); - }); - rand.shuffle(cardinal_points.len(), |i, j| { - cardinal_points.swap(i, j); - }); + rand.shuffle(&mut corner_points); + rand.shuffle(&mut cardinal_points); let mut start_points = Vec::with_capacity(corner_points.len() + cardinal_points.len()); if rand.int_n(2) == 0 { @@ -106,12 +122,12 @@ fn validate(input_rand: &mut SeedRand, requests: &[Request], seed: i64, log: boo start_points.extend_from_slice(&corner_points); } - for i in 0..request.board.snakes.len() { - if start_points[i] != request.board.snakes[i].head { + for (snake, start_point) in request.board.snakes.iter().zip(start_points.iter()) { + if *start_point != snake.head { if log { error!( "Starting position is not the same: {} != {}", - start_points[i], request.board.snakes[i].head + start_point, snake.head ); } return false; @@ -119,22 +135,16 @@ fn validate(input_rand: &mut SeedRand, requests: &[Request], seed: i64, log: boo } if log { - info!("Checking food placement"); + debug!("Checking food placement"); } for i in 1..requests.len() { let previous_request = &requests[i - 1]; let request = &requests[i]; if previous_request.turn + 1 != request.turn { - if log { - warn!( - "nonconsecutive requests: {} + 1 != {}", - previous_request.turn, request.turn - ); - } continue; } if log { - info!("Checking turn {}", request.turn); + debug!("Checking turn {}", request.turn); } let rand = get_rand(input_rand, seed, i64::from(request.turn - 1)); let mut forged_request = request.clone(); @@ -160,9 +170,6 @@ fn validate(input_rand: &mut SeedRand, requests: &[Request], seed: i64, log: boo &forged_request, )); } - if log { - debug!("snake head: {:?}", request.board.snakes[0].head); - } if forged_request.board.food != request.board.food { let forged: Vec<_> = forged_request .board @@ -190,14 +197,216 @@ fn validate(input_rand: &mut SeedRand, requests: &[Request], seed: i64, log: boo true } +#[derive(Debug, Clone)] +struct PreparedGame { + corners: [Option; 4], + cardinals: [Option; 4], + corner_first: bool, + food_spawns: Vec>, + food_chance: u8, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum FoodSpawn { + Forced { selected: u16, free_spots: u16 }, + Random { selected: u16, free_spots: u16 }, +} + +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()) + }); + + 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)| { + // no new food, no food eaten + if previous.board.food == next.board.food { + return None; + } + + 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 None; + } + + 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); + + 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) { + Some(FoodSpawn::Forced { + selected, + free_spots, + }) + } else { + Some(FoodSpawn::Random { + selected, + free_spots, + }) + } + }) + .collect(); + + Self { + corners, + cardinals, + corner_first, + food_spawns, + food_chance, + } + } + + fn validate(&self, input_rand: &mut SeedRand, seed: i64, log: bool) -> bool { + if log { + debug!("Checking Start positions"); + } + let mut corners = [None; 4]; + for (a, b) in self.corners.iter().zip(corners.iter_mut()) { + *b = a.map(usize::from); + } + let mut cardinals = [None; 4]; + for (a, b) in self.cardinals.iter().zip(cardinals.iter_mut()) { + *b = a.map(usize::from); + } + let rand = get_rand(input_rand, seed, 0); + if !rand.shuffle_check(&mut corners) { + if log { + error!("Corners are shuffled wrong"); + } + return false; + } + if !rand.shuffle_check(&mut cardinals) { + if log { + error!("Cardinals are shuffled wrong"); + } + return false; + } + if rand.int_n(2) == usize::from(self.corner_first) { + if log { + error!("Corners and cardinals are flipped: {}", self.corner_first); + } + return false; + } + + self.food_spawns.iter().enumerate().all(|(turn, spawn)| { + if log { + debug!("Checking turn {turn}"); + } + let rand = get_rand(input_rand, seed, (turn).az()); + + match spawn { + Some( + spawn @ (FoodSpawn::Forced { + selected, + free_spots, + } + | FoodSpawn::Random { + selected, + free_spots, + }), + ) => { + if matches!(spawn, FoodSpawn::Random { .. }) + && 100 - rand.int_n(100) >= usize::from(self.food_chance) + { + if log { + error!("Food not spawned, when some should have been spawned"); + } + return false; + } + + let mut foods = vec![None; usize::from(*free_spots)]; + foods[usize::from(*selected)] = Some(0); + let correct = rand.shuffle_check(&mut foods); + if log && !correct { + error!("Wrong food selected: {foods:?}"); + } + correct + } + None => { + let correct = 100 - rand.int_n(100) >= usize::from(self.food_chance); + if log && !correct { + error!("Food spawned, when none should have been spawned"); + } + correct + } + } + }) + } +} + #[must_use] -#[flame] +//#[flame] fn get_rand(rand: &mut SeedRand, seed: i64, turn: i64) -> &mut SeedRand { rand.rand.seed(seed + turn); rand } -#[flame] +//#[flame] fn check_food_needing_placement(rand: &mut impl Rand, request: &Request) -> usize { let min_food = request.game.ruleset.settings.minimum_food as usize; let food_spawn_chance = request.game.ruleset.settings.food_spawn_chance as usize; @@ -213,13 +422,13 @@ fn check_food_needing_placement(rand: &mut impl Rand, request: &Request) -> usiz 0 } -#[flame] +//#[flame] fn place_food_randomly(rand: &mut impl Rand, n: usize, request: &Request) -> Vec { let unoccupied_points = get_unoccupied_points(request); place_food_randomly_at_positions(rand, n, unoccupied_points) } -#[flame] +//#[flame] fn place_food_randomly_at_positions( rand: &mut impl Rand, n: usize, @@ -227,9 +436,7 @@ fn place_food_randomly_at_positions( ) -> Vec { let n = n.min(positions.len()); - rand.shuffle(positions.len(), |i, j| { - positions.swap(i, j); - }); + rand.shuffle(&mut positions); let mut out = Vec::new(); let mut i = 0; @@ -240,7 +447,7 @@ fn place_food_randomly_at_positions( out } -#[flame] +//#[flame] fn get_unoccupied_points(request: &Request) -> Vec { let possible_moves: Vec<_> = request .board @@ -275,7 +482,10 @@ trait Rand { fn range(&mut self, min: usize, max: usize) -> usize; /// shuffle an array of n values using the swap function #[allow(unused)] - fn shuffle(&mut self, n: usize, swap: impl FnMut(usize, usize)); + fn shuffle(&mut self, data: &mut [T]); + /// checks shuffle an array of n values using the swap function + #[allow(unused)] + fn shuffle_check(&mut self, data: &mut [Option]) -> bool; } #[derive(Debug, PartialEq, Eq, Clone)] @@ -284,7 +494,7 @@ struct SeedRand { } impl SeedRand { - #[flame] + //#[flame] fn new(seed: i64) -> Self { Self { rand: GoRand::new(GoSource::new(seed)), @@ -293,19 +503,23 @@ impl SeedRand { } impl Rand for SeedRand { - #[flame] + //#[flame] fn int_n(&mut self, n: usize) -> usize { self.rand.int_n(n) } - #[flame] + //#[flame] fn range(&mut self, min: usize, max: usize) -> usize { self.rand.int_n(max - min + 1) + min } - #[flame] - fn shuffle(&mut self, n: usize, swap: impl FnMut(usize, usize)) { - self.rand.shuffle(n, swap); + //#[flame] + fn shuffle(&mut self, data: &mut [T]) { + self.rand.shuffle(data); + } + + fn shuffle_check(&mut self, data: &mut [Option]) -> bool { + self.rand.shuffle_check(data) } } @@ -315,26 +529,26 @@ struct GoRand { } impl GoRand { - #[flame] + //#[flame] const fn new(src: GoSource) -> Self { Self { src } } // Seed uses the provided seed value to initialize the generator to a deterministic state. // Seed should not be called concurrently with any other [Rand] method. - #[flame] + //#[flame] fn seed(&mut self, seed: i64) { self.src.seed(seed); } /// returns a non-negative pseudo-random 63-bit integer - #[flame] + //#[flame] fn int63(&mut self) -> u64 { self.src.int63() } /// returns a non-negative pseudo-random 63-bit integer - #[flame] + //#[flame] fn int31(&mut self) -> u32 { (self.src.int63() >> 32) as u32 } @@ -343,7 +557,7 @@ impl GoRand { /// /// # Panics /// if n <= 0 - #[flame] + //#[flame] fn int63_n(&mut self, n: u64) -> u64 { assert!((n > 0), "invalid argument to int63_n"); if n.is_power_of_two() { @@ -362,7 +576,7 @@ impl GoRand { /// /// # Panics /// if n <= 0 - #[flame] + //#[flame] fn int31_n(&mut self, n: u32) -> u32 { assert!((n > 0), "invalid argument to in32_n"); if n.is_power_of_two() { @@ -377,12 +591,12 @@ impl GoRand { v % n } - #[flame] + //#[flame] fn uint32(&mut self) -> u32 { (self.int63() >> 31).az() } - #[flame] + //#[flame] fn int31_n2(&mut self, n: u32) -> u32 { let mut v = self.uint32(); let mut prod = u64::from(v) * u64::from(n); @@ -402,7 +616,7 @@ impl GoRand { /// /// # Panics /// if n <= 0 - #[flame] + //#[flame] fn int_n(&mut self, n: usize) -> usize { assert!((n > 0), "invalid argument to int_n"); if n < (1 << 31) { @@ -413,34 +627,65 @@ impl GoRand { } #[allow(unused)] - #[flame] + //#[flame] fn int(&mut self) -> usize { self.int63().az() } // Shuffle pseudo-randomizes the order of elements. n is the number of elements. Shouffle // panics if n < 0. swap swaps the elements with indices i and j. - #[flame] - fn shuffle(&mut self, n: usize, mut swap: impl FnMut(usize, usize)) { + //#[flame] + fn shuffle(&mut self, data: &mut [T]) { // Fisher-Yates shuffle: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle // Shuffle really ought not be called with n that doesn't fit in 32 bits. // Not only will it take a very long time, but with 2³¹! possible permutations, // there's no way that any PRNG can have a big enough internal state to // generate even a minuscule percentage of the possible permutations. // Nevertheless, the right API signature accepts an int n, so handle it as best we can. - let mut i = n.saturating_sub(1); + let mut i = data.len().saturating_sub(1); while i > (1 << 31) - 1 - 1 { let j = self.int63_n((i + 1) as u64).az(); - swap(i, j); + data.swap(i, j); i -= 1; } let mut i: u32 = i.az(); while i > 0 { let j = self.int31_n2(i + 1); - swap(i.az(), j.az()); + data.swap(i.az(), j.az()); i -= 1; } } + + // Shuffle the array and check if it is deshuffled afterwards + fn shuffle_check(&mut self, data: &mut [Option]) -> bool { + let mut i = data.len().saturating_sub(1); + while i > (1 << 31) - 1 - 1 { + let j = self.int63_n((i + 1) as u64).az(); + let datum = data[j]; + if let Some(new_i) = datum { + if new_i != i { + return false; + } + } + data.swap(i, j); + i -= 1; + } + let mut i: u32 = i.az(); + while i > 0 { + let j = self.int31_n2(i + 1); + let i_usize = i.az(); + let j_usize = j.az(); + let datum = data[j_usize]; + if let Some(i) = datum { + if i != i_usize { + return false; + } + } + data.swap(i_usize, j_usize); + i -= 1; + } + true + } } static CHAIN_RAW: LazyLock = LazyLock::new(|| unsafe { @@ -468,7 +713,7 @@ impl GoSource { const MAX: u64 = 1 << 63; const MASK: u64 = Self::MAX - 1; - #[flame] + //#[flame] fn new(seed: i64) -> Self { let seed: i32 = (seed % i64::from(i32::MAX)).az(); let seed = match seed { @@ -490,13 +735,13 @@ impl GoSource { } impl GoSource { - #[flame] + //#[flame] fn int63(&mut self) -> u64 { self.uint64() & Self::MASK } #[allow(unused)] - #[flame] + //#[flame] fn seed(&mut self, seed: i64) { let seed: i32 = (seed % i64::from(i32::MAX)).az(); let seed = match seed { @@ -514,7 +759,7 @@ impl GoSource { self.vec.clear(); } - #[flame] + //#[flame] fn uint64(&mut self) -> u64 { self.tap = self.tap.checked_sub(1).unwrap_or(Self::LEN - 1); self.feed = self.feed.checked_sub(1).unwrap_or(Self::LEN - 1); @@ -536,7 +781,7 @@ impl GoSource { } } -#[flame] +//#[flame] fn seed_one(base: usize, i: u16) -> i64 { let i = usize::from(i); let base = base + 20 + i * 3; @@ -554,7 +799,7 @@ fn seed_one(base: usize, i: u16) -> i64 { u ^ GoSource::COOKED[i] } -#[flame] +//#[flame] const fn seedrand(x: i32) -> i32 { const A: i32 = 48_271; const Q: i32 = 44_488;