Skip to content

Commit

Permalink
Use ArrayVec instead of Vec to store moves to avoid expensive heap al…
Browse files Browse the repository at this point in the history
…locations
  • Loading branch information
dezyh committed Jan 28, 2022
1 parent 7f5dc00 commit 2aab75b
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 108 deletions.
14 changes: 3 additions & 11 deletions engine/cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
use engine::{
bitboard::Bitboard,
bitwise::Bitwise,
constants::{BLACK, LOSS, WHITE, WIN},
format::Format,
constants::{LOSS, WIN},
search::Search,
transpose::TranspositionTable,
};

fn search(n: u8) {
let mut tt = TranspositionTable::new(29);
let mut bb = Bitboard::empty()
.with(Bitwise::pos(0), 2, BLACK)
.with(Bitwise::pos(10), 1, WHITE)
.with(Bitwise::pos(40), 1, WHITE);

Format::frame(bb.board[WHITE], "white");
Format::frame(bb.board[BLACK], "black");
let mut tt = TranspositionTable::new(28);
let mut bb = Bitboard::new();

for i in 1..n {
let result = Search::negamax_move(&mut bb, &mut tt, i, LOSS, WIN);
Expand Down
5 changes: 5 additions & 0 deletions engine/engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,9 @@ criterion = { version = "0.3", features = ["html_reports"] }
name = "my_bench"
harness = false

[[bench]]
name = "bitboard"
harness = false

[dependencies]
tinyvec = "1.5.1"
10 changes: 10 additions & 0 deletions engine/engine/benches/bitboard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use engine::bitboard::Bitboard;

fn criterion_benchmark(c: &mut Criterion) {
let bitboard = Bitboard::new();
c.bench_function("height branching", |b| b.iter(|| bitboard.height(black_box(4))));
}

criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
192 changes: 107 additions & 85 deletions engine/engine/src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,117 @@ use crate::bitboard::Bitboard;
use crate::bitwise::Bitwise;
use crate::constants::*;
use crate::format::Format;
use tinyvec::{Array, ArrayVec};

#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, Default)]
pub struct Action {
pub source: u8,
pub target: u8,
pub robots: u8,
}

// TODO: Optimize heap allocations away
pub struct Actions {
pub attacks: Vec<Action>,
pub defends: Vec<Action>,
pub others: Vec<Action>,
pub special: Vec<Action>,
}
//

impl Actions {
pub fn new() -> Self {
Self { attacks: Vec::new(), defends: Vec::new(), others: Vec::new(), special: Vec::new() }
}
}
// Precomputations
//let allies_attack_range = Bitwise::adj_any(bitboard.board[bitboard.turn]);
// let enemies_attack_range = Bitwise::adj_any(bitboard.board[opponent]);
// let enemies_unattacked =
// bitboard.board[opponent] & !Bitwise::adj_any(bitboard.board[bitboard.turn]);
//let allies_attacked = bitboard.board[bitboard.turn] & enemies_attack_range;
//let allies_stacked = bitboard.board[bitboard.turn] & !bitboard.board[1];
//let enemies_stacked = bitboard.board[opponent] & !bitboard.board[1];

impl Action {
pub fn generate(bitboard: &Bitboard) -> Vec<Action> {
let mut actions = Actions::new();
pub fn generate(bitboard: &Bitboard) -> ArrayVec<[Action; 256]> {
let mut actions = ArrayVec::<[Action; 256]>::new();
Action::generate_booms(&bitboard, &mut actions);
for height in (1..=12).rev() {
Action::generate_for_height(bitboard, height, &mut actions);
Action::generate_for_height(&bitboard, height, &mut actions);
}
actions
}

/// Sort our actions in space based on a heuristic
// ------------------------------------------------------------------------------------------------
// General
// 1. If we are ever up in pieces, it is in our interested to spread pieces and take 1 for 1
// trades.
// ------------------------------------------------------------------------------------------------
// Booms
// 1. We don't need to determine how good a boom is because it is only 1 move deep until we do
// that anyway and then the evaluation function will tell us.
// 2. Thus, we should always explore booms and never prune them from the search tree.
// ------------------------------------------------------------------------------------------------
// Stacking
// 1. Use this stacking order, powers of 2, then even, then odd. 8, 4, 2, 6, 7, 5, 3.
// 2. It's probably better to stack powers of 2 and then even numbers because otherwise you're wasting turns to build such stacks.
// 3. We will ignore stacking past 8. This is because, stacks of 8 can cross the entire
// board. We could even try stopping at 7 but stacks of 8 are probably more common due
// to being a power of 2.
// 4. Stacking 5, 6, 7, 8 in the very back row is awarded more than stacking 5, 6, 7, 8 in the front row because the front row is wasting reach.
// ------------------------------------------------------------------------------------------------
// Catapults
// 1. Landing next to 0 enemies is not very efficient, it is better to catapult towards 1 or
// even better, 2 enemies as 1 enemy is like check while 2 enemies is checkmate, a boom
// cannot be avoided, it also gains a tempo, because the opponent has to evade.
// ------------------------------------------------------------------------------------------------
// Reposition
// 1. Moving a large stack to the other side of the board could be very useful so that we can
// target weak spots when our opponents are not very mobile.
// 2. Moving 6, 5, 4, 3, 2, 1 positions adjacently
// 3. Moving backwards is never really a good idea
// ------------------------------------------------------------------------------------------------
fn sort(actions: &mut ArrayVec<[Action; 256]>) {
()
}

/// Generates all good booms.
// NOTE: Only considers booms which could have a positive net value to the turn player.
// This is decided by ensuring that the boom takes at least 1 enemy robot with it and
// calculates this by checking for adjacent enemy robots to the source.
// NOTE: Multiple booms which could result in the same gamestate, could be eliminated by calculating islands within
// possible boom sources, but hopefully the TT will eliminate us having to
// search these branches (unless the TT gets overwritten).
// NOTE: It might be smart to value booms higher in the TT as their might often be multiple
// booms which lead the same game state, and thus reducing lots of branching.
fn generate_booms(bitboard: &Bitboard, generated: &mut ArrayVec<[Action; 256]>) {
let bots = bitboard.board[bitboard.turn];
let opps = bitboard.board[bitboard.opponent];

// Find all possible boom locations that could provide net value. This only occurs when the
// boom source is touching an enemy robot.
let mut sources = bots & Bitwise::adj_any(opps);

// We could compute the boom islands to elimite booms that would lead to the same game
// state but hopefully the transition table will solve this issue for us.

while sources != 0 {
// Pop a source from our sources
let source_pos = Bitwise::lsb(sources);
sources ^= source_pos;

// Calculate the source position index
let source_idx = Bitwise::idx(source_pos) as u8;

// Store the boom
generated.push(Action { source: source_idx, target: 0, robots: 0 });
}
actions.special.append(&mut actions.attacks);
actions.special.append(&mut actions.defends);
actions.special.append(&mut actions.others);
actions.special
}

fn generate_for_height(bitboard: &Bitboard, height: usize, generated: &mut Actions) {
fn generate_for_height(
bitboard: &Bitboard,
height: usize,
generated: &mut ArrayVec<[Action; 256]>,
) {
// Get all the turn players robots at the given height
let mut bots = bitboard.board[height] & bitboard.board[bitboard.turn];
let opponent = if bitboard.turn == WHITE { BLACK } else { WHITE };

// Precomputations
//let allies_attack_range = Bitwise::adj_any(bitboard.board[bitboard.turn]);
let enemies_attack_range = Bitwise::adj_any(bitboard.board[opponent]);
let enemies_unattacked =
bitboard.board[opponent] & !Bitwise::adj_any(bitboard.board[bitboard.turn]);
//let allies_attacked = bitboard.board[bitboard.turn] & enemies_attack_range;
//let allies_stacked = bitboard.board[bitboard.turn] & !bitboard.board[1];
//let enemies_stacked = bitboard.board[opponent] & !bitboard.board[1];

// If there is none, we can return early
if bots == 0 {
return;
}

// Otherwise
while bots != 0 {
// Pick a bot to generate moves for and remove it from the queue
let source = Bitwise::lsb(bots);
Expand All @@ -56,73 +121,30 @@ impl Action {
// ???
let source_adj = Bitwise::adj(source);

// Generate moves using a lookup table
// Generate all possible target squares using a lookup table
let source_pos = Bitwise::idx(source);
let mut actions = MOVES_LOOKUP[height][source_pos];
actions &= !bitboard.board[opponent];
// Remove all invalid positions to move to which are those occupied by opponents robots
actions &= !bitboard.board[bitboard.opponent];

while actions != 0 {
// Pick a target from the available target space and remove it from the space for
// future iterations
let target = Bitwise::lsb(actions);
actions ^= target;

let target_adj = Bitwise::adj(target);
// Convert the target square position into an index between 0-63
let target_pos = Bitwise::idx(target);

if Action::is_attack(target_adj, enemies_unattacked) {
let mut size = 1;
while size <= height && size < 3 {
generated.attacks.push(Action {
source: source_pos as u8,
target: target_pos as u8,
robots: size as u8,
});
size += 1;
}
while size <= height {
generated.others.push(Action {
source: source_pos as u8,
target: target_pos as u8,
robots: size as u8,
});
size += 1;
}
} else if Action::is_defend(source_adj, target_adj, enemies_attack_range) {
let mut size = height;
while size > 0 && size >= height - 1 {
generated.defends.push(Action {
source: source_pos as u8,
target: target_pos as u8,
robots: size as u8,
});
size -= 1;
}
while size > 0 {
generated.others.push(Action {
source: source_pos as u8,
target: target_pos as u8,
robots: size as u8,
});
size -= 1;
}
} else {
let mut size = height;
while size > 0 {
generated.others.push(Action {
source: source_pos as u8,
target: target_pos as u8,
robots: size as u8,
});
size -= 1;
}
// For each robot stack size
for moved in 1..=height {
generated.push(Action {
source: source_pos as u8,
target: target_pos as u8,
robots: moved as u8,
});
}
}

// Add all explosions
if (source & enemies_attack_range) != 0 {
generated.attacks.push(Action { source: source_pos as u8, target: 0, robots: 0 });
}
}
}

Expand Down
Loading

0 comments on commit 2aab75b

Please sign in to comment.