1use std::f32::consts::TAU;
18
19use egui::{
20 Color32, CornerRadius, CursorIcon, FontId, Pos2, Rect, Response, Sense, Shape, Stroke,
21 StrokeKind, Ui, Vec2, Widget,
22};
23
24use crate::game::{Dir, GameStatus, HexwebGame, Node, NodeId, PieceId};
25
26const SQRT_3: f32 = 1.732_050_8;
29
30const PIECE_RADIUS: f32 = 0.88;
33const ARROW_TIP: f32 = 0.62;
37const ARROW_BACK: f32 = 0.30;
38const ARROW_SPREAD: f32 = 0.17;
39const ARROW_STROKE: f32 = 0.085;
41const RING_RADIUS: f32 = 0.80;
45
46pub fn content_size(game: &HexwebGame, cell_size: f32) -> Vec2 {
52 let (min, max) = center_bounds(game, cell_size);
53 (max - min) + Vec2::new(2.0, SQRT_3) * cell_size
54}
55
56pub fn fit_cell_size(game: &HexwebGame, available: Vec2) -> f32 {
62 let unit = content_size(game, 1.0);
63 (available.x / unit.x).min(available.y / unit.y).max(4.0)
64}
65
66fn node_center(q: i32, r: i32, size: f32) -> Vec2 {
70 Vec2::new(
71 1.5 * size * q as f32,
72 SQRT_3 * size * (r as f32 + 0.5 * q as f32),
73 )
74}
75
76fn dir_angle(dir: Dir) -> f32 {
80 -TAU / 4.0 + TAU / 6.0 * dir as u8 as f32
81}
82
83fn center_bounds(game: &HexwebGame, size: f32) -> (Vec2, Vec2) {
85 let mut min = Vec2::splat(f32::INFINITY);
86 let mut max = Vec2::splat(f32::NEG_INFINITY);
87 for node in game.nodes() {
88 let c = node_center(node.q, node.r, size);
89 min = min.min(c);
90 max = max.max(c);
91 }
92 (min, max)
93}
94
95fn hex_corners(center: Pos2, radius: f32) -> Vec<Pos2> {
97 (0..6)
98 .map(|i| center + Vec2::angled(TAU / 6.0 * i as f32) * radius)
99 .collect()
100}
101
102fn draw_arrow(painter: &egui::Painter, center: Pos2, dir: Dir, radius: f32, stroke: Stroke) {
104 let v = Vec2::angled(dir_angle(dir));
105 let tip = center + v * (radius * ARROW_TIP);
106 let back = center + v * (radius * ARROW_BACK);
107 let spread = v.rot90() * (radius * ARROW_SPREAD);
108 painter.add(Shape::line(vec![back + spread, tip, back - spread], stroke));
109}
110
111fn can_drop_at(game: &HexwebGame, piece: PieceId, node: NodeId) -> bool {
116 match game.piece_at(node) {
117 Some(other) => other != piece && game.can_swap(piece, other),
118 None => game.can_move(piece, node),
119 }
120}
121
122#[derive(Clone, Copy)]
126struct Geometry {
127 origin: Pos2,
129 cell: f32,
131 min: Vec2,
134}
135
136impl Geometry {
137 fn new(origin: Pos2, cell: f32, game: &HexwebGame) -> Self {
138 Self {
139 origin,
140 cell,
141 min: center_bounds(game, cell).0,
142 }
143 }
144
145 fn center(&self, node: &Node) -> Pos2 {
147 self.origin + node_center(node.q, node.r, self.cell) - self.min
148 + Vec2::new(self.cell, SQRT_3 * 0.5 * self.cell)
149 }
150
151 fn node_at(&self, game: &HexwebGame, pos: Pos2) -> Option<NodeId> {
156 let mut best = None;
157 let mut best_d = f32::INFINITY;
158 for (id, node) in game.nodes().iter().enumerate() {
159 let d = pos.distance(self.center(node));
160 if d < best_d {
161 best = Some(id);
162 best_d = d;
163 }
164 }
165 match best {
166 Some(id) if best_d <= self.cell => Some(id),
167 _ => None,
168 }
169 }
170}
171
172pub struct HexwebWidget<'a> {
185 game: &'a mut HexwebGame,
186 cell_size: Option<f32>,
187 win_message: Option<String>,
188 interactive: bool,
189 satisfied_color: Option<Color32>,
190 unsatisfied_color: Option<Color32>,
191}
192
193impl<'a> HexwebWidget<'a> {
194 pub fn new(game: &'a mut HexwebGame) -> Self {
195 Self {
196 game,
197 cell_size: None,
198 win_message: None,
199 interactive: true,
200 satisfied_color: None,
201 unsatisfied_color: None,
202 }
203 }
204
205 pub fn cell_size(mut self, size: f32) -> Self {
209 self.cell_size = Some(size);
210 self
211 }
212
213 pub fn win_message(mut self, message: impl Into<String>) -> Self {
217 self.win_message = Some(message.into());
218 self
219 }
220
221 pub fn satisfied_color(mut self, color: Color32) -> Self {
224 self.satisfied_color = Some(color);
225 self
226 }
227
228 pub fn unsatisfied_color(mut self, color: Color32) -> Self {
232 self.unsatisfied_color = Some(color);
233 self
234 }
235
236 pub fn interactive(mut self, interactive: bool) -> Self {
239 self.interactive = interactive;
240 self
241 }
242}
243
244impl Widget for HexwebWidget<'_> {
245 fn ui(self, ui: &mut Ui) -> Response {
246 let Self {
247 game,
248 cell_size,
249 win_message,
250 interactive,
251 satisfied_color,
252 unsatisfied_color,
253 } = self;
254
255 let cell = cell_size.unwrap_or_else(|| fit_cell_size(game, ui.available_size()));
256 let total_size = content_size(game, cell);
257
258 let sense = if interactive {
259 Sense::click_and_drag()
260 } else {
261 Sense::hover()
262 };
263 let (response, painter) = ui.allocate_painter(total_size, sense);
264 let geometry = Geometry::new(response.rect.min, cell, game);
265
266 let drag_id = response.id.with("hexweb_drag_piece");
271 let select_id = response.id.with("hexweb_selected_piece");
272 let can_play = interactive && game.status() == GameStatus::InProgress;
273 let pointer = response.interact_pointer_pos();
274 let mut dragging: Option<PieceId> = ui.ctx().data(|d| d.get_temp(drag_id));
275
276 if can_play {
277 if response.clicked() {
278 let hit = pointer.and_then(|pos| geometry.node_at(game, pos));
281 let hit_piece = hit.and_then(|node| game.piece_at(node));
282 if let Some(piece) = hit_piece {
283 let already =
284 ui.ctx().data(|d| d.get_temp::<PieceId>(select_id)) == Some(piece);
285 ui.ctx().data_mut(|d| {
286 if already {
287 d.remove_temp::<PieceId>(select_id);
288 } else {
289 d.insert_temp(select_id, piece);
290 }
291 });
292 } else if let Some(node) = hit {
293 if let Some(selected) = ui.ctx().data(|d| d.get_temp::<PieceId>(select_id)) {
294 if game.move_piece(selected, node) {
295 ui.ctx().data_mut(|d| d.remove_temp::<PieceId>(select_id));
296 }
297 }
298 } else {
299 ui.ctx().data_mut(|d| d.remove_temp::<PieceId>(select_id));
300 }
301 } else if response.drag_started() {
302 let grabbed = ui
308 .input(|i| i.pointer.press_origin())
309 .and_then(|pos| geometry.node_at(game, pos))
310 .and_then(|node| game.piece_at(node));
311 if let Some(piece) = grabbed {
312 ui.ctx().data_mut(|d| {
313 d.insert_temp(drag_id, piece);
314 d.remove_temp::<PieceId>(select_id);
315 });
316 dragging = Some(piece);
317 }
318 }
319 }
320
321 let drag_target = if can_play {
323 dragging.and_then(|piece| {
324 pointer
325 .and_then(|pos| geometry.node_at(game, pos))
326 .filter(|&node| can_drop_at(game, piece, node))
327 })
328 } else {
329 None
330 };
331
332 if response.drag_stopped() {
333 if let Some(piece) = ui.ctx().data_mut(|d| d.remove_temp::<PieceId>(drag_id)) {
334 if let Some(node) = pointer
335 .and_then(|pos| geometry.node_at(game, pos))
336 .filter(|&node| can_drop_at(game, piece, node))
337 {
338 match game.piece_at(node) {
339 Some(other) => {
340 game.swap_pieces(piece, other);
341 }
342 None => {
343 game.move_piece(piece, node);
344 }
345 }
346 }
347 }
348 dragging = None;
349 }
350
351 if can_play {
352 if dragging.is_some() {
353 ui.ctx().set_cursor_icon(CursorIcon::Grabbing);
354 } else if let Some(node) = response
355 .hover_pos()
356 .and_then(|pos| geometry.node_at(game, pos))
357 {
358 if game.piece_at(node).is_some() {
359 ui.ctx().set_cursor_icon(CursorIcon::Grab);
360 } else if ui
361 .ctx()
362 .data(|d| d.get_temp::<PieceId>(select_id).is_some())
363 {
364 ui.ctx().set_cursor_icon(CursorIcon::PointingHand);
365 }
366 }
367 }
368
369 let visuals = ui.visuals();
371 let satisfied_color = satisfied_color.unwrap_or(visuals.strong_text_color());
372 let unsatisfied_color = unsatisfied_color.unwrap_or(visuals.error_fg_color);
373
374 let (piece_fill, piece_edge) = if visuals.dark_mode {
375 (Color32::from_gray(0x5C), Color32::from_gray(0x82))
376 } else {
377 (Color32::from_gray(0xC9), Color32::from_gray(0x8E))
378 };
379 let socket_fill = visuals.extreme_bg_color;
380 let socket_edge = visuals.widgets.noninteractive.bg_stroke.color;
381
382 for node in game.nodes() {
384 painter.add(Shape::convex_polygon(
385 hex_corners(geometry.center(node), cell),
386 socket_fill,
387 Stroke::new(1.0, socket_edge),
388 ));
389 }
390
391 if can_play && dragging.is_none() {
393 if let Some(selected) = ui.ctx().data(|d| d.get_temp::<PieceId>(select_id)) {
394 if let Some(node) = game.node_of(selected) {
395 painter.circle_stroke(
396 geometry.center(&game.nodes()[node]),
397 cell * RING_RADIUS,
398 Stroke::new(cell * 0.075, satisfied_color),
399 );
400 }
401 }
402 }
403
404 for piece in 0..game.piece_count() {
406 let Some(node) = game.node_of(piece) else {
407 continue;
408 };
409 let center = geometry.center(&game.nodes()[node]);
410 let in_hand = dragging == Some(piece);
411 let alpha = if in_hand { 0.35 } else { 1.0 };
412 let arrows = game.pieces()[piece].arrows;
413 let unsatisfied = game.unsatisfied_arrows(piece);
414
415 painter.add(Shape::convex_polygon(
416 hex_corners(center, cell * PIECE_RADIUS),
417 piece_fill.gamma_multiply(alpha),
418 Stroke::new(cell * 0.05, piece_edge.gamma_multiply(alpha)),
419 ));
420 for dir in arrows.iter() {
421 let color = if unsatisfied.contains(dir) {
422 unsatisfied_color
423 } else {
424 satisfied_color
425 };
426 draw_arrow(
427 &painter,
428 center,
429 dir,
430 cell * PIECE_RADIUS,
431 Stroke::new(cell * ARROW_STROKE, color.gamma_multiply(alpha)),
432 );
433 }
434 }
435
436 if can_play {
442 if let Some(dragged) = dragging {
443 for (id, node) in game.nodes().iter().enumerate() {
444 if !can_drop_at(game, dragged, id) {
445 continue;
446 }
447 let faint = if game.piece_at(id).is_some() {
448 piece_edge
449 } else {
450 piece_fill
451 };
452 let stroke = if drag_target == Some(id) {
453 Stroke::new(cell * 0.10, satisfied_color)
454 } else {
455 Stroke::new(cell * 0.05, faint)
456 };
457 painter.circle_stroke(geometry.center(node), cell * RING_RADIUS, stroke);
458 }
459 }
460 }
461
462 if game.status() == GameStatus::Won {
464 let message = win_message.unwrap_or_else(|| "Solved!".to_owned());
465 let font = FontId::proportional((cell * 0.6).clamp(14.0, 40.0));
466 let galley = painter.layout_no_wrap(message, font, satisfied_color);
467 let size = galley.size() + Vec2::new(cell, cell * 0.6);
472 let banner = Rect::from_center_size(
473 Pos2::new(
474 response.rect.center().x,
475 response.rect.min.y + size.y * 0.5 + cell * 0.15,
476 ),
477 size,
478 );
479 painter.rect_filled(banner, CornerRadius::same(6), visuals.window_fill);
480 painter.rect_stroke(
481 banner,
482 CornerRadius::same(6),
483 Stroke::new(1.0, satisfied_color),
484 StrokeKind::Inside,
485 );
486 painter.galley(
487 banner.center() - galley.size() * 0.5,
488 galley,
489 Color32::PLACEHOLDER,
490 );
491 }
492
493 response
494 }
495}
496
497#[cfg(test)]
498mod tests {
499 use super::*;
500 use crate::game::{build_nodes, random_symmetric_board, Arrows, Params, Piece};
501
502 fn preset_game(nodes: usize, pieces: usize, seed: u64) -> HexwebGame {
504 HexwebGame::random(
505 Params {
506 nodes: random_symmetric_board(nodes, seed),
507 pieces,
508 min_arrows: 2,
509 max_arrows: 4,
510 },
511 seed,
512 )
513 }
514
515 #[test]
516 fn dir_angles_match_the_pixel_deltas() {
517 for dir in Dir::ALL {
518 let (dq, dr) = dir.delta();
519 let pixel = Vec2::new(1.5 * dq as f32, SQRT_3 * (dr as f32 + 0.5 * dq as f32));
520 let angled = Vec2::angled(dir_angle(dir));
521 assert!(
522 (pixel.normalized() - angled).length() < 1e-4,
523 "{dir:?} paints toward {angled:?}, lattice delta is {pixel:?}"
524 );
525 }
526 }
527
528 #[test]
529 fn content_size_and_fit_round_trip() {
530 let game = preset_game(12, 9, 7);
531 let size = content_size(&game, 10.0);
532 assert!((fit_cell_size(&game, size) - 10.0).abs() < 1e-4);
533 assert!(size.x != size.y);
535 assert!(fit_cell_size(&game, Vec2::splat(20.0)) >= 4.0);
536 }
537
538 #[test]
539 fn node_at_round_trips_through_every_center() {
540 let game = preset_game(16, 12, 3);
541 let geometry = Geometry::new(Pos2::ZERO, 20.0, &game);
542 for (id, node) in game.nodes().iter().enumerate() {
543 assert_eq!(geometry.node_at(&game, geometry.center(node)), Some(id));
544 let near = geometry.center(node) + Vec2::new(6.0, -4.0);
546 assert_eq!(geometry.node_at(&game, near), Some(id));
547 }
548 assert_eq!(
549 geometry.node_at(&game, Pos2::new(-999.0, -999.0)),
550 None,
551 "far away from the board there is nothing to hit"
552 );
553 }
554
555 const TEST_CELL: f32 = 24.0;
556
557 struct Harness {
562 ctx: egui::Context,
563 game: HexwebGame,
564 rect: Rect,
565 time: f64,
566 }
567
568 impl Harness {
569 fn new(game: HexwebGame) -> Self {
570 let mut harness = Self {
571 ctx: egui::Context::default(),
572 game,
573 rect: Rect::ZERO,
574 time: 0.0,
575 };
576 harness.pass(Vec::new());
578 harness
579 }
580
581 fn pass(&mut self, events: Vec<egui::Event>) -> Rect {
582 self.time += 1.0 / 60.0;
583 let input = egui::RawInput {
584 screen_rect: Some(Rect::from_min_size(Pos2::ZERO, Vec2::new(800.0, 600.0))),
585 events,
586 time: Some(self.time),
587 ..Default::default()
588 };
589 let game = &mut self.game;
590 let rect = std::cell::Cell::new(Rect::ZERO);
591 let _ = self.ctx.run_ui(input, |ui| {
592 let response = ui.add(HexwebWidget::new(game).cell_size(TEST_CELL));
593 rect.set(response.rect);
594 });
595 self.rect = rect.get();
596 self.rect
597 }
598
599 fn geometry(&self) -> Geometry {
600 Geometry::new(self.rect.min, TEST_CELL, &self.game)
601 }
602
603 fn center(&self, node: NodeId) -> Pos2 {
604 self.geometry().center(&self.game.nodes()[node])
605 }
606
607 fn press(&mut self, pos: Pos2) {
608 self.pass(vec![
609 egui::Event::PointerMoved(pos),
610 egui::Event::PointerButton {
611 pos,
612 button: egui::PointerButton::Primary,
613 pressed: true,
614 modifiers: Default::default(),
615 },
616 ]);
617 }
618
619 fn drag(&mut self, pos: Pos2) {
620 self.pass(vec![egui::Event::PointerMoved(pos)]);
621 }
622
623 fn release(&mut self, pos: Pos2) {
624 self.pass(vec![
625 egui::Event::PointerMoved(pos),
626 egui::Event::PointerButton {
627 pos,
628 button: egui::PointerButton::Primary,
629 pressed: false,
630 modifiers: Default::default(),
631 },
632 ]);
633 }
634
635 fn click(&mut self, pos: Pos2) {
636 self.press(pos);
637 self.release(pos);
638 }
639 }
640
641 fn empty_node(game: &HexwebGame) -> NodeId {
642 (0..game.node_count())
643 .find(|&node| game.piece_at(node).is_none())
644 .expect("presets always leave empty nodes")
645 }
646
647 fn occupied_node(game: &HexwebGame) -> NodeId {
648 (0..game.node_count())
649 .find(|&node| game.piece_at(node).is_some())
650 .expect("presets always have pieces")
651 }
652
653 #[test]
654 fn click_piece_then_empty_node_moves_it() {
655 let mut harness = Harness::new(preset_game(8, 6, 11));
656 let from = occupied_node(&harness.game);
657 let piece = harness.game.piece_at(from).unwrap();
658 let target = empty_node(&harness.game);
659
660 harness.click(harness.center(from));
661 assert_eq!(harness.game.node_of(piece), Some(from), "select only");
662 harness.click(harness.center(target));
663
664 assert_eq!(harness.game.node_of(piece), Some(target));
665 assert_eq!(harness.game.moves(), 1);
666 }
667
668 #[test]
669 fn clicking_an_empty_node_without_a_selection_does_nothing() {
670 let mut harness = Harness::new(preset_game(8, 6, 11));
671 harness.click(harness.center(empty_node(&harness.game)));
672 assert_eq!(harness.game.moves(), 0);
673 }
674
675 #[test]
676 fn clicking_the_selected_piece_again_deselects_it() {
677 let mut harness = Harness::new(preset_game(8, 6, 11));
678 let from = occupied_node(&harness.game);
679 harness.click(harness.center(from));
680 harness.click(harness.center(from));
681 harness.click(harness.center(empty_node(&harness.game)));
682 assert_eq!(harness.game.moves(), 0);
683 }
684
685 #[test]
686 fn dragging_a_piece_to_an_empty_node_moves_it() {
687 let mut harness = Harness::new(preset_game(12, 9, 5));
688 let from = occupied_node(&harness.game);
689 let piece = harness.game.piece_at(from).unwrap();
690 let target = empty_node(&harness.game);
691
692 harness.press(harness.center(from));
693 harness.drag(harness.center(from) + Vec2::new(16.0, 16.0));
695 harness.drag(harness.center(target));
696 harness.release(harness.center(target));
697
698 assert_eq!(harness.game.node_of(piece), Some(target));
699 assert_eq!(harness.game.moves(), 1);
700 }
701
702 #[test]
703 fn dropping_off_the_board_snaps_back() {
704 let mut harness = Harness::new(preset_game(12, 9, 5));
705 let from = occupied_node(&harness.game);
706 let piece = harness.game.piece_at(from).unwrap();
707
708 harness.press(harness.center(from));
709 harness.drag(harness.center(from) + Vec2::new(16.0, 16.0));
710 harness.release(Pos2::new(-100.0, -100.0));
711
712 assert_eq!(harness.game.node_of(piece), Some(from));
713 assert_eq!(harness.game.moves(), 0);
714 }
715
716 fn swappable_node(game: &HexwebGame, piece: PieceId, except: NodeId) -> Option<NodeId> {
719 (0..game.node_count()).find(|&node| match game.piece_at(node) {
720 Some(other) => node != except && game.can_swap(piece, other),
721 None => false,
722 })
723 }
724
725 #[test]
726 fn dropping_on_an_occupied_node_swaps_the_pieces() {
727 let mut harness = Harness::new(preset_game(12, 9, 5));
728 let from = occupied_node(&harness.game);
729 let piece = harness.game.piece_at(from).unwrap();
730 let taken = swappable_node(&harness.game, piece, from)
731 .expect("some other piece carries different arrows");
732 let taken_piece = harness.game.piece_at(taken).unwrap();
733
734 harness.press(harness.center(from));
735 harness.drag(harness.center(from) + Vec2::new(16.0, 16.0));
737 harness.release(harness.center(taken));
738
739 assert_eq!(harness.game.node_of(piece), Some(taken));
740 assert_eq!(harness.game.node_of(taken_piece), Some(from));
741 assert_eq!(harness.game.moves(), 1);
742 }
743
744 #[test]
745 fn releasing_on_the_dragged_pieces_own_node_snaps_back() {
746 let mut harness = Harness::new(preset_game(12, 9, 5));
747 let from = occupied_node(&harness.game);
748 let piece = harness.game.piece_at(from).unwrap();
749
750 harness.press(harness.center(from));
751 harness.drag(harness.center(from) + Vec2::new(16.0, 16.0));
752 harness.release(harness.center(from));
753
754 assert_eq!(harness.game.node_of(piece), Some(from));
755 assert_eq!(harness.game.moves(), 0);
756 }
757
758 fn identical_pair_game() -> HexwebGame {
762 let (nodes, _) = build_nodes(&[(0, 0), (0, -1), (0, 1), (0, 2)]);
763 let pieces = vec![
764 Piece {
765 arrows: Arrows::from_dir(Dir::Up),
766 },
767 Piece {
768 arrows: Arrows::from_dir(Dir::Up),
769 },
770 ];
771 let cell_piece = vec![Some(0), None, None, Some(1)];
772 HexwebGame::from_parts(nodes, pieces, cell_piece.clone(), cell_piece)
773 }
774
775 #[test]
776 fn dropping_on_an_identical_piece_is_free() {
777 let mut harness = Harness::new(identical_pair_game());
778 let from = occupied_node(&harness.game);
779 let piece = harness.game.piece_at(from).unwrap();
780 let taken = (0..harness.game.node_count())
781 .find(|&node| node != from && harness.game.piece_at(node).is_some())
782 .expect("the second piece exists");
783
784 harness.press(harness.center(from));
785 harness.drag(harness.center(from) + Vec2::new(16.0, 16.0));
786 harness.release(harness.center(taken));
787
788 assert_eq!(harness.game.node_of(piece), Some(taken));
791 assert_eq!(harness.game.moves(), 0);
792 }
793
794 fn play_solution(game: &mut HexwebGame) {
799 let Some(solution) = game.solution() else {
800 panic!("test boards always have a solution");
801 };
802 let mut placed: Vec<bool> = (0..game.piece_count())
803 .map(|piece| game.node_of(piece) == Some(solution[piece]))
804 .collect();
805 while placed.iter().any(|&done| !done) {
806 let step = (0..game.piece_count())
808 .filter(|&piece| !placed[piece])
809 .find_map(|piece| {
810 game.piece_at(solution[piece])
811 .is_none()
812 .then_some((piece, solution[piece]))
813 })
814 .or_else(|| {
815 let piece = (0..game.piece_count()).find(|&piece| !placed[piece])?;
818 let node =
819 (0..game.node_count()).find(|&node| game.piece_at(node).is_none())?;
820 Some((piece, node))
821 })
822 .expect("a solvable board is always finishable");
823 let (piece, node) = step;
824 assert!(game.move_piece(piece, node));
825 placed[piece] = game.node_of(piece) == Some(solution[piece]);
826 }
827 }
828
829 #[test]
830 fn a_won_board_ignores_input() {
831 let mut harness = Harness::new(preset_game(8, 6, 2));
832 play_solution(&mut harness.game);
833 assert_eq!(harness.game.status(), GameStatus::Won);
834 let moves = harness.game.moves();
835
836 let from = occupied_node(&harness.game);
837 let target = empty_node(&harness.game);
838 let taken = (0..harness.game.node_count())
839 .find(|&node| node != from && harness.game.piece_at(node).is_some())
840 .expect("a second occupied node exists");
841 harness.click(harness.center(from));
843 harness.click(harness.center(target));
844 harness.press(harness.center(from));
847 harness.drag(harness.center(from) + Vec2::new(16.0, 16.0));
848 harness.release(harness.center(target));
849 harness.press(harness.center(from));
850 harness.drag(harness.center(from) + Vec2::new(16.0, 16.0));
851 harness.release(harness.center(taken));
852
853 assert_eq!(harness.game.moves(), moves);
854 }
855}