1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
mod bitboard;
mod solver;
mod transposition_table;
use self::bitboard::PlayerStones;
use std::{fmt, io, str::FromStr};
use bitboard::{heuristic, AllStones, NonLoosingMoves};
pub use solver::score;
/// An integer ranging from 0 to 6 representing a column of the connect four board.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Column(u8);
impl Column {
/// Column index ranges from 0 to 6
pub const fn from_index(index: u8) -> Column {
assert!(index < 7);
Column(index)
}
}
impl FromStr for Column {
type Err = &'static str;
fn from_str(source: &str) -> Result<Column, Self::Err> {
match source.as_bytes().first() {
Some(v @ b'1'..=b'7') => Ok(Column(v - b'1')),
_ => Err("Only digits from 1 to 7 count as valid moves."),
}
}
}
impl fmt::Display for Column {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Column: {}", self.0)
}
}
/// State of a field in a four in a row board
#[derive(Clone, Copy, PartialEq, Eq)]
enum Cell {
/// Field is not captured by either player
Empty,
/// Field contains a stone from Player 1
PlayerOne,
/// Field contains a stone from Player 2
PlayerTwo,
}
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct ConnectFour {
/// Bitborad encoding the stones of the player who did insert the last stone. Starts with Player
/// two.
last: PlayerStones,
/// Bitboard encoding all cells containing stones, no matter the player.
both: AllStones,
}
impl ConnectFour {
/// Create an empty board
pub fn new() -> ConnectFour {
ConnectFour {
last: PlayerStones::new(),
both: AllStones::default(),
}
}
/// Inserts a stone for the current player. `true` if move has been legal
pub fn play(&mut self, column: Column) -> bool {
// Let's check if the move is legal, otherwise return false.
if self.both.is_full(column.0) {
return false;
}
// Now we add a stone to the bitmask for both player.
self.both.insert(column.0);
// Flip players after adding the stone, so the stone is accounted for the last player
self.last.flip(self.both);
true
}
/// `true` if the column is not full.
pub fn is_legal_move(&self, column: Column) -> bool {
!self.both.is_full(column.0)
}
/// Create a game state from a sequence of moves. Each move represented as a number from 1 to 7
/// standing for the column the player put in their stones.
pub fn from_move_list(move_list: &str) -> ConnectFour {
let mut game = ConnectFour::new();
for c in move_list
.as_bytes()
.iter()
.map(|c| c - b'1')
.map(Column::from_index)
{
if !game.play(c) {
panic!("Illegal move in String describing Connect Four Game")
}
}
game
}
/// Prints out a text representation of a board to `out`
pub fn print_to(&self, mut out: impl io::Write) -> io::Result<()> {
for row in (0..6).rev() {
for field in (0..7).map(|column| self.cell(row, column)) {
let c = match field {
Cell::PlayerOne => 'X',
Cell::PlayerTwo => 'O',
Cell::Empty => ' ',
};
write!(out, "|{}", c)?;
}
writeln!(out, "|")?;
}
writeln!(out, "---------------\n 1 2 3 4 5 6 7")
}
/// Access any cell of the board and find out whether it is empty, or holding a stone of Player
/// One or Two.
fn cell(&self, row: u8, column: u8) -> Cell {
let players = [Cell::PlayerOne, Cell::PlayerTwo];
if self.both.is_empty(row, column) {
Cell::Empty
} else if self.last.is_empty(row, column) {
players[self.both.stones() as usize % 2]
} else {
players[(self.both.stones() as usize + 1) % 2]
}
}
/// Heurisitc used to decide which moves to explore first, in order to allow for better pruning
/// of the search tree. Higher means better for the player which put in the last stone.
fn heuristic(&self) -> u32 {
heuristic(self.last, self.both)
}
/// Number of stones in the board
pub fn stones(&self) -> u8 {
self.both.stones()
}
/// `true` if the player which did insert the last stone has one the game.
pub fn is_victory(&self) -> bool {
self.last.is_win()
}
/// Uses the first 49 Bits to uniquely encode the board.
pub fn encode(&self) -> u64 {
self.last.key(self.both)
}
/// `true` if the current player has winning moves available
pub fn can_win_in_next_move(&self) -> bool {
let mut current = self.last;
current.flip(self.both);
self.both.possible() & current.winning_positions() != 0
}
/// `true` if game has a winner or is a draw.
pub fn is_over(&self) -> bool {
self.stones() == 42 || self.is_victory()
}
// Only valid to call if `can_win_in_next_move` is `false`.
fn non_loosing_moves(&self) -> NonLoosingMoves {
debug_assert!(!self.can_win_in_next_move());
NonLoosingMoves::new(self.last, self.both)
}
}
impl fmt::Display for ConnectFour {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for row in (0..6).rev() {
for field in (0..7).map(|column| self.cell(row, column)) {
let c = match field {
Cell::PlayerOne => 'X',
Cell::PlayerTwo => 'O',
Cell::Empty => ' ',
};
write!(f, "|{}", c)?;
}
writeln!(f, "|")?;
}
writeln!(f, "---------------\n 0 1 2 3 4 5 6")
}
}