-
Notifications
You must be signed in to change notification settings - Fork 57
Battleship board game implementation in Turtle #246
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
base: master
Are you sure you want to change the base?
Changes from 17 commits
9791ef7
20e18ec
7eb6895
7e3bbb9
51ded43
258591e
098b5b0
92128b1
bd9e640
8974d8d
4a36f02
dcd8c53
658d185
dcdf18b
3a53c82
d3c5d3c
5acbc11
2451f94
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,239 @@ | ||||||
| use serde::{Deserialize, Serialize}; | ||||||
| use std::{convert::TryInto, fmt::Display}; | ||||||
| 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 Display for BattleState { | ||||||
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||||
| let output = self | ||||||
| .ship_grid | ||||||
| .iter() | ||||||
| .map(|row| { | ||||||
| row.iter() | ||||||
| .map(|cell| match cell { | ||||||
| Cell::Carrier => 'C', | ||||||
| Cell::Battleship => 'B', | ||||||
| Cell::Cruiser => 'R', | ||||||
| Cell::Submarine => 'S', | ||||||
| Cell::Destroyer => 'D', | ||||||
| _ => '.', | ||||||
| }) | ||||||
| .collect::<String>() | ||||||
| }) | ||||||
| .collect::<Vec<_>>(); | ||||||
| write!(f, "{}", output.join("\n")) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| 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::Carrier | Cell::Battleship | Cell::Cruiser | Cell::Submarine | Cell::Destroyer => { | ||||||
| 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[attacked_cell as usize]; | ||||||
| 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 | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| _ => unreachable!(), | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. having a default like that is nice and easy but you move a compile time error to a runtime error if you add a new Cell variant. So in general these should be avoided and rather list the remaining cases just as you did with the ship cases.
Suggested change
|
||||||
| } | ||||||
| } | ||||||
| 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, | ||||||
| _ => unreachable!(), | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Same here |
||||||
| } | ||||||
| } | ||||||
| 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); | ||||||
| } | ||||||
| } | ||||||
| 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, | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you implement
DisplayonCellyou can shorten this considerably: