rotchess_core/
emulator.rs

1//! The main entrypoint for any rotchess user.
2//!
3//! design doc:
4//! - on hovering over move/cap points, highlight if it's a possible move
5//! - only draw guides for non-jumpers (might be drawer's responsibility)
6//! - piece selection on mousedown
7//!   - but if piece was alr selected, or no action could be taken, deselect the only selected piece.
8//!   - hmm. wondering now, we could probably move only selected to a Option<usize> in each Pieces.
9//!   - but that raises the question, can we move everything GamePieceData related to Board?
10//! - drag move/cap point to rotate, or mouseup without having dragged to move (if was possible)
11
12use crate::{
13    piece::{Piece, Pieces},
14    turn::Turns,
15};
16
17/// Mouse buttons a chess board can respond to.
18///
19/// This enum may add new variants.
20#[derive(Clone, Copy)]
21pub enum MouseButton {
22    LEFT,
23    RIGHT,
24}
25
26/// User events a chess board can respond to.
27#[derive(Clone, Copy)]
28pub enum Event {
29    ButtonDown {
30        x: f32,
31        y: f32,
32        button: MouseButton,
33    },
34    ButtonUp {
35        x: f32,
36        y: f32,
37        button: MouseButton,
38    },
39    MouseMotion {
40        x: f32,
41        y: f32,
42    },
43    FirstTurn,
44    PrevTurn,
45    NextTurn,
46    LastTurn,
47    /// We've been told to rotate idx to r.
48    RotateUnchecked(usize, f32),
49    /// We've been told to move idx to x, y.
50    MoveUnchecked(usize, f32, f32),
51}
52
53#[derive(PartialEq, Debug)]
54pub enum TravelKind {
55    Capture,
56    Move,
57}
58
59#[derive(Debug)]
60pub struct TravelPoint {
61    pub x: f32,
62    pub y: f32,
63    pub travelable: bool,
64    pub kind: TravelKind,
65}
66
67pub struct RotchessEmulator {
68    /// A valid representation of travelpoints a user needs to draw iff we
69    ///  update this every time a piece.core changes and `self.selected.is_some()`.
70    travelpoints_buffer: Vec<TravelPoint>,
71    /// Whether a piece is selected.
72    ///
73    /// If `Some(sel_i)`, then `self.turns[curr_turn].inner[sel_i]` is the
74    /// selected piece. Additionally, `travelpoints_buffer` is the travel
75    /// points that that piece has access to.
76    selected_piece: Option<usize>,
77    /// (idx of travelpoint within buffer, angle offset of drag, whether we have dragged yet)
78    ///
79    /// Set when we mbd to hold a travel point, updated when we drag it around.
80    selected_travelpoint: Option<(usize, f32, bool)>,
81
82    turns: Turns,
83    // Uhhhh. theses should probably be abstracted in yet another struct for turn management, skull.
84    // don't feel like doing it rn.
85}
86
87/// Misc.
88impl RotchessEmulator {
89    /// Create an emulator with an empty board.
90    pub fn new() -> Self {
91        todo!()
92    }
93
94    /// Create an enmulator with pieces.
95    pub fn with(pieces: Pieces) -> Self {
96        Self {
97            travelpoints_buffer: vec![],
98            selected_piece: None,
99            selected_travelpoint: None,
100            turns: Turns::with(pieces),
101        }
102    }
103}
104
105/// Angle between from and to, given a pivot.
106fn calc_angle_offset(pivot: (f32, f32), from: (f32, f32), to: (f32, f32)) -> f32 {
107    let from = (from.0 - pivot.0, from.1 - pivot.1);
108    let to = (to.0 - pivot.0, to.1 - pivot.1);
109
110    let from_angle = f32::atan2(from.1, from.0);
111    let to_angle = f32::atan2(to.1, to.0);
112
113    to_angle - from_angle
114}
115
116pub enum ThingHappened {
117    FirstTurn,
118    PrevTurn,
119    NextTurn,
120    LastTurn,
121    /// We rotated the piece at usize to r
122    Rotate(usize, f32),
123    /// We moved the piece at usize to x, y
124    Move(usize, f32, f32),
125}
126
127/// Helpful functions for the update portion of a game loop implementing rotchess.
128///
129/// TODO Future plans: add another impl block for a headless updater? ie, "can i move this piece here,"
130/// "where can i move this piece". useful for ml?
131impl RotchessEmulator {
132    /// Log the current selected piece's travelpoints in the internal buffer.
133    ///
134    /// Such a piece index must exist.
135    /// Will initialize the piece's internal auxiliary data if required.
136    /// Will update internal auxiliary data always.
137    pub fn update_travelpoints_unchecked(&mut self) {
138        let piece = &mut self
139            .turns
140            .working_board_mut()
141            .get_mut(self.selected_piece.expect("Invariant"))
142            .unwrap();
143        if piece.needs_init() {
144            piece.init_auxiliary_data();
145        } else {
146            piece.update_capture_points_unchecked();
147            piece.update_move_points_unchecked();
148        }
149
150        let piece = &self
151            .turns
152            .working_board_ref()
153            .get(self.selected_piece.expect("Invariant"))
154            .unwrap();
155        self.travelpoints_buffer.clear();
156        for &(x, y) in piece.move_points_unchecked() {
157            self.travelpoints_buffer.push(TravelPoint {
158                x,
159                y,
160                travelable: self.turns.working_board_ref().travelable(
161                    piece,
162                    x,
163                    y,
164                    TravelKind::Move,
165                ),
166                kind: TravelKind::Move,
167            });
168        }
169        for &(x, y) in piece.capture_points_unchecked() {
170            self.travelpoints_buffer.push(TravelPoint {
171                x,
172                y,
173                travelable: self.turns.working_board_ref().travelable(
174                    piece,
175                    x,
176                    y,
177                    TravelKind::Capture,
178                ),
179                kind: TravelKind::Capture,
180            });
181        }
182    }
183
184    pub fn make_best_move(&mut self) {
185        self.turns.make_best_move();
186    }
187
188    /// Handle an event.
189    ///
190    /// Priority order (high to low) for clicks:
191    ///
192    /// 1. rotation dragging
193    /// 1. captures
194    /// 1. piece selection
195    /// 1. moves
196    pub fn handle_event(&mut self, e: Event) -> Option<ThingHappened> {
197        match e {
198            Event::MouseMotion { x, y } => {
199                // println!("dragged: {} {}", x, y);
200                if let Some((tvp_idx, angle_offset, _)) = self.selected_travelpoint {
201                    let piece_idx = self
202                        .selected_piece
203                        .expect("A piece is sel by invariant of tvp.is_some().");
204                    let piece = &mut self.turns.working_board_mut().get_mut(piece_idx).unwrap();
205                    let piece_center = piece.center();
206
207                    // mouse_angle is the angle with piece as pivot, with 0rad being up. because for
208                    // some godforsaken reason I made the 0 angle up.
209                    let mouse_angle = -calc_angle_offset(
210                        piece_center,
211                        (piece_center.0, piece_center.1 - 10.), // and also, up is the negative y axis because macroquad.
212                        (x, y),
213                    );
214                    piece.set_angle(mouse_angle + angle_offset);
215                    self.update_travelpoints_unchecked();
216
217                    self.selected_travelpoint = Some((tvp_idx, angle_offset, true));
218                }
219                None
220            }
221            Event::ButtonDown {
222                x,
223                y,
224                button: MouseButton::RIGHT,
225            } => {
226                debug_assert!(
227                    self.selected_travelpoint.is_none(),
228                    "Should not be possible to buttondown without having the
229                    travel point be deselected already."
230                );
231
232                let idx_of_piece_at_xy = self.turns.working_board_ref().get_piece(x, y);
233
234                // handle piece selection
235                match (idx_of_piece_at_xy, self.selected_piece) {
236                    (Some(new_i), Some(curr_sel_i)) => {
237                        // we clicked on a piece, and a piece is already selected.
238                        if new_i == curr_sel_i {
239                            // we clicked on the already-selected piece, deselect it.
240                            self.selected_piece = None;
241                        } else {
242                            // we clicked on a different piece, select that instead.
243                            self.selected_piece = Some(new_i);
244                            self.update_travelpoints_unchecked();
245                        }
246                        return None;
247                    }
248                    (Some(new_i), None) => {
249                        // we clicked on a piece, and None pieces were selected.
250                        self.selected_piece = Some(new_i);
251                        self.update_travelpoints_unchecked();
252                        return None;
253                    }
254                    (None, _) => {
255                        self.selected_piece = None;
256                    }
257                }
258                None
259            }
260            Event::ButtonDown {
261                x,
262                y,
263                button: MouseButton::LEFT,
264            } => {
265                debug_assert!(
266                    self.selected_travelpoint.is_none(),
267                    "Should not be possible to buttondown without having the
268                    travel point be deselected already."
269                );
270
271                let idx_of_piece_at_xy = self.turns.working_board_ref().get_piece(x, y);
272                // println!("{}", idx_of_piece_at_xy.is_some());
273
274                // handle clicking a travelpoint
275                //
276                // if we click a travelpoint, store in emulator data that we've sel'd a tvp
277                // with such an angle offset from our mousepos to the tvp center
278                let pieces = &mut self.turns.working_board_ref();
279                if let Some(sel_idx) = self.selected_piece {
280                    for (tvp_idx, tp) in self.travelpoints_buffer.iter().enumerate() {
281                        if Piece::collidepoint_generic(x, y, tp.x, tp.y) {
282                            self.selected_travelpoint = Some((
283                                tvp_idx,
284                                calc_angle_offset(
285                                    pieces.get(sel_idx).unwrap().center(),
286                                    (
287                                        pieces.get(sel_idx).unwrap().x(),
288                                        pieces.get(sel_idx).unwrap().y() - 10.,
289                                    ),
290                                    (x, y),
291                                ) + pieces.get(sel_idx).unwrap().angle(),
292                                false,
293                            ));
294                            if tp.travelable {
295                                return None;
296                            }
297                        }
298                    }
299                    if self.selected_travelpoint.is_some() {
300                        return None;
301                    }
302                }
303
304                // handle piece selection
305                match (idx_of_piece_at_xy, self.selected_piece) {
306                    (Some(new_i), Some(curr_sel_i)) => {
307                        // we clicked on a piece, and a piece is already selected.
308                        if new_i == curr_sel_i {
309                            // we clicked on the already-selected piece, deselect it.
310                            self.selected_piece = None;
311                        } else {
312                            // we clicked on a different piece, select that instead.
313                            self.selected_piece = Some(new_i);
314                            self.update_travelpoints_unchecked();
315                        }
316                        return None;
317                    }
318                    (Some(new_i), None) => {
319                        // we clicked on a piece, and None pieces were selected.
320                        self.selected_piece = Some(new_i);
321                        self.update_travelpoints_unchecked();
322                        return None;
323                    }
324                    (None, _) => {
325                        if self.selected_travelpoint.is_none() {
326                            self.selected_piece = None;
327                        }
328                    }
329                }
330                None
331            }
332            Event::ButtonUp {
333                x,
334                y,
335                button: MouseButton::LEFT,
336            } => {
337                // println!("up: {} {}", x, y);
338
339                if let Some((trav_idx, _, false)) = self.selected_travelpoint {
340                    // if we selected a travelpoint and it hasn't been moved yet, we want to try
341                    // to initiate the travel.
342                    let tp = &self.travelpoints_buffer[trav_idx];
343                    let (tp_x, tp_y) = (tp.x, tp.y);
344                    debug_assert!(Piece::collidepoint_generic(x, y, tp_x, tp_y));
345
346                    if tp.travelable {
347                        // if it is indeed travelable, travel.
348                        let pieces = &mut self.turns.working_board_mut();
349                        let piece_idx = self // the idx of the piece that moves
350                            .selected_piece
351                            .expect("Invariant of selected_travelpoint.issome");
352                        let new_piece_idx = pieces.travel(piece_idx, tp_x, tp_y);
353                        self.selected_piece = Some(new_piece_idx);
354                        self.update_travelpoints_unchecked();
355                        self.selected_piece = None;
356                        self.selected_travelpoint = None;
357                        self.turns.save_turn();
358                        self.turns
359                            .set_to_move(self.pieces()[new_piece_idx].side().toggle());
360                        return Some(ThingHappened::Move(piece_idx, tp_x, tp_y));
361                    }
362                    self.selected_travelpoint = None;
363                }
364
365                if let Some((_, _, true)) = self.selected_travelpoint {
366                    self.selected_travelpoint = None;
367                    self.turns.save_turn();
368
369                    let piece_idx = self
370                        .selected_piece
371                        .expect("Invariant of sel travelpt.is_some");
372
373                    self.turns.set_to_move(self.pieces()[piece_idx].side());
374                    let r = self.pieces()[piece_idx].angle();
375                    self.turns
376                        .set_to_move(self.pieces()[piece_idx].side().toggle());
377                    return Some(ThingHappened::Rotate(piece_idx, r));
378                }
379
380                None
381            }
382            Event::FirstTurn => {
383                self.turns.first();
384                self.selected_piece = None;
385                self.selected_travelpoint = None;
386                Some(ThingHappened::FirstTurn)
387            }
388            Event::PrevTurn => {
389                _ = self.turns.prev();
390                self.selected_piece = None;
391                self.selected_travelpoint = None;
392                Some(ThingHappened::PrevTurn)
393            }
394            Event::NextTurn => {
395                _ = self.turns.next();
396                self.selected_piece = None;
397                self.selected_travelpoint = None;
398                Some(ThingHappened::NextTurn)
399            }
400            Event::LastTurn => {
401                self.turns.last();
402                self.selected_piece = None;
403                self.selected_travelpoint = None;
404                Some(ThingHappened::LastTurn)
405            }
406            Event::RotateUnchecked(piece_idx, r) => {
407                // to elaborate, it would suck to be playing around and then a piece
408                // rotates for "no reason"
409                assert!(
410                    self.selected_travelpoint.is_none() && self.selected_piece.is_none(),
411                    "It would probably be a good idea for this check
412                     to never fail, but who knows. Go find the dev if
413                     you see this error message."
414                );
415
416                self.turns
417                    .working_board_mut()
418                    .get_mut(piece_idx)
419                    .expect("Invariant of unchecked")
420                    .set_angle(r);
421                self.turns.save_turn();
422                self.turns
423                    .set_to_move(self.pieces()[piece_idx].side().toggle());
424                None
425            }
426            Event::MoveUnchecked(piece_idx, x, y) => {
427                // to elaborate, it would suck to be playing around and then a piece
428                // move for "no reason"
429                assert!(
430                    self.selected_travelpoint.is_none() && self.selected_piece.is_none(),
431                    "It would probably be a good idea for this check
432                     to never fail, but who knows. Go find the dev if
433                     you see this error message."
434                );
435
436                let pieces = &mut self.turns.working_board_mut();
437                let new_piece_idx = pieces.travel(piece_idx, x, y);
438                self.selected_piece = Some(new_piece_idx);
439                self.update_travelpoints_unchecked();
440                self.selected_piece = None;
441                self.turns.save_turn();
442                self.turns
443                    .set_to_move(self.pieces()[piece_idx].side().toggle());
444                None
445            }
446            _ => None,
447        }
448    }
449}
450
451/// Helpful functions for the draw portion of a game loop implementing rotchess.
452impl RotchessEmulator {
453    pub fn pieces(&self) -> &[Piece] {
454        self.turns.working_board_ref().inner_ref()
455    }
456
457    /// Whether there is a selected piece.
458    ///
459    /// If Some, it contains the piece and its possible travelpoints.
460    pub fn selected(&self) -> Option<(&Piece, &[TravelPoint])> {
461        self.selected_piece.map(|sel_i| {
462            (
463                self.turns
464                    .working_board_ref()
465                    .get(sel_i)
466                    .expect("exists because selected_piece.is_some()."),
467                self.travelpoints_buffer.as_slice(),
468            )
469        })
470    }
471}