use std::{cmp::Ordering, ops::Deref};
use thiserror::Error;
pub use crate::board::rectangular::Coordinate;
use crate::{
board::{self, rectangular::RectDimensions, BoardSetup},
game::uniform,
ships::{Line, ShapeProjection},
};
pub type ShipRef<'a> = board::ShipRef<'a, Ship, RectDimensions>;
pub type CellRef<'a> = board::CellRef<'a, Ship, RectDimensions>;
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Player {
P1,
P2,
}
impl Player {
pub fn opponent(self) -> Self {
match self {
Player::P1 => Player::P2,
Player::P2 => Player::P1,
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Ship {
Carrier,
Battleship,
Cruiser,
Submarine,
Destroyer,
}
impl Ship {
fn get_shape(self) -> Line {
Line::new(self.len())
}
pub const ALL: &'static [Ship] = &[
Ship::Carrier,
Ship::Battleship,
Ship::Cruiser,
Ship::Submarine,
Ship::Destroyer,
];
pub fn len(self) -> usize {
match self {
Ship::Carrier => 5,
Ship::Battleship => 4,
Ship::Cruiser => 3,
Ship::Submarine => 3,
Ship::Destroyer => 2,
}
}
}
#[derive(Debug, Error, Copy, Clone, Eq, PartialEq)]
pub enum CannotPlaceReason {
#[error("insufficient space for the ship at the specified position")]
InsufficientSpace,
#[error("specified ship was already placed")]
AlreadyPlaced,
#[error("the specified position was already occupied")]
AlreadyOccupied,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Orientation {
Up,
Down,
Left,
Right,
}
impl Orientation {
fn check_dir(self, proj: &ShapeProjection<Coordinate>) -> bool {
if proj.len() < 2 {
true
} else {
let dx = proj[0].x.cmp(&proj[1].x);
let dy = proj[0].y.cmp(&proj[1].y);
match (self, dx, dy) {
(Orientation::Up, Ordering::Equal, Ordering::Greater) => true,
(Orientation::Down, Ordering::Equal, Ordering::Less) => true,
(Orientation::Left, Ordering::Greater, Ordering::Equal) => true,
(Orientation::Right, Ordering::Less, Ordering::Equal) => true,
_ => false,
}
}
}
}
pub struct Placement([Coordinate]);
impl Placement {
fn from_coords(coords: &[Coordinate]) -> &Placement {
unsafe { std::mem::transmute(coords) }
}
pub fn orientation(&self) -> Orientation {
if self.len() < 2 {
Orientation::Up
} else {
let dx = self[0].x.cmp(&self[1].x);
let dy = self[0].y.cmp(&self[1].y);
match (dx, dy) {
(Ordering::Equal, Ordering::Greater) => Orientation::Up,
(Ordering::Equal, Ordering::Less) => Orientation::Down,
(Ordering::Greater, Ordering::Equal) => Orientation::Left,
(Ordering::Less, Ordering::Equal) => Orientation::Right,
_ => panic!("Coordinates don't point along a valid orientation"),
}
}
}
pub fn start(&self) -> &Coordinate {
&self[0]
}
}
impl Deref for Placement {
type Target = [Coordinate];
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub struct GameSetup(uniform::GameSetup<Player, Ship, RectDimensions, Line>);
impl GameSetup {
pub fn new() -> Self {
let mut setup = uniform::GameSetup::new();
Self::add_ships(
setup
.add_player(Player::P1, RectDimensions::new(10, 10))
.unwrap(),
);
Self::add_ships(
setup
.add_player(Player::P2, RectDimensions::new(10, 10))
.unwrap(),
);
GameSetup(setup)
}
fn add_ships(board: &mut BoardSetup<Ship, RectDimensions, Line>) {
Self::add_ship(Ship::Carrier, board);
Self::add_ship(Ship::Battleship, board);
Self::add_ship(Ship::Cruiser, board);
Self::add_ship(Ship::Submarine, board);
Self::add_ship(Ship::Destroyer, board);
}
fn add_ship(ship: Ship, board: &mut BoardSetup<Ship, RectDimensions, Line>) {
board.add_ship(ship, ship.get_shape()).unwrap();
}
pub fn start(self) -> Result<Game, Self> {
match self.0.start() {
Ok(game) => Ok(Game(game)),
Err(setup) => Err(GameSetup(setup)),
}
}
pub fn ready(&self) -> bool {
self.0.ready()
}
pub fn is_player_ready(&self, player: Player) -> bool {
self.0.get_board(&player).unwrap().ready()
}
pub fn get_ships<'a>(
&'a self,
player: Player,
) -> impl 'a + Iterator<Item = (Ship, Option<&'a Placement>)> {
self.0.get_board(&player).unwrap().iter_ships().map(|ship| {
(
*ship.id(),
ship.placement().map(|v| Placement::from_coords(v)),
)
})
}
pub fn get_pending_ships<'a>(&'a self, player: Player) -> impl 'a + Iterator<Item = Ship> {
self.get_ships(player)
.filter_map(|(ship, placement)| match placement {
Some(_) => None,
None => Some(ship),
})
}
pub fn get_placement(&self, player: Player, ship: Ship) -> Option<&Placement> {
self.0
.get_board(&player)
.unwrap()
.get_ship(ship)
.unwrap()
.placement()
.map(|v| Placement::from_coords(v))
}
pub fn check_placement(
&self,
player: Player,
ship: Ship,
start: Coordinate,
dir: Orientation,
) -> Result<(), CannotPlaceReason> {
let board = self.0.get_board(&player).unwrap();
let ship = board.get_ship(ship).unwrap();
let proj = ship
.get_placements(start)
.find(|proj| dir.check_dir(proj))
.ok_or(CannotPlaceReason::InsufficientSpace)?;
ship.check_placement(&proj).map_err(|err| match err {
board::CannotPlaceReason::AlreadyOccupied => CannotPlaceReason::AlreadyOccupied,
board::CannotPlaceReason::AlreadyPlaced => CannotPlaceReason::AlreadyPlaced,
board::CannotPlaceReason::InvalidProjection => unreachable!(),
})
}
pub fn place_ship(
&mut self,
player: Player,
ship: Ship,
start: Coordinate,
dir: Orientation,
) -> Result<(), CannotPlaceReason> {
let board = self.0.get_board_mut(&player).unwrap();
let mut ship = board.get_ship_mut(ship).unwrap();
let proj = ship
.get_placements(start)
.find(|proj| dir.check_dir(proj))
.ok_or(CannotPlaceReason::InsufficientSpace)?;
ship.place(proj).map_err(|err| match err.reason() {
board::CannotPlaceReason::AlreadyOccupied => CannotPlaceReason::AlreadyOccupied,
board::CannotPlaceReason::AlreadyPlaced => CannotPlaceReason::AlreadyPlaced,
board::CannotPlaceReason::InvalidProjection => unreachable!(),
})
}
pub fn unplace_ship(&mut self, player: Player, ship: Ship) -> bool {
self.0
.get_board_mut(&player)
.unwrap()
.get_ship_mut(ship)
.unwrap()
.unplace()
.is_some()
}
pub fn iter_board<'a>(
&'a self,
player: Player,
) -> impl 'a + Iterator<Item = impl 'a + Iterator<Item = Option<Ship>>> {
let board = self.0.get_board(&player).unwrap();
board
.dimensions()
.iter_coordinates()
.map(move |row| row.map(move |coord| board.get_coord(&coord).copied()))
}
}
#[derive(Debug, Error, Copy, Clone, Eq, PartialEq)]
pub enum CannotShootReason {
#[error("the game is already over")]
AlreadyOver,
#[error("player attempted to shoot out of turn")]
OutOfTurn,
#[error("the target coordinate is out of bounds")]
OutOfBounds,
#[error("the target cell was already shot")]
AlreadyShot,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum ShotOutcome {
Miss,
Hit(Ship),
Sunk(Ship),
Victory(Ship),
}
pub struct Game(uniform::Game<Player, Ship, RectDimensions>);
impl Game {
pub fn current(&self) -> Player {
*self.0.current()
}
pub fn winner(&self) -> Option<Player> {
self.0.winner().copied()
}
pub fn iter_board<'a>(
&'a self,
player: Player,
) -> impl 'a + Iterator<Item = impl 'a + Iterator<Item = CellRef<'a>>> {
let board = self.0.get_board(&player).unwrap();
board
.dimensions()
.iter_coordinates()
.map(move |row| row.map(move |coord| board.get_coord(coord).unwrap()))
}
pub fn iter_ships<'a>(&'a self, player: Player) -> impl 'a + Iterator<Item = ShipRef<'a>> {
self.0.get_board(&player).unwrap().iter_ships()
}
pub fn get_coord(&self, player: Player, coord: Coordinate) -> Option<CellRef> {
self.0.get_board(&player).unwrap().get_coord(coord)
}
pub fn get_ship(&self, player: Player, ship: Ship) -> ShipRef {
self.0.get_board(&player).unwrap().get_ship(&ship).unwrap()
}
pub fn shoot(
&mut self,
target: Player,
coord: Coordinate,
) -> Result<ShotOutcome, CannotShootReason> {
self.0
.shoot(target, coord)
.map(|outcome| match outcome {
uniform::ShotOutcome::Miss => ShotOutcome::Miss,
uniform::ShotOutcome::Hit(ship) => ShotOutcome::Hit(ship),
uniform::ShotOutcome::Sunk(ship) => ShotOutcome::Sunk(ship),
uniform::ShotOutcome::Defeated(_) => unreachable!(),
uniform::ShotOutcome::Victory(ship) => ShotOutcome::Victory(ship),
})
.map_err(|err| match err.reason() {
uniform::CannotShootReason::AlreadyOver => CannotShootReason::AlreadyOver,
uniform::CannotShootReason::SelfShot => CannotShootReason::OutOfTurn,
uniform::CannotShootReason::UnknownPlayer => unreachable!(),
uniform::CannotShootReason::AlreadyDefeated => unreachable!(),
uniform::CannotShootReason::OutOfBounds => CannotShootReason::OutOfBounds,
uniform::CannotShootReason::AlreadyShot => CannotShootReason::AlreadyShot,
})
}
}
#[cfg(feature = "rng_gen")]
mod rand_impl {
use super::{Orientation, Player};
use once_cell::sync::Lazy;
use rand::{
distributions::{Distribution, Standard, Uniform},
Rng,
};
static PLAYER_SAMPLER: Lazy<Uniform<u8>> = Lazy::new(|| Uniform::new(0, 2));
impl Distribution<Player> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Player {
match rng.sample(&*PLAYER_SAMPLER) {
0 => Player::P1,
_ => Player::P2,
}
}
}
static ORIENTATION_SAMPLER: Lazy<Uniform<u8>> = Lazy::new(|| Uniform::new(0, 4));
impl Distribution<Orientation> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Orientation {
match rng.sample(&*ORIENTATION_SAMPLER) {
0 => Orientation::Up,
1 => Orientation::Down,
2 => Orientation::Left,
_ => Orientation::Right,
}
}
}
}