rotchess_core/
turn.rs

1use crate::{
2    emulator::TravelKind,
3    piece::{Pieces, Side},
4};
5
6pub struct Turns {
7    working_board: Pieces,
8    curr_turn: usize,
9    turns: Vec<Pieces>,
10    /// Whose turn it is.
11    ///
12    /// Update this manually, which is odd. Recall we have the playground style
13    /// of board where turn order may not matter.
14    to_move: Side,
15}
16
17/// Generic turn methods.
18impl Turns {
19    pub fn with(pieces: Pieces) -> Self {
20        Self {
21            working_board: pieces.clone(),
22            curr_turn: 0,
23            turns: vec![pieces],
24            to_move: Side::White,
25        }
26    }
27
28    pub fn set_to_move(&mut self, side: Side) {
29        self.to_move = side;
30    }
31
32    pub fn curr_turn(&self) -> usize {
33        self.curr_turn
34    }
35
36    pub fn inner_ref(&self) -> &Vec<Pieces> {
37        &self.turns
38    }
39
40    pub fn working_board_ref(&self) -> &Pieces {
41        &self.working_board
42    }
43
44    pub fn working_board_mut(&mut self) -> &mut Pieces {
45        &mut self.working_board
46    }
47
48    /// Saves the working board as a turn.
49    ///
50    /// Follows standard saving procedure used throughout applications. When the turn is:
51    ///
52    /// - not the most recent turn: truncates the turns s.t. the current turn is the most recent, continue below
53    /// - the most recent turn: pushes a clone of the working board to the turns, increments curr_turn
54    pub fn save_turn(&mut self) {
55        if self.turns.get(self.curr_turn + 1).is_some() {
56            self.turns
57                .resize_with(self.curr_turn + 1, || unreachable!("see if guard"));
58        }
59
60        self.turns.push(self.working_board.clone());
61        self.curr_turn += 1;
62    }
63
64    pub fn first(&mut self) {
65        self.load_turn(0);
66    }
67
68    pub fn last(&mut self) {
69        self.load_turn(self.turns.len() - 1);
70    }
71
72    pub fn prev(&mut self) -> Result<(), ()> {
73        if self.curr_turn == 0 {
74            Err(())
75        } else {
76            self.curr_turn -= 1;
77            self.load_turn(self.curr_turn);
78            Ok(())
79        }
80    }
81
82    pub fn next(&mut self) -> Result<(), ()> {
83        if self.curr_turn + 1 >= self.turns.len() {
84            Err(())
85        } else {
86            self.curr_turn += 1;
87            self.load_turn(self.curr_turn);
88            Ok(())
89        }
90    }
91
92    fn load_turn(&mut self, turn: usize) {
93        self.working_board.clone_from(&self.turns[turn]);
94        self.curr_turn = turn;
95    }
96}
97
98/// A move as represented by our engine.
99struct EngineMove {
100    travel: (usize, f32, f32),
101    rotate: (usize, f32),
102}
103
104/// Score for how good a position is.
105pub type Score = f32;
106/// depth of negamax search in plies
107const DEPTH: usize = 3;
108
109/// Engine code.
110impl Turns {
111    /// Returns the score, statically evaluated at the current position.
112    ///
113    /// A float with more positive favoring the current player from `self.to_move`, 0 even.
114    fn eval(&self) -> Score {
115        let mult = match self.to_move {
116            Side::Black => -1.,
117            Side::White => 1.,
118        };
119        let mut ans = 0.0;
120        for piece in self.working_board.inner_ref() {
121            // add score value of each piece
122            ans +=
123                mult * match piece.side() {
124                    Side::Black => -1.,
125                    Side::White => 1.,
126                } * piece.kind().value()
127                    * 100.;
128
129            // make pieces go toward center.
130            /// Center of the board in rotchess units.
131            const CENTER_X: f32 = 4.0;
132            /// Center of the board in rotchess units.
133            const CENTER_Y: f32 = 4.0;
134            ans +=
135                mult * match piece.side() {
136                    Side::Black => -1.,
137                    Side::White => 1.,
138                } * (5.0
139                    - Score::sqrt((piece.x() - CENTER_X).powi(2) + (piece.y() - CENTER_Y).powi(2)));
140        }
141        ans
142    }
143
144    /// Return the score we get in `depth` plies when minimizing our maximum loss.
145    ///
146    /// - "We" should be `self.to_move`.
147    /// - alpha is the highest score we already found. (if we see a score lower than it,
148    ///   no need to consider it.)
149    /// - beta is the best score we are able to get before the opponent is able to deny it
150    ///   with a reply we already found.
151    fn negamax_ab(&mut self, depth: usize, mut alpha: Score, beta: Score) -> Score {
152        // println!("depth is {depth}");
153        if depth == 0 {
154            return self.eval();
155        }
156
157        let mut best_score = Score::NEG_INFINITY;
158
159        for move_ in self.all_moves() {
160            self.apply(&move_);
161            let score = -self.negamax_ab(depth - 1, -beta, -alpha);
162            self.unapply();
163
164            if score > best_score {
165                best_score = score;
166                if score > alpha {
167                    alpha = score;
168                }
169            }
170            if score >= beta {
171                break;
172            }
173        }
174
175        best_score
176    }
177
178    /// Make the best move for the current player from `self.to_move`.
179    pub fn make_best_move(&mut self) {
180        let mut best_score: Score = Score::NEG_INFINITY;
181        let mut best_move: Option<EngineMove> = None;
182
183        for piece in self
184            .working_board
185            .inner_mut()
186            .iter_mut()
187            .chain(self.turns[self.curr_turn].inner_mut())
188        {
189            piece.init_auxiliary_data();
190        }
191
192        let moves = self.all_moves();
193        assert!(!moves.is_empty());
194        for move_ in moves {
195            self.apply(&move_);
196            let score = -self.negamax_ab(DEPTH, Score::NEG_INFINITY, Score::INFINITY);
197            self.unapply();
198
199            if score >= best_score {
200                best_score = score;
201                best_move = Some(move_);
202            }
203        }
204
205        self.apply(&best_move.expect("should've found a valid move."));
206
207        println!("best move had score {best_score}");
208        println!(
209            "current board state has score {} according to {:?}",
210            self.eval(),
211            self.to_move
212        );
213    }
214
215    /// Reverses effects of [`apply`][`Turns::apply`].
216    fn unapply(&mut self) {
217        self.prev().expect("There will be a prev move.");
218        self.to_move = self.to_move.toggle();
219    }
220
221    /// Applies a move to the current board, saves the turn, and toggles the side to_move.
222    ///
223    /// Since we save, this will remove future turns that were saved!
224    /// Also, the entire turn is saved as one turn, not two, which would
225    /// happen if a user were to move.
226    fn apply(&mut self, move_: &EngineMove) {
227        // println!("tomove is {:?}", self.to_move);
228        debug_assert_eq!(
229            self.working_board
230                .get_mut(move_.travel.0)
231                .expect("EngineMove supplied wasn't valid")
232                .side(),
233            self.to_move
234        );
235        debug_assert_eq!(
236            self.working_board
237                .get_mut(move_.rotate.0)
238                .expect("EngineMove supplied wasn't valid")
239                .side(),
240            self.to_move
241        );
242
243        let (i, x, y) = move_.travel;
244
245        let i = self.working_board.travel(i, x, y);
246        self.working_board
247            .get_mut(i)
248            .expect("EngineMove supplied wasn't valid")
249            // .init_auxiliary_data();
250            .update_capmove_points_unchecked();
251
252        let (_, r) = move_.rotate;
253        self.working_board
254            .get_mut(i)
255            .expect("EngineMove supplied wasn't valid")
256            .set_angle(r);
257
258        self.save_turn();
259        self.to_move = self.to_move.toggle();
260    }
261
262    /// Return all possible moves that the current player can make.
263    ///
264    /// Current player defined by `self.to_move`.
265    fn all_moves(&mut self) -> Vec<EngineMove> {
266        let mut ans = vec![];
267        for piece in self.working_board.inner_mut().iter_mut() {
268            piece.init_auxiliary_data();
269        }
270        for (i, piece) in self.working_board.inner_ref().iter().enumerate() {
271            if piece.side() != self.to_move {
272                continue;
273            }
274            for (tvk, x, y) in piece
275                .move_points_unchecked()
276                .map(|&(x, y)| (TravelKind::Move, x, y))
277                .chain(
278                    piece
279                        .capture_points_unchecked()
280                        .map(|&(x, y)| (TravelKind::Capture, x, y)),
281                )
282            {
283                if self.working_board_ref().travelable(piece, x, y, tvk) {
284                    ans.push(EngineMove {
285                        travel: (i, x, y),
286                        rotate: (i, piece.angle()),
287                    });
288                }
289            }
290        }
291        ans
292    }
293}