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

Conversation

sathwikmatsa
Copy link
Contributor

This PR adds battleship board game implementation to turtle examples - #23

  • single player mode
  • multiplayer mode
  • arrow keys & enter based controls

@enaut
Copy link
Contributor

enaut commented Jul 24, 2021

Firstly - I don't have commit rights here!

I did not really read the whole code. But at a first glance, I think it would be nice to have some more documentation - as the examples are made to be read for learning. The audience is less experienced and is often very happy for explaining comments - so an overview in main.rs over the code might be helpful, as well as some function documentations for the less obvious things...

That said, the game itself looks great!

@sathwikmatsa
Copy link
Contributor Author

Hey @enaut, Thanks for your feedback. I've added some documentation as per your suggestion. Let me know if you have any comments.

Copy link
Contributor

@enaut enaut left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I have limited time. I skimmed most of it and read main.rs and ship.rs in more detail. I added comments and suggestions in the files.

Please do read my comments as suggestions for improvement. I highly value your PR and it works as it should I just added my opinion on some topics - feel free to have a different one and refuse it ;)

examples/battleship/main.rs Outdated Show resolved Hide resolved
examples/battleship/main.rs Outdated Show resolved Hide resolved
examples/battleship/main.rs Outdated Show resolved Hide resolved
examples/battleship/main.rs Outdated Show resolved Hide resolved
examples/battleship/main.rs Outdated Show resolved Hide resolved
examples/battleship/main.rs Outdated Show resolved Hide resolved
Comment on lines 4 to 7
pub struct ShipPosition {
pub top_left: (u8, u8),
pub bottom_right: (u8, u8),
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open for discussion: As you have the size as well as the orientation it might not be necessary to have 4 coordinates. It should be enough to have the top_left corner. If you have top_left and bottom_right that could be contradicting to what's specified in the orientation field. Or you could have like a 3x3 ship. When designing the structs it is good practise to disallow invalid states by typesystem... This could be intentional but is not part of the default battleship game. I think the cleanest solution would be to only keep top_left

Comment on lines 40 to 44
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Ship {
pub kind: ShipKind,
pub position: ShipPosition,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you only calculate the orientation when needed. I would have made that struct contain kind, position, orientation and calculate the area on the fly. But you may have good reasons to do it as you did - I'm just starting to go through the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense to me. I don't really need a separate struct ShipPosition. I've removed it and modified Ship struct to have kind, top_left and orientation fields.

Comment on lines 77 to 88
pub fn coordinates(&self) -> Vec<(u8, u8)> {
let orientation = self.orientation();
let x = self.position.top_left.0;
let y = self.position.top_left.1;

(0..self.kind.size())
.map(|i| match orientation {
Orientation::Horizontal => (x + i, y),
Orientation::Veritcal => (x, y + i),
})
.collect()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't that what you stored anyway in self.position? But I like that implementation - I'd name it bounding_box and let it return ShipBoundingBox

pub fn area(&self) -> ShipBoundingBox { ... }

struct ShipBoundingBox {
    top_left: ShipCoordinate,
    bottom_right:  ShipCoordinate
}

struct ShipCoordinate {
    x: u8,
    y: u8
}

Copy link
Contributor

@enaut enaut left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again some changes I would suggest. - still to review is the network and bot.

Comment on lines 47 to 54
.map(|cell| match cell {
Cell::Carrier => 'C',
Cell::Battleship => 'B',
Cell::Cruiser => 'R',
Cell::Submarine => 'S',
Cell::Destroyer => 'D',
_ => '.',
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you implement Display on Cell you can shorten this considerably:

Suggested change
.map(|cell| match cell {
Cell::Carrier => 'C',
Cell::Battleship => 'B',
Cell::Cruiser => 'R',
Cell::Submarine => 'S',
Cell::Destroyer => 'D',
_ => '.',
})
.map(Cell::to_string)

}
}
}
_ => unreachable!(),
Copy link
Contributor

Choose a reason for hiding this comment

The 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
_ => unreachable!(),
Cell::Bombed | Cell::Unattacked | Cell::Missed | Cell::Destroyed => unreachable!(),

match self.attack_grid.get(pos) {
Cell::Bombed | Cell::Destroyed | Cell::Missed => false,
Cell::Unattacked => true,
_ => unreachable!(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_ => unreachable!(),
Cell::Carrier | Cell::Battleship | Cell::Cruiser | Cell::Submarine | Cell::Destroyer | Cell::Empty => unreachable!(),

Same here

Comment on lines 5 to 19
pub enum Cell {
Carrier,
Battleship,
Cruiser,
Submarine,
Destroyer,
/// clear cell on ShipGrid
Empty,
/// clear cell on AttackGrid
Unattacked,
Missed,
Bombed,
/// Denotes a ship cell of a completely destroyed ship
Destroyed,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would make sense to nest ship kind here:

Suggested change
pub enum Cell {
Carrier,
Battleship,
Cruiser,
Submarine,
Destroyer,
/// clear cell on ShipGrid
Empty,
/// clear cell on AttackGrid
Unattacked,
Missed,
Bombed,
/// Denotes a ship cell of a completely destroyed ship
Destroyed,
}
pub enum Cell {
Ship(ShipKind),
/// clear cell on ShipGrid
Empty,
/// clear cell on AttackGrid
Unattacked,
Missed,
Bombed,
/// Denotes a ship cell of a completely destroyed ship
Destroyed,
}

That way you have one duplication less when adding a new ship. Of course this change needs some adjustments in the rest of the code - but it should be easy with rust-analyzer as guide :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's neat!

Comment on lines 22 to 30
pub fn to_cell(self) -> Cell {
match self {
ShipKind::Carrier => Cell::Carrier,
ShipKind::Battleship => Cell::Battleship,
ShipKind::Cruiser => Cell::Cruiser,
ShipKind::Submarine => Cell::Submarine,
ShipKind::Destroyer => Cell::Destroyer,
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the above change this could be simplified like so:

Suggested change
pub fn to_cell(self) -> Cell {
match self {
ShipKind::Carrier => Cell::Carrier,
ShipKind::Battleship => Cell::Battleship,
ShipKind::Cruiser => Cell::Cruiser,
ShipKind::Submarine => Cell::Submarine,
ShipKind::Destroyer => Cell::Destroyer,
}
}
pub fn to_cell(self) -> Cell {
Cell::Ship(self)
}

RightArrow => crosshair.move_right(self),
UpArrow => crosshair.move_up(self),
DownArrow => crosshair.move_down(self),
Return => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Return => {
Return | Space => {

At least that is what I tried intuitively - note the import above has to be adjusted too. Also the documentation in main.

Comment on lines +34 to +38
Cell::Ship(ShipKind::Carrier) => Self::CARRIER_COLOR.into(),
Cell::Ship(ShipKind::Battleship) => Self::BATTLESHIP_COLOR.into(),
Cell::Ship(ShipKind::Cruiser) => Self::CRUISER_COLOR.into(),
Cell::Ship(ShipKind::Submarine) => Self::SUBMARINE_COLOR.into(),
Cell::Ship(ShipKind::Destroyer) => Self::DESTROYER_COLOR.into(),
Copy link
Contributor

@enaut enaut Jul 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really necessary but sometimes it is more readable to nest matches in that case:

Cell::Ship(kind) => match kind {
            ShipKind::Carrier => Self::CARRIER_COLOR.into(),
            ShipKind::Battleship => Self::BATTLESHIP_COLOR.into(),
            ShipKind::Cruiser => Self::CRUISER_COLOR.into(),
            ShipKind::Submarine => Self::SUBMARINE_COLOR.into(),
            ShipKind::Destroyer => Self::DESTROYER_COLOR.into()
}

But as I said in this case its perfectly readable so no need to change.

Copy link
Contributor

@enaut enaut left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have anymore issues with your code. Thank you a lot for your time and effort!

Now @sunjay has to add his opinion and review the changes as I do not have the ability to commit.

Of course there are still things like if one opponent closes his window in a multiplayer setup the other window just crashes. But I think those are out of scope for an example program.

@sunjay
Copy link
Owner

sunjay commented Aug 4, 2021

Thanks for taking the time to contribute this example! Appreciate the first pass at reviewing @enaut. I have been swamped with things at work lately but hopefully I will have time to review this in the coming week. If I don't get to it by next weekend, please ping me. Sorry for the delay!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants