-
Notifications
You must be signed in to change notification settings - Fork 53
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
base: master
Are you sure you want to change the base?
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. |
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.
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.
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
examples/battleship/ship.rs
Outdated
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] | ||
pub struct Ship { | ||
pub kind: ShipKind, | ||
pub position: ShipPosition, | ||
} |
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.
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.
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.
examples/battleship/ship.rs
Outdated
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.
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
}
Co-authored-by: Franz Dietrich <dietrich@teilgedanken.de>
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.
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.
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:
.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.
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.
_ => 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_ => unreachable!(), | |
Cell::Carrier | Cell::Battleship | Cell::Cruiser | Cell::Submarine | Cell::Destroyer | Cell::Empty => unreachable!(), |
Same here
examples/battleship/grid.rs
Outdated
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.
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:
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 :)
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.
That's neat!
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.
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:
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
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.
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