Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Battleship board game implementation in Turtle #246

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 222 additions & 0 deletions examples/battleship/battlestate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
use serde::{Deserialize, Serialize};
use std::convert::TryInto;
use turtle::rand::{choose, random_range};

use crate::{
grid::{Cell, Grid},
ship::{Orientation, Ship, ShipKind},
};

#[derive(Copy, Clone, Debug, PartialEq, Deserialize, Serialize)]
pub enum AttackOutcome {
Miss,
Hit,
Destroyed(Ship),
}

#[derive(Copy, Clone)]
pub enum Position {
ShipGrid((u8, u8)),
AttackGrid((u8, u8)),
}

impl Position {
pub fn get(self) -> (u8, u8) {
match self {
Self::ShipGrid(p) => p,
Self::AttackGrid(p) => p,
}
}
}

pub struct BattleState {
ship_grid: Grid,
attack_grid: Grid,
ships: [Ship; 5],
pub destroyed_rival_ships: u8,
pub ships_lost: u8,
}

impl BattleState {
pub fn new() -> Self {
let (ships, ship_grid) = Self::random_ship_grid();
Self {
ships,
ship_grid,
attack_grid: Grid::new(Cell::Unattacked),
destroyed_rival_ships: 0,
ships_lost: 0,
}
}
pub fn incoming_attack(&mut self, pos: &(u8, u8)) -> AttackOutcome {
let attacked_cell = self.ship_grid.get(pos);
match attacked_cell {
Cell::Empty => AttackOutcome::Miss,
Cell::Ship(_) => {
let standing_ship_parts = self.ship_grid.count(&attacked_cell);
match standing_ship_parts {
1 => {
// If the attack is on the last standing ship part,
// change all the Cells of the Ship to Destroyed
let lost_ship = self
.ships
.iter()
.find(|ship| ship.kind.to_cell() == attacked_cell)
.copied()
.unwrap();
lost_ship
.coordinates()
.into_iter()
.for_each(|loc| *self.ship_grid.get_mut(&loc) = Cell::Destroyed);
self.ships_lost += 1;
AttackOutcome::Destroyed(lost_ship)
}
_ => {
*self.ship_grid.get_mut(pos) = Cell::Bombed;
AttackOutcome::Hit
}
}
}
Cell::Bombed | Cell::Missed | Cell::Destroyed | Cell::Unattacked => unreachable!(),
}
}
pub fn can_bomb(&self, pos: &(u8, u8)) -> bool {
match self.attack_grid.get(pos) {
Cell::Bombed | Cell::Destroyed | Cell::Missed => false,
Cell::Unattacked => true,
Cell::Ship(_) | Cell::Empty => unreachable!(),
}
}
pub fn set_attack_outcome(&mut self, attacked_pos: &(u8, u8), outcome: AttackOutcome) {
match outcome {
AttackOutcome::Miss => *self.attack_grid.get_mut(attacked_pos) = Cell::Missed,
AttackOutcome::Hit => *self.attack_grid.get_mut(attacked_pos) = Cell::Bombed,
AttackOutcome::Destroyed(ship) => {
for pos in ship.coordinates() {
*self.attack_grid.get_mut(&pos) = Cell::Destroyed;
}
self.destroyed_rival_ships += 1;
}
}
}
fn random_ship_grid() -> ([Ship; 5], Grid) {
let ship_types = [
ShipKind::Carrier,
ShipKind::Battleship,
ShipKind::Cruiser,
ShipKind::Submarine,
ShipKind::Destroyer,
];
let mut grid = Grid::new(Cell::Empty);
let mut ships = Vec::new();

// Randomly select a position and orientation for a ship type to create a Ship
// Check if the ship doesn't overlap with other ships already added to Grid
// Check if the ship is within the Grid bounds
// If the above two conditions are met, add the ship to the Grid
// And proceed with next ship type
for kind in ship_types {
loop {
let x: u8 = random_range(0, 9);
let y: u8 = random_range(0, 9);
let orient: Orientation = choose(&[Orientation::Horizontal, Orientation::Veritcal]).copied().unwrap();

let ship = Ship::new(kind, (x, y), orient);

let no_overlap = ships
.iter()
.all(|other: &Ship| other.coordinates().iter().all(|pos| !ship.is_located_over(pos)));

let within_board = ship
.coordinates()
.iter()
.all(|pos| matches!(pos.0, 0..=9) && matches!(pos.1, 0..=9));

if no_overlap && within_board {
ships.push(ship);
ship.coordinates().iter().for_each(|pos| {
*grid.get_mut(pos) = kind.to_cell();
});
break;
}
}
}

(ships.try_into().unwrap(), grid)
}
pub fn ship_grid(&self) -> &'_ Grid {
&self.ship_grid
}
pub fn attack_grid(&self) -> &'_ Grid {
&self.attack_grid
}
}

#[cfg(test)]
mod test {
use super::*;

fn custom_battlestate(ships: [Ship; 5]) -> BattleState {
let mut ship_grid = Grid::new(Cell::Empty);
ships.iter().for_each(|ship| {
ship.coordinates().iter().for_each(|pos| {
*ship_grid.get_mut(pos) = ship.kind.to_cell();
})
});
BattleState {
ships,
ship_grid,
attack_grid: Grid::new(Cell::Unattacked),
destroyed_rival_ships: 0,
ships_lost: 0,
}
}

#[test]
fn battle_actions() {
let ships = [
Ship::new(ShipKind::Carrier, (2, 4), Orientation::Veritcal),
Ship::new(ShipKind::Battleship, (1, 0), Orientation::Horizontal),
Ship::new(ShipKind::Cruiser, (5, 2), Orientation::Horizontal),
Ship::new(ShipKind::Submarine, (8, 4), Orientation::Veritcal),
Ship::new(ShipKind::Destroyer, (6, 7), Orientation::Horizontal),
];
// Player's ship grid Opponent's ship grid
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
// 0 . B B B B . . . . . 0 . . . . . . . . . .
// 1 . . . . . . . . . . 1 . . . . . S S S . .
// 2 . . . . . R R R . . 2 . . . . . . D D . .
// 3 . . . . . . . . . . 3 . . . . . . . . . .
// 4 . . C . . . . . S . 4 . . . . B B B B . .
// 5 . . C . . . . . S . 5 . . . C C C C C . .
// 6 . . C . . . . . S . 6 . . . . . . . . . .
// 7 . . C . . . D D . . 7 . . . R R R . . . .
// 8 . . C . . . . . . . 8 . . . . . . . . . .
// 9 . . . . . . . . . . 9 . . . . . . . . . .
let mut state = custom_battlestate(ships);
// turn 1: player attacks (2, 2) - misses
state.set_attack_outcome(&(2, 2), AttackOutcome::Miss);
assert_eq!(state.attack_grid.get(&(2, 2)), Cell::Missed);
// turn 2: opponent attacks (6, 7) - hits
let outcome = state.incoming_attack(&(6, 7));
assert_eq!(outcome, AttackOutcome::Hit);
assert_eq!(state.ship_grid.get(&(6, 7)), Cell::Bombed);
// turn 3: opponent attacks (again) (7, 7) - destroys D
let outcome = state.incoming_attack(&(7, 7));
assert_eq!(outcome, AttackOutcome::Destroyed(ships[4]));
assert_eq!(state.ship_grid.get(&(7, 7)), Cell::Destroyed);
assert_eq!(state.ship_grid.get(&(6, 7)), Cell::Destroyed);
assert_eq!(state.ships_lost, 1);
// turn 4: player attacks (7, 2) - hits
state.set_attack_outcome(&(7, 2), AttackOutcome::Hit);
assert_eq!(state.attack_grid.get(&(7, 2)), Cell::Bombed);
// turn 5: player attacks (6, 2) - destroys D
state.set_attack_outcome(
&(6, 2),
AttackOutcome::Destroyed(Ship::new(ShipKind::Destroyer, (6, 2), Orientation::Horizontal)),
);
assert_eq!(state.attack_grid.get(&(6, 2)), Cell::Destroyed);
assert_eq!(state.attack_grid.get(&(7, 2)), Cell::Destroyed);
assert_eq!(state.destroyed_rival_ships, 1);
}
}
112 changes: 112 additions & 0 deletions examples/battleship/bot.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use std::net::{IpAddr, Ipv4Addr, SocketAddr};

use turtle::rand::random_range;

use crate::{
battlestate::{AttackOutcome, BattleState},
channel::{Channel, Message},
game::Turn,
grid::Cell,
};

pub struct Bot {
channel: Channel,
state: BattleState,
turn: Turn,
}

impl Bot {
pub fn new(port: u16) -> Self {
Self {
channel: Channel::client(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port)),
state: BattleState::new(),
turn: Turn::Opponent,
}
}

fn random_attack_location(&self) -> (u8, u8) {
loop {
let x = random_range(0, 9);
let y = random_range(0, 9);
if self.state.can_bomb(&(x, y)) {
return (x, y);
}
}
}

fn get_attack_location(&self) -> (u8, u8) {
// Iterator on positions of all the bombed (Hit, not Destroyed) locations in AttackGrid
let bombed_locations = self
.state
.attack_grid()
.iter()
.flatten()
.enumerate()
.filter(|(_, &cell)| cell == Cell::Bombed)
.map(|(loc, _)| ((loc as f32 / 10.0).floor() as i32, loc as i32 % 10));

// Iterate over each bombed location until an attackable position
// is found in the neighbourhood of the bombed location and return it
for loc in bombed_locations {
let attackable = [(-1, 0), (1, 0), (0, -1), (0, 1)]
.iter()
.map(|n| (n.0 + loc.0, n.1 + loc.1))
.filter(|pos| matches!(pos.0, 0..=9) && matches!(pos.1, 0..=9))
.map(|pos| (pos.0 as u8, pos.1 as u8))
.find(|pos| self.state.can_bomb(&pos));

if let Some(pos) = attackable {
return pos;
}
}
// Otherwise return a random attack location if no bombed locations are present
self.random_attack_location()
}

/// Similar to Game::run but without graphics
pub fn play(&mut self) {
loop {
match self.turn {
Turn::Me => {
let attack_location = self.get_attack_location();
self.channel.send_message(&Message::AttackCoordinates(attack_location));
match self.channel.receive_message() {
Message::AttackResult(outcome) => {
self.state.set_attack_outcome(&attack_location, outcome);
match outcome {
AttackOutcome::Miss | AttackOutcome::Destroyed(_) => {
self.turn.flip();
}
_ => (),
}
}
_ => panic!("Expected Message of AttackResult from Opponent."),
}
}
Turn::Opponent => match self.channel.receive_message() {
Message::AttackCoordinates(p) => {
let outcome = self.state.incoming_attack(&p);
self.channel.send_message(&Message::AttackResult(outcome));
match outcome {
AttackOutcome::Miss | AttackOutcome::Destroyed(_) => {
self.turn.flip();
}
AttackOutcome::Hit => (),
}
}
_ => panic!("Expected Message of AttackCoordinates from Opponent"),
},
}

match (self.state.ships_lost, self.state.destroyed_rival_ships) {
(5, _) => {
break;
}
(_, 5) => {
break;
}
(_, _) => continue,
}
}
}
}
52 changes: 52 additions & 0 deletions examples/battleship/channel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use std::net::{SocketAddr, TcpListener, TcpStream};

use crate::battlestate::AttackOutcome;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub enum Message {
AttackCoordinates((u8, u8)),
AttackResult(AttackOutcome),
}

pub enum ChannelType {
Server,
Client(SocketAddr),
UseListener(TcpListener),
}

pub struct Channel {
stream: TcpStream,
}

impl Channel {
pub fn client(socket_addr: SocketAddr) -> Self {
Self {
stream: TcpStream::connect(socket_addr).expect("Couldn't connect to the server"),
}
}

pub fn server() -> Self {
let listener = TcpListener::bind("0.0.0.0:0").expect("Failed to bind to port");
println!(
"Listening on port: {}, Waiting for connection..(See help: -h, --help)",
listener.local_addr().unwrap().port()
);
let (stream, _) = listener.accept().expect("Couldn't connect to the client");
Self { stream }
}

pub fn serve_using_listener(listener: TcpListener) -> Self {
let (stream, _) = listener.accept().expect("Couldn't connect to the client");
Self { stream }
}

pub fn send_message(&mut self, msg: &Message) {
serde_json::to_writer(&self.stream, &msg).expect("Failed to send message");
}

pub fn receive_message(&mut self) -> Message {
let mut de = serde_json::Deserializer::from_reader(&self.stream);
Message::deserialize(&mut de).expect("Failed to deserialize message")
}
}
Loading