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())
    }
}