calculate less
All checks were successful
Build / build (push) Successful in 2m38s

Speed up validation by precalculating some stuff and toing a fast return
wherever possible
This commit is contained in:
2025-06-24 20:07:25 +02:00
parent 081c41b753
commit 263b0642f4

View File

@ -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<dyn std::error::Error>;
@ -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<u8>; 4],
cardinals: [Option<u8>; 4],
corner_first: bool,
food_spawns: Vec<Option<FoodSpawn>>,
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<Coord> {
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<Coord> {
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<Coord> {
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<T>(&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<usize>]) -> 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<T>(&mut self, data: &mut [T]) {
self.rand.shuffle(data);
}
fn shuffle_check(&mut self, data: &mut [Option<usize>]) -> 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<T>(&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<usize>]) -> 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<Mmap> = 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;