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}