connect_four_solver/
lib.rs

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