diff --git a/examples/reversi/board.rs b/examples/reversi/board.rs new file mode 100644 index 00000000..4d534ac9 --- /dev/null +++ b/examples/reversi/board.rs @@ -0,0 +1,218 @@ +use std::ops::Deref; +use std::collections::HashSet; + +use turtle::Color; + +/// (Row, Column) +pub type Position = (usize, usize); + +type Tiles = [[Option; 8]; 8]; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Piece { + A, + B, +} + +impl Piece { + pub fn name(self) -> &'static str { + match self { + Piece::A => "red", + Piece::B => "blue", + } + } + + pub fn other(self) -> Self { + match self { + Piece::A => Piece::B, + Piece::B => Piece::A, + } + } + + pub fn color(self) -> Color { + match self { + Piece::A => "#f44336".into(), + Piece::B => "#2196F3".into(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Board { + current: Piece, + /// None - empty tile + /// Some(Piece::A) - occupied by piece A + /// Some(Piece::B) - occupied by piece B + /// + /// Each array in Board is a row of the board + tiles: Tiles, + valid_moves: HashSet +} + +impl Deref for Board { + type Target = Tiles; + + fn deref(&self) -> &Self::Target { + &self.tiles + } +} + +impl Board { + pub fn new() -> Self { + let mut tiles: Tiles = Default::default(); + tiles[3][3] = Some(Piece::A); + tiles[3][4] = Some(Piece::B); + tiles[4][3] = Some(Piece::B); + tiles[4][4] = Some(Piece::A); + let current = Piece::A; + + let mut board = Self { + current, + tiles, + valid_moves: HashSet::new(), + }; + board.update_valid_moves(current); + board + } + + pub fn current(&self) -> Piece { + self.current + } + + pub fn valid_moves(&self) -> &HashSet { + &self.valid_moves + } + + pub fn is_valid_move(&self, position: &Position) -> bool { + self.valid_moves.contains(position) + } + + /// Returns the tiles that were flipped + pub fn play_piece(&mut self, pos: Position) -> Vec { + if self.is_valid_move(&pos) { + assert!(self[pos.0][pos.1].is_none(), "Valid move was not an empty tile!"); + self.tiles[pos.0][pos.1] = Some(self.current); + let flipped = self.flip_tiles(pos); + + self.current = self.current.other(); + + //TODO: When nested method calls are enabled, this can be done in one line + // Link: https://github.com/rust-lang/rust/issues/44100 + let current = self.current; + self.update_valid_moves(current); + flipped + } + else { + unreachable!("Game should check for whether a valid move was used before playing it"); + } + } + + fn flip_tiles(&mut self, (row, col): Position) -> Vec { + let piece = self.current; + assert_eq!(self.tiles[row][col], Some(piece)); + let other = piece.other(); + let rows = self.tiles.len() as isize; + let cols = self.tiles[0].len() as isize; + + let mut flipped = Vec::new(); + for (adj_row, adj_col) in self.adjacent_positions((row, col)) { + if self.tiles[adj_row][adj_col] == Some(other) + && self.find_piece((row, col), (adj_row, adj_col), piece) { + // Perform flips + let delta_row = adj_row as isize - row as isize; + let delta_col = adj_col as isize - col as isize; + let mut curr_row = adj_row as isize; + let mut curr_col = adj_col as isize; + while curr_row >= 0 && curr_row < rows && curr_col >= 0 && curr_col < cols { + let current = &mut self.tiles[curr_row as usize][curr_col as usize]; + if *current == Some(other) { + *current = Some(piece); + flipped.push((curr_row as usize, curr_col as usize)); + } + curr_row += delta_row; + curr_col += delta_col; + } + } + } + flipped + } + + fn update_valid_moves(&mut self, piece: Piece) { + self.valid_moves.clear(); + + // Explanation: A valid move is an empty tile which has `piece` in a vertical, horizontal, + // or diagonal line from it with only `piece.other()` between the empty tile and piece. + // Example: E = empty, p = piece, o = other piece + // A B C D E F G H I J K + // E E o o o p o p p E o + // Tile A is *not* a valid move. Tile B is a valid move for p. None of the other tiles are + // valid moves for p. + // Algorithm: For each empty tile, look for at least one adjacent `other` piece. If one is + // found, look for another `piece` in that direction that isn't preceeded by an empty tile. + + let other = piece.other(); + for (i, row) in self.tiles.iter().enumerate() { + for (j, tile) in row.iter().enumerate() { + // Only empty tiles can be valid moves + if tile.is_some() { + continue; + } + + for (row, col) in self.adjacent_positions((i, j)) { + // Look for at least one `other` tile before finding `piece` + if self.tiles[row][col] == Some(other) + && self.find_piece((i, j), (row, col), piece) { + self.valid_moves.insert((i, j)); + // Don't want to keep searching this tile now that we've added it + break; + } + } + } + } + + // We need to shrink to fit because clear does not reduce the capacity and we do not want + // to leak memory by allowing the valid_moves Vec to grow uncontrollably + self.valid_moves.shrink_to_fit(); + } + + /// Searches in the direction of the given target starting from the target. Returns true if it + /// finds piece AND only encounters piece.other() along the way. + fn find_piece(&self, pos: Position, (target_row, target_col): Position, piece: Piece) -> bool { + let other = piece.other(); + let rows = self.tiles.len() as isize; + let cols = self.tiles[0].len() as isize; + + let delta_row = target_row as isize - pos.0 as isize; + let delta_col = target_col as isize - pos.1 as isize; + + let mut curr_row = target_row as isize + delta_row; + let mut curr_col = target_col as isize + delta_col; + while curr_row >= 0 && curr_row < rows && curr_col >= 0 && curr_col < cols { + let current = self.tiles[curr_row as usize][curr_col as usize]; + curr_row += delta_row; + curr_col += delta_col; + if current == Some(other) { + continue; + } + else if current == Some(piece) { + return true; + } + else { + return false; + } + } + return false; + } + + //TODO: Replace return type with `impl Iterator` when the "impl Trait" + // feature is stable. + fn adjacent_positions(&self, (row, col): Position) -> Vec { + let rows = self.tiles.len(); + let cols = self.tiles[0].len(); + [(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)].iter() + .map(|&(r, c)| (row as isize + r, col as isize + c)) + .filter(|&(r, c)| r >= 0 && c >= 0 && r < rows as isize && c < cols as isize) + .map(|(r, c)| (r as usize, c as usize)) + .collect() + } +} diff --git a/examples/reversi/main.rs b/examples/reversi/main.rs new file mode 100644 index 00000000..8f5c534e --- /dev/null +++ b/examples/reversi/main.rs @@ -0,0 +1,248 @@ +//! Reversi +//! +//! https://en.wikipedia.org/wiki/Reversi + +// To run this example, use the command: cargo run --features unstable --example reversi +#[cfg(all(not(feature = "unstable")))] +compile_error!("This example relies on unstable features. Run with `--features unstable`"); + +extern crate turtle; + +mod board; + +use std::f64::consts::PI; + +use turtle::{Drawing, Turtle, Point, Color, Event}; +use turtle::event::{MouseButton, PressedState}; + +use board::{Board, Piece}; + +#[derive(Debug, Clone)] +struct Dimensions { + pub width: f64, + pub height: f64, + pub rows: usize, + pub cols: usize, + pub tile_width: f64, + pub tile_height: f64, +} + +fn main() { + let mut drawing = Drawing::new(); + let mut turtle = drawing.add_turtle(); + drawing.set_background_color("#B3E5FC"); + turtle.set_pen_color("#757575"); + turtle.set_pen_size(2.0); + turtle.set_speed(23); + + let width = 580.0; + let height = 580.0; + let board = Board::new(); + let rows = board.len(); + let cols = board[0].len(); + + // These values are used quite often, so it makes sense to compute them in advance so that + // we don't need to keep repeating ourselves + let dim = Dimensions { + width, + height, + rows, + cols, + tile_width: width / cols as f64, + tile_height: height / rows as f64, + }; + + turtle.pen_up(); + turtle.forward(height / 2.0); + turtle.right(90.0); + turtle.backward(width / 2.0); + turtle.pen_down(); + + println!("Drawing the board...\n"); + draw_board(&mut turtle, &dim); + draw_board_pieces(&mut turtle, &board, &dim); + draw_valid_moves(&mut turtle, &board, &dim); + + // Get rid of any events that may have accumulated while drawing + drain_events(&mut drawing); + + play_game(&mut drawing, &mut turtle, board, &dim); +} + +fn draw_board(turtle: &mut Turtle, dim: &Dimensions) { + turtle.forward(dim.width); + for i in 0..dim.rows { + turtle.right((i % 2) as f64 * -180.0 + 90.0); + turtle.pen_up(); + turtle.forward(dim.height / dim.rows as f64); + turtle.pen_down(); + turtle.right((i % 2) as f64 * -180.0 + 90.0); + turtle.forward(dim.width); + } + + turtle.left(90.0); + turtle.forward(dim.height); + for i in 0..dim.cols { + turtle.left((i % 2) as f64 * -180.0 + 90.0); + turtle.pen_up(); + turtle.forward(dim.width / dim.cols as f64); + turtle.pen_down(); + turtle.left((i % 2) as f64 * -180.0 + 90.0); + turtle.forward(dim.height); + } +} + +fn draw_board_pieces(turtle: &mut Turtle, board: &Board, dim: &Dimensions) { + // Draw starting pieces + for (row, row_pieces) in board.iter().enumerate() { + for (col, piece) in row_pieces.iter().enumerate() { + if let &Some(piece) = piece { + move_to_tile(turtle, (row, col), &dim); + draw_piece(turtle, piece, &dim); + } + } + } +} + +fn play_game(drawing: &mut Drawing, turtle: &mut Turtle, mut board: Board, dim: &Dimensions) { + println!("Click on a tile to make a move."); + println!("Current Player: {}", board.current().name()); + + let mut mouse = Point::origin(); + loop { + let event = drawing.poll_event(); + // Sometimes it is more convenient to use `if let` instead of `match`. In this case, it's + // really up to your personal preference. We chose to demonstrate what `if let` would look + // like if used for this code. + if let Some(Event::MouseMove(mouse_pos)) = event { + mouse = mouse_pos; + } + else if let Some(Event::MouseButton(MouseButton::LeftButton, PressedState::Released)) = event { + // Figure out which row and column was clicked + // If these formulas seem unclear, try some example values to see what you get + let row = ((1.0 - (mouse[1] + dim.height/2.0) / dim.height) * dim.rows as f64).floor() as isize; + let col = ((mouse[0] + dim.width/2.0) / dim.width * dim.cols as f64).floor() as isize; + + if row >= 0 && row < dim.rows as isize + && col >= 0 && col < dim.cols as isize + && board.is_valid_move(&(row as usize, col as usize)) { + let row = row as usize; + let col = col as usize; + erase_valid_moves(drawing, turtle, &board, dim); + + let current = board.current(); + let flipped = board.play_piece((row, col)); + + move_to_tile(turtle, (row, col), &dim); + draw_piece(turtle, current, &dim); + + let background = drawing.background_color(); + draw_tile_circles(turtle, 0.9, background, dim, flipped.iter()); + draw_tile_circles(turtle, 0.8, current.color(), dim, flipped.iter()); + + draw_valid_moves(turtle, &board, dim); + + println!("Current Player: {}", board.current().name()); + + // Get rid of any events that may have accumulated while drawing + drain_events(drawing); + } + } + } +} + +/// Moves to the center of the given tile +fn move_to_tile(turtle: &mut Turtle, (row, col): (usize, usize), dim: &Dimensions) { + let x = col as f64 / dim.cols as f64 * dim.width + dim.tile_width / 2.0 - dim.width / 2.0; + let y = -(row as f64) / dim.rows as f64 * dim.height - dim.tile_height / 2.0 + dim.height / 2.0; + + turtle.pen_up(); + + turtle.turn_towards([x, y]); + turtle.go_to([x, y]); + turtle.set_heading(90.0); + + turtle.pen_down(); +} + +fn erase_valid_moves(drawing: &Drawing, turtle: &mut Turtle, board: &Board, dim: &Dimensions) { + let background = drawing.background_color(); + draw_tile_circles( + turtle, + 0.5, + background, + dim, + board.valid_moves().iter(), + ); +} + +fn draw_valid_moves(turtle: &mut Turtle, board: &Board, dim: &Dimensions) { + draw_tile_circles( + turtle, + 0.2, + board.current().color().with_alpha(0.8), + dim, + board.valid_moves().iter(), + ); +} + +fn draw_tile_circles<'a, T: Iterator>( + turtle: &mut Turtle, + relative_size: f64, + fill: Color, + dim: &Dimensions, + tiles: T, +) { + let speed = turtle.speed(); + turtle.set_speed("instant"); + for pos in tiles { + move_to_tile(turtle, *pos, &dim); + tile_circle(turtle, relative_size, fill, dim); + } + turtle.set_speed(speed); +} + +/// Draws the given piece +fn draw_piece(turtle: &mut Turtle, piece: Piece, dim: &Dimensions) { + turtle.show(); + tile_circle(turtle, 0.8, piece.color(), dim); + turtle.hide(); +} + +fn tile_circle(turtle: &mut Turtle, relative_size: f64, fill: Color, dim: &Dimensions) { + let radius = dim.tile_width.min(dim.tile_height) / 2.0 * relative_size; + + filled_circle(turtle, radius, fill); +} + +fn filled_circle(turtle: &mut Turtle, radius: f64, fill: Color) { + turtle.set_fill_color(fill); + turtle.pen_up(); + + turtle.forward(radius); + turtle.right(90.0); + + turtle.begin_fill(); + circle(turtle, radius); + turtle.end_fill(); + + turtle.pen_down(); +} + +fn circle(turtle: &mut Turtle, radius: f64) { + let degrees = 180.0; + + let circumference = 2.0*PI*radius; + let step = circumference / degrees; + let rotation = 360.0 / degrees; + + for _ in 0..degrees as i32 { + turtle.forward(step); + turtle.right(rotation); + } +} + +/// Clear out all events that may have accumulated +fn drain_events(drawing: &mut Drawing) { + while let Some(_) = drawing.poll_event() {} +}