translated the algorithm code from cpp to rust
This commit is contained in:
commit
da18a7e6a7
5 changed files with 251 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
7
Cargo.lock
generated
Normal file
7
Cargo.lock
generated
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "connect4"
|
||||||
|
version = "0.1.0"
|
6
Cargo.toml
Normal file
6
Cargo.toml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[package]
|
||||||
|
name = "connect4"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
205
src/connect4.rs
Normal file
205
src/connect4.rs
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
// code based fully on https://github.com/balkarjun/ConnectFourAI/blob/main/main.cpp
|
||||||
|
// rewritten from cpp to rust
|
||||||
|
|
||||||
|
/*
|
||||||
|
6 13 20 27 34 41 48 55 62
|
||||||
|
---------------------
|
||||||
|
| 5 12 19 26 33 40 47 | 54 61
|
||||||
|
| 4 11 18 25 32 39 46 | 53 60
|
||||||
|
| 3 10 17 24 31 38 45 | 52 59
|
||||||
|
| 2 9 16 23 30 37 44 | 51 58
|
||||||
|
| 1 8 15 22 29 36 43 | 50 57
|
||||||
|
| 0 7 14 21 28 35 42 | 49 56 63
|
||||||
|
---------------------
|
||||||
|
Bitboard Representation
|
||||||
|
*/
|
||||||
|
|
||||||
|
const NROWS: usize = 6;
|
||||||
|
const NCOLS: usize = 7;
|
||||||
|
// bit indices for the top padding row
|
||||||
|
const PAD_TOP: [i64; NCOLS] = [6, 13, 20, 27, 34, 41, 48];
|
||||||
|
// simple heuristic, where a position's value is the number of possible 4-in-a-rows from that position
|
||||||
|
const SCOREMAP: [i64; NCOLS * (NROWS + 1)] = [
|
||||||
|
3, 4, 5, 5, 4, 3, 0, 4, 6, 8, 8, 6, 4, 0, 5, 8, 11, 11, 8, 5, 0, 7, 9, 13, 13, 9, 7, 0, 5, 8,
|
||||||
|
11, 11, 8, 5, 0, 4, 6, 8, 8, 6, 4, 0, 3, 4, 5, 5, 4, 3, 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub struct GameState {
|
||||||
|
pub bitboards: [i64; 2],
|
||||||
|
pub counter: i64,
|
||||||
|
pub heights: [i64; NCOLS],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GameState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let bitboards: [i64; 2] = [0, 0];
|
||||||
|
// number of moves made
|
||||||
|
let counter: i64 = 0;
|
||||||
|
// bit indices where next move should be made
|
||||||
|
let heights: [i64; NCOLS] = [0, 7, 14, 21, 28, 35, 42];
|
||||||
|
|
||||||
|
GameState {
|
||||||
|
bitboards,
|
||||||
|
counter,
|
||||||
|
heights,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_is_valid(&self, icol: usize) -> bool {
|
||||||
|
return self.heights[icol] < PAD_TOP[icol];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// assumes that the move is valid
|
||||||
|
pub fn make_move(&mut self, icol: usize) {
|
||||||
|
// flip the appropriate bit (0 -> 1)
|
||||||
|
self.bitboards[(self.counter & 1) as usize] ^= 1 << self.heights[icol];
|
||||||
|
|
||||||
|
self.counter += 1;
|
||||||
|
self.heights[icol] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// assumes it is called right after a move is made
|
||||||
|
pub fn undo_move(&mut self, icol: usize) {
|
||||||
|
self.counter -= 1;
|
||||||
|
self.heights[icol] -= 1;
|
||||||
|
|
||||||
|
// flip the appropriate bit (1 -> 0)
|
||||||
|
self.bitboards[(self.counter & 1) as usize] ^= 1 << self.heights[icol];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_win(&self) -> i64 {
|
||||||
|
for idx in 0..=1 {
|
||||||
|
let board: i64 = self.bitboards[idx];
|
||||||
|
|
||||||
|
if (board & (board >> 7) & (board >> 14) & (board >> 21)) != 0 {
|
||||||
|
return idx as i64;
|
||||||
|
} // horizontal
|
||||||
|
if (board & (board >> 1) & (board >> 2) & (board >> 3)) != 0 {
|
||||||
|
return idx as i64;
|
||||||
|
} // vertical
|
||||||
|
if (board & (board >> 8) & (board >> 16) & (board >> 24)) != 0 {
|
||||||
|
return idx as i64;
|
||||||
|
} // positive diagonal
|
||||||
|
if (board & (board >> 6) & (board >> 12) & (board >> 18)) != 0 {
|
||||||
|
return idx as i64;
|
||||||
|
} // negative diagonal
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns -1 if not in an end state, or an appropriate score
|
||||||
|
pub fn end_state_reached(&self) -> i64 {
|
||||||
|
let winner = self.check_win();
|
||||||
|
if winner == 0 {
|
||||||
|
return 100000;
|
||||||
|
} // white won
|
||||||
|
if winner == 1 {
|
||||||
|
return -100000;
|
||||||
|
} // black won
|
||||||
|
|
||||||
|
if self.counter >= 42 {
|
||||||
|
return 0;
|
||||||
|
} // tie
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// outputs a score based on the current state of the board
|
||||||
|
/// assumes that the board is not in an end state
|
||||||
|
pub fn evaluate(&self) -> i64 {
|
||||||
|
// for each cell, add score for white and subtract score for black
|
||||||
|
let mut score = 0;
|
||||||
|
for idx in 0..48 {
|
||||||
|
score += SCOREMAP[idx]
|
||||||
|
* (((self.bitboards[0] >> idx) & 1) - ((self.bitboards[1] >> idx) & 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn display(&self) {
|
||||||
|
for irow in (0..NROWS).rev() {
|
||||||
|
print!("\n ");
|
||||||
|
for icol in 0..NCOLS {
|
||||||
|
let idx = irow + 7 * icol;
|
||||||
|
|
||||||
|
if (self.bitboards[0] >> idx) & 1 != 0 {
|
||||||
|
print!("● ");
|
||||||
|
} else if (self.bitboards[1] >> idx) & 1 != 0 {
|
||||||
|
print!("○ ");
|
||||||
|
} else {
|
||||||
|
print!("· ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print!("\n 1 2 3 4 5 6 7\n\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn minimax(board: &mut GameState, mut alpha: i64, beta: i64, depth: i64) -> i64 {
|
||||||
|
let score = board.end_state_reached();
|
||||||
|
if score != -1 {
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
if depth == 0 {
|
||||||
|
return board.evaluate();
|
||||||
|
}
|
||||||
|
|
||||||
|
let sign: i64 = 1 - 2 * (board.counter & 1); // 1 if white, -1 if black
|
||||||
|
|
||||||
|
for icol in 0..NCOLS {
|
||||||
|
// skip filled columns
|
||||||
|
if !board.move_is_valid(icol) {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
board.make_move(icol);
|
||||||
|
let score = minimax(board, beta, alpha, depth - 1);
|
||||||
|
board.undo_move(icol);
|
||||||
|
|
||||||
|
if sign * score > sign * alpha {
|
||||||
|
alpha = score
|
||||||
|
};
|
||||||
|
if sign * alpha >= sign * beta {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return alpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_minimax_move(board: &mut GameState, depth: i64) -> usize {
|
||||||
|
// +inf for black (1), -inf for white (0)
|
||||||
|
let mut alpha: i64 = if board.counter & 1 != 0 {
|
||||||
|
1000000
|
||||||
|
} else {
|
||||||
|
-1000000
|
||||||
|
};
|
||||||
|
let beta: i64 = -alpha;
|
||||||
|
|
||||||
|
let mut best_move: usize = 0;
|
||||||
|
let sign = 1 - 2 * (board.counter & 1); // 1 if white, -1 if black
|
||||||
|
|
||||||
|
for icol in 0..NCOLS {
|
||||||
|
// skip filled columns
|
||||||
|
if !board.move_is_valid(icol) {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
board.make_move(icol);
|
||||||
|
let score = minimax(board, beta, alpha, depth - 1);
|
||||||
|
board.undo_move(icol);
|
||||||
|
|
||||||
|
if sign * score > sign * alpha {
|
||||||
|
alpha = score;
|
||||||
|
best_move = icol;
|
||||||
|
}
|
||||||
|
|
||||||
|
if sign * alpha >= sign * beta {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return best_move;
|
||||||
|
}
|
32
src/main.rs
Normal file
32
src/main.rs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
mod connect4;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut board = connect4::GameState::new();
|
||||||
|
let depth = 7;
|
||||||
|
|
||||||
|
let mut end_score: i64 = -1;
|
||||||
|
|
||||||
|
while end_score == -1 {
|
||||||
|
println!("Move {}:", board.counter);
|
||||||
|
board.display();
|
||||||
|
let best_move =
|
||||||
|
connect4::get_minimax_move(
|
||||||
|
&mut board,
|
||||||
|
depth
|
||||||
|
);
|
||||||
|
|
||||||
|
board.make_move(best_move);
|
||||||
|
end_score = board.end_state_reached();
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Move {}:", board.counter);
|
||||||
|
board.display();
|
||||||
|
|
||||||
|
if end_score == 0 {
|
||||||
|
println!("It's a Tie");
|
||||||
|
} else if end_score > 0 {
|
||||||
|
println!("White (●) Won!");
|
||||||
|
} else {
|
||||||
|
println!("Black (○) Won!");
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue