use std::{
borrow::Borrow,
collections::{hash_map::Entry, HashMap},
fmt::Debug,
hash::Hash,
};
use crate::{
board::{Board, BoardSetup, Dimensions, ShotOutcome as BoardShotOutcome},
ships::{ShipId, ShipShape},
};
pub use self::errors::{AddPlayerError, CannotShootReason, ShotError};
mod errors;
pub trait PlayerId: Debug + Clone + Eq + Hash {}
impl<T: Debug + Clone + Eq + Hash> PlayerId for T {}
pub struct GameSetup<P: PlayerId, I: ShipId, D: Dimensions, S: ShipShape<D>> {
boards: HashMap<P, BoardSetup<I, D, S>>,
turn_order: Vec<P>,
}
impl<P: PlayerId, I: ShipId, D: Dimensions, S: ShipShape<D>> GameSetup<P, I, D, S> {
pub fn new() -> Self {
Self {
boards: HashMap::new(),
turn_order: Vec::new(),
}
}
pub fn start(self) -> Result<Game<P, I, D>, Self> {
if !self.ready() {
Err(self)
} else {
Ok(Game {
boards: self
.boards
.into_iter()
.map(|(pid, board)| match board.start() {
Ok(board) => (pid, board),
Err(_) => unreachable!(),
})
.collect(),
turn_order: self.turn_order,
current: 0,
})
}
}
pub fn add_player(
&mut self,
pid: P,
dim: D,
) -> Result<&mut BoardSetup<I, D, S>, AddPlayerError<P, D>> {
match self.boards.entry(pid.clone()) {
Entry::Occupied(_) => Err(AddPlayerError::new(pid, dim)),
Entry::Vacant(entry) => {
self.turn_order.push(pid);
Ok(entry.insert(BoardSetup::new(dim)))
}
}
}
pub fn ready(&self) -> bool {
self.boards.len() >= 2 && self.boards.values().all(|board| board.ready())
}
pub fn get_board<Q: ?Sized>(&self, pid: &Q) -> Option<&BoardSetup<I, D, S>>
where
P: Borrow<Q>,
Q: Eq + Hash,
{
self.boards.get(pid)
}
pub fn get_board_mut<Q: ?Sized>(&mut self, pid: &Q) -> Option<&mut BoardSetup<I, D, S>>
where
P: Borrow<Q>,
Q: Eq + Hash,
{
self.boards.get_mut(pid)
}
}
impl<P: PlayerId, I: ShipId, D: Dimensions, S: ShipShape<D>> Default for GameSetup<P, I, D, S> {
fn default() -> Self {
Self::new()
}
}
pub enum ShotOutcome<I> {
Miss,
Hit(I),
Sunk(I),
Defeated(I),
Victory(I),
}
impl<I> ShotOutcome<I> {
pub fn ship(&self) -> Option<&I> {
match self {
ShotOutcome::Miss => None,
ShotOutcome::Hit(ref id)
| ShotOutcome::Sunk(ref id)
| ShotOutcome::Defeated(ref id)
| ShotOutcome::Victory(ref id) => Some(id),
}
}
pub fn into_ship(self) -> Option<I> {
match self {
ShotOutcome::Miss => None,
ShotOutcome::Hit(id)
| ShotOutcome::Sunk(id)
| ShotOutcome::Defeated(id)
| ShotOutcome::Victory(id) => Some(id),
}
}
}
impl<I> From<BoardShotOutcome<I>> for ShotOutcome<I> {
fn from(shot: BoardShotOutcome<I>) -> Self {
match shot {
BoardShotOutcome::Miss => ShotOutcome::Miss,
BoardShotOutcome::Hit(id) => ShotOutcome::Hit(id),
BoardShotOutcome::Sunk(id) => ShotOutcome::Sunk(id),
BoardShotOutcome::Defeated(id) => ShotOutcome::Defeated(id),
}
}
}
pub struct Game<P: PlayerId, I: ShipId, D: Dimensions> {
boards: HashMap<P, Board<I, D>>,
turn_order: Vec<P>,
current: usize,
}
impl<P: PlayerId, I: ShipId, D: Dimensions> Game<P, I, D> {
pub fn current(&self) -> &P {
&self.turn_order[self.current]
}
pub fn winner(&self) -> Option<&P> {
let remaining = self
.boards
.values()
.filter(|board| !board.defeated())
.count();
debug_assert!(remaining > 0);
if remaining == 1 {
Some(self.current())
} else {
None
}
}
pub fn get_board<Q: ?Sized>(&self, pid: &Q) -> Option<&Board<I, D>>
where
P: Borrow<Q>,
Q: Eq + Hash,
{
self.boards.get(pid)
}
pub fn iter_boards(&self) -> impl Iterator<Item = (&P, &Board<I, D>)> {
self.turn_order
.iter()
.map(move |pid| (pid, &self.boards[pid]))
}
pub fn shoot(
&mut self,
target: P,
coord: D::Coordinate,
) -> Result<ShotOutcome<I>, ShotError<P, D::Coordinate>> {
if self.winner().is_some() {
Err(ShotError::new(
CannotShootReason::AlreadyOver,
target,
coord,
))
} else if self.current() == &target {
Err(ShotError::new(CannotShootReason::SelfShot, target, coord))
} else if let Some(board) = self.boards.get_mut(&target) {
match board.shoot(coord) {
Ok(BoardShotOutcome::Defeated(id)) if self.winner().is_some() => {
Ok(ShotOutcome::Victory(id))
}
Ok(res) => {
self.current = (self.current + 1) % self.turn_order.len();
Ok(res.into())
}
Err(err) => Err(ShotError::add_context(err, target)),
}
} else {
Err(ShotError::new(
CannotShootReason::UnknownPlayer,
target,
coord,
))
}
}
}