Battleship board game implementation in Turtle#246
Battleship board game implementation in Turtle#246sathwikmatsa wants to merge 18 commits intosunjay:masterfrom
Conversation
|
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 That said, the game itself looks great! |
|
Hey @enaut, Thanks for your feedback. I've added some documentation as per your suggestion. Let me know if you have any comments. |
enaut
left a comment
There was a problem hiding this comment.
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/ship.rs
Outdated
| pub struct ShipPosition { | ||
| pub top_left: (u8, u8), | ||
| pub bottom_right: (u8, u8), | ||
| } |
There was a problem hiding this comment.
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
| #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] | ||
| pub struct Ship { | ||
| pub kind: ShipKind, | ||
| pub position: ShipPosition, | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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() | ||
| } |
There was a problem hiding this comment.
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
}Co-authored-by: Franz Dietrich <[email protected]>
enaut
left a comment
There was a problem hiding this comment.
Again some changes I would suggest. - still to review is the network and bot.
examples/battleship/battlestate.rs
Outdated
| .map(|cell| match cell { | ||
| Cell::Carrier => 'C', | ||
| Cell::Battleship => 'B', | ||
| Cell::Cruiser => 'R', | ||
| Cell::Submarine => 'S', | ||
| Cell::Destroyer => 'D', | ||
| _ => '.', | ||
| }) |
There was a problem hiding this comment.
If you implement Display on Cell you can shorten this considerably:
| .map(|cell| match cell { | |
| Cell::Carrier => 'C', | |
| Cell::Battleship => 'B', | |
| Cell::Cruiser => 'R', | |
| Cell::Submarine => 'S', | |
| Cell::Destroyer => 'D', | |
| _ => '.', | |
| }) | |
| .map(Cell::to_string) |
examples/battleship/battlestate.rs
Outdated
| } | ||
| } | ||
| } | ||
| _ => unreachable!(), |
There was a problem hiding this comment.
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.
| _ => unreachable!(), | |
| Cell::Bombed | Cell::Unattacked | Cell::Missed | Cell::Destroyed => unreachable!(), |
examples/battleship/battlestate.rs
Outdated
| match self.attack_grid.get(pos) { | ||
| Cell::Bombed | Cell::Destroyed | Cell::Missed => false, | ||
| Cell::Unattacked => true, | ||
| _ => unreachable!(), |
There was a problem hiding this comment.
| _ => unreachable!(), | |
| Cell::Carrier | Cell::Battleship | Cell::Cruiser | Cell::Submarine | Cell::Destroyer | Cell::Empty => unreachable!(), |
Same here
| 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, | ||
| } |
There was a problem hiding this comment.
I think it would make sense to nest ship kind here:
| 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 :)
examples/battleship/grid.rs
Outdated
| 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, | ||
| } | ||
| } |
There was a problem hiding this comment.
With the above change this could be simplified like so:
| 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) | |
| } |
examples/battleship/game.rs
Outdated
| RightArrow => crosshair.move_right(self), | ||
| UpArrow => crosshair.move_up(self), | ||
| DownArrow => crosshair.move_down(self), | ||
| Return => { |
There was a problem hiding this comment.
| 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.
| 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(), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
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! |
This PR adds battleship board game implementation to turtle examples - #23