1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
// Copyright 2020 Zachary Stewart // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Implements the setup phase of the board. use std::collections::{hash_map::Entry, HashMap}; use crate::{ board::{AddShipError, Board, CannotPlaceReason, Dimensions, Grid, PlaceError}, ships::{ProjectIter, ShapeProjection, ShipId, ShipShape}, }; /// Reference to a particular ship's placement info as well as the grid, providing access /// to the methods necessary to check it's placement status. pub struct ShipEntry<'a, I, D: Dimensions, S> { /// ID of this ship. id: I, /// Grid that the ship may occupy. grid: &'a Grid<I, D>, /// Placement info for the ship. ship: &'a ShipPlacementInfo<S, D::Coordinate>, } impl<'a, I: ShipId, D: Dimensions, S: ShipShape<D>> ShipEntry<'a, I, D, S> { /// If the ship is placed, get the placement. Otherwise return `None`. // Has to be specialized for mut and non-mut because mut variants can't return a // projection that lives as long as 'a, since that would potentially alias the &mut // ref. With a const ref, we can give back a ref that lives as long as self rather // than just as long as this method call. pub fn placement(&self) -> Option<&'a ShapeProjection<D::Coordinate>> { self.ship.placement.as_ref() } } /// Reference to a particular ship's placement info as well as the grid, providing access /// to the methods necessary to check it's placement status and place or unplace it. pub struct ShipEntryMut<'a, I, D: Dimensions, S> { /// ID of this ship id: I, /// Grid that ships are being placed into. grid: &'a mut Grid<I, D>, /// Back ref to the ship. ship: &'a mut ShipPlacementInfo<S, D::Coordinate>, } /// Implementation of the shared parts of ShipEntry. macro_rules! ship_entry_shared { ($t:ident) => { impl<'a, I: ShipId, D: Dimensions, S: ShipShape<D>> $t<'a, I, D, S> { /// Get the ID of this ship. pub fn id(&self) -> &I { &self.id } /// Returns true if this ship has been placed. pub fn placed(&self) -> bool { self.ship.placement.is_some() } /// Get an interator over possible projections of the shape for this ship that /// start from the given [`Coordinate`]. If there are no possible placements /// from the given coordinate, including if the coordinate is out of bounds, /// the resulting iterator will be empty. pub fn get_placements( &self, coord: D::Coordinate, ) -> ProjectIter<D, S::ProjectIterState> { self.ship.shape.project(coord, &self.grid.dim) } /// Check if the specified placement is valid for this ship. pub fn check_placement( &self, placement: &ShapeProjection<D::Coordinate>, ) -> Result<(), CannotPlaceReason> { if self.placed() { Err(CannotPlaceReason::AlreadyPlaced) } else if !self .ship .shape .is_valid_placement(placement, &self.grid.dim) { Err(CannotPlaceReason::InvalidProjection) } else { for coord in placement.iter() { match self.grid.get(coord) { None => return Err(CannotPlaceReason::InvalidProjection), Some(cell) if cell.ship.is_some() => { return Err(CannotPlaceReason::AlreadyOccupied) } _ => {} } } Ok(()) } } } }; } ship_entry_shared!(ShipEntry); ship_entry_shared!(ShipEntryMut); impl<'a, I: ShipId, D: Dimensions, S: ShipShape<D>> ShipEntryMut<'a, I, D, S> { /// If the ship is placed, get the placement. Otherwise return `None`. // Has to be specialized for mut and non-mut because mut variants can't return a // projection that lives as long as 'a, since that would potentially alias the &mut // ref. pub fn placement(&self) -> Option<&ShapeProjection<D::Coordinate>> { self.ship.placement.as_ref() } /// Attempts to place the ship with onto the given coordinates. If the ship is already /// placed, returns `Err` with the attempted placement and reason placement failed, /// otherwise returns `Ok(())` pub fn place( &mut self, placement: ShapeProjection<D::Coordinate>, ) -> Result<(), PlaceError<ShapeProjection<D::Coordinate>>> { if self.placed() { Err(PlaceError::new(CannotPlaceReason::AlreadyPlaced, placement)) } else if !self .ship .shape .is_valid_placement(&placement, &self.grid.dim) { Err(PlaceError::new( CannotPlaceReason::InvalidProjection, placement, )) } else { for coord in placement.iter() { match self.grid.get(coord) { None => { // ShipShape should ensure that all coordinates are valid, but don't // trust it. return Err(PlaceError::new( CannotPlaceReason::InvalidProjection, placement, )); } Some(cell) if cell.ship.is_some() => { return Err(PlaceError::new( CannotPlaceReason::AlreadyOccupied, placement, )); } _ => {} } } // Already ensured that every position is valid and not occupied. for coord in placement.iter() { self.grid[coord].ship = Some(self.id.to_owned()); } self.ship.placement = Some(placement); Ok(()) } } /// Attempt to clear the placement of the ship. Returns the previous placement of the /// ship if any. Returns `None` if the ship has not been placed. pub fn unplace(&mut self) -> Option<ShapeProjection<D::Coordinate>> { self.ship.placement.take().map(|placement| { for coord in placement.iter() { // We should only allow placement on valid cells, so unwrap is fine. self.grid[coord].ship = None; } placement }) } } /// Contains a ship's shape and current placement status in the grid. struct ShipPlacementInfo<S, C> { /// Shape being placed. shape: S, /// Placement of this ship, if it has been placed. placement: Option<ShapeProjection<C>>, } /// Setup phase for a [`Board`]. Allows placing ships and does not allow shooting. pub struct BoardSetup<I: ShipId, D: Dimensions, S: ShipShape<D>> { /// Grid for placement of ships. grid: Grid<I, D>, /// Mapping of added ShipIds to coresponding placement info. ships: HashMap<I, ShipPlacementInfo<S, D::Coordinate>>, } impl<I: ShipId, D: Dimensions, S: ShipShape<D>> BoardSetup<I, D, S> { /// Begin game setup by constructing a new board with the given [`Dimensions`]. pub fn new(dim: D) -> Self { Self { grid: Grid::new(dim), ships: HashMap::new(), } } /// Get the [`Dimesnsions`] of this [`Board`]. pub fn dimensions(&self) -> &D { &self.grid.dim } /// Tries to start the game. If all ships are placed, returns a [`Board`] with the /// current placements. If no ships have been added or any ship has not been placed, /// returns self. pub fn start(self) -> Result<Board<I, D>, Self> { if !self.ready() { Err(self) } else { Ok(Board { grid: self.grid, ships: self .ships .into_iter() .map(|(id, info)| match info.placement { Some(placement) => (id, placement), None => unreachable!(), }) .collect(), }) } } /// Checks if this board is ready to start. Returns `true` if at least one ship has /// been added and all ships are placed. pub fn ready(&self) -> bool { !self.ships.is_empty() && self.ships.values().all(|ship| ship.placement.is_some()) } /// Get an iterator over the ships configured on this board. pub fn iter_ships(&self) -> impl Iterator<Item = ShipEntry<I, D, S>> { let grid = &self.grid; self.ships.iter().map(move |(id, ship)| ShipEntry { id: id.clone(), grid, ship, }) } /// Attempts to add a ship with the given ID. If the given ShipID is already used, /// returns the shape passed to this function. Otherwise adds the shape and returns /// the ShipEntryMut for it to allow placement. pub fn add_ship( &mut self, id: I, shape: S, ) -> Result<ShipEntryMut<I, D, S>, AddShipError<I, S>> { match self.ships.entry(id.clone()) { Entry::Occupied(_) => Err(AddShipError::new(id, shape)), Entry::Vacant(entry) => { let ship = entry.insert(ShipPlacementInfo { shape, placement: None, }); Ok(ShipEntryMut { id, grid: &mut self.grid, ship, }) } } } /// Get the [`ShipEntry`] for the ship with the specified ID if such a ship exists. pub fn get_ship(&self, id: I) -> Option<ShipEntry<I, D, S>> { let grid = &self.grid; self.ships .get(&id) .map(move |ship| ShipEntry { id, grid, ship }) } /// Get the [`ShipEntryMut`] for the ship with the specified ID if such a ship exists. pub fn get_ship_mut(&mut self, id: I) -> Option<ShipEntryMut<I, D, S>> { let grid = &mut self.grid; self.ships .get_mut(&id) .map(move |ship| ShipEntryMut { id, grid, ship }) } /// Get the ID of the ship placed at the specified coordinate if any. Returns None if /// the coordinate is out of bounds or no ship was placed on the specified point. pub fn get_coord(&self, coord: &D::Coordinate) -> Option<&I> { self.grid.get(coord).and_then(|cell| cell.ship.as_ref()) } }