From 9791ef781f0c78ee8d6274dcf097d97080e5eac4 Mon Sep 17 00:00:00 2001 From: Sathwik Matsa Date: Thu, 15 Jul 2021 12:56:27 +0530 Subject: [PATCH 01/16] Add destroy to Drawing to close window --- examples/runtest.rs | 12 +- src/async_drawing.rs | 15 +- src/drawing.rs | 34 ++++- src/ipc_protocol/messages.rs | 13 +- src/ipc_protocol/protocol.rs | 112 ++++++++------ src/renderer_server.rs | 138 ++++++++---------- src/renderer_server/event_loop_notifier.rs | 8 +- src/renderer_server/handlers.rs | 22 +-- .../handlers/destroy_drawing.rs | 7 + src/renderer_server/main.rs | 123 ++++++++-------- .../test_event_loop_notifier.rs | 6 +- 11 files changed, 265 insertions(+), 225 deletions(-) create mode 100644 src/renderer_server/handlers/destroy_drawing.rs diff --git a/examples/runtest.rs b/examples/runtest.rs index 45cd9cd1..82a0226b 100644 --- a/examples/runtest.rs +++ b/examples/runtest.rs @@ -1,18 +1,16 @@ //! This is NOT a real example. This is a test designed to see if we can actually run the turtle //! process -use std::process; - -use turtle::Turtle; +use turtle::Drawing; fn main() { - let mut turtle = Turtle::new(); + let mut drawing = Drawing::new(); + + let mut turtle = drawing.add_turtle(); turtle.set_speed(2); turtle.right(90.0); turtle.forward(50.0); - //TODO: Exiting the process currently doesn't cause the window to get closed. We should add a - // `close(self)` or `quit(self)` method to `Drawing` that closes the window explicitly. - process::exit(0); + drawing.destroy(); } diff --git a/src/async_drawing.rs b/src/async_drawing.rs index 42931ea1..bea8a6a1 100644 --- a/src/async_drawing.rs +++ b/src/async_drawing.rs @@ -1,11 +1,11 @@ use std::fmt::Debug; use std::path::Path; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; -use crate::ipc_protocol::ProtocolClient; use crate::async_turtle::AsyncTurtle; -use crate::{Drawing, Point, Color, Event, ExportError}; +use crate::ipc_protocol::ProtocolClient; +use crate::{Color, Drawing, Event, ExportError, Point}; /// Represents a size /// @@ -71,9 +71,8 @@ impl AsyncDrawing { // of many programs that use the turtle crate. crate::start(); - let client = ProtocolClient::new().await - .expect("unable to create renderer client"); - Self {client} + let client = ProtocolClient::new().await.expect("unable to create renderer client"); + Self { client } } pub async fn add_turtle(&mut self) -> AsyncTurtle { @@ -187,4 +186,8 @@ impl AsyncDrawing { pub async fn debug(&self) -> impl Debug { self.client.debug_drawing().await } + + pub fn destroy(self) { + self.client.destroy(); + } } diff --git a/src/drawing.rs b/src/drawing.rs index 014e9eb1..6be4129b 100644 --- a/src/drawing.rs +++ b/src/drawing.rs @@ -1,9 +1,9 @@ use std::fmt::{self, Debug}; use std::path::Path; -use crate::{Turtle, Color, Point, Size, ExportError}; use crate::async_drawing::AsyncDrawing; use crate::sync_runtime::block_on; +use crate::{Color, ExportError, Point, Size, Turtle}; /// Provides access to properties of the drawing that the turtle is creating /// @@ -70,7 +70,7 @@ impl From for Drawing { fn from(drawing: AsyncDrawing) -> Self { //TODO: There is no way to set `turtles` properly here, but that's okay since it is going // to be removed soon. - Self {drawing, turtles: 1} + Self { drawing, turtles: 1 } } } @@ -627,6 +627,30 @@ impl Drawing { pub fn save_svg>(&self, path: P) -> Result<(), ExportError> { block_on(self.drawing.save_svg(path)) } + + /// Destroys underlying window and drops self. + /// + /// Subsequent commands to turtle, created using [`Drawing::add_turtle`], might panic. + /// + /// ```rust + /// use turtle::Drawing; + /// + /// let mut drawing = Drawing::new(); + /// let mut turtle = drawing.add_turtle(); + /// + /// turtle.set_speed(2); + /// turtle.right(90.0); + /// turtle.forward(50.0); + /// + /// // close window + /// drawing.destroy(); + /// + /// // this will panic! + /// // turtle.forward(100.0) + /// ``` + pub fn destroy(self) { + self.drawing.destroy(); + } } #[cfg(test)] @@ -634,7 +658,9 @@ mod tests { use super::*; #[test] - #[should_panic(expected = "Invalid color: Color { red: NaN, green: 0.0, blue: 0.0, alpha: 0.0 }. See the color module documentation for more information.")] + #[should_panic( + expected = "Invalid color: Color { red: NaN, green: 0.0, blue: 0.0, alpha: 0.0 }. See the color module documentation for more information." + )] fn rejects_invalid_background_color() { let mut drawing = Drawing::new(); drawing.set_background_color(Color { @@ -655,7 +681,7 @@ mod tests { #[test] fn ignores_center_nan_inf() { - let center = Point {x: 5.0, y: 10.0}; + let center = Point { x: 5.0, y: 10.0 }; let mut drawing = Drawing::new(); drawing.set_center(center); diff --git a/src/ipc_protocol/messages.rs b/src/ipc_protocol/messages.rs index 0fdb1f18..ccb76879 100644 --- a/src/ipc_protocol/messages.rs +++ b/src/ipc_protocol/messages.rs @@ -1,10 +1,10 @@ use std::path::PathBuf; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; -use crate::{Color, Point, Speed, Event, Distance, Size}; -use crate::renderer_server::{TurtleId, ExportError}; -use crate::{async_turtle::AngleUnit, radians::Radians, debug}; +use crate::renderer_server::{ExportError, TurtleId}; +use crate::{async_turtle::AngleUnit, debug, radians::Radians}; +use crate::{Color, Distance, Event, Point, Size, Speed}; /// The different kinds of requests that can be sent from a client /// @@ -152,6 +152,11 @@ pub enum ClientRequest { /// /// Response: `ServerResponse::DebugDrawing` DebugDrawing, + + /// Destroys drawing window. + /// + /// Response: N/A + DestroyDrawing, } #[derive(Debug, Serialize, Deserialize)] diff --git a/src/ipc_protocol/protocol.rs b/src/ipc_protocol/protocol.rs index d389773b..4608648d 100644 --- a/src/ipc_protocol/protocol.rs +++ b/src/ipc_protocol/protocol.rs @@ -1,22 +1,13 @@ use std::path::PathBuf; -use crate::renderer_client::RendererClient; -use crate::renderer_server::{TurtleId, ExportError}; use crate::radians::Radians; -use crate::{Distance, Point, Color, Speed, Event, Size, async_turtle::AngleUnit, debug}; +use crate::renderer_client::RendererClient; +use crate::renderer_server::{ExportError, TurtleId}; +use crate::{async_turtle::AngleUnit, debug, Color, Distance, Event, Point, Size, Speed}; use super::{ - ConnectionError, - ClientRequest, - ServerResponse, - ExportFormat, - DrawingProp, - DrawingPropValue, - TurtleProp, - TurtlePropValue, - PenProp, - PenPropValue, - RotationDirection, + ClientRequest, ConnectionError, DrawingProp, DrawingPropValue, ExportFormat, PenProp, PenPropValue, RotationDirection, ServerResponse, + TurtleProp, TurtlePropValue, }; /// A wrapper for `RendererClient` that encodes the the IPC protocol in a type-safe manner @@ -26,7 +17,7 @@ pub struct ProtocolClient { impl From for ProtocolClient { fn from(client: RendererClient) -> Self { - Self {client} + Self { client } } } @@ -137,26 +128,37 @@ impl ProtocolClient { } pub fn drawing_set_background(&self, value: Color) { - debug_assert!(value.is_valid(), "bug: colors should be validated before sending to renderer server"); + debug_assert!( + value.is_valid(), + "bug: colors should be validated before sending to renderer server" + ); self.client.send(ClientRequest::SetDrawingProp(DrawingPropValue::Background(value))) } pub fn drawing_set_center(&self, value: Point) { - debug_assert!(value.is_finite(), "bug: center should be validated before sending to renderer server"); + debug_assert!( + value.is_finite(), + "bug: center should be validated before sending to renderer server" + ); self.client.send(ClientRequest::SetDrawingProp(DrawingPropValue::Center(value))) } pub fn drawing_set_size(&self, value: Size) { - debug_assert!(value.width > 0 && value.height > 0, "bug: size should be validated before sending to renderer server"); + debug_assert!( + value.width > 0 && value.height > 0, + "bug: size should be validated before sending to renderer server" + ); self.client.send(ClientRequest::SetDrawingProp(DrawingPropValue::Size(value))) } pub fn drawing_set_is_maximized(&self, value: bool) { - self.client.send(ClientRequest::SetDrawingProp(DrawingPropValue::IsMaximized(value))) + self.client + .send(ClientRequest::SetDrawingProp(DrawingPropValue::IsMaximized(value))) } pub fn drawing_set_is_fullscreen(&self, value: bool) { - self.client.send(ClientRequest::SetDrawingProp(DrawingPropValue::IsFullscreen(value))) + self.client + .send(ClientRequest::SetDrawingProp(DrawingPropValue::IsFullscreen(value))) } pub fn drawing_reset_center(&self) { @@ -175,7 +177,7 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::Pen(PenPropValue::IsEnabled(value))) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - }, + } _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } @@ -188,7 +190,7 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::Pen(PenPropValue::Thickness(value))) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - }, + } _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } @@ -201,7 +203,7 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::Pen(PenPropValue::Color(value))) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - }, + } _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } @@ -214,7 +216,7 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::FillColor(value)) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - }, + } _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } @@ -227,7 +229,7 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::IsFilling(value)) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - }, + } _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } @@ -240,7 +242,7 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::Position(value)) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - }, + } _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } @@ -253,7 +255,7 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::Heading(value)) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - }, + } _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } @@ -266,7 +268,7 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::Speed(value)) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - }, + } _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } @@ -279,28 +281,45 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::IsVisible(value)) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - }, + } _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } pub fn turtle_pen_set_is_enabled(&self, id: TurtleId, value: bool) { - self.client.send(ClientRequest::SetTurtleProp(id, TurtlePropValue::Pen(PenPropValue::IsEnabled(value)))) + self.client.send(ClientRequest::SetTurtleProp( + id, + TurtlePropValue::Pen(PenPropValue::IsEnabled(value)), + )) } pub fn turtle_pen_set_thickness(&self, id: TurtleId, value: f64) { - debug_assert!(value >= 0.0 && value.is_finite(), "bug: pen size should be validated before sending to renderer server"); - self.client.send(ClientRequest::SetTurtleProp(id, TurtlePropValue::Pen(PenPropValue::Thickness(value)))) + debug_assert!( + value >= 0.0 && value.is_finite(), + "bug: pen size should be validated before sending to renderer server" + ); + self.client.send(ClientRequest::SetTurtleProp( + id, + TurtlePropValue::Pen(PenPropValue::Thickness(value)), + )) } pub fn turtle_pen_set_color(&self, id: TurtleId, value: Color) { - debug_assert!(value.is_valid(), "bug: colors should be validated before sending to renderer server"); - self.client.send(ClientRequest::SetTurtleProp(id, TurtlePropValue::Pen(PenPropValue::Color(value)))) + debug_assert!( + value.is_valid(), + "bug: colors should be validated before sending to renderer server" + ); + self.client + .send(ClientRequest::SetTurtleProp(id, TurtlePropValue::Pen(PenPropValue::Color(value)))) } pub fn turtle_set_fill_color(&self, id: TurtleId, value: Color) { - debug_assert!(value.is_valid(), "bug: colors should be validated before sending to renderer server"); - self.client.send(ClientRequest::SetTurtleProp(id, TurtlePropValue::FillColor(value))) + debug_assert!( + value.is_valid(), + "bug: colors should be validated before sending to renderer server" + ); + self.client + .send(ClientRequest::SetTurtleProp(id, TurtlePropValue::FillColor(value))) } pub fn turtle_set_speed(&self, id: TurtleId, value: Speed) { @@ -308,7 +327,8 @@ impl ProtocolClient { } pub fn turtle_set_is_visible(&self, id: TurtleId, value: bool) { - self.client.send(ClientRequest::SetTurtleProp(id, TurtlePropValue::IsVisible(value))) + self.client + .send(ClientRequest::SetTurtleProp(id, TurtlePropValue::IsVisible(value))) } pub fn turtle_reset_heading(&self, id: TurtleId) { @@ -330,7 +350,7 @@ impl ProtocolClient { match response { ServerResponse::AnimationComplete(recv_id) => { debug_assert_eq!(id, recv_id, "bug: notified of complete animation for incorrect turtle"); - }, + } _ => unreachable!("bug: expected to receive `AnimationComplete` in response to `MoveForward` request"), } } @@ -346,7 +366,7 @@ impl ProtocolClient { match response { ServerResponse::AnimationComplete(recv_id) => { debug_assert_eq!(id, recv_id, "bug: notified of complete animation for incorrect turtle"); - }, + } _ => unreachable!("bug: expected to receive `AnimationComplete` in response to `MoveTo` request"), } } @@ -362,7 +382,7 @@ impl ProtocolClient { match response { ServerResponse::AnimationComplete(recv_id) => { debug_assert_eq!(id, recv_id, "bug: notified of complete animation for incorrect turtle"); - }, + } _ => unreachable!("bug: expected to receive `AnimationComplete` in response to `RotateInPlace` request"), } } @@ -372,7 +392,7 @@ impl ProtocolClient { return; } - let steps = 250; // Arbitrary value for now. + let steps = 250; // Arbitrary value for now. let step = radius.abs() * extent.to_radians() / steps as f64; let rotation = radius.signum() * extent / steps as f64; @@ -406,7 +426,7 @@ impl ProtocolClient { ServerResponse::DebugTurtle(recv_id, state) => { debug_assert_eq!(id, recv_id, "bug: received debug turtle for incorrect turtle"); state - }, + } _ => unreachable!("bug: expected to receive `DebugTurtle` in response to `DebugTurtle` request"), } } @@ -416,10 +436,12 @@ impl ProtocolClient { let response = self.client.recv().await; match response { - ServerResponse::DebugDrawing(state) => { - state - }, + ServerResponse::DebugDrawing(state) => state, _ => unreachable!("bug: expected to receive `DebugDrawing` in response to `DebugDrawing` request"), } } + + pub fn destroy(self) { + self.client.send(ClientRequest::DestroyDrawing); + } } diff --git a/src/renderer_server.rs b/src/renderer_server.rs index 04fdad80..1e70a99d 100644 --- a/src/renderer_server.rs +++ b/src/renderer_server.rs @@ -1,11 +1,11 @@ -mod state; +mod animation; mod app; -mod coords; -mod renderer; mod backend; -mod animation; +mod coords; mod handlers; +mod renderer; mod start; +mod state; cfg_if::cfg_if! { if #[cfg(any(feature = "test", test))] { @@ -24,16 +24,16 @@ pub use renderer::export::ExportError; pub use start::start; use ipc_channel::ipc::IpcError; +use parking_lot::{Mutex, RwLock}; use tokio::sync::mpsc; -use parking_lot::{RwLock, Mutex}; -use crate::ipc_protocol::{ServerSender, ServerOneshotSender, ServerReceiver, ClientRequest}; +use crate::ipc_protocol::{ClientRequest, ServerOneshotSender, ServerReceiver, ServerSender}; use crate::Event; -use app::{SharedApp, App}; -use renderer::display_list::{SharedDisplayList, DisplayList}; -use event_loop_notifier::EventLoopNotifier; use animation::AnimationRunner; +use app::{App, SharedApp}; +use event_loop_notifier::EventLoopNotifier; +use renderer::display_list::{DisplayList, SharedDisplayList}; /// Serves requests from the client forever async fn serve( @@ -45,12 +45,7 @@ async fn serve( mut events_receiver: mpsc::UnboundedReceiver, mut server_shutdown_receiver: mpsc::Receiver<()>, ) { - let anim_runner = AnimationRunner::new( - conn.clone(), - app.clone(), - display_list.clone(), - event_loop.clone(), - ); + let anim_runner = AnimationRunner::new(conn.clone(), app.clone(), display_list.clone(), event_loop.clone()); loop { // This will either receive the next request or end this task @@ -77,7 +72,6 @@ async fn serve( &anim_runner, request, )); - } } @@ -92,84 +86,66 @@ fn dispatch_request( ) -> Result<(), handlers::HandlerError> { use ClientRequest::*; match request { - CreateTurtle => { - handlers::create_turtle(conn, &mut app.write(), event_loop) - }, - - Export(path, format) => { - handlers::export_drawings(conn, &app.read(), &display_list.lock(), &path, format) - }, - - PollEvent => { - handlers::poll_event(conn, events_receiver) - }, - - DrawingProp(prop) => { - handlers::drawing_prop(conn, &app.read(), prop) - }, - SetDrawingProp(prop_value) => { - handlers::set_drawing_prop(&mut app.write(), event_loop, prop_value) - }, - ResetDrawingProp(prop) => { - handlers::reset_drawing_prop(&mut app.write(), event_loop, prop) - }, - - TurtleProp(id, prop) => { - handlers::turtle_prop(conn, &app.read(), id, prop) - }, - SetTurtleProp(id, prop_value) => { - handlers::set_turtle_prop(&mut app.write(), &mut display_list.lock(), event_loop, id, prop_value) - }, - ResetTurtleProp(id, prop) => { - handlers::reset_turtle_prop(&mut app.write(), &mut display_list.lock(), event_loop, id, prop) - }, - ResetTurtle(id) => { - handlers::reset_turtle(&mut app.write(), &mut display_list.lock(), event_loop, id) - }, - - MoveForward(id, distance) => { - handlers::move_forward(conn, &mut app.write(), &mut display_list.lock(), event_loop, anim_runner, id, distance) - }, - MoveTo(id, target_pos) => { - handlers::move_to(conn, &mut app.write(), &mut display_list.lock(), event_loop, anim_runner, id, target_pos) - }, + CreateTurtle => handlers::create_turtle(conn, &mut app.write(), event_loop), + + Export(path, format) => handlers::export_drawings(conn, &app.read(), &display_list.lock(), &path, format), + + PollEvent => handlers::poll_event(conn, events_receiver), + + DrawingProp(prop) => handlers::drawing_prop(conn, &app.read(), prop), + SetDrawingProp(prop_value) => handlers::set_drawing_prop(&mut app.write(), event_loop, prop_value), + ResetDrawingProp(prop) => handlers::reset_drawing_prop(&mut app.write(), event_loop, prop), + + TurtleProp(id, prop) => handlers::turtle_prop(conn, &app.read(), id, prop), + SetTurtleProp(id, prop_value) => handlers::set_turtle_prop(&mut app.write(), &mut display_list.lock(), event_loop, id, prop_value), + ResetTurtleProp(id, prop) => handlers::reset_turtle_prop(&mut app.write(), &mut display_list.lock(), event_loop, id, prop), + ResetTurtle(id) => handlers::reset_turtle(&mut app.write(), &mut display_list.lock(), event_loop, id), + + MoveForward(id, distance) => handlers::move_forward( + conn, + &mut app.write(), + &mut display_list.lock(), + event_loop, + anim_runner, + id, + distance, + ), + MoveTo(id, target_pos) => handlers::move_to( + conn, + &mut app.write(), + &mut display_list.lock(), + event_loop, + anim_runner, + id, + target_pos, + ), RotateInPlace(id, angle, direction) => { handlers::rotate_in_place(conn, &mut app.write(), event_loop, anim_runner, id, angle, direction) - }, - - BeginFill(id) => { - handlers::begin_fill(&mut app.write(), &mut display_list.lock(), event_loop, id) - }, - EndFill(id) => { - handlers::end_fill(&mut app.write(), id) - }, - - ClearAll => { - handlers::clear_all(&mut app.write(), &mut display_list.lock(), event_loop, anim_runner) - }, - ClearTurtle(id) => { - handlers::clear_turtle(&mut app.write(), &mut display_list.lock(), event_loop, id) - }, - - DebugTurtle(id, angle_unit) => { - handlers::debug_turtle(conn, &app.read(), id, angle_unit) - }, - DebugDrawing => { - handlers::debug_drawing(conn, &app.read()) - }, + } + + BeginFill(id) => handlers::begin_fill(&mut app.write(), &mut display_list.lock(), event_loop, id), + EndFill(id) => handlers::end_fill(&mut app.write(), id), + + ClearAll => handlers::clear_all(&mut app.write(), &mut display_list.lock(), event_loop, anim_runner), + ClearTurtle(id) => handlers::clear_turtle(&mut app.write(), &mut display_list.lock(), event_loop, id), + + DebugTurtle(id, angle_unit) => handlers::debug_turtle(conn, &app.read(), id, angle_unit), + DebugDrawing => handlers::debug_drawing(conn, &app.read()), + + DestroyDrawing => handlers::destroy_drawing(event_loop), } } fn handle_handler_result(res: Result<(), handlers::HandlerError>) { use handlers::HandlerError::*; match res { - Ok(_) => {}, + Ok(_) => {} Err(IpcChannelError(err)) => panic!("Error while serializing response: {}", err), // Task managing window has ended, this task will end soon too. //TODO: This potentially leaves the turtle/drawing state in an inconsistent state. Should // we deal with that somehow? Panicking doesn't seem appropriate since this probably isn't // an error, but we should definitely stop processing commands and make sure the process // ends shortly after. - Err(EventLoopClosed(_)) => {}, + Err(EventLoopClosed(_)) => {} } } diff --git a/src/renderer_server/event_loop_notifier.rs b/src/renderer_server/event_loop_notifier.rs index 5010b2e3..c9c56b4a 100644 --- a/src/renderer_server/event_loop_notifier.rs +++ b/src/renderer_server/event_loop_notifier.rs @@ -27,6 +27,8 @@ pub enum MainThreadAction { SetIsMaximized(bool), /// Change the fullscreen state of the window SetIsFullscreen(bool), + /// Exit event loop (close window) + Exit, } /// Notifies the main loop when actions need to take place @@ -37,7 +39,11 @@ pub struct EventLoopNotifier { impl EventLoopNotifier { pub fn new(event_loop: EventLoopProxy) -> Self { - Self {event_loop} + Self { event_loop } + } + + pub fn exit(&self) -> Result<(), EventLoopClosed> { + self.send_action(MainThreadAction::Exit) } pub fn request_redraw(&self) -> Result<(), EventLoopClosed> { diff --git a/src/renderer_server/handlers.rs b/src/renderer_server/handlers.rs index 3c8fda10..a19799a7 100644 --- a/src/renderer_server/handlers.rs +++ b/src/renderer_server/handlers.rs @@ -1,22 +1,24 @@ +mod animation; +mod clear; mod create_turtle; +mod debug; +mod destroy_drawing; +mod drawing_prop; mod export_drawings; +mod fill; mod poll_event; -mod drawing_prop; mod turtle_prop; -mod animation; -mod fill; -mod clear; -mod debug; +pub(crate) use animation::*; +pub(crate) use clear::*; pub(crate) use create_turtle::*; +pub(crate) use debug::*; +pub(crate) use destroy_drawing::*; +pub(crate) use drawing_prop::*; pub(crate) use export_drawings::*; +pub(crate) use fill::*; pub(crate) use poll_event::*; -pub(crate) use drawing_prop::*; pub(crate) use turtle_prop::*; -pub(crate) use animation::*; -pub(crate) use fill::*; -pub(crate) use clear::*; -pub(crate) use debug::*; use thiserror::Error; diff --git a/src/renderer_server/handlers/destroy_drawing.rs b/src/renderer_server/handlers/destroy_drawing.rs new file mode 100644 index 00000000..a99d1dc4 --- /dev/null +++ b/src/renderer_server/handlers/destroy_drawing.rs @@ -0,0 +1,7 @@ +use super::super::event_loop_notifier::EventLoopNotifier; +use super::HandlerError; + +pub(crate) fn destroy_drawing(event_loop: &EventLoopNotifier) -> Result<(), HandlerError> { + event_loop.exit()?; + Ok(()) +} diff --git a/src/renderer_server/main.rs b/src/renderer_server/main.rs index de0c81ce..7710d7cf 100644 --- a/src/renderer_server/main.rs +++ b/src/renderer_server/main.rs @@ -1,41 +1,27 @@ -use std::time::{Instant, Duration}; use std::future::Future; +use std::time::{Duration, Instant}; use glutin::{ - GlProfile, - GlRequest, - ContextBuilder, - WindowedContext, - PossiblyCurrent, dpi::{LogicalSize, PhysicalPosition}, - window::{WindowBuilder, Fullscreen}, - event::{ - Event as GlutinEvent, - StartCause, - WindowEvent, - KeyboardInput, - VirtualKeyCode, - ElementState, - }, + event::{ElementState, Event as GlutinEvent, KeyboardInput, StartCause, VirtualKeyCode, WindowEvent}, event_loop::{ControlFlow, EventLoop}, platform::run_return::EventLoopExtRunReturn, + window::{Fullscreen, WindowBuilder}, + ContextBuilder, GlProfile, GlRequest, PossiblyCurrent, WindowedContext, }; -use tokio::{ - sync::mpsc, - runtime::Handle, -}; +use tokio::{runtime::Handle, sync::mpsc}; +use crate::ipc_protocol::{ConnectionError, ServerReceiver, ServerSender}; use crate::Event; -use crate::ipc_protocol::{ServerSender, ServerReceiver, ConnectionError}; use super::{ - app::{SharedApp, App}, + app::{App, SharedApp}, coords::ScreenPoint, + event_loop_notifier::{EventLoopNotifier, MainThreadAction}, renderer::{ + display_list::{DisplayList, SharedDisplayList}, Renderer, - display_list::{SharedDisplayList, DisplayList}, }, - event_loop_notifier::{EventLoopNotifier, MainThreadAction}, }; /// The maximum rendering FPS allowed @@ -77,7 +63,7 @@ pub fn run_main( handle: Handle, // Polled to establish the server connection - establish_connection: impl Future> + Send + 'static, + establish_connection: impl Future> + Send + 'static, ) { // The state of the drawing and the state/drawings associated with each turtle let app = SharedApp::default(); @@ -105,9 +91,10 @@ pub fn run_main( let window_builder = { let app = app.read(); let drawing = app.drawing(); - WindowBuilder::new() - .with_title(&drawing.title) - .with_inner_size(LogicalSize {width: drawing.width, height: drawing.height}) + WindowBuilder::new().with_title(&drawing.title).with_inner_size(LogicalSize { + width: drawing.width, + height: drawing.height, + }) }; // Create an OpenGL 3.x context for Pathfinder to use @@ -152,43 +139,47 @@ pub fn run_main( establish_connection.take().expect("bug: init event should only occur once"), server_shutdown_receiver.take().expect("bug: init event should only occur once"), ); - }, + } - GlutinEvent::NewEvents(StartCause::ResumeTimeReached {..}) => { + GlutinEvent::NewEvents(StartCause::ResumeTimeReached { .. }) => { // A render was delayed in the `RedrawRequested` so let's try to do it again now that // we have resumed gl_context.window().request_redraw(); - }, + } // Quit if the window is closed or if Esc is pressed and then released GlutinEvent::WindowEvent { event: WindowEvent::CloseRequested, .. - } | GlutinEvent::WindowEvent { + } + | GlutinEvent::WindowEvent { event: WindowEvent::Destroyed, .. - } | GlutinEvent::WindowEvent { - event: WindowEvent::KeyboardInput { - input: KeyboardInput { - state: ElementState::Released, - virtual_keycode: Some(VirtualKeyCode::Escape), + } + | GlutinEvent::WindowEvent { + event: + WindowEvent::KeyboardInput { + input: + KeyboardInput { + state: ElementState::Released, + virtual_keycode: Some(VirtualKeyCode::Escape), + .. + }, .. }, - .. - }, .. } => { *control_flow = ControlFlow::Exit; - }, + } GlutinEvent::WindowEvent { - event: WindowEvent::ScaleFactorChanged {scale_factor, ..}, + event: WindowEvent::ScaleFactorChanged { scale_factor, .. }, .. } => { renderer.set_scale_factor(scale_factor); - }, + } - GlutinEvent::WindowEvent {event, ..} => { + GlutinEvent::WindowEvent { event, .. } => { let scale_factor = renderer.scale_factor(); match event { WindowEvent::Resized(size) => { @@ -197,12 +188,11 @@ pub fn run_main( let mut drawing = app.drawing_mut(); drawing.width = size.width; drawing.height = size.height; - }, + } //TODO: There are currently no events for updating is_maximized, so that property // should not be relied on. https://github.com/rust-windowing/glutin/issues/1298 - - _ => {}, + _ => {} } // Converts to logical coordinates, only locking the drawing if this is actually called @@ -228,32 +218,38 @@ pub fn run_main( // main process ends. This is not a fatal error though so we just ignore it. events_sender.send(event).unwrap_or(()); } - }, + } // Window events are currently sufficient for the turtle event API - GlutinEvent::DeviceEvent {..} => {}, + GlutinEvent::DeviceEvent { .. } => {} GlutinEvent::UserEvent(MainThreadAction::Redraw) => { gl_context.window().request_redraw(); - }, + } GlutinEvent::UserEvent(MainThreadAction::SetTitle(title)) => { gl_context.window().set_title(&title); - }, + } GlutinEvent::UserEvent(MainThreadAction::SetSize(size)) => { gl_context.window().set_inner_size(size); - }, + } GlutinEvent::UserEvent(MainThreadAction::SetIsMaximized(is_maximized)) => { gl_context.window().set_maximized(is_maximized); - }, + } GlutinEvent::UserEvent(MainThreadAction::SetIsFullscreen(is_fullscreen)) => { gl_context.window().set_fullscreen(if is_fullscreen { Some(Fullscreen::Borderless(gl_context.window().current_monitor())) - } else { None }); - }, + } else { + None + }); + } + + GlutinEvent::UserEvent(MainThreadAction::Exit) => { + *control_flow = ControlFlow::Exit; + } GlutinEvent::RedrawRequested(_) => { // Check if we just rendered @@ -273,24 +269,19 @@ pub fn run_main( // // This is why the window has 0 CPU usage when nothing is happening *control_flow = ControlFlow::Wait; - }, + } GlutinEvent::LoopDestroyed => { // Notify the server that it should shutdown, ignoring the error if the channel has // been dropped since that just means that the server task has ended already handle.block_on(server_shutdown.send(())).unwrap_or(()); - }, + } - _ => {}, + _ => {} }); } -fn redraw( - app: &App, - display_list: &DisplayList, - gl_context: &WindowedContext, - renderer: &mut Renderer, -) { +fn redraw(app: &App, display_list: &DisplayList, gl_context: &WindowedContext, renderer: &mut Renderer) { let draw_size = gl_context.window().inner_size(); let drawing = app.drawing(); let turtle_states = app.turtles().map(|(_, turtle)| &turtle.state); @@ -305,12 +296,11 @@ fn spawn_async_server( display_list: SharedDisplayList, event_loop: EventLoopNotifier, events_receiver: mpsc::UnboundedReceiver, - establish_connection: impl Future> + Send + 'static, + establish_connection: impl Future> + Send + 'static, server_shutdown_receiver: mpsc::Receiver<()>, ) { handle.spawn(async { - let (conn_sender, conn_receiver) = establish_connection.await - .expect("unable to establish turtle server connection"); + let (conn_sender, conn_receiver) = establish_connection.await.expect("unable to establish turtle server connection"); super::serve( conn_sender, @@ -320,6 +310,7 @@ fn spawn_async_server( event_loop, events_receiver, server_shutdown_receiver, - ).await; + ) + .await; }); } diff --git a/src/renderer_server/test_event_loop_notifier.rs b/src/renderer_server/test_event_loop_notifier.rs index c12ee61b..0dff7f87 100644 --- a/src/renderer_server/test_event_loop_notifier.rs +++ b/src/renderer_server/test_event_loop_notifier.rs @@ -1,5 +1,5 @@ -use thiserror::Error; use glutin::dpi::LogicalSize; +use thiserror::Error; #[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] #[error("event loop closed while messages were still being sent to it")] @@ -14,6 +14,10 @@ impl EventLoopNotifier { Self {} } + pub fn exit(&self) -> Result<(), EventLoopClosed> { + Ok(()) + } + pub fn request_redraw(&self) -> Result<(), EventLoopClosed> { Ok(()) } From 20e18ec23381f46e730570899be63c933094269f Mon Sep 17 00:00:00 2001 From: Sathwik Matsa Date: Fri, 16 Jul 2021 11:27:10 +0530 Subject: [PATCH 02/16] undo rustfmt --- examples/runtest.rs | 12 +- src/async_drawing.rs | 11 +- src/drawing.rs | 10 +- src/ipc_protocol/messages.rs | 8 +- src/ipc_protocol/protocol.rs | 108 ++++++------- src/renderer_server.rs | 142 +++++++++++------- src/renderer_server/event_loop_notifier.rs | 10 +- src/renderer_server/handlers.rs | 24 +-- src/renderer_server/main.rs | 119 ++++++++------- .../test_event_loop_notifier.rs | 10 +- 10 files changed, 239 insertions(+), 215 deletions(-) diff --git a/examples/runtest.rs b/examples/runtest.rs index 82a0226b..45cd9cd1 100644 --- a/examples/runtest.rs +++ b/examples/runtest.rs @@ -1,16 +1,18 @@ //! This is NOT a real example. This is a test designed to see if we can actually run the turtle //! process -use turtle::Drawing; +use std::process; -fn main() { - let mut drawing = Drawing::new(); +use turtle::Turtle; - let mut turtle = drawing.add_turtle(); +fn main() { + let mut turtle = Turtle::new(); turtle.set_speed(2); turtle.right(90.0); turtle.forward(50.0); - drawing.destroy(); + //TODO: Exiting the process currently doesn't cause the window to get closed. We should add a + // `close(self)` or `quit(self)` method to `Drawing` that closes the window explicitly. + process::exit(0); } diff --git a/src/async_drawing.rs b/src/async_drawing.rs index bea8a6a1..499ef40b 100644 --- a/src/async_drawing.rs +++ b/src/async_drawing.rs @@ -1,11 +1,11 @@ use std::fmt::Debug; use std::path::Path; -use serde::{Deserialize, Serialize}; +use serde::{Serialize, Deserialize}; -use crate::async_turtle::AsyncTurtle; use crate::ipc_protocol::ProtocolClient; -use crate::{Color, Drawing, Event, ExportError, Point}; +use crate::async_turtle::AsyncTurtle; +use crate::{Drawing, Point, Color, Event, ExportError}; /// Represents a size /// @@ -71,8 +71,9 @@ impl AsyncDrawing { // of many programs that use the turtle crate. crate::start(); - let client = ProtocolClient::new().await.expect("unable to create renderer client"); - Self { client } + let client = ProtocolClient::new().await + .expect("unable to create renderer client"); + Self {client} } pub async fn add_turtle(&mut self) -> AsyncTurtle { diff --git a/src/drawing.rs b/src/drawing.rs index 6be4129b..f9fab197 100644 --- a/src/drawing.rs +++ b/src/drawing.rs @@ -1,9 +1,9 @@ use std::fmt::{self, Debug}; use std::path::Path; +use crate::{Turtle, Color, Point, Size, ExportError}; use crate::async_drawing::AsyncDrawing; use crate::sync_runtime::block_on; -use crate::{Color, ExportError, Point, Size, Turtle}; /// Provides access to properties of the drawing that the turtle is creating /// @@ -70,7 +70,7 @@ impl From for Drawing { fn from(drawing: AsyncDrawing) -> Self { //TODO: There is no way to set `turtles` properly here, but that's okay since it is going // to be removed soon. - Self { drawing, turtles: 1 } + Self {drawing, turtles: 1} } } @@ -658,9 +658,7 @@ mod tests { use super::*; #[test] - #[should_panic( - expected = "Invalid color: Color { red: NaN, green: 0.0, blue: 0.0, alpha: 0.0 }. See the color module documentation for more information." - )] + #[should_panic(expected = "Invalid color: Color { red: NaN, green: 0.0, blue: 0.0, alpha: 0.0 }. See the color module documentation for more information.")] fn rejects_invalid_background_color() { let mut drawing = Drawing::new(); drawing.set_background_color(Color { @@ -681,7 +679,7 @@ mod tests { #[test] fn ignores_center_nan_inf() { - let center = Point { x: 5.0, y: 10.0 }; + let center = Point {x: 5.0, y: 10.0}; let mut drawing = Drawing::new(); drawing.set_center(center); diff --git a/src/ipc_protocol/messages.rs b/src/ipc_protocol/messages.rs index ccb76879..ceb975eb 100644 --- a/src/ipc_protocol/messages.rs +++ b/src/ipc_protocol/messages.rs @@ -1,10 +1,10 @@ use std::path::PathBuf; -use serde::{Deserialize, Serialize}; +use serde::{Serialize, Deserialize}; -use crate::renderer_server::{ExportError, TurtleId}; -use crate::{async_turtle::AngleUnit, debug, radians::Radians}; -use crate::{Color, Distance, Event, Point, Size, Speed}; +use crate::{Color, Point, Speed, Event, Distance, Size}; +use crate::renderer_server::{TurtleId, ExportError}; +use crate::{async_turtle::AngleUnit, radians::Radians, debug}; /// The different kinds of requests that can be sent from a client /// diff --git a/src/ipc_protocol/protocol.rs b/src/ipc_protocol/protocol.rs index 4608648d..0ec25550 100644 --- a/src/ipc_protocol/protocol.rs +++ b/src/ipc_protocol/protocol.rs @@ -1,13 +1,22 @@ use std::path::PathBuf; -use crate::radians::Radians; use crate::renderer_client::RendererClient; -use crate::renderer_server::{ExportError, TurtleId}; -use crate::{async_turtle::AngleUnit, debug, Color, Distance, Event, Point, Size, Speed}; +use crate::renderer_server::{TurtleId, ExportError}; +use crate::radians::Radians; +use crate::{Distance, Point, Color, Speed, Event, Size, async_turtle::AngleUnit, debug}; use super::{ - ClientRequest, ConnectionError, DrawingProp, DrawingPropValue, ExportFormat, PenProp, PenPropValue, RotationDirection, ServerResponse, - TurtleProp, TurtlePropValue, + ConnectionError, + ClientRequest, + ServerResponse, + ExportFormat, + DrawingProp, + DrawingPropValue, + TurtleProp, + TurtlePropValue, + PenProp, + PenPropValue, + RotationDirection, }; /// A wrapper for `RendererClient` that encodes the the IPC protocol in a type-safe manner @@ -17,7 +26,7 @@ pub struct ProtocolClient { impl From for ProtocolClient { fn from(client: RendererClient) -> Self { - Self { client } + Self {client} } } @@ -128,37 +137,26 @@ impl ProtocolClient { } pub fn drawing_set_background(&self, value: Color) { - debug_assert!( - value.is_valid(), - "bug: colors should be validated before sending to renderer server" - ); + debug_assert!(value.is_valid(), "bug: colors should be validated before sending to renderer server"); self.client.send(ClientRequest::SetDrawingProp(DrawingPropValue::Background(value))) } pub fn drawing_set_center(&self, value: Point) { - debug_assert!( - value.is_finite(), - "bug: center should be validated before sending to renderer server" - ); + debug_assert!(value.is_finite(), "bug: center should be validated before sending to renderer server"); self.client.send(ClientRequest::SetDrawingProp(DrawingPropValue::Center(value))) } pub fn drawing_set_size(&self, value: Size) { - debug_assert!( - value.width > 0 && value.height > 0, - "bug: size should be validated before sending to renderer server" - ); + debug_assert!(value.width > 0 && value.height > 0, "bug: size should be validated before sending to renderer server"); self.client.send(ClientRequest::SetDrawingProp(DrawingPropValue::Size(value))) } pub fn drawing_set_is_maximized(&self, value: bool) { - self.client - .send(ClientRequest::SetDrawingProp(DrawingPropValue::IsMaximized(value))) + self.client.send(ClientRequest::SetDrawingProp(DrawingPropValue::IsMaximized(value))) } pub fn drawing_set_is_fullscreen(&self, value: bool) { - self.client - .send(ClientRequest::SetDrawingProp(DrawingPropValue::IsFullscreen(value))) + self.client.send(ClientRequest::SetDrawingProp(DrawingPropValue::IsFullscreen(value))) } pub fn drawing_reset_center(&self) { @@ -177,7 +175,7 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::Pen(PenPropValue::IsEnabled(value))) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - } + }, _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } @@ -190,7 +188,7 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::Pen(PenPropValue::Thickness(value))) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - } + }, _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } @@ -203,7 +201,7 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::Pen(PenPropValue::Color(value))) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - } + }, _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } @@ -216,7 +214,7 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::FillColor(value)) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - } + }, _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } @@ -229,7 +227,7 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::IsFilling(value)) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - } + }, _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } @@ -242,7 +240,7 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::Position(value)) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - } + }, _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } @@ -255,7 +253,7 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::Heading(value)) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - } + }, _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } @@ -268,7 +266,7 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::Speed(value)) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - } + }, _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } @@ -281,45 +279,28 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::IsVisible(value)) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - } + }, _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } pub fn turtle_pen_set_is_enabled(&self, id: TurtleId, value: bool) { - self.client.send(ClientRequest::SetTurtleProp( - id, - TurtlePropValue::Pen(PenPropValue::IsEnabled(value)), - )) + self.client.send(ClientRequest::SetTurtleProp(id, TurtlePropValue::Pen(PenPropValue::IsEnabled(value)))) } pub fn turtle_pen_set_thickness(&self, id: TurtleId, value: f64) { - debug_assert!( - value >= 0.0 && value.is_finite(), - "bug: pen size should be validated before sending to renderer server" - ); - self.client.send(ClientRequest::SetTurtleProp( - id, - TurtlePropValue::Pen(PenPropValue::Thickness(value)), - )) + debug_assert!(value >= 0.0 && value.is_finite(), "bug: pen size should be validated before sending to renderer server"); + self.client.send(ClientRequest::SetTurtleProp(id, TurtlePropValue::Pen(PenPropValue::Thickness(value)))) } pub fn turtle_pen_set_color(&self, id: TurtleId, value: Color) { - debug_assert!( - value.is_valid(), - "bug: colors should be validated before sending to renderer server" - ); - self.client - .send(ClientRequest::SetTurtleProp(id, TurtlePropValue::Pen(PenPropValue::Color(value)))) + debug_assert!(value.is_valid(), "bug: colors should be validated before sending to renderer server"); + self.client.send(ClientRequest::SetTurtleProp(id, TurtlePropValue::Pen(PenPropValue::Color(value)))) } pub fn turtle_set_fill_color(&self, id: TurtleId, value: Color) { - debug_assert!( - value.is_valid(), - "bug: colors should be validated before sending to renderer server" - ); - self.client - .send(ClientRequest::SetTurtleProp(id, TurtlePropValue::FillColor(value))) + debug_assert!(value.is_valid(), "bug: colors should be validated before sending to renderer server"); + self.client.send(ClientRequest::SetTurtleProp(id, TurtlePropValue::FillColor(value))) } pub fn turtle_set_speed(&self, id: TurtleId, value: Speed) { @@ -327,8 +308,7 @@ impl ProtocolClient { } pub fn turtle_set_is_visible(&self, id: TurtleId, value: bool) { - self.client - .send(ClientRequest::SetTurtleProp(id, TurtlePropValue::IsVisible(value))) + self.client.send(ClientRequest::SetTurtleProp(id, TurtlePropValue::IsVisible(value))) } pub fn turtle_reset_heading(&self, id: TurtleId) { @@ -350,7 +330,7 @@ impl ProtocolClient { match response { ServerResponse::AnimationComplete(recv_id) => { debug_assert_eq!(id, recv_id, "bug: notified of complete animation for incorrect turtle"); - } + }, _ => unreachable!("bug: expected to receive `AnimationComplete` in response to `MoveForward` request"), } } @@ -366,7 +346,7 @@ impl ProtocolClient { match response { ServerResponse::AnimationComplete(recv_id) => { debug_assert_eq!(id, recv_id, "bug: notified of complete animation for incorrect turtle"); - } + }, _ => unreachable!("bug: expected to receive `AnimationComplete` in response to `MoveTo` request"), } } @@ -382,7 +362,7 @@ impl ProtocolClient { match response { ServerResponse::AnimationComplete(recv_id) => { debug_assert_eq!(id, recv_id, "bug: notified of complete animation for incorrect turtle"); - } + }, _ => unreachable!("bug: expected to receive `AnimationComplete` in response to `RotateInPlace` request"), } } @@ -392,7 +372,7 @@ impl ProtocolClient { return; } - let steps = 250; // Arbitrary value for now. + let steps = 250; // Arbitrary value for now. let step = radius.abs() * extent.to_radians() / steps as f64; let rotation = radius.signum() * extent / steps as f64; @@ -426,7 +406,7 @@ impl ProtocolClient { ServerResponse::DebugTurtle(recv_id, state) => { debug_assert_eq!(id, recv_id, "bug: received debug turtle for incorrect turtle"); state - } + }, _ => unreachable!("bug: expected to receive `DebugTurtle` in response to `DebugTurtle` request"), } } @@ -436,7 +416,9 @@ impl ProtocolClient { let response = self.client.recv().await; match response { - ServerResponse::DebugDrawing(state) => state, + ServerResponse::DebugDrawing(state) => { + state + }, _ => unreachable!("bug: expected to receive `DebugDrawing` in response to `DebugDrawing` request"), } } diff --git a/src/renderer_server.rs b/src/renderer_server.rs index 1e70a99d..54b90354 100644 --- a/src/renderer_server.rs +++ b/src/renderer_server.rs @@ -1,11 +1,11 @@ -mod animation; +mod state; mod app; -mod backend; mod coords; -mod handlers; mod renderer; +mod backend; +mod animation; +mod handlers; mod start; -mod state; cfg_if::cfg_if! { if #[cfg(any(feature = "test", test))] { @@ -24,16 +24,16 @@ pub use renderer::export::ExportError; pub use start::start; use ipc_channel::ipc::IpcError; -use parking_lot::{Mutex, RwLock}; use tokio::sync::mpsc; +use parking_lot::{RwLock, Mutex}; -use crate::ipc_protocol::{ClientRequest, ServerOneshotSender, ServerReceiver, ServerSender}; +use crate::ipc_protocol::{ServerSender, ServerOneshotSender, ServerReceiver, ClientRequest}; use crate::Event; -use animation::AnimationRunner; -use app::{App, SharedApp}; +use app::{SharedApp, App}; +use renderer::display_list::{SharedDisplayList, DisplayList}; use event_loop_notifier::EventLoopNotifier; -use renderer::display_list::{DisplayList, SharedDisplayList}; +use animation::AnimationRunner; /// Serves requests from the client forever async fn serve( @@ -45,7 +45,12 @@ async fn serve( mut events_receiver: mpsc::UnboundedReceiver, mut server_shutdown_receiver: mpsc::Receiver<()>, ) { - let anim_runner = AnimationRunner::new(conn.clone(), app.clone(), display_list.clone(), event_loop.clone()); + let anim_runner = AnimationRunner::new( + conn.clone(), + app.clone(), + display_list.clone(), + event_loop.clone(), + ); loop { // This will either receive the next request or end this task @@ -72,6 +77,7 @@ async fn serve( &anim_runner, request, )); + } } @@ -86,66 +92,88 @@ fn dispatch_request( ) -> Result<(), handlers::HandlerError> { use ClientRequest::*; match request { - CreateTurtle => handlers::create_turtle(conn, &mut app.write(), event_loop), - - Export(path, format) => handlers::export_drawings(conn, &app.read(), &display_list.lock(), &path, format), - - PollEvent => handlers::poll_event(conn, events_receiver), - - DrawingProp(prop) => handlers::drawing_prop(conn, &app.read(), prop), - SetDrawingProp(prop_value) => handlers::set_drawing_prop(&mut app.write(), event_loop, prop_value), - ResetDrawingProp(prop) => handlers::reset_drawing_prop(&mut app.write(), event_loop, prop), - - TurtleProp(id, prop) => handlers::turtle_prop(conn, &app.read(), id, prop), - SetTurtleProp(id, prop_value) => handlers::set_turtle_prop(&mut app.write(), &mut display_list.lock(), event_loop, id, prop_value), - ResetTurtleProp(id, prop) => handlers::reset_turtle_prop(&mut app.write(), &mut display_list.lock(), event_loop, id, prop), - ResetTurtle(id) => handlers::reset_turtle(&mut app.write(), &mut display_list.lock(), event_loop, id), - - MoveForward(id, distance) => handlers::move_forward( - conn, - &mut app.write(), - &mut display_list.lock(), - event_loop, - anim_runner, - id, - distance, - ), - MoveTo(id, target_pos) => handlers::move_to( - conn, - &mut app.write(), - &mut display_list.lock(), - event_loop, - anim_runner, - id, - target_pos, - ), + CreateTurtle => { + handlers::create_turtle(conn, &mut app.write(), event_loop) + }, + + Export(path, format) => { + handlers::export_drawings(conn, &app.read(), &display_list.lock(), &path, format) + }, + + PollEvent => { + handlers::poll_event(conn, events_receiver) + }, + + DrawingProp(prop) => { + handlers::drawing_prop(conn, &app.read(), prop) + }, + SetDrawingProp(prop_value) => { + handlers::set_drawing_prop(&mut app.write(), event_loop, prop_value) + }, + ResetDrawingProp(prop) => { + handlers::reset_drawing_prop(&mut app.write(), event_loop, prop) + }, + + TurtleProp(id, prop) => { + handlers::turtle_prop(conn, &app.read(), id, prop) + }, + SetTurtleProp(id, prop_value) => { + handlers::set_turtle_prop(&mut app.write(), &mut display_list.lock(), event_loop, id, prop_value) + }, + ResetTurtleProp(id, prop) => { + handlers::reset_turtle_prop(&mut app.write(), &mut display_list.lock(), event_loop, id, prop) + }, + ResetTurtle(id) => { + handlers::reset_turtle(&mut app.write(), &mut display_list.lock(), event_loop, id) + }, + + MoveForward(id, distance) => { + handlers::move_forward(conn, &mut app.write(), &mut display_list.lock(), event_loop, anim_runner, id, distance) + }, + MoveTo(id, target_pos) => { + handlers::move_to(conn, &mut app.write(), &mut display_list.lock(), event_loop, anim_runner, id, target_pos) + }, RotateInPlace(id, angle, direction) => { handlers::rotate_in_place(conn, &mut app.write(), event_loop, anim_runner, id, angle, direction) - } - - BeginFill(id) => handlers::begin_fill(&mut app.write(), &mut display_list.lock(), event_loop, id), - EndFill(id) => handlers::end_fill(&mut app.write(), id), - - ClearAll => handlers::clear_all(&mut app.write(), &mut display_list.lock(), event_loop, anim_runner), - ClearTurtle(id) => handlers::clear_turtle(&mut app.write(), &mut display_list.lock(), event_loop, id), - - DebugTurtle(id, angle_unit) => handlers::debug_turtle(conn, &app.read(), id, angle_unit), - DebugDrawing => handlers::debug_drawing(conn, &app.read()), - - DestroyDrawing => handlers::destroy_drawing(event_loop), + }, + + BeginFill(id) => { + handlers::begin_fill(&mut app.write(), &mut display_list.lock(), event_loop, id) + }, + EndFill(id) => { + handlers::end_fill(&mut app.write(), id) + }, + + ClearAll => { + handlers::clear_all(&mut app.write(), &mut display_list.lock(), event_loop, anim_runner) + }, + ClearTurtle(id) => { + handlers::clear_turtle(&mut app.write(), &mut display_list.lock(), event_loop, id) + }, + + DebugTurtle(id, angle_unit) => { + handlers::debug_turtle(conn, &app.read(), id, angle_unit) + }, + DebugDrawing => { + handlers::debug_drawing(conn, &app.read()) + }, + + DestroyDrawing => { + handlers::destroy_drawing(event_loop) + }, } } fn handle_handler_result(res: Result<(), handlers::HandlerError>) { use handlers::HandlerError::*; match res { - Ok(_) => {} + Ok(_) => {}, Err(IpcChannelError(err)) => panic!("Error while serializing response: {}", err), // Task managing window has ended, this task will end soon too. //TODO: This potentially leaves the turtle/drawing state in an inconsistent state. Should // we deal with that somehow? Panicking doesn't seem appropriate since this probably isn't // an error, but we should definitely stop processing commands and make sure the process // ends shortly after. - Err(EventLoopClosed(_)) => {} + Err(EventLoopClosed(_)) => {}, } } diff --git a/src/renderer_server/event_loop_notifier.rs b/src/renderer_server/event_loop_notifier.rs index c9c56b4a..9be612fd 100644 --- a/src/renderer_server/event_loop_notifier.rs +++ b/src/renderer_server/event_loop_notifier.rs @@ -39,11 +39,7 @@ pub struct EventLoopNotifier { impl EventLoopNotifier { pub fn new(event_loop: EventLoopProxy) -> Self { - Self { event_loop } - } - - pub fn exit(&self) -> Result<(), EventLoopClosed> { - self.send_action(MainThreadAction::Exit) + Self {event_loop} } pub fn request_redraw(&self) -> Result<(), EventLoopClosed> { @@ -66,6 +62,10 @@ impl EventLoopNotifier { self.send_action(MainThreadAction::SetIsFullscreen(is_fullscreen)) } + pub fn exit(&self) -> Result<(), EventLoopClosed> { + self.send_action(MainThreadAction::Exit) + } + fn send_action(&self, action: MainThreadAction) -> Result<(), EventLoopClosed> { Ok(self.event_loop.send_event(action)?) } diff --git a/src/renderer_server/handlers.rs b/src/renderer_server/handlers.rs index a19799a7..11167e20 100644 --- a/src/renderer_server/handlers.rs +++ b/src/renderer_server/handlers.rs @@ -1,24 +1,24 @@ -mod animation; -mod clear; mod create_turtle; -mod debug; -mod destroy_drawing; -mod drawing_prop; mod export_drawings; -mod fill; mod poll_event; +mod drawing_prop; mod turtle_prop; +mod animation; +mod fill; +mod clear; +mod debug; +mod destroy_drawing; -pub(crate) use animation::*; -pub(crate) use clear::*; pub(crate) use create_turtle::*; -pub(crate) use debug::*; -pub(crate) use destroy_drawing::*; -pub(crate) use drawing_prop::*; pub(crate) use export_drawings::*; -pub(crate) use fill::*; pub(crate) use poll_event::*; +pub(crate) use drawing_prop::*; pub(crate) use turtle_prop::*; +pub(crate) use animation::*; +pub(crate) use fill::*; +pub(crate) use clear::*; +pub(crate) use debug::*; +pub(crate) use destroy_drawing::*; use thiserror::Error; diff --git a/src/renderer_server/main.rs b/src/renderer_server/main.rs index 7710d7cf..2b4ba320 100644 --- a/src/renderer_server/main.rs +++ b/src/renderer_server/main.rs @@ -1,27 +1,41 @@ +use std::time::{Instant, Duration}; use std::future::Future; -use std::time::{Duration, Instant}; use glutin::{ + GlProfile, + GlRequest, + ContextBuilder, + WindowedContext, + PossiblyCurrent, dpi::{LogicalSize, PhysicalPosition}, - event::{ElementState, Event as GlutinEvent, KeyboardInput, StartCause, VirtualKeyCode, WindowEvent}, + window::{WindowBuilder, Fullscreen}, + event::{ + Event as GlutinEvent, + StartCause, + WindowEvent, + KeyboardInput, + VirtualKeyCode, + ElementState, + }, event_loop::{ControlFlow, EventLoop}, platform::run_return::EventLoopExtRunReturn, - window::{Fullscreen, WindowBuilder}, - ContextBuilder, GlProfile, GlRequest, PossiblyCurrent, WindowedContext, }; -use tokio::{runtime::Handle, sync::mpsc}; +use tokio::{ + sync::mpsc, + runtime::Handle, +}; -use crate::ipc_protocol::{ConnectionError, ServerReceiver, ServerSender}; use crate::Event; +use crate::ipc_protocol::{ServerSender, ServerReceiver, ConnectionError}; use super::{ - app::{App, SharedApp}, + app::{SharedApp, App}, coords::ScreenPoint, - event_loop_notifier::{EventLoopNotifier, MainThreadAction}, renderer::{ - display_list::{DisplayList, SharedDisplayList}, Renderer, + display_list::{SharedDisplayList, DisplayList}, }, + event_loop_notifier::{EventLoopNotifier, MainThreadAction}, }; /// The maximum rendering FPS allowed @@ -63,7 +77,7 @@ pub fn run_main( handle: Handle, // Polled to establish the server connection - establish_connection: impl Future> + Send + 'static, + establish_connection: impl Future> + Send + 'static, ) { // The state of the drawing and the state/drawings associated with each turtle let app = SharedApp::default(); @@ -91,10 +105,9 @@ pub fn run_main( let window_builder = { let app = app.read(); let drawing = app.drawing(); - WindowBuilder::new().with_title(&drawing.title).with_inner_size(LogicalSize { - width: drawing.width, - height: drawing.height, - }) + WindowBuilder::new() + .with_title(&drawing.title) + .with_inner_size(LogicalSize {width: drawing.width, height: drawing.height}) }; // Create an OpenGL 3.x context for Pathfinder to use @@ -139,47 +152,43 @@ pub fn run_main( establish_connection.take().expect("bug: init event should only occur once"), server_shutdown_receiver.take().expect("bug: init event should only occur once"), ); - } + }, - GlutinEvent::NewEvents(StartCause::ResumeTimeReached { .. }) => { + GlutinEvent::NewEvents(StartCause::ResumeTimeReached {..}) => { // A render was delayed in the `RedrawRequested` so let's try to do it again now that // we have resumed gl_context.window().request_redraw(); - } + }, // Quit if the window is closed or if Esc is pressed and then released GlutinEvent::WindowEvent { event: WindowEvent::CloseRequested, .. - } - | GlutinEvent::WindowEvent { + } | GlutinEvent::WindowEvent { event: WindowEvent::Destroyed, .. - } - | GlutinEvent::WindowEvent { - event: - WindowEvent::KeyboardInput { - input: - KeyboardInput { - state: ElementState::Released, - virtual_keycode: Some(VirtualKeyCode::Escape), - .. - }, + } | GlutinEvent::WindowEvent { + event: WindowEvent::KeyboardInput { + input: KeyboardInput { + state: ElementState::Released, + virtual_keycode: Some(VirtualKeyCode::Escape), .. }, + .. + }, .. } => { *control_flow = ControlFlow::Exit; - } + }, GlutinEvent::WindowEvent { - event: WindowEvent::ScaleFactorChanged { scale_factor, .. }, + event: WindowEvent::ScaleFactorChanged {scale_factor, ..}, .. } => { renderer.set_scale_factor(scale_factor); - } + }, - GlutinEvent::WindowEvent { event, .. } => { + GlutinEvent::WindowEvent {event, ..} => { let scale_factor = renderer.scale_factor(); match event { WindowEvent::Resized(size) => { @@ -188,11 +197,12 @@ pub fn run_main( let mut drawing = app.drawing_mut(); drawing.width = size.width; drawing.height = size.height; - } + }, //TODO: There are currently no events for updating is_maximized, so that property // should not be relied on. https://github.com/rust-windowing/glutin/issues/1298 - _ => {} + + _ => {}, } // Converts to logical coordinates, only locking the drawing if this is actually called @@ -218,34 +228,32 @@ pub fn run_main( // main process ends. This is not a fatal error though so we just ignore it. events_sender.send(event).unwrap_or(()); } - } + }, // Window events are currently sufficient for the turtle event API - GlutinEvent::DeviceEvent { .. } => {} + GlutinEvent::DeviceEvent {..} => {}, GlutinEvent::UserEvent(MainThreadAction::Redraw) => { gl_context.window().request_redraw(); - } + }, GlutinEvent::UserEvent(MainThreadAction::SetTitle(title)) => { gl_context.window().set_title(&title); - } + }, GlutinEvent::UserEvent(MainThreadAction::SetSize(size)) => { gl_context.window().set_inner_size(size); - } + }, GlutinEvent::UserEvent(MainThreadAction::SetIsMaximized(is_maximized)) => { gl_context.window().set_maximized(is_maximized); - } + }, GlutinEvent::UserEvent(MainThreadAction::SetIsFullscreen(is_fullscreen)) => { gl_context.window().set_fullscreen(if is_fullscreen { Some(Fullscreen::Borderless(gl_context.window().current_monitor())) - } else { - None - }); - } + } else { None }); + }, GlutinEvent::UserEvent(MainThreadAction::Exit) => { *control_flow = ControlFlow::Exit; @@ -269,19 +277,24 @@ pub fn run_main( // // This is why the window has 0 CPU usage when nothing is happening *control_flow = ControlFlow::Wait; - } + }, GlutinEvent::LoopDestroyed => { // Notify the server that it should shutdown, ignoring the error if the channel has // been dropped since that just means that the server task has ended already handle.block_on(server_shutdown.send(())).unwrap_or(()); - } + }, - _ => {} + _ => {}, }); } -fn redraw(app: &App, display_list: &DisplayList, gl_context: &WindowedContext, renderer: &mut Renderer) { +fn redraw( + app: &App, + display_list: &DisplayList, + gl_context: &WindowedContext, + renderer: &mut Renderer, +) { let draw_size = gl_context.window().inner_size(); let drawing = app.drawing(); let turtle_states = app.turtles().map(|(_, turtle)| &turtle.state); @@ -296,11 +309,12 @@ fn spawn_async_server( display_list: SharedDisplayList, event_loop: EventLoopNotifier, events_receiver: mpsc::UnboundedReceiver, - establish_connection: impl Future> + Send + 'static, + establish_connection: impl Future> + Send + 'static, server_shutdown_receiver: mpsc::Receiver<()>, ) { handle.spawn(async { - let (conn_sender, conn_receiver) = establish_connection.await.expect("unable to establish turtle server connection"); + let (conn_sender, conn_receiver) = establish_connection.await + .expect("unable to establish turtle server connection"); super::serve( conn_sender, @@ -310,7 +324,6 @@ fn spawn_async_server( event_loop, events_receiver, server_shutdown_receiver, - ) - .await; + ).await; }); } diff --git a/src/renderer_server/test_event_loop_notifier.rs b/src/renderer_server/test_event_loop_notifier.rs index 0dff7f87..637fe518 100644 --- a/src/renderer_server/test_event_loop_notifier.rs +++ b/src/renderer_server/test_event_loop_notifier.rs @@ -1,5 +1,5 @@ -use glutin::dpi::LogicalSize; use thiserror::Error; +use glutin::dpi::LogicalSize; #[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] #[error("event loop closed while messages were still being sent to it")] @@ -14,10 +14,6 @@ impl EventLoopNotifier { Self {} } - pub fn exit(&self) -> Result<(), EventLoopClosed> { - Ok(()) - } - pub fn request_redraw(&self) -> Result<(), EventLoopClosed> { Ok(()) } @@ -37,4 +33,8 @@ impl EventLoopNotifier { pub fn set_is_fullscreen(&self, _is_fullscreen: bool) -> Result<(), EventLoopClosed> { Ok(()) } + + pub fn exit(&self) -> Result<(), EventLoopClosed> { + Ok(()) + } } From 7eb689537199bf70348a8373a2cc34ad66736511 Mon Sep 17 00:00:00 2001 From: Sathwik Matsa Date: Fri, 16 Jul 2021 12:59:02 +0530 Subject: [PATCH 03/16] Mark Drawing::destroy() as unstable --- src/drawing.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/drawing.rs b/src/drawing.rs index f9fab197..37d01282 100644 --- a/src/drawing.rs +++ b/src/drawing.rs @@ -648,6 +648,8 @@ impl Drawing { /// // this will panic! /// // turtle.forward(100.0) /// ``` + #[cfg(feature = "unstable")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] pub fn destroy(self) { self.drawing.destroy(); } From 7e3bbb94b37548361c94af7f88eaf3746f30c2a5 Mon Sep 17 00:00:00 2001 From: Sathwik Matsa Date: Fri, 16 Jul 2021 13:43:40 +0530 Subject: [PATCH 04/16] update runtest.rs example with Drawing::destroy() --- examples/runtest.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/runtest.rs b/examples/runtest.rs index 45cd9cd1..ee5b23ae 100644 --- a/examples/runtest.rs +++ b/examples/runtest.rs @@ -1,18 +1,18 @@ //! This is NOT a real example. This is a test designed to see if we can actually run the turtle //! process +// To run, use the command: cargo run --features unstable --example runtest +#[cfg(all(not(feature = "unstable")))] +compile_error!("This example relies on unstable features. Run with `--features unstable`"); -use std::process; - -use turtle::Turtle; +use turtle::Drawing; fn main() { - let mut turtle = Turtle::new(); + let mut drawing = Drawing::new(); + let mut turtle = drawing.add_turtle(); turtle.set_speed(2); turtle.right(90.0); turtle.forward(50.0); - //TODO: Exiting the process currently doesn't cause the window to get closed. We should add a - // `close(self)` or `quit(self)` method to `Drawing` that closes the window explicitly. - process::exit(0); + drawing.destroy(); } From 51ded433d25912f2800145a4887561f718d7899b Mon Sep 17 00:00:00 2001 From: Sathwik Matsa Date: Mon, 19 Jul 2021 23:01:58 +0530 Subject: [PATCH 05/16] Battleship logic --- examples/battleship/battlestate.rs | 302 +++++++++++++++++++++++++++++ examples/battleship/main.rs | 80 ++++++++ examples/battleship/ship.rs | 145 ++++++++++++++ 3 files changed, 527 insertions(+) create mode 100644 examples/battleship/battlestate.rs create mode 100644 examples/battleship/main.rs create mode 100644 examples/battleship/ship.rs diff --git a/examples/battleship/battlestate.rs b/examples/battleship/battlestate.rs new file mode 100644 index 00000000..8afa9a29 --- /dev/null +++ b/examples/battleship/battlestate.rs @@ -0,0 +1,302 @@ +use std::{convert::TryInto, fmt::Display, ops::Deref}; + +use turtle::rand::{choose, random_range}; + +use super::ship::*; + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum Cell { + // ship grid + Carrier = 0, + Battleship = 1, + Cruiser = 2, + Submarine = 3, + Destroyer = 4, + Empty, + + // attack grid + Unattacked, + Missed, + + // common + Bombed, + Destroyed, +} + +impl ShipKind { + 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, + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum AttackOutcome { + Miss, + Hit, + Destroyed(Ship), +} + +struct Grid([[Cell; 10]; 10]); + +impl Deref for Grid { + type Target = [[Cell; 10]; 10]; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Grid { + fn get(&self, pos: &(u8, u8)) -> Cell { + self.0[pos.0 as usize][pos.1 as usize] + } + fn get_mut(&mut self, pos: &(u8, u8)) -> &mut Cell { + &mut self.0[pos.0 as usize][pos.1 as usize] + } + fn count(&mut self, cell: &Cell) -> usize { + self.iter().flatten().filter(|&c| c == cell).count() + } +} + +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::() + }) + .collect::>(); + write!(f, "{}", output.join("\n")) + } +} + +impl BattleState { + pub fn custom(ships: [Ship; 5]) -> Self { + let mut ship_grid = Grid { + 0: [[Cell::Empty; 10]; 10], + }; + ships.iter().for_each(|ship| { + ship.coordinates().iter().for_each(|pos| { + *ship_grid.get_mut(pos) = ship.kind.to_cell(); + }) + }); + Self { + ships, + ship_grid, + attack_grid: Grid { + 0: [[Cell::Unattacked; 10]; 10], + }, + destroyed_rival_ships: 0, + ships_lost: 0, + } + } + pub fn new() -> Self { + let (ships, ship_grid) = Self::random_ship_grid(); + Self { + ships, + ship_grid, + attack_grid: Grid { + 0: [[Cell::Unattacked; 10]; 10], + }, + 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 count = self.ship_grid.count(&attacked_cell); + match count { + 1 => { + 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!(), + } + } + 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!(), + } + } + pub fn set_attack_outcome(&mut self, pos: &(u8, u8), cell: Cell) { + *self.attack_grid.get_mut(pos) = cell; + } + pub fn set_destroyed_ship(&mut self, ship: &Ship) { + ship.coordinates() + .into_iter() + .for_each(|pos| *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 { + 0: [[Cell::Empty; 10]; 10], + }; + let mut ships = Vec::new(); + + 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_coords = (0..kind.size()) + .map(|i| match orient { + Orientation::Horizontal => (x + i, y), + Orientation::Veritcal => (x, y + i), + }) + .collect::>(); + + let no_overlap = ships + .iter() + .all(|ship: &Ship| ship_coords.iter().all(|pos| !ship.is_located_over(pos))); + let within_board = ship_coords.iter().all(|pos| matches!(pos.0, 0..=9) && matches!(pos.1, 0..=9)); + + if no_overlap && within_board { + let ship = Ship::new( + kind, + ShipPosition::new(ship_coords.first().copied().unwrap(), ship_coords.last().copied().unwrap()), + ); + ships.push(ship); + ship_coords.iter().for_each(|pos| { + *grid.get_mut(pos) = kind.to_cell(); + }); + break; + } + } + } + + (ships.try_into().unwrap(), grid) + } +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn battle_actions() { + let ships = [ + Ship { + kind: ShipKind::Carrier, + position: ShipPosition { + top_left: (2, 4), + bottom_right: (2, 8), + }, + }, + Ship { + kind: ShipKind::Battleship, + position: ShipPosition { + top_left: (1, 0), + bottom_right: (4, 0), + }, + }, + Ship { + kind: ShipKind::Cruiser, + position: ShipPosition { + top_left: (5, 2), + bottom_right: (7, 2), + }, + }, + Ship { + kind: ShipKind::Submarine, + position: ShipPosition { + top_left: (8, 4), + bottom_right: (8, 6), + }, + }, + Ship { + kind: ShipKind::Destroyer, + position: ShipPosition { + top_left: (6, 7), + bottom_right: (9, 7), + }, + }, + ]; + // 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 = BattleState::custom(ships); + // turn 1: player attacks (2, 2) - misses + state.set_attack_outcome(&(2, 2), Cell::Missed); + 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), Cell::Bombed); + assert_eq!(state.attack_grid.get(&(7, 2)), Cell::Bombed); + // turn 5: player attacks (6, 2) - destroys D + state.set_destroyed_ship(&Ship { + kind: ShipKind::Destroyer, + position: ShipPosition { + top_left: (6, 2), + bottom_right: (7, 2), + }, + }); + 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); + } +} diff --git a/examples/battleship/main.rs b/examples/battleship/main.rs new file mode 100644 index 00000000..abf1efcd --- /dev/null +++ b/examples/battleship/main.rs @@ -0,0 +1,80 @@ +use battlestate::*; +use turtle::rand::random_range; + +mod battlestate; +mod ship; + +#[derive(Debug, Copy, Clone)] +enum Turn { + Player, + Opponent, +} + +impl Turn { + fn flip(&mut self) { + match self { + Turn::Player => *self = Turn::Opponent, + Turn::Opponent => *self = Turn::Player, + } + } + fn next(&mut self, attacker: &mut BattleState, defender: &mut BattleState) { + let attack_pos = random_attack_target(&attacker); + println!("{:?} attacks {:?}", self, attack_pos); + let outcome = defender.incoming_attack(&attack_pos); + println!("Outcome: {:#?}", outcome); + match outcome { + AttackOutcome::Hit => attacker.set_attack_outcome(&attack_pos, Cell::Bombed), + AttackOutcome::Miss => { + attacker.set_attack_outcome(&attack_pos, Cell::Missed); + self.flip(); + } + AttackOutcome::Destroyed(ship) => { + attacker.set_destroyed_ship(&ship); + self.flip(); + } + } + } +} + +fn random_attack_target(state: &BattleState) -> (u8, u8) { + loop { + let x = random_range(0, 9); + let y = random_range(0, 9); + if state.can_bomb(&(x, y)) { + return (x, y); + } + } +} + +fn main() { + println!("Welcome to Battleship!"); + let mut player_state = battlestate::BattleState::new(); + println!("PLAYER: \n{}", player_state); + let mut opponent_state = battlestate::BattleState::new(); + println!("OPPONENT: \n{}", opponent_state); + + let mut turn = Turn::Player; + + loop { + match turn { + Turn::Player => { + turn.next(&mut player_state, &mut opponent_state); + }, + Turn::Opponent => { + turn.next(&mut opponent_state, &mut player_state); + } + } + + match (player_state.ships_lost, player_state.destroyed_rival_ships) { + (5, _) => { + println!("Opponent Won!"); + break; + } + (_, 5) => { + println!("Player Won!"); + break; + } + (_, _) => continue, + } + } +} diff --git a/examples/battleship/ship.rs b/examples/battleship/ship.rs new file mode 100644 index 00000000..6b755415 --- /dev/null +++ b/examples/battleship/ship.rs @@ -0,0 +1,145 @@ +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct ShipPosition { + pub top_left: (u8, u8), + pub bottom_right: (u8, u8), +} + +impl ShipPosition { + pub fn new(top_left: (u8, u8), bottom_right: (u8, u8)) -> Self { + Self { top_left, bottom_right } + } +} + +// Based on https://en.wikipedia.org/wiki/Battleship_(game)#Description +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum ShipKind { + Carrier, + Battleship, + Cruiser, + Submarine, + Destroyer, +} + +impl ShipKind { + pub fn size(&self) -> u8 { + match self { + Self::Carrier => 5, + Self::Battleship => 4, + Self::Cruiser => 3, + Self::Submarine => 3, + Self::Destroyer => 2, + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Orientation { + Horizontal, + Veritcal, +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct Ship { + pub kind: ShipKind, + pub position: ShipPosition, +} + +impl Ship { + pub fn new(kind: ShipKind, position: ShipPosition) -> Self { + Self { kind, position } + } + pub fn orientation(&self) -> Orientation { + let diff_x = self.position.top_left.0 as i32 - self.position.bottom_right.0 as i32; + let diff_y = self.position.top_left.1 as i32 - self.position.bottom_right.1 as i32; + match (diff_x, diff_y) { + (0, _) => Orientation::Veritcal, + (_, 0) => Orientation::Horizontal, + (_, _) => unreachable!(), + } + } + pub fn is_located_over(&self, pos: &(u8, u8)) -> bool { + let collinear = { + (pos.0 as i32 - self.position.top_left.0 as i32) * (self.position.top_left.1 as i32 - self.position.bottom_right.1 as i32) + - (self.position.top_left.0 as i32 - self.position.bottom_right.0 as i32) * (pos.1 as i32 - self.position.top_left.1 as i32) + == 0 + }; + let x_within_bounds = (self.position.top_left.0..=self.position.bottom_right.0).contains(&pos.0); + let y_within_bounds = (self.position.top_left.1..=self.position.bottom_right.1).contains(&pos.1); + collinear && x_within_bounds && y_within_bounds + } + 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() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn ship_orientation() { + let carrier = Ship { + kind: ShipKind::Carrier, + position: ShipPosition { + top_left: (1, 2), + bottom_right: (1, 6), + }, + }; + + let battleship = Ship { + kind: ShipKind::Battleship, + position: ShipPosition { + top_left: (3, 2), + bottom_right: (6, 2), + }, + }; + + assert_eq!(carrier.orientation(), Orientation::Veritcal); + assert_eq!(battleship.orientation(), Orientation::Horizontal); + } + + #[test] + fn ship_intersection() { + let carrier = Ship { + kind: ShipKind::Carrier, + position: ShipPosition { + top_left: (1, 2), + bottom_right: (1, 6), + }, + }; + let cspan: Vec<_> = (2..=6).map(|y| (1, y)).collect(); + + let battleship = Ship { + kind: ShipKind::Battleship, + position: ShipPosition { + top_left: (3, 2), + bottom_right: (6, 2), + }, + }; + let bspan: Vec<_> = (3..=6).map(|x| (x, 2)).collect(); + + for x in 0..10 { + for y in 0..10 { + let pos = (x as u8, y as u8); + if cspan.contains(&pos) { + assert!(carrier.is_located_over(&pos)); + } else { + assert!(!carrier.is_located_over(&pos)); + } + if bspan.contains(&pos) { + assert!(battleship.is_located_over(&pos)); + } else { + assert!(!battleship.is_located_over(&pos)); + } + } + } + } +} From 098b5b0bfbb6a8cbbb194da778b311ae4ec451b2 Mon Sep 17 00:00:00 2001 From: Sathwik Matsa Date: Wed, 21 Jul 2021 20:27:45 +0530 Subject: [PATCH 06/16] Add Config, Channel, Game structs --- examples/battleship/battlestate.rs | 22 ++-- examples/battleship/channel.rs | 38 +++++++ examples/battleship/config.rs | 37 +++++++ examples/battleship/game.rs | 73 +++++++++++++ examples/battleship/main.rs | 161 ++++++++++++++++------------- examples/battleship/ship.rs | 13 ++- 6 files changed, 258 insertions(+), 86 deletions(-) create mode 100644 examples/battleship/channel.rs create mode 100644 examples/battleship/config.rs create mode 100644 examples/battleship/game.rs diff --git a/examples/battleship/battlestate.rs b/examples/battleship/battlestate.rs index 8afa9a29..628c4a5f 100644 --- a/examples/battleship/battlestate.rs +++ b/examples/battleship/battlestate.rs @@ -1,9 +1,8 @@ +use super::ship::*; +use serde::{Deserialize, Serialize}; use std::{convert::TryInto, fmt::Display, ops::Deref}; - use turtle::rand::{choose, random_range}; -use super::ship::*; - #[derive(Debug, Copy, Clone, PartialEq)] pub enum Cell { // ship grid @@ -35,14 +34,15 @@ impl ShipKind { } } -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Copy, Clone, Debug, PartialEq, Deserialize, Serialize)] pub enum AttackOutcome { Miss, Hit, Destroyed(Ship), } -struct Grid([[Cell; 10]; 10]); +#[derive(Debug, Copy, Clone)] +pub struct Grid([[Cell; 10]; 10]); impl Deref for Grid { type Target = [[Cell; 10]; 10]; @@ -52,13 +52,13 @@ impl Deref for Grid { } impl Grid { - fn get(&self, pos: &(u8, u8)) -> Cell { + pub fn get(&self, pos: &(u8, u8)) -> Cell { self.0[pos.0 as usize][pos.1 as usize] } - fn get_mut(&mut self, pos: &(u8, u8)) -> &mut Cell { + pub fn get_mut(&mut self, pos: &(u8, u8)) -> &mut Cell { &mut self.0[pos.0 as usize][pos.1 as usize] } - fn count(&mut self, cell: &Cell) -> usize { + pub fn count(&mut self, cell: &Cell) -> usize { self.iter().flatten().filter(|&c| c == cell).count() } } @@ -214,6 +214,12 @@ impl BattleState { (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)] diff --git a/examples/battleship/channel.rs b/examples/battleship/channel.rs new file mode 100644 index 00000000..0acc41e6 --- /dev/null +++ b/examples/battleship/channel.rs @@ -0,0 +1,38 @@ +use std::net::{TcpListener, TcpStream}; + +use crate::battlestate::AttackOutcome; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub enum Message { + AttackCoordinates((u8, u8)), + AttackResult(AttackOutcome), +} + +pub struct Channel { + stream: TcpStream, +} + +impl Channel { + pub fn client(ip_port: &str) -> Self { + Self { + stream: TcpStream::connect(ip_port).expect("Couldn't connect to the server"), + } + } + + pub fn server() -> Self { + let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to port"); + println!("Listening on: {:?}, Waiting for connection..", listener.local_addr()); + 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") + } +} diff --git a/examples/battleship/config.rs b/examples/battleship/config.rs new file mode 100644 index 00000000..2ca15664 --- /dev/null +++ b/examples/battleship/config.rs @@ -0,0 +1,37 @@ +use super::battlestate::Cell; +use turtle::Color; +pub struct Config {} + +impl Config { + pub const EMPTY_COLOR: &'static str = "#55dde0"; + pub const UNATTACKED_COLOR: &'static str = "#55dde0"; + pub const CARRIER_COLOR: &'static str = "#f6ae2d"; + pub const BATTLESHIP_COLOR: &'static str = "#f48923"; + pub const CRUISER_COLOR: &'static str = "#947757"; + pub const SUBMARINE_COLOR: &'static str = "#2f4858"; + pub const DESTROYER_COLOR: &'static str = "#238cf4"; + pub const MISSED_COLOR: &'static str = "#33658a"; + pub const BOMBED_COLOR: &'static str = "#f26419"; + pub const DESTROYED_COLOR: &'static str = "#947757"; + + pub const CELL_SIZE: f64 = 40.0; + pub const SPACE_BETWEEN_GRIDS: f64 = 50.0; + + pub const SHIP_GRID_TOP_LEFT: (f64, f64) = (-Self::SPACE_BETWEEN_GRIDS / 2.0 - 10.0 * Self::CELL_SIZE, 5.0 * Self::CELL_SIZE); + pub const ATTACK_GRID_TOP_LEFT: (f64, f64) = (Self::SPACE_BETWEEN_GRIDS / 2.0, 5.0 * Self::CELL_SIZE); + + pub fn cell_color(cell: &Cell) -> Color { + match cell { + Cell::Carrier => Self::CARRIER_COLOR.into(), + Cell::Battleship => Self::BATTLESHIP_COLOR.into(), + Cell::Cruiser => Self::CRUISER_COLOR.into(), + Cell::Submarine => Self::SUBMARINE_COLOR.into(), + Cell::Destroyer => Self::DESTROYER_COLOR.into(), + Cell::Empty => Self::UNATTACKED_COLOR.into(), + Cell::Unattacked => Self::UNATTACKED_COLOR.into(), + Cell::Missed => Self::MISSED_COLOR.into(), + Cell::Bombed => Self::BOMBED_COLOR.into(), + Cell::Destroyed => Self::DESTROYED_COLOR.into(), + } + } +} diff --git a/examples/battleship/game.rs b/examples/battleship/game.rs new file mode 100644 index 00000000..aeb2f3ab --- /dev/null +++ b/examples/battleship/game.rs @@ -0,0 +1,73 @@ +use turtle::Turtle; + +use crate::{ + battlestate::{BattleState, Cell}, + channel::Channel, + config::Config, +}; + +pub struct Game { + state: BattleState, + turtle: Turtle, + //channel: Channel, +} + +pub enum Player<'a> { + Server, + Client(&'a str), +} + +enum GridType { + ShipGrid, + AttackGrid, +} + +impl Game { + pub fn new(player: Player, turtle: Turtle) -> Self { + let mut turtle = turtle; + turtle.set_speed("instant"); + turtle.hide(); + let state = BattleState::new(); + //let channel = match player { + // Server => Channel::server(), + // Client(addr) => Channel::client(addr), + //}; + Self { + state, + turtle, /*channel*/ + } + } + + pub fn draw_cell(&mut self, cell: Cell, pos: (u8, u8), grid: GridType) { + let (x, y) = match grid { + GridType::ShipGrid => Config::SHIP_GRID_TOP_LEFT, + GridType::AttackGrid => Config::ATTACK_GRID_TOP_LEFT, + }; + let start = (x + Config::CELL_SIZE * pos.1 as f64, y - Config::CELL_SIZE * pos.0 as f64); + + self.turtle.pen_up(); + self.turtle.go_to(start); + self.turtle.pen_down(); + self.turtle.set_heading(0.0); // face East + self.turtle.set_fill_color(Config::cell_color(&cell)); + self.turtle.begin_fill(); + for _ in 0..4 { + self.turtle.forward(Config::CELL_SIZE); + self.turtle.right(90.0); + } + self.turtle.end_fill(); + } + + pub fn draw_board(&mut self) { + let ship_grid = self.state.ship_grid(); + println!("{}", self.state); + let attack_grid = self.state.attack_grid(); + + for x in 0..10 { + for y in 0..10 { + self.draw_cell(ship_grid.get(&(x, y)), (x, y), GridType::ShipGrid); + self.draw_cell(attack_grid.get(&(x, y)), (x, y), GridType::AttackGrid); + } + } + } +} diff --git a/examples/battleship/main.rs b/examples/battleship/main.rs index abf1efcd..f5af122e 100644 --- a/examples/battleship/main.rs +++ b/examples/battleship/main.rs @@ -1,80 +1,95 @@ use battlestate::*; -use turtle::rand::random_range; +use game::{Game, Player}; +use turtle::{rand::random_range, Drawing}; mod battlestate; +mod channel; +mod config; +mod game; mod ship; -#[derive(Debug, Copy, Clone)] -enum Turn { - Player, - Opponent, -} - -impl Turn { - fn flip(&mut self) { - match self { - Turn::Player => *self = Turn::Opponent, - Turn::Opponent => *self = Turn::Player, - } - } - fn next(&mut self, attacker: &mut BattleState, defender: &mut BattleState) { - let attack_pos = random_attack_target(&attacker); - println!("{:?} attacks {:?}", self, attack_pos); - let outcome = defender.incoming_attack(&attack_pos); - println!("Outcome: {:#?}", outcome); - match outcome { - AttackOutcome::Hit => attacker.set_attack_outcome(&attack_pos, Cell::Bombed), - AttackOutcome::Miss => { - attacker.set_attack_outcome(&attack_pos, Cell::Missed); - self.flip(); - } - AttackOutcome::Destroyed(ship) => { - attacker.set_destroyed_ship(&ship); - self.flip(); - } - } - } -} - -fn random_attack_target(state: &BattleState) -> (u8, u8) { - loop { - let x = random_range(0, 9); - let y = random_range(0, 9); - if state.can_bomb(&(x, y)) { - return (x, y); - } - } -} - fn main() { - println!("Welcome to Battleship!"); - let mut player_state = battlestate::BattleState::new(); - println!("PLAYER: \n{}", player_state); - let mut opponent_state = battlestate::BattleState::new(); - println!("OPPONENT: \n{}", opponent_state); - - let mut turn = Turn::Player; - - loop { - match turn { - Turn::Player => { - turn.next(&mut player_state, &mut opponent_state); - }, - Turn::Opponent => { - turn.next(&mut opponent_state, &mut player_state); - } - } - - match (player_state.ships_lost, player_state.destroyed_rival_ships) { - (5, _) => { - println!("Opponent Won!"); - break; - } - (_, 5) => { - println!("Player Won!"); - break; - } - (_, _) => continue, - } - } + let mut drawing = Drawing::new(); + let args: Vec = std::env::args().collect(); + let player = match args.len() { + 0 => Player::Server, + _ => Player::Client(&args[0]), + }; + let mut game = Game::new(player, drawing.add_turtle()); + game.draw_board(); } +//#[derive(Debug, Copy, Clone)] +//enum Turn { +// Player, +// Opponent, +//} +// +//impl Turn { +// fn flip(&mut self) { +// match self { +// Turn::Player => *self = Turn::Opponent, +// Turn::Opponent => *self = Turn::Player, +// } +// } +// fn next(&mut self, attacker: &mut BattleState, defender: &mut BattleState) { +// let attack_pos = random_attack_target(&attacker); +// println!("{:?} attacks {:?}", self, attack_pos); +// let outcome = defender.incoming_attack(&attack_pos); +// println!("Outcome: {:#?}", outcome); +// match outcome { +// AttackOutcome::Hit => attacker.set_attack_outcome(&attack_pos, Cell::Bombed), +// AttackOutcome::Miss => { +// attacker.set_attack_outcome(&attack_pos, Cell::Missed); +// self.flip(); +// } +// AttackOutcome::Destroyed(ship) => { +// attacker.set_destroyed_ship(&ship); +// self.flip(); +// } +// } +// } +//} +// +//fn random_attack_target(state: &BattleState) -> (u8, u8) { +// loop { +// let x = random_range(0, 9); +// let y = random_range(0, 9); +// if state.can_bomb(&(x, y)) { +// return (x, y); +// } +// } +//} +// +//fn john() { +// println!("Welcome to Battleship!"); +// let mut player_state = battlestate::BattleState::new(); +// println!("PLAYER: \n{}", player_state); +// let mut opponent_state = battlestate::BattleState::new(); +// println!("OPPONENT: \n{}", opponent_state); +// +// let mut turn = Turn::Player; +// +// loop { +// match turn { +// Turn::Player => { +// turn.next(&mut player_state, &mut opponent_state); +// } +// Turn::Opponent => { +// turn.next(&mut opponent_state, &mut player_state); +// } +// } +// +// match (player_state.ships_lost, player_state.destroyed_rival_ships) { +// (5, _) => { +// println!("Opponent Won!"); +// break; +// } +// (_, 5) => { +// println!("Player Won!"); +// break; +// } +// (_, _) => continue, +// } +// } +//} +// diff --git a/examples/battleship/ship.rs b/examples/battleship/ship.rs index 6b755415..83134f26 100644 --- a/examples/battleship/ship.rs +++ b/examples/battleship/ship.rs @@ -1,4 +1,6 @@ -#[derive(Copy, Clone, Debug, PartialEq)] +use serde::{Deserialize, Serialize}; + +#[derive(Copy, Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct ShipPosition { pub top_left: (u8, u8), pub bottom_right: (u8, u8), @@ -11,7 +13,7 @@ impl ShipPosition { } // Based on https://en.wikipedia.org/wiki/Battleship_(game)#Description -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, Deserialize, Serialize)] pub enum ShipKind { Carrier, Battleship, @@ -38,7 +40,7 @@ pub enum Orientation { Veritcal, } -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Ship { pub kind: ShipKind, pub position: ShipPosition, @@ -68,7 +70,7 @@ impl Ship { collinear && x_within_bounds && y_within_bounds } pub fn coordinates(&self) -> Vec<(u8, u8)> { - let orientation = self.orientation(); + let orientation = self.orientation(); let x = self.position.top_left.0; let y = self.position.top_left.1; @@ -76,7 +78,8 @@ impl Ship { .map(|i| match orientation { Orientation::Horizontal => (x + i, y), Orientation::Veritcal => (x, y + i), - }).collect() + }) + .collect() } } From 92128b1ecd83d46a672b312290fa178c348bba11 Mon Sep 17 00:00:00 2001 From: Sathwik Matsa Date: Fri, 23 Jul 2021 18:05:15 +0530 Subject: [PATCH 07/16] Add Game loop, user input event loop logic --- examples/battleship/battlestate.rs | 8 +- examples/battleship/config.rs | 11 +- examples/battleship/game.rs | 343 +++++++++++++++++++++++++---- examples/battleship/main.rs | 93 +------- examples/square.rs | 9 +- 5 files changed, 334 insertions(+), 130 deletions(-) diff --git a/examples/battleship/battlestate.rs b/examples/battleship/battlestate.rs index 628c4a5f..45e76f8d 100644 --- a/examples/battleship/battlestate.rs +++ b/examples/battleship/battlestate.rs @@ -214,11 +214,11 @@ impl BattleState { (ships.try_into().unwrap(), grid) } - pub fn ship_grid(&self) -> Grid { - self.ship_grid + pub fn ship_grid(&self) -> &'_ Grid { + &self.ship_grid } - pub fn attack_grid(&self) -> Grid { - self.attack_grid + pub fn attack_grid(&self) -> &'_ Grid { + &self.attack_grid } } diff --git a/examples/battleship/config.rs b/examples/battleship/config.rs index 2ca15664..c0c2a57e 100644 --- a/examples/battleship/config.rs +++ b/examples/battleship/config.rs @@ -12,7 +12,8 @@ impl Config { pub const DESTROYER_COLOR: &'static str = "#238cf4"; pub const MISSED_COLOR: &'static str = "#33658a"; pub const BOMBED_COLOR: &'static str = "#f26419"; - pub const DESTROYED_COLOR: &'static str = "#947757"; + pub const DESTROYED_COLOR: &'static str = "#000000"; + pub const TARGET_COLOR: &'static str = "#f26419"; pub const CELL_SIZE: f64 = 40.0; pub const SPACE_BETWEEN_GRIDS: f64 = 50.0; @@ -20,6 +21,12 @@ impl Config { pub const SHIP_GRID_TOP_LEFT: (f64, f64) = (-Self::SPACE_BETWEEN_GRIDS / 2.0 - 10.0 * Self::CELL_SIZE, 5.0 * Self::CELL_SIZE); pub const ATTACK_GRID_TOP_LEFT: (f64, f64) = (Self::SPACE_BETWEEN_GRIDS / 2.0, 5.0 * Self::CELL_SIZE); + pub const MISSED_CIRCLE_DIAMETER: f64 = 0.25 * Self::CELL_SIZE; + pub const BOMBED_CIRCLE_DIAMETER: f64 = 0.75 * Self::CELL_SIZE; + + pub const CROSSHAIR_SIZE: f64 = 0.4 * Self::CELL_SIZE; + pub const CROSSHAIR_PEN_SIZE: f64 = 4.0; + pub fn cell_color(cell: &Cell) -> Color { match cell { Cell::Carrier => Self::CARRIER_COLOR.into(), @@ -27,7 +34,7 @@ impl Config { Cell::Cruiser => Self::CRUISER_COLOR.into(), Cell::Submarine => Self::SUBMARINE_COLOR.into(), Cell::Destroyer => Self::DESTROYER_COLOR.into(), - Cell::Empty => Self::UNATTACKED_COLOR.into(), + Cell::Empty => Self::EMPTY_COLOR.into(), Cell::Unattacked => Self::UNATTACKED_COLOR.into(), Cell::Missed => Self::MISSED_COLOR.into(), Cell::Bombed => Self::BOMBED_COLOR.into(), diff --git a/examples/battleship/game.rs b/examples/battleship/game.rs index aeb2f3ab..fe4f1696 100644 --- a/examples/battleship/game.rs +++ b/examples/battleship/game.rs @@ -1,15 +1,35 @@ -use turtle::Turtle; +use turtle::{ + event::{Key, PressedState}, + rand::random_range, + Drawing, Event, Turtle, +}; use crate::{ - battlestate::{BattleState, Cell}, - channel::Channel, + battlestate::{AttackOutcome, BattleState, Cell}, + channel::{Channel, Message}, config::Config, }; +use std::f64::consts::PI; + +enum Turn { + Me, + Opponent, +} + +impl Turn { + fn flip(&mut self) { + match self { + Turn::Me => *self = Turn::Opponent, + Turn::Opponent => *self = Turn::Me, + } + } +} + pub struct Game { state: BattleState, - turtle: Turtle, - //channel: Channel, + channel: Channel, + turn: Turn, } pub enum Player<'a> { @@ -17,56 +37,299 @@ pub enum Player<'a> { Client(&'a str), } -enum GridType { - ShipGrid, - AttackGrid, +struct Crosshair<'a> { + pos: (u8, u8), + state: &'a BattleState, + turtle: &'a mut Turtle, +} + +impl<'a> Crosshair<'a> { + fn random_attackable_location(state: &BattleState) -> (u8, u8) { + loop { + let x = random_range(0, 9); + let y = random_range(0, 9); + if state.can_bomb(&(x, y)) { + return (x, y); + } + } + } + + fn draw_crosshair(pos: (u8, u8), turtle: &mut Turtle) { + let (x, y) = Config::ATTACK_GRID_TOP_LEFT; + turtle.set_pen_color(Config::TARGET_COLOR); + turtle.set_pen_size(Config::CROSSHAIR_PEN_SIZE); + let start = ( + x + Config::CELL_SIZE * (0.5 + pos.1 as f64), + y - Config::CELL_SIZE * (0.5 + pos.0 as f64), + ); + turtle.pen_up(); + turtle.go_to(start); + turtle.pen_down(); + turtle.set_heading(0.0); + for _ in 0..4 { + turtle.forward(Config::CROSSHAIR_SIZE); + turtle.pen_up(); + turtle.backward(Config::CROSSHAIR_SIZE); + turtle.pen_down(); + turtle.right(90.0); + } + turtle.set_pen_color("black"); + turtle.set_pen_size(1.0); + } + + fn new(state: &'a BattleState, turtle: &'a mut Turtle) -> Self { + let pos = Self::random_attackable_location(state); + Self::draw_crosshair(pos, turtle); + Self { pos, state, turtle } + } + + fn move_left(&mut self) { + // TODO: use inclusive range + let new_y = (0..self.pos.1).rev().find(|&y| self.state.can_bomb(&(self.pos.0, y))); + if let Some(y) = new_y { + let cell = self.state.attack_grid().get(&self.pos); + Game::draw_cell(cell, Position::AttackGrid(self.pos), &mut self.turtle); + + let new_pos = (self.pos.0, y); + Self::draw_crosshair(new_pos, self.turtle); + self.pos = new_pos; + } + } + fn move_right(&mut self) { + let new_y = (self.pos.1 + 1..10).find(|&y| self.state.can_bomb(&(self.pos.0, y))); + if let Some(y) = new_y { + let cell = self.state.attack_grid().get(&self.pos); + Game::draw_cell(cell, Position::AttackGrid(self.pos), &mut self.turtle); + + let new_pos = (self.pos.0, y); + Self::draw_crosshair(new_pos, self.turtle); + self.pos = new_pos; + } + } + fn move_up(&mut self) { + let new_x = (0..self.pos.0).rev().find(|&x| self.state.can_bomb(&(x, self.pos.1))); + if let Some(x) = new_x { + let cell = self.state.attack_grid().get(&self.pos); + Game::draw_cell(cell, Position::AttackGrid(self.pos), &mut self.turtle); + + let new_pos = (x, self.pos.1); + Self::draw_crosshair(new_pos, self.turtle); + self.pos = new_pos; + } + } + fn move_down(&mut self) { + let new_x = (self.pos.0 + 1..10).find(|&x| self.state.can_bomb(&(x, self.pos.1))); + if let Some(x) = new_x { + let cell = self.state.attack_grid().get(&self.pos); + Game::draw_cell(cell, Position::AttackGrid(self.pos), &mut self.turtle); + + let new_pos = (x, self.pos.1); + Self::draw_crosshair(new_pos, self.turtle); + self.pos = new_pos; + } + } + fn lock_target(&mut self) -> (u8, u8) { + let cell = self.state.attack_grid().get(&self.pos); + Game::draw_cell(cell, Position::AttackGrid(self.pos), &mut self.turtle); + return self.pos; + } +} + +enum Position { + ShipGrid((u8, u8)), + AttackGrid((u8, u8)), +} + +impl Position { + fn get(self) -> (u8, u8) { + match self { + Self::ShipGrid(p) => p, + Self::AttackGrid(p) => p, + } + } } impl Game { - pub fn new(player: Player, turtle: Turtle) -> Self { - let mut turtle = turtle; - turtle.set_speed("instant"); - turtle.hide(); + pub fn init(player: Player) -> Self { let state = BattleState::new(); - //let channel = match player { - // Server => Channel::server(), - // Client(addr) => Channel::client(addr), - //}; - Self { - state, - turtle, /*channel*/ - } + let channel = match player { + Player::Server => Channel::server(), + Player::Client(addr) => Channel::client(addr), + }; + let turn = match player { + Player::Client(_) => Turn::Opponent, + Player::Server => Turn::Me, + }; + + Self { state, channel, turn } } - pub fn draw_cell(&mut self, cell: Cell, pos: (u8, u8), grid: GridType) { - let (x, y) = match grid { - GridType::ShipGrid => Config::SHIP_GRID_TOP_LEFT, - GridType::AttackGrid => Config::ATTACK_GRID_TOP_LEFT, + fn draw_cell(cell: Cell, pos: Position, turtle: &mut Turtle) { + fn draw_circle(turtle: &mut Turtle, diameter: f64) { + turtle.set_heading(0.0); + turtle.begin_fill(); + for _ in 0..360 { + turtle.forward(PI * diameter / 360.0); + turtle.right(1.0); + } + turtle.end_fill(); + } + fn draw_square(turtle: &mut Turtle, size: f64) { + turtle.set_heading(0.0); + turtle.begin_fill(); + for _ in 0..4 { + turtle.forward(size); + turtle.right(90.0); + } + turtle.end_fill(); + } + let (x, y) = match pos { + Position::ShipGrid(_) => Config::SHIP_GRID_TOP_LEFT, + Position::AttackGrid(_) => Config::ATTACK_GRID_TOP_LEFT, }; - let start = (x + Config::CELL_SIZE * pos.1 as f64, y - Config::CELL_SIZE * pos.0 as f64); - - self.turtle.pen_up(); - self.turtle.go_to(start); - self.turtle.pen_down(); - self.turtle.set_heading(0.0); // face East - self.turtle.set_fill_color(Config::cell_color(&cell)); - self.turtle.begin_fill(); - for _ in 0..4 { - self.turtle.forward(Config::CELL_SIZE); - self.turtle.right(90.0); + + let pos = pos.get(); + + match cell { + Cell::Missed | Cell::Bombed => { + let diameter = if cell == Cell::Missed { + Config::MISSED_CIRCLE_DIAMETER + } else { + Config::BOMBED_CIRCLE_DIAMETER + }; + let start = ( + x + Config::CELL_SIZE * (pos.1 as f64 + 0.5), + y - Config::CELL_SIZE * pos.0 as f64 - (Config::CELL_SIZE / 2.0 - diameter / 2.0), + ); + turtle.pen_up(); + turtle.go_to(start); + turtle.pen_down(); + turtle.set_fill_color(Config::cell_color(&cell)); + draw_circle(turtle, diameter); + } + _ => { + let start = (x + Config::CELL_SIZE * pos.1 as f64, y - Config::CELL_SIZE * pos.0 as f64); + turtle.pen_up(); + turtle.go_to(start); + turtle.pen_down(); + turtle.set_fill_color(Config::cell_color(&cell)); + draw_square(turtle, Config::CELL_SIZE); + } } - self.turtle.end_fill(); } - pub fn draw_board(&mut self) { + fn draw_board(&self, turtle: &mut Turtle) { let ship_grid = self.state.ship_grid(); - println!("{}", self.state); let attack_grid = self.state.attack_grid(); for x in 0..10 { for y in 0..10 { - self.draw_cell(ship_grid.get(&(x, y)), (x, y), GridType::ShipGrid); - self.draw_cell(attack_grid.get(&(x, y)), (x, y), GridType::AttackGrid); + Self::draw_cell(ship_grid.get(&(x, y)), Position::ShipGrid((x, y)), turtle); + Self::draw_cell(attack_grid.get(&(x, y)), Position::AttackGrid((x, y)), turtle); + } + } + } + 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, drawing: &mut Drawing, turtle: &mut Turtle) -> (u8, u8) { + let mut crosshair = Crosshair::new(&self.state, turtle); + loop { + while let Some(event) = drawing.poll_event() { + use Key::{DownArrow, LeftArrow, Return, RightArrow, UpArrow}; + match event { + Event::Key(key, PressedState::Pressed) => match key { + LeftArrow => crosshair.move_left(), + RightArrow => crosshair.move_right(), + UpArrow => crosshair.move_up(), + DownArrow => crosshair.move_down(), + Return => { + return crosshair.lock_target(); + } + _ => {} + }, + _ => {} + } + } + } + } + + pub fn run(&mut self) { + let mut drawing = Drawing::new(); + let mut turtle = drawing.add_turtle(); + + turtle.hide(); + turtle.set_speed("instant"); + self.draw_board(&mut turtle); + + loop { + match self.turn { + Turn::Me => { + let attack_location = self.get_attack_location(&mut drawing, &mut turtle); + self.channel.send_message(&Message::AttackCoordinates(attack_location)); + match self.channel.receive_message() { + Message::AttackResult(outcome) => match outcome { + AttackOutcome::Miss => { + self.state.set_attack_outcome(&attack_location, Cell::Missed); + Self::draw_cell(Cell::Missed, Position::AttackGrid(attack_location), &mut turtle); + self.turn.flip(); + } + AttackOutcome::Hit => { + self.state.set_attack_outcome(&attack_location, Cell::Bombed); + Self::draw_cell(Cell::Bombed, Position::AttackGrid(attack_location), &mut turtle); + } + AttackOutcome::Destroyed(ship) => { + self.state.set_destroyed_ship(&ship); + ship.coordinates() + .into_iter() + .for_each(|pos| Self::draw_cell(Cell::Destroyed, Position::AttackGrid(pos), &mut turtle)); + 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 => { + Self::draw_cell(Cell::Missed, Position::ShipGrid(p), &mut turtle); + self.turn.flip(); + } + AttackOutcome::Hit => { + Self::draw_cell(Cell::Bombed, Position::ShipGrid(p), &mut turtle); + } + AttackOutcome::Destroyed(ship) => { + ship.coordinates() + .into_iter() + .for_each(|pos| Self::draw_cell(Cell::Destroyed, Position::ShipGrid(pos), &mut turtle)); + self.turn.flip(); + } + } + } + _ => panic!("Expected Message of AttackCoordinates from Opponent"), + }, + } + + match (self.state.ships_lost, self.state.destroyed_rival_ships) { + (5, _) => { + println!("Nice try."); + break; + } + (_, 5) => { + println!("GG!"); + break; + } + (_, _) => continue, } } } diff --git a/examples/battleship/main.rs b/examples/battleship/main.rs index f5af122e..185f386a 100644 --- a/examples/battleship/main.rs +++ b/examples/battleship/main.rs @@ -1,6 +1,6 @@ -use battlestate::*; -use game::{Game, Player}; -use turtle::{rand::random_range, Drawing}; +// To run, use the command: cargo run --features unstable --example battleship +#[cfg(all(not(feature = "unstable")))] +compile_error!("This example relies on unstable features. Run with `--features unstable`"); mod battlestate; mod channel; @@ -8,88 +8,15 @@ mod config; mod game; mod ship; +use game::{Game, Player}; + fn main() { - let mut drawing = Drawing::new(); let args: Vec = std::env::args().collect(); let player = match args.len() { - 0 => Player::Server, - _ => Player::Client(&args[0]), + 0 => unreachable!(), + 1 => Player::Server, + _ => Player::Client(&args[1]), }; - let mut game = Game::new(player, drawing.add_turtle()); - game.draw_board(); + let mut game = Game::init(player); + game.run(); } -//#[derive(Debug, Copy, Clone)] -//enum Turn { -// Player, -// Opponent, -//} -// -//impl Turn { -// fn flip(&mut self) { -// match self { -// Turn::Player => *self = Turn::Opponent, -// Turn::Opponent => *self = Turn::Player, -// } -// } -// fn next(&mut self, attacker: &mut BattleState, defender: &mut BattleState) { -// let attack_pos = random_attack_target(&attacker); -// println!("{:?} attacks {:?}", self, attack_pos); -// let outcome = defender.incoming_attack(&attack_pos); -// println!("Outcome: {:#?}", outcome); -// match outcome { -// AttackOutcome::Hit => attacker.set_attack_outcome(&attack_pos, Cell::Bombed), -// AttackOutcome::Miss => { -// attacker.set_attack_outcome(&attack_pos, Cell::Missed); -// self.flip(); -// } -// AttackOutcome::Destroyed(ship) => { -// attacker.set_destroyed_ship(&ship); -// self.flip(); -// } -// } -// } -//} -// -//fn random_attack_target(state: &BattleState) -> (u8, u8) { -// loop { -// let x = random_range(0, 9); -// let y = random_range(0, 9); -// if state.can_bomb(&(x, y)) { -// return (x, y); -// } -// } -//} -// -//fn john() { -// println!("Welcome to Battleship!"); -// let mut player_state = battlestate::BattleState::new(); -// println!("PLAYER: \n{}", player_state); -// let mut opponent_state = battlestate::BattleState::new(); -// println!("OPPONENT: \n{}", opponent_state); -// -// let mut turn = Turn::Player; -// -// loop { -// match turn { -// Turn::Player => { -// turn.next(&mut player_state, &mut opponent_state); -// } -// Turn::Opponent => { -// turn.next(&mut opponent_state, &mut player_state); -// } -// } -// -// match (player_state.ships_lost, player_state.destroyed_rival_ships) { -// (5, _) => { -// println!("Opponent Won!"); -// break; -// } -// (_, 5) => { -// println!("Player Won!"); -// break; -// } -// (_, _) => continue, -// } -// } -//} -// diff --git a/examples/square.rs b/examples/square.rs index 13494a9c..3d6648df 100644 --- a/examples/square.rs +++ b/examples/square.rs @@ -1,10 +1,17 @@ -use turtle::Turtle; +use turtle::*; fn main() { let mut turtle = Turtle::new(); + turtle.pen_up(); + turtle.go_to((-100.0, -100.0)); + turtle.pen_down(); + turtle.begin_fill(); + turtle.set_pen_color("green"); + turtle.set_fill_color("green"); for _ in 0..4 { turtle.forward(200.0); turtle.right(90.0); } + turtle.end_fill(); } From bd9e640b7b7939ffa4010f873a6bdb1fdc48f236 Mon Sep 17 00:00:00 2001 From: Sathwik Matsa Date: Fri, 23 Jul 2021 20:42:16 +0530 Subject: [PATCH 08/16] Listen on all interfaces --- examples/battleship/channel.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/battleship/channel.rs b/examples/battleship/channel.rs index 0acc41e6..f29e43d2 100644 --- a/examples/battleship/channel.rs +++ b/examples/battleship/channel.rs @@ -21,7 +21,7 @@ impl Channel { } pub fn server() -> Self { - let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to port"); + let listener = TcpListener::bind("0.0.0.0:0").expect("Failed to bind to port"); println!("Listening on: {:?}, Waiting for connection..", listener.local_addr()); let (stream, _) = listener.accept().expect("Couldn't connect to the client"); Self { stream } From 8974d8dea138f4f8bb9d859e09821d27c6146288 Mon Sep 17 00:00:00 2001 From: Sathwik Matsa Date: Sat, 24 Jul 2021 05:40:50 +0530 Subject: [PATCH 09/16] Add single player, improve UX --- examples/battleship/battlestate.rs | 5 +- examples/battleship/bot.rs | 111 ++++++++++++++++++++++ examples/battleship/channel.rs | 6 ++ examples/battleship/config.rs | 7 +- examples/battleship/game.rs | 148 ++++++++++++++--------------- examples/battleship/main.rs | 46 ++++++++- 6 files changed, 239 insertions(+), 84 deletions(-) create mode 100644 examples/battleship/bot.rs diff --git a/examples/battleship/battlestate.rs b/examples/battleship/battlestate.rs index 45e76f8d..20dab189 100644 --- a/examples/battleship/battlestate.rs +++ b/examples/battleship/battlestate.rs @@ -23,7 +23,7 @@ pub enum Cell { } impl ShipKind { - fn to_cell(&self) -> Cell { + fn to_cell(self) -> Cell { match self { ShipKind::Carrier => Cell::Carrier, ShipKind::Battleship => Cell::Battleship, @@ -94,7 +94,8 @@ impl Display for BattleState { } impl BattleState { - pub fn custom(ships: [Ship; 5]) -> Self { + #[allow(dead_code)] + fn custom(ships: [Ship; 5]) -> Self { let mut ship_grid = Grid { 0: [[Cell::Empty; 10]; 10], }; diff --git a/examples/battleship/bot.rs b/examples/battleship/bot.rs new file mode 100644 index 00000000..8f549003 --- /dev/null +++ b/examples/battleship/bot.rs @@ -0,0 +1,111 @@ +use turtle::rand::random_range; + +use crate::{ + battlestate::{AttackOutcome, BattleState, Cell}, + channel::{Channel, Message}, + game::Turn, +}; + +pub struct Bot { + channel: Channel, + state: BattleState, + turn: Turn, +} + +impl Bot { + pub fn new(port: u16) -> Self { + Self { + channel: Channel::client(&format!("127.0.0.1:{}", 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) { + 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)); + + for loc in bombed_locations { + let bombable = [(-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) = bombable { + return pos; + } + } + self.random_attack_location() + } + + 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) => match outcome { + AttackOutcome::Miss => { + self.state.set_attack_outcome(&attack_location, Cell::Missed); + self.turn.flip(); + } + AttackOutcome::Hit => { + self.state.set_attack_outcome(&attack_location, Cell::Bombed); + } + AttackOutcome::Destroyed(ship) => { + self.state.set_destroyed_ship(&ship); + 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 => { + self.turn.flip(); + } + AttackOutcome::Hit => {} + AttackOutcome::Destroyed(_) => { + self.turn.flip(); + } + } + } + _ => panic!("Expected Message of AttackCoordinates from Opponent"), + }, + } + + match (self.state.ships_lost, self.state.destroyed_rival_ships) { + (5, _) => { + break; + } + (_, 5) => { + break; + } + (_, _) => continue, + } + } + } +} diff --git a/examples/battleship/channel.rs b/examples/battleship/channel.rs index f29e43d2..67979eaf 100644 --- a/examples/battleship/channel.rs +++ b/examples/battleship/channel.rs @@ -27,6 +27,12 @@ impl Channel { Self { stream } } + pub fn serve_on_port(port: u16) -> Self { + let listener = TcpListener::bind(&format!("0.0.0.0:{}", port)).expect("Failed to bind to port"); + 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"); } diff --git a/examples/battleship/config.rs b/examples/battleship/config.rs index c0c2a57e..085c58ac 100644 --- a/examples/battleship/config.rs +++ b/examples/battleship/config.rs @@ -12,8 +12,9 @@ impl Config { pub const DESTROYER_COLOR: &'static str = "#238cf4"; pub const MISSED_COLOR: &'static str = "#33658a"; pub const BOMBED_COLOR: &'static str = "#f26419"; - pub const DESTROYED_COLOR: &'static str = "#000000"; - pub const TARGET_COLOR: &'static str = "#f26419"; + pub const DESTROYED_COLOR: &'static str = "#349c9e"; + pub const CROSSHAIR_COLOR: &'static str = "#f26419"; + pub const DISABLED_CROSSHAIR_COLOR: &'static str = "#000000"; pub const CELL_SIZE: f64 = 40.0; pub const SPACE_BETWEEN_GRIDS: f64 = 50.0; @@ -24,7 +25,7 @@ impl Config { pub const MISSED_CIRCLE_DIAMETER: f64 = 0.25 * Self::CELL_SIZE; pub const BOMBED_CIRCLE_DIAMETER: f64 = 0.75 * Self::CELL_SIZE; - pub const CROSSHAIR_SIZE: f64 = 0.4 * Self::CELL_SIZE; + pub const CROSSHAIR_SIZE: f64 = 0.2 * Self::CELL_SIZE; pub const CROSSHAIR_PEN_SIZE: f64 = 4.0; pub fn cell_color(cell: &Cell) -> Color { diff --git a/examples/battleship/game.rs b/examples/battleship/game.rs index fe4f1696..6a6cdff1 100644 --- a/examples/battleship/game.rs +++ b/examples/battleship/game.rs @@ -1,6 +1,5 @@ use turtle::{ event::{Key, PressedState}, - rand::random_range, Drawing, Event, Turtle, }; @@ -12,13 +11,13 @@ use crate::{ use std::f64::consts::PI; -enum Turn { +pub enum Turn { Me, Opponent, } impl Turn { - fn flip(&mut self) { + pub fn flip(&mut self) { match self { Turn::Me => *self = Turn::Opponent, Turn::Opponent => *self = Turn::Me, @@ -35,6 +34,12 @@ pub struct Game { pub enum Player<'a> { Server, Client(&'a str), + ServeOnPort(u16), +} + +enum CrosshairType { + LockTarget, + Disabled, } struct Crosshair<'a> { @@ -44,19 +49,25 @@ struct Crosshair<'a> { } impl<'a> Crosshair<'a> { - fn random_attackable_location(state: &BattleState) -> (u8, u8) { - loop { - let x = random_range(0, 9); - let y = random_range(0, 9); - if state.can_bomb(&(x, y)) { - return (x, y); - } + fn new(state: &'a BattleState, turtle: &'a mut Turtle, last_bombed_pos: Option<(u8, u8)>) -> Self { + let pos; + if let Some(bombed_pos) = last_bombed_pos { + pos = bombed_pos; + Self::draw_crosshair(pos, turtle, CrosshairType::Disabled); + } else { + pos = (4, 4); + Self::draw_crosshair(pos, turtle, CrosshairType::LockTarget); } + Self { pos, state, turtle } } - fn draw_crosshair(pos: (u8, u8), turtle: &mut Turtle) { + fn draw_crosshair(pos: (u8, u8), turtle: &mut Turtle, crosshair: CrosshairType) { let (x, y) = Config::ATTACK_GRID_TOP_LEFT; - turtle.set_pen_color(Config::TARGET_COLOR); + turtle.set_pen_color(if matches!(crosshair, CrosshairType::Disabled) { + Config::DISABLED_CROSSHAIR_COLOR + } else { + Config::CROSSHAIR_COLOR + }); turtle.set_pen_size(Config::CROSSHAIR_PEN_SIZE); let start = ( x + Config::CELL_SIZE * (0.5 + pos.1 as f64), @@ -77,64 +88,52 @@ impl<'a> Crosshair<'a> { turtle.set_pen_size(1.0); } - fn new(state: &'a BattleState, turtle: &'a mut Turtle) -> Self { - let pos = Self::random_attackable_location(state); - Self::draw_crosshair(pos, turtle); - Self { pos, state, turtle } + fn move_to(&mut self, pos: (u8, u8)) { + //remove crosshair by redrawing the cell + let cell = self.state.attack_grid().get(&self.pos); + Game::draw_cell(cell, Position::AttackGrid(self.pos), self.turtle); + + let crosshair = match self.state.can_bomb(&pos) { + true => CrosshairType::LockTarget, + false => CrosshairType::Disabled, + }; + Self::draw_crosshair(pos, self.turtle, crosshair); + self.pos = pos; } fn move_left(&mut self) { - // TODO: use inclusive range - let new_y = (0..self.pos.1).rev().find(|&y| self.state.can_bomb(&(self.pos.0, y))); - if let Some(y) = new_y { - let cell = self.state.attack_grid().get(&self.pos); - Game::draw_cell(cell, Position::AttackGrid(self.pos), &mut self.turtle); - - let new_pos = (self.pos.0, y); - Self::draw_crosshair(new_pos, self.turtle); - self.pos = new_pos; + if self.pos.1 > 0 { + self.move_to((self.pos.0, self.pos.1 - 1)); } } + fn move_right(&mut self) { - let new_y = (self.pos.1 + 1..10).find(|&y| self.state.can_bomb(&(self.pos.0, y))); - if let Some(y) = new_y { - let cell = self.state.attack_grid().get(&self.pos); - Game::draw_cell(cell, Position::AttackGrid(self.pos), &mut self.turtle); - - let new_pos = (self.pos.0, y); - Self::draw_crosshair(new_pos, self.turtle); - self.pos = new_pos; + if self.pos.1 < 9 { + self.move_to((self.pos.0, self.pos.1 + 1)); } } + fn move_up(&mut self) { - let new_x = (0..self.pos.0).rev().find(|&x| self.state.can_bomb(&(x, self.pos.1))); - if let Some(x) = new_x { - let cell = self.state.attack_grid().get(&self.pos); - Game::draw_cell(cell, Position::AttackGrid(self.pos), &mut self.turtle); - - let new_pos = (x, self.pos.1); - Self::draw_crosshair(new_pos, self.turtle); - self.pos = new_pos; + if self.pos.0 > 0 { + self.move_to((self.pos.0 - 1, self.pos.1)); } } + fn move_down(&mut self) { - let new_x = (self.pos.0 + 1..10).find(|&x| self.state.can_bomb(&(x, self.pos.1))); - if let Some(x) = new_x { - let cell = self.state.attack_grid().get(&self.pos); - Game::draw_cell(cell, Position::AttackGrid(self.pos), &mut self.turtle); - - let new_pos = (x, self.pos.1); - Self::draw_crosshair(new_pos, self.turtle); - self.pos = new_pos; + if self.pos.0 < 9 { + self.move_to((self.pos.0 + 1, self.pos.1)); } } - fn lock_target(&mut self) -> (u8, u8) { - let cell = self.state.attack_grid().get(&self.pos); - Game::draw_cell(cell, Position::AttackGrid(self.pos), &mut self.turtle); - return self.pos; + + fn try_bomb(&mut self) -> Option<(u8, u8)> { + if self.state.can_bomb(&self.pos) { + return Some(self.pos); + } + None } } +#[derive(Copy, Clone)] enum Position { ShipGrid((u8, u8)), AttackGrid((u8, u8)), @@ -155,17 +154,20 @@ impl Game { let channel = match player { Player::Server => Channel::server(), Player::Client(addr) => Channel::client(addr), + Player::ServeOnPort(port) => Channel::serve_on_port(port), }; let turn = match player { Player::Client(_) => Turn::Opponent, - Player::Server => Turn::Me, + _ => Turn::Me, }; Self { state, channel, turn } } - fn draw_cell(cell: Cell, pos: Position, turtle: &mut Turtle) { + fn draw_cell(cell: Cell, loc: Position, turtle: &mut Turtle) { fn draw_circle(turtle: &mut Turtle, diameter: f64) { + let pen_color = turtle.pen_color(); + turtle.set_pen_color("transparent"); turtle.set_heading(0.0); turtle.begin_fill(); for _ in 0..360 { @@ -173,6 +175,7 @@ impl Game { turtle.right(1.0); } turtle.end_fill(); + turtle.set_pen_color(pen_color); } fn draw_square(turtle: &mut Turtle, size: f64) { turtle.set_heading(0.0); @@ -183,15 +186,16 @@ impl Game { } turtle.end_fill(); } - let (x, y) = match pos { + let (x, y) = match loc { Position::ShipGrid(_) => Config::SHIP_GRID_TOP_LEFT, Position::AttackGrid(_) => Config::ATTACK_GRID_TOP_LEFT, }; - let pos = pos.get(); + let pos = loc.get(); match cell { Cell::Missed | Cell::Bombed => { + Self::draw_cell(Cell::Empty, loc, turtle); let diameter = if cell == Cell::Missed { Config::MISSED_CIRCLE_DIAMETER } else { @@ -229,33 +233,25 @@ impl Game { } } } - 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, drawing: &mut Drawing, turtle: &mut Turtle) -> (u8, u8) { - let mut crosshair = Crosshair::new(&self.state, turtle); + fn get_attack_location(&self, drawing: &mut Drawing, turtle: &mut Turtle, last_bombed_location: Option<(u8, u8)>) -> (u8, u8) { + let mut crosshair = Crosshair::new(&self.state, turtle, last_bombed_location); loop { while let Some(event) = drawing.poll_event() { use Key::{DownArrow, LeftArrow, Return, RightArrow, UpArrow}; - match event { - Event::Key(key, PressedState::Pressed) => match key { + if let Event::Key(key, PressedState::Pressed) = event { + match key { LeftArrow => crosshair.move_left(), RightArrow => crosshair.move_right(), UpArrow => crosshair.move_up(), DownArrow => crosshair.move_down(), Return => { - return crosshair.lock_target(); + if let Some(pos) = crosshair.try_bomb() { + return pos; + } } _ => {} - }, - _ => {} + } } } } @@ -264,6 +260,7 @@ impl Game { pub fn run(&mut self) { let mut drawing = Drawing::new(); let mut turtle = drawing.add_turtle(); + let mut last_bombed_location = None; turtle.hide(); turtle.set_speed("instant"); @@ -272,7 +269,8 @@ impl Game { loop { match self.turn { Turn::Me => { - let attack_location = self.get_attack_location(&mut drawing, &mut turtle); + let attack_location = self.get_attack_location(&mut drawing, &mut turtle, last_bombed_location); + last_bombed_location = Some(attack_location); self.channel.send_message(&Message::AttackCoordinates(attack_location)); match self.channel.receive_message() { Message::AttackResult(outcome) => match outcome { @@ -332,5 +330,7 @@ impl Game { (_, _) => continue, } } + + drawing.destroy(); } } diff --git a/examples/battleship/main.rs b/examples/battleship/main.rs index 185f386a..1e5f2a94 100644 --- a/examples/battleship/main.rs +++ b/examples/battleship/main.rs @@ -3,20 +3,56 @@ compile_error!("This example relies on unstable features. Run with `--features unstable`"); mod battlestate; +mod bot; mod channel; mod config; mod game; mod ship; +use std::{net::TcpListener, thread, time::Duration}; + use game::{Game, Player}; +use crate::bot::Bot; + +fn get_available_tcp_port() -> u16 { + for port in 49152..=65535 { + if TcpListener::bind(&format!("127.0.0.1:{}", port)).is_ok() { + return port; + } + } + panic!("No ports available!"); +} + fn main() { let args: Vec = std::env::args().collect(); - let player = match args.len() { + let config = match args.len() { 0 => unreachable!(), - 1 => Player::Server, - _ => Player::Client(&args[1]), + 1 => "", + _ => &args[1], }; - let mut game = Game::init(player); - game.run(); + + match config { + "" => { + let mut game = Game::init(Player::Server); + game.run(); + } + "bot" => { + // May fail due to TOCTOU + let port = get_available_tcp_port(); + let handle = thread::spawn(move || { + // delay to let game server bind to port + thread::sleep(Duration::from_millis(10)); + let mut bot = Bot::new(port); + bot.play(); + }); + let mut game = Game::init(Player::ServeOnPort(port)); + game.run(); + handle.join().unwrap(); + } + addr => { + let mut game = Game::init(Player::Client(addr)); + game.run(); + } + } } From 4a36f02f9354ea80c03c60a8dd324d42173c227e Mon Sep 17 00:00:00 2001 From: Sathwik Matsa Date: Sat, 24 Jul 2021 17:54:11 +0530 Subject: [PATCH 10/16] Refactor --- examples/battleship/battlestate.rs | 84 ++++---------- examples/battleship/bot.rs | 8 +- examples/battleship/channel.rs | 11 +- examples/battleship/config.rs | 11 +- examples/battleship/crosshair.rs | 102 +++++++++++++++++ examples/battleship/game.rs | 173 +++++------------------------ examples/battleship/grid.rs | 53 +++++++++ examples/battleship/main.rs | 23 ++-- 8 files changed, 241 insertions(+), 224 deletions(-) create mode 100644 examples/battleship/crosshair.rs create mode 100644 examples/battleship/grid.rs diff --git a/examples/battleship/battlestate.rs b/examples/battleship/battlestate.rs index 20dab189..51a4812a 100644 --- a/examples/battleship/battlestate.rs +++ b/examples/battleship/battlestate.rs @@ -1,38 +1,11 @@ -use super::ship::*; use serde::{Deserialize, Serialize}; -use std::{convert::TryInto, fmt::Display, ops::Deref}; +use std::{convert::TryInto, fmt::Display}; use turtle::rand::{choose, random_range}; -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum Cell { - // ship grid - Carrier = 0, - Battleship = 1, - Cruiser = 2, - Submarine = 3, - Destroyer = 4, - Empty, - - // attack grid - Unattacked, - Missed, - - // common - Bombed, - Destroyed, -} - -impl ShipKind { - 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, - } - } -} +use crate::{ + grid::{Cell, Grid}, + ship::*, +}; #[derive(Copy, Clone, Debug, PartialEq, Deserialize, Serialize)] pub enum AttackOutcome { @@ -41,25 +14,18 @@ pub enum AttackOutcome { Destroyed(Ship), } -#[derive(Debug, Copy, Clone)] -pub struct Grid([[Cell; 10]; 10]); - -impl Deref for Grid { - type Target = [[Cell; 10]; 10]; - fn deref(&self) -> &Self::Target { - &self.0 - } +#[derive(Copy, Clone)] +pub enum Position { + ShipGrid((u8, u8)), + AttackGrid((u8, u8)), } -impl Grid { - pub fn get(&self, pos: &(u8, u8)) -> Cell { - self.0[pos.0 as usize][pos.1 as usize] - } - pub fn get_mut(&mut self, pos: &(u8, u8)) -> &mut Cell { - &mut self.0[pos.0 as usize][pos.1 as usize] - } - pub fn count(&mut self, cell: &Cell) -> usize { - self.iter().flatten().filter(|&c| c == cell).count() +impl Position { + pub fn get(self) -> (u8, u8) { + match self { + Self::ShipGrid(p) => p, + Self::AttackGrid(p) => p, + } } } @@ -96,9 +62,7 @@ impl Display for BattleState { impl BattleState { #[allow(dead_code)] fn custom(ships: [Ship; 5]) -> Self { - let mut ship_grid = Grid { - 0: [[Cell::Empty; 10]; 10], - }; + 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(); @@ -107,9 +71,7 @@ impl BattleState { Self { ships, ship_grid, - attack_grid: Grid { - 0: [[Cell::Unattacked; 10]; 10], - }, + attack_grid: Grid::new(Cell::Unattacked), destroyed_rival_ships: 0, ships_lost: 0, } @@ -119,9 +81,7 @@ impl BattleState { Self { ships, ship_grid, - attack_grid: Grid { - 0: [[Cell::Unattacked; 10]; 10], - }, + attack_grid: Grid::new(Cell::Unattacked), destroyed_rival_ships: 0, ships_lost: 0, } @@ -131,8 +91,8 @@ impl BattleState { match attacked_cell { Cell::Empty => AttackOutcome::Miss, Cell::Carrier | Cell::Battleship | Cell::Cruiser | Cell::Submarine | Cell::Destroyer => { - let count = self.ship_grid.count(&attacked_cell); - match count { + let standing_ship_parts = self.ship_grid.count(&attacked_cell); + match standing_ship_parts { 1 => { let lost_ship = self.ships[attacked_cell as usize]; lost_ship @@ -176,9 +136,7 @@ impl BattleState { ShipKind::Submarine, ShipKind::Destroyer, ]; - let mut grid = Grid { - 0: [[Cell::Empty; 10]; 10], - }; + let mut grid = Grid::new(Cell::Empty); let mut ships = Vec::new(); for kind in ship_types { diff --git a/examples/battleship/bot.rs b/examples/battleship/bot.rs index 8f549003..93378240 100644 --- a/examples/battleship/bot.rs +++ b/examples/battleship/bot.rs @@ -1,9 +1,10 @@ use turtle::rand::random_range; use crate::{ - battlestate::{AttackOutcome, BattleState, Cell}, + battlestate::{AttackOutcome, BattleState}, channel::{Channel, Message}, game::Turn, + grid::Cell, }; pub struct Bot { @@ -41,15 +42,16 @@ impl Bot { .filter(|(_, &cell)| cell == Cell::Bombed) .map(|(loc, _)| ((loc as f32 / 10.0).floor() as i32, loc as i32 % 10)); + // Check neighbours of bombed (successfully hit) locations and return if attackable for loc in bombed_locations { - let bombable = [(-1, 0), (1, 0), (0, -1), (0, 1)] + 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) = bombable { + if let Some(pos) = attackable { return pos; } } diff --git a/examples/battleship/channel.rs b/examples/battleship/channel.rs index 67979eaf..f9b1ebdf 100644 --- a/examples/battleship/channel.rs +++ b/examples/battleship/channel.rs @@ -9,6 +9,12 @@ pub enum Message { AttackResult(AttackOutcome), } +pub enum ChannelType<'a> { + Server, + Client(&'a str), + ServeOnPort(u16), +} + pub struct Channel { stream: TcpStream, } @@ -22,7 +28,10 @@ impl Channel { pub fn server() -> Self { let listener = TcpListener::bind("0.0.0.0:0").expect("Failed to bind to port"); - println!("Listening on: {:?}, Waiting for connection..", listener.local_addr()); + println!( + "Listening on port: {}, Waiting for connection..", + listener.local_addr().unwrap().port() + ); let (stream, _) = listener.accept().expect("Couldn't connect to the client"); Self { stream } } diff --git a/examples/battleship/config.rs b/examples/battleship/config.rs index 085c58ac..40172008 100644 --- a/examples/battleship/config.rs +++ b/examples/battleship/config.rs @@ -1,20 +1,19 @@ -use super::battlestate::Cell; use turtle::Color; + +use crate::grid::Cell; pub struct Config {} impl Config { pub const EMPTY_COLOR: &'static str = "#55dde0"; pub const UNATTACKED_COLOR: &'static str = "#55dde0"; - pub const CARRIER_COLOR: &'static str = "#f6ae2d"; + pub const CARRIER_COLOR: &'static str = "#fde74c"; pub const BATTLESHIP_COLOR: &'static str = "#f48923"; pub const CRUISER_COLOR: &'static str = "#947757"; - pub const SUBMARINE_COLOR: &'static str = "#2f4858"; + pub const SUBMARINE_COLOR: &'static str = "#9bc53d"; pub const DESTROYER_COLOR: &'static str = "#238cf4"; pub const MISSED_COLOR: &'static str = "#33658a"; pub const BOMBED_COLOR: &'static str = "#f26419"; pub const DESTROYED_COLOR: &'static str = "#349c9e"; - pub const CROSSHAIR_COLOR: &'static str = "#f26419"; - pub const DISABLED_CROSSHAIR_COLOR: &'static str = "#000000"; pub const CELL_SIZE: f64 = 40.0; pub const SPACE_BETWEEN_GRIDS: f64 = 50.0; @@ -27,6 +26,8 @@ impl Config { pub const CROSSHAIR_SIZE: f64 = 0.2 * Self::CELL_SIZE; pub const CROSSHAIR_PEN_SIZE: f64 = 4.0; + pub const CROSSHAIR_COLOR: &'static str = "#f26419"; + pub const DISABLED_CROSSHAIR_COLOR: &'static str = "#000000"; pub fn cell_color(cell: &Cell) -> Color { match cell { diff --git a/examples/battleship/crosshair.rs b/examples/battleship/crosshair.rs new file mode 100644 index 00000000..250d87f4 --- /dev/null +++ b/examples/battleship/crosshair.rs @@ -0,0 +1,102 @@ +use crate::{ + battlestate::{BattleState, Position}, + config::Config, + game::Game, +}; +use turtle::Turtle; + +pub enum CrosshairType { + LockTarget, + Disabled, +} + +pub struct Crosshair<'a> { + pos: (u8, u8), + state: &'a BattleState, + turtle: &'a mut Turtle, +} + +impl<'a> Crosshair<'a> { + pub fn new(state: &'a BattleState, turtle: &'a mut Turtle, last_bombed_pos: Option<(u8, u8)>) -> Self { + let pos; + if let Some(bombed_pos) = last_bombed_pos { + pos = bombed_pos; + Self::draw_crosshair(pos, turtle, CrosshairType::Disabled); + } else { + pos = (4, 4); + Self::draw_crosshair(pos, turtle, CrosshairType::LockTarget); + } + Self { pos, state, turtle } + } + + fn draw_crosshair(pos: (u8, u8), turtle: &mut Turtle, crosshair: CrosshairType) { + let (x, y) = Config::ATTACK_GRID_TOP_LEFT; + turtle.set_pen_color(if matches!(crosshair, CrosshairType::Disabled) { + Config::DISABLED_CROSSHAIR_COLOR + } else { + Config::CROSSHAIR_COLOR + }); + turtle.set_pen_size(Config::CROSSHAIR_PEN_SIZE); + let start = ( + x + Config::CELL_SIZE * (0.5 + pos.1 as f64), + y - Config::CELL_SIZE * (0.5 + pos.0 as f64), + ); + turtle.pen_up(); + turtle.go_to(start); + turtle.pen_down(); + turtle.set_heading(0.0); + for _ in 0..4 { + turtle.forward(Config::CROSSHAIR_SIZE); + turtle.pen_up(); + turtle.backward(Config::CROSSHAIR_SIZE); + turtle.pen_down(); + turtle.right(90.0); + } + turtle.set_pen_color("black"); + turtle.set_pen_size(1.0); + } + + fn move_to(&mut self, pos: (u8, u8), game: &Game) { + //remove crosshair by redrawing the cell + let cell = self.state.attack_grid().get(&self.pos); + game.draw_cell(cell, Position::AttackGrid(self.pos), self.turtle); + + let crosshair = match self.state.can_bomb(&pos) { + true => CrosshairType::LockTarget, + false => CrosshairType::Disabled, + }; + Self::draw_crosshair(pos, self.turtle, crosshair); + self.pos = pos; + } + + pub fn move_left(&mut self, game: &Game) { + if self.pos.1 > 0 { + self.move_to((self.pos.0, self.pos.1 - 1), game); + } + } + + pub fn move_right(&mut self, game: &Game) { + if self.pos.1 < 9 { + self.move_to((self.pos.0, self.pos.1 + 1), game); + } + } + + pub fn move_up(&mut self, game: &Game) { + if self.pos.0 > 0 { + self.move_to((self.pos.0 - 1, self.pos.1), game); + } + } + + pub fn move_down(&mut self, game: &Game) { + if self.pos.0 < 9 { + self.move_to((self.pos.0 + 1, self.pos.1), game); + } + } + + pub fn try_bomb(&mut self) -> Option<(u8, u8)> { + if self.state.can_bomb(&self.pos) { + return Some(self.pos); + } + None + } +} diff --git a/examples/battleship/game.rs b/examples/battleship/game.rs index 6a6cdff1..b3081046 100644 --- a/examples/battleship/game.rs +++ b/examples/battleship/game.rs @@ -4,12 +4,14 @@ use turtle::{ }; use crate::{ - battlestate::{AttackOutcome, BattleState, Cell}, - channel::{Channel, Message}, + battlestate::{AttackOutcome, BattleState, Position}, + channel::{Channel, ChannelType, Message}, config::Config, + crosshair::Crosshair, + grid::Cell, }; -use std::f64::consts::PI; +use std::{f64::consts::PI, thread, time::Duration}; pub enum Turn { Me, @@ -31,140 +33,23 @@ pub struct Game { turn: Turn, } -pub enum Player<'a> { - Server, - Client(&'a str), - ServeOnPort(u16), -} - -enum CrosshairType { - LockTarget, - Disabled, -} - -struct Crosshair<'a> { - pos: (u8, u8), - state: &'a BattleState, - turtle: &'a mut Turtle, -} - -impl<'a> Crosshair<'a> { - fn new(state: &'a BattleState, turtle: &'a mut Turtle, last_bombed_pos: Option<(u8, u8)>) -> Self { - let pos; - if let Some(bombed_pos) = last_bombed_pos { - pos = bombed_pos; - Self::draw_crosshair(pos, turtle, CrosshairType::Disabled); - } else { - pos = (4, 4); - Self::draw_crosshair(pos, turtle, CrosshairType::LockTarget); - } - Self { pos, state, turtle } - } - - fn draw_crosshair(pos: (u8, u8), turtle: &mut Turtle, crosshair: CrosshairType) { - let (x, y) = Config::ATTACK_GRID_TOP_LEFT; - turtle.set_pen_color(if matches!(crosshair, CrosshairType::Disabled) { - Config::DISABLED_CROSSHAIR_COLOR - } else { - Config::CROSSHAIR_COLOR - }); - turtle.set_pen_size(Config::CROSSHAIR_PEN_SIZE); - let start = ( - x + Config::CELL_SIZE * (0.5 + pos.1 as f64), - y - Config::CELL_SIZE * (0.5 + pos.0 as f64), - ); - turtle.pen_up(); - turtle.go_to(start); - turtle.pen_down(); - turtle.set_heading(0.0); - for _ in 0..4 { - turtle.forward(Config::CROSSHAIR_SIZE); - turtle.pen_up(); - turtle.backward(Config::CROSSHAIR_SIZE); - turtle.pen_down(); - turtle.right(90.0); - } - turtle.set_pen_color("black"); - turtle.set_pen_size(1.0); - } - - fn move_to(&mut self, pos: (u8, u8)) { - //remove crosshair by redrawing the cell - let cell = self.state.attack_grid().get(&self.pos); - Game::draw_cell(cell, Position::AttackGrid(self.pos), self.turtle); - - let crosshair = match self.state.can_bomb(&pos) { - true => CrosshairType::LockTarget, - false => CrosshairType::Disabled, - }; - Self::draw_crosshair(pos, self.turtle, crosshair); - self.pos = pos; - } - - fn move_left(&mut self) { - if self.pos.1 > 0 { - self.move_to((self.pos.0, self.pos.1 - 1)); - } - } - - fn move_right(&mut self) { - if self.pos.1 < 9 { - self.move_to((self.pos.0, self.pos.1 + 1)); - } - } - - fn move_up(&mut self) { - if self.pos.0 > 0 { - self.move_to((self.pos.0 - 1, self.pos.1)); - } - } - - fn move_down(&mut self) { - if self.pos.0 < 9 { - self.move_to((self.pos.0 + 1, self.pos.1)); - } - } - - fn try_bomb(&mut self) -> Option<(u8, u8)> { - if self.state.can_bomb(&self.pos) { - return Some(self.pos); - } - None - } -} - -#[derive(Copy, Clone)] -enum Position { - ShipGrid((u8, u8)), - AttackGrid((u8, u8)), -} - -impl Position { - fn get(self) -> (u8, u8) { - match self { - Self::ShipGrid(p) => p, - Self::AttackGrid(p) => p, - } - } -} - impl Game { - pub fn init(player: Player) -> Self { + pub fn init(channel_type: ChannelType) -> Self { let state = BattleState::new(); - let channel = match player { - Player::Server => Channel::server(), - Player::Client(addr) => Channel::client(addr), - Player::ServeOnPort(port) => Channel::serve_on_port(port), + let channel = match channel_type { + ChannelType::Server => Channel::server(), + ChannelType::Client(addr) => Channel::client(addr), + ChannelType::ServeOnPort(port) => Channel::serve_on_port(port), }; - let turn = match player { - Player::Client(_) => Turn::Opponent, + let turn = match channel_type { + ChannelType::Client(_) => Turn::Opponent, _ => Turn::Me, }; Self { state, channel, turn } } - fn draw_cell(cell: Cell, loc: Position, turtle: &mut Turtle) { + pub fn draw_cell(&self, cell: Cell, loc: Position, turtle: &mut Turtle) { fn draw_circle(turtle: &mut Turtle, diameter: f64) { let pen_color = turtle.pen_color(); turtle.set_pen_color("transparent"); @@ -195,7 +80,7 @@ impl Game { match cell { Cell::Missed | Cell::Bombed => { - Self::draw_cell(Cell::Empty, loc, turtle); + self.draw_cell(Cell::Empty, loc, turtle); let diameter = if cell == Cell::Missed { Config::MISSED_CIRCLE_DIAMETER } else { @@ -228,8 +113,8 @@ impl Game { for x in 0..10 { for y in 0..10 { - Self::draw_cell(ship_grid.get(&(x, y)), Position::ShipGrid((x, y)), turtle); - Self::draw_cell(attack_grid.get(&(x, y)), Position::AttackGrid((x, y)), turtle); + self.draw_cell(ship_grid.get(&(x, y)), Position::ShipGrid((x, y)), turtle); + self.draw_cell(attack_grid.get(&(x, y)), Position::AttackGrid((x, y)), turtle); } } } @@ -241,16 +126,16 @@ impl Game { use Key::{DownArrow, LeftArrow, Return, RightArrow, UpArrow}; if let Event::Key(key, PressedState::Pressed) = event { match key { - LeftArrow => crosshair.move_left(), - RightArrow => crosshair.move_right(), - UpArrow => crosshair.move_up(), - DownArrow => crosshair.move_down(), + LeftArrow => crosshair.move_left(self), + RightArrow => crosshair.move_right(self), + UpArrow => crosshair.move_up(self), + DownArrow => crosshair.move_down(self), Return => { if let Some(pos) = crosshair.try_bomb() { return pos; } } - _ => {} + _ => (), } } } @@ -276,18 +161,18 @@ impl Game { Message::AttackResult(outcome) => match outcome { AttackOutcome::Miss => { self.state.set_attack_outcome(&attack_location, Cell::Missed); - Self::draw_cell(Cell::Missed, Position::AttackGrid(attack_location), &mut turtle); + self.draw_cell(Cell::Missed, Position::AttackGrid(attack_location), &mut turtle); self.turn.flip(); } AttackOutcome::Hit => { self.state.set_attack_outcome(&attack_location, Cell::Bombed); - Self::draw_cell(Cell::Bombed, Position::AttackGrid(attack_location), &mut turtle); + self.draw_cell(Cell::Bombed, Position::AttackGrid(attack_location), &mut turtle); } AttackOutcome::Destroyed(ship) => { self.state.set_destroyed_ship(&ship); ship.coordinates() .into_iter() - .for_each(|pos| Self::draw_cell(Cell::Destroyed, Position::AttackGrid(pos), &mut turtle)); + .for_each(|pos| self.draw_cell(Cell::Destroyed, Position::AttackGrid(pos), &mut turtle)); self.turn.flip(); } }, @@ -300,16 +185,16 @@ impl Game { self.channel.send_message(&Message::AttackResult(outcome)); match outcome { AttackOutcome::Miss => { - Self::draw_cell(Cell::Missed, Position::ShipGrid(p), &mut turtle); + self.draw_cell(Cell::Missed, Position::ShipGrid(p), &mut turtle); self.turn.flip(); } AttackOutcome::Hit => { - Self::draw_cell(Cell::Bombed, Position::ShipGrid(p), &mut turtle); + self.draw_cell(Cell::Bombed, Position::ShipGrid(p), &mut turtle); } AttackOutcome::Destroyed(ship) => { ship.coordinates() .into_iter() - .for_each(|pos| Self::draw_cell(Cell::Destroyed, Position::ShipGrid(pos), &mut turtle)); + .for_each(|pos| self.draw_cell(Cell::Destroyed, Position::ShipGrid(pos), &mut turtle)); self.turn.flip(); } } @@ -320,11 +205,11 @@ impl Game { match (self.state.ships_lost, self.state.destroyed_rival_ships) { (5, _) => { - println!("Nice try."); + println!("NT"); break; } (_, 5) => { - println!("GG!"); + println!("GG"); break; } (_, _) => continue, diff --git a/examples/battleship/grid.rs b/examples/battleship/grid.rs new file mode 100644 index 00000000..0ecb7ed1 --- /dev/null +++ b/examples/battleship/grid.rs @@ -0,0 +1,53 @@ +use crate::ship::ShipKind; +use std::ops::Deref; + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum Cell { + Carrier = 0, + Battleship = 1, + Cruiser = 2, + Submarine = 3, + Destroyer = 4, + Empty, + Unattacked, + Missed, + Bombed, + Destroyed, +} + +impl ShipKind { + 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, + } + } +} + +#[derive(Debug, Copy, Clone)] +pub struct Grid([[Cell; 10]; 10]); + +impl Deref for Grid { + type Target = [[Cell; 10]; 10]; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Grid { + pub fn new(cell: Cell) -> Self { + Self { 0: [[cell; 10]; 10] } + } + pub fn get(&self, pos: &(u8, u8)) -> Cell { + self.0[pos.0 as usize][pos.1 as usize] + } + pub fn get_mut(&mut self, pos: &(u8, u8)) -> &mut Cell { + &mut self.0[pos.0 as usize][pos.1 as usize] + } + pub fn count(&mut self, cell: &Cell) -> usize { + self.iter().flatten().filter(|&c| c == cell).count() + } +} diff --git a/examples/battleship/main.rs b/examples/battleship/main.rs index 1e5f2a94..08460cfc 100644 --- a/examples/battleship/main.rs +++ b/examples/battleship/main.rs @@ -1,3 +1,9 @@ +//! Battleship board Game +//! Usage: +//! $> ./battleship # No arguments - outputs a TCP port waiting for the opponent to connect. +//! $> ./battleship # - connects to a server with the specified ip port +//! $> ./battleship bot # play with computer (single player). + // To run, use the command: cargo run --features unstable --example battleship #[cfg(all(not(feature = "unstable")))] compile_error!("This example relies on unstable features. Run with `--features unstable`"); @@ -6,16 +12,17 @@ mod battlestate; mod bot; mod channel; mod config; +mod crosshair; mod game; +mod grid; mod ship; +use bot::Bot; +use channel::ChannelType; +use game::Game; use std::{net::TcpListener, thread, time::Duration}; -use game::{Game, Player}; - -use crate::bot::Bot; - -fn get_available_tcp_port() -> u16 { +pub fn get_available_tcp_port() -> u16 { for port in 49152..=65535 { if TcpListener::bind(&format!("127.0.0.1:{}", port)).is_ok() { return port; @@ -34,7 +41,7 @@ fn main() { match config { "" => { - let mut game = Game::init(Player::Server); + let mut game = Game::init(ChannelType::Server); game.run(); } "bot" => { @@ -46,12 +53,12 @@ fn main() { let mut bot = Bot::new(port); bot.play(); }); - let mut game = Game::init(Player::ServeOnPort(port)); + let mut game = Game::init(ChannelType::ServeOnPort(port)); game.run(); handle.join().unwrap(); } addr => { - let mut game = Game::init(Player::Client(addr)); + let mut game = Game::init(ChannelType::Client(addr)); game.run(); } } From dcd8c532f1d4dc93abbf15b2061aa9568675ff5e Mon Sep 17 00:00:00 2001 From: Sathwik Matsa Date: Sat, 24 Jul 2021 20:00:30 +0530 Subject: [PATCH 11/16] reduce CPU usage --- examples/battleship/game.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/battleship/game.rs b/examples/battleship/game.rs index b3081046..32bfc053 100644 --- a/examples/battleship/game.rs +++ b/examples/battleship/game.rs @@ -139,6 +139,8 @@ impl Game { } } } + // reduce CPU usage + thread::sleep(Duration::from_millis(16)); } } From dcdf18b36e30390f3ee3046c6ae831c9c6099801 Mon Sep 17 00:00:00 2001 From: Sathwik Matsa Date: Sat, 24 Jul 2021 20:08:20 +0530 Subject: [PATCH 12/16] revert changes in square.rs --- examples/square.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/examples/square.rs b/examples/square.rs index 3d6648df..13494a9c 100644 --- a/examples/square.rs +++ b/examples/square.rs @@ -1,17 +1,10 @@ -use turtle::*; +use turtle::Turtle; fn main() { let mut turtle = Turtle::new(); - turtle.pen_up(); - turtle.go_to((-100.0, -100.0)); - turtle.pen_down(); - turtle.begin_fill(); - turtle.set_pen_color("green"); - turtle.set_fill_color("green"); for _ in 0..4 { turtle.forward(200.0); turtle.right(90.0); } - turtle.end_fill(); } From 3a53c82e186d6be4cfa3d8f6363e98c392fa91ef Mon Sep 17 00:00:00 2001 From: Sathwik Matsa Date: Tue, 27 Jul 2021 17:39:07 +0530 Subject: [PATCH 13/16] Add documentation, refactor --- examples/battleship/battlestate.rs | 69 ++++++------ examples/battleship/bot.rs | 37 ++++--- examples/battleship/channel.rs | 17 ++- examples/battleship/config.rs | 4 +- examples/battleship/crosshair.rs | 16 ++- examples/battleship/game.rs | 162 ++++++++++++++++------------- examples/battleship/grid.rs | 3 + examples/battleship/main.rs | 73 +++++++------ examples/battleship/ship.rs | 27 +++-- examples/battleship/utils.rs | 56 ++++++++++ 10 files changed, 287 insertions(+), 177 deletions(-) create mode 100644 examples/battleship/utils.rs diff --git a/examples/battleship/battlestate.rs b/examples/battleship/battlestate.rs index 51a4812a..c1b7ead5 100644 --- a/examples/battleship/battlestate.rs +++ b/examples/battleship/battlestate.rs @@ -94,6 +94,8 @@ impl BattleState { 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() @@ -118,15 +120,17 @@ impl BattleState { _ => unreachable!(), } } - pub fn set_attack_outcome(&mut self, pos: &(u8, u8), cell: Cell) { - *self.attack_grid.get_mut(pos) = cell; - } - pub fn set_destroyed_ship(&mut self, ship: &Ship) { - ship.coordinates() - .into_iter() - .for_each(|pos| *self.attack_grid.get_mut(&pos) = Cell::Destroyed); - - self.destroyed_rival_ships += 1; + 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 = [ @@ -139,31 +143,31 @@ impl BattleState { 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_coords = (0..kind.size()) - .map(|i| match orient { - Orientation::Horizontal => (x + i, y), - Orientation::Veritcal => (x, y + i), - }) - .collect::>(); + let ship = Ship::new(kind, (x, y), orient); let no_overlap = ships .iter() - .all(|ship: &Ship| ship_coords.iter().all(|pos| !ship.is_located_over(pos))); - let within_board = ship_coords.iter().all(|pos| matches!(pos.0, 0..=9) && matches!(pos.1, 0..=9)); + .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 { - let ship = Ship::new( - kind, - ShipPosition::new(ship_coords.first().copied().unwrap(), ship_coords.last().copied().unwrap()), - ); ships.push(ship); - ship_coords.iter().for_each(|pos| { + ship.coordinates().iter().for_each(|pos| { *grid.get_mut(pos) = kind.to_cell(); }); break; @@ -237,7 +241,7 @@ mod test { // 9 . . . . . . . . . . 9 . . . . . . . . . . let mut state = BattleState::custom(ships); // turn 1: player attacks (2, 2) - misses - state.set_attack_outcome(&(2, 2), Cell::Missed); + 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)); @@ -250,16 +254,19 @@ mod test { 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), Cell::Bombed); + 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_destroyed_ship(&Ship { - kind: ShipKind::Destroyer, - position: ShipPosition { - top_left: (6, 2), - bottom_right: (7, 2), - }, - }); + state.set_attack_outcome( + &(6, 2), + AttackOutcome::Destroyed(Ship { + kind: ShipKind::Destroyer, + position: ShipPosition { + top_left: (6, 2), + bottom_right: (7, 2), + }, + }), + ); 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); diff --git a/examples/battleship/bot.rs b/examples/battleship/bot.rs index 93378240..5bf40c29 100644 --- a/examples/battleship/bot.rs +++ b/examples/battleship/bot.rs @@ -1,3 +1,5 @@ +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use turtle::rand::random_range; use crate::{ @@ -16,7 +18,7 @@ pub struct Bot { impl Bot { pub fn new(port: u16) -> Self { Self { - channel: Channel::client(&format!("127.0.0.1:{}", port)), + channel: Channel::client(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port)), state: BattleState::new(), turn: Turn::Opponent, } @@ -33,6 +35,7 @@ impl Bot { } 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() @@ -42,7 +45,8 @@ impl Bot { .filter(|(_, &cell)| cell == Cell::Bombed) .map(|(loc, _)| ((loc as f32 / 10.0).floor() as i32, loc as i32 % 10)); - // Check neighbours of bombed (successfully hit) locations and return if attackable + // 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() @@ -55,9 +59,11 @@ impl Bot { 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 { @@ -65,19 +71,15 @@ impl Bot { let attack_location = self.get_attack_location(); self.channel.send_message(&Message::AttackCoordinates(attack_location)); match self.channel.receive_message() { - Message::AttackResult(outcome) => match outcome { - AttackOutcome::Miss => { - self.state.set_attack_outcome(&attack_location, Cell::Missed); - self.turn.flip(); - } - AttackOutcome::Hit => { - self.state.set_attack_outcome(&attack_location, Cell::Bombed); - } - AttackOutcome::Destroyed(ship) => { - self.state.set_destroyed_ship(&ship); - self.turn.flip(); + 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."), } } @@ -86,13 +88,10 @@ impl Bot { let outcome = self.state.incoming_attack(&p); self.channel.send_message(&Message::AttackResult(outcome)); match outcome { - AttackOutcome::Miss => { - self.turn.flip(); - } - AttackOutcome::Hit => {} - AttackOutcome::Destroyed(_) => { + AttackOutcome::Miss | AttackOutcome::Destroyed(_) => { self.turn.flip(); } + AttackOutcome::Hit => (), } } _ => panic!("Expected Message of AttackCoordinates from Opponent"), diff --git a/examples/battleship/channel.rs b/examples/battleship/channel.rs index f9b1ebdf..673bf9c9 100644 --- a/examples/battleship/channel.rs +++ b/examples/battleship/channel.rs @@ -1,4 +1,4 @@ -use std::net::{TcpListener, TcpStream}; +use std::net::{SocketAddr, TcpListener, TcpStream}; use crate::battlestate::AttackOutcome; use serde::{Deserialize, Serialize}; @@ -9,10 +9,10 @@ pub enum Message { AttackResult(AttackOutcome), } -pub enum ChannelType<'a> { +pub enum ChannelType { Server, - Client(&'a str), - ServeOnPort(u16), + Client(SocketAddr), + UseListener(TcpListener), } pub struct Channel { @@ -20,24 +20,23 @@ pub struct Channel { } impl Channel { - pub fn client(ip_port: &str) -> Self { + pub fn client(socket_addr: SocketAddr) -> Self { Self { - stream: TcpStream::connect(ip_port).expect("Couldn't connect to the server"), + 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..", + "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_on_port(port: u16) -> Self { - let listener = TcpListener::bind(&format!("0.0.0.0:{}", port)).expect("Failed to bind to port"); + pub fn serve_using_listener(listener: TcpListener) -> Self { let (stream, _) = listener.accept().expect("Couldn't connect to the client"); Self { stream } } diff --git a/examples/battleship/config.rs b/examples/battleship/config.rs index 40172008..03a94d9c 100644 --- a/examples/battleship/config.rs +++ b/examples/battleship/config.rs @@ -21,8 +21,8 @@ impl Config { pub const SHIP_GRID_TOP_LEFT: (f64, f64) = (-Self::SPACE_BETWEEN_GRIDS / 2.0 - 10.0 * Self::CELL_SIZE, 5.0 * Self::CELL_SIZE); pub const ATTACK_GRID_TOP_LEFT: (f64, f64) = (Self::SPACE_BETWEEN_GRIDS / 2.0, 5.0 * Self::CELL_SIZE); - pub const MISSED_CIRCLE_DIAMETER: f64 = 0.25 * Self::CELL_SIZE; - pub const BOMBED_CIRCLE_DIAMETER: f64 = 0.75 * Self::CELL_SIZE; + pub const MISSED_CIRCLE_RADIUS: f64 = 0.25 * Self::CELL_SIZE * 0.5; + pub const BOMBED_CIRCLE_RADIUS: f64 = 0.75 * Self::CELL_SIZE * 0.5; pub const CROSSHAIR_SIZE: f64 = 0.2 * Self::CELL_SIZE; pub const CROSSHAIR_PEN_SIZE: f64 = 4.0; diff --git a/examples/battleship/crosshair.rs b/examples/battleship/crosshair.rs index 250d87f4..3c5d8efe 100644 --- a/examples/battleship/crosshair.rs +++ b/examples/battleship/crosshair.rs @@ -17,11 +17,14 @@ pub struct Crosshair<'a> { } impl<'a> Crosshair<'a> { - pub fn new(state: &'a BattleState, turtle: &'a mut Turtle, last_bombed_pos: Option<(u8, u8)>) -> Self { + pub fn new(state: &'a BattleState, turtle: &'a mut Turtle, last_attacked_pos: Option<(u8, u8)>) -> Self { let pos; - if let Some(bombed_pos) = last_bombed_pos { - pos = bombed_pos; + // Draw crosshair in disabled mode on last attacked position + if let Some(attacked_pos) = last_attacked_pos { + pos = attacked_pos; Self::draw_crosshair(pos, turtle, CrosshairType::Disabled); + // There's no last attacked position -> player's first chance + // Draw crosshair in abled mode at the center. } else { pos = (4, 4); Self::draw_crosshair(pos, turtle, CrosshairType::LockTarget); @@ -57,14 +60,17 @@ impl<'a> Crosshair<'a> { } fn move_to(&mut self, pos: (u8, u8), game: &Game) { - //remove crosshair by redrawing the cell + // Remove crosshair on previous pos by redrawing the Cell at that pos. let cell = self.state.attack_grid().get(&self.pos); game.draw_cell(cell, Position::AttackGrid(self.pos), self.turtle); + // Set Crosshair in disabled or abled mode based on new pos let crosshair = match self.state.can_bomb(&pos) { true => CrosshairType::LockTarget, false => CrosshairType::Disabled, }; + + // Draw Crosshair at new pos Self::draw_crosshair(pos, self.turtle, crosshair); self.pos = pos; } @@ -93,6 +99,8 @@ impl<'a> Crosshair<'a> { } } + // Retuns Some(pos) if the crosshair pos is attackable + // None otherwise pub fn try_bomb(&mut self) -> Option<(u8, u8)> { if self.state.can_bomb(&self.pos) { return Some(self.pos); diff --git a/examples/battleship/game.rs b/examples/battleship/game.rs index 32bfc053..32ce5078 100644 --- a/examples/battleship/game.rs +++ b/examples/battleship/game.rs @@ -11,7 +11,7 @@ use crate::{ grid::Cell, }; -use std::{f64::consts::PI, thread, time::Duration}; +use std::{thread, time::Duration}; pub enum Turn { Me, @@ -36,29 +36,22 @@ pub struct Game { impl Game { pub fn init(channel_type: ChannelType) -> Self { let state = BattleState::new(); - let channel = match channel_type { - ChannelType::Server => Channel::server(), - ChannelType::Client(addr) => Channel::client(addr), - ChannelType::ServeOnPort(port) => Channel::serve_on_port(port), - }; - let turn = match channel_type { - ChannelType::Client(_) => Turn::Opponent, - _ => Turn::Me, + let (channel, turn) = match channel_type { + ChannelType::Server => (Channel::server(), Turn::Me), + ChannelType::Client(addr) => (Channel::client(addr), Turn::Opponent), + ChannelType::UseListener(listener) => (Channel::serve_using_listener(listener), Turn::Me), }; Self { state, channel, turn } } pub fn draw_cell(&self, cell: Cell, loc: Position, turtle: &mut Turtle) { - fn draw_circle(turtle: &mut Turtle, diameter: f64) { + fn draw_circle(turtle: &mut Turtle, radius: f64) { let pen_color = turtle.pen_color(); turtle.set_pen_color("transparent"); turtle.set_heading(0.0); turtle.begin_fill(); - for _ in 0..360 { - turtle.forward(PI * diameter / 360.0); - turtle.right(1.0); - } + turtle.arc_right(radius, 360.0); turtle.end_fill(); turtle.set_pen_color(pen_color); } @@ -80,21 +73,26 @@ impl Game { match cell { Cell::Missed | Cell::Bombed => { - self.draw_cell(Cell::Empty, loc, turtle); - let diameter = if cell == Cell::Missed { - Config::MISSED_CIRCLE_DIAMETER + if cell == Cell::Missed { + // Crosshair::move_to calls Game::draw_cell method to remove crosshair from previous pos. + // When Cell::Missed drawn, the circle is so small that it leaves some parts of crosshair + // This redundant call to draw an empty cell is made so that it won't happen + self.draw_cell(Cell::Empty, loc, turtle); + } + let radius = if cell == Cell::Missed { + Config::MISSED_CIRCLE_RADIUS } else { - Config::BOMBED_CIRCLE_DIAMETER + Config::BOMBED_CIRCLE_RADIUS }; let start = ( x + Config::CELL_SIZE * (pos.1 as f64 + 0.5), - y - Config::CELL_SIZE * pos.0 as f64 - (Config::CELL_SIZE / 2.0 - diameter / 2.0), + y - Config::CELL_SIZE * pos.0 as f64 - (Config::CELL_SIZE / 2.0 - radius), ); turtle.pen_up(); turtle.go_to(start); turtle.pen_down(); turtle.set_fill_color(Config::cell_color(&cell)); - draw_circle(turtle, diameter); + draw_circle(turtle, radius); } _ => { let start = (x + Config::CELL_SIZE * pos.1 as f64, y - Config::CELL_SIZE * pos.0 as f64); @@ -119,9 +117,13 @@ impl Game { } } - fn get_attack_location(&self, drawing: &mut Drawing, turtle: &mut Turtle, last_bombed_location: Option<(u8, u8)>) -> (u8, u8) { - let mut crosshair = Crosshair::new(&self.state, turtle, last_bombed_location); + fn get_attack_location(&self, drawing: &mut Drawing, turtle: &mut Turtle, last_attacked_location: Option<(u8, u8)>) -> (u8, u8) { + let mut crosshair = Crosshair::new(&self.state, turtle, last_attacked_location); + // Event loop that busy waits for user input and moves crosshair based on user actions + // Exits and returns attack location when user presses "Enter", if location is attackable loop { + // TODO: Replace poll_event with blocking next_event after Events API is stabilized + // https://github.com/sunjay/turtle/issues/178 while let Some(event) = drawing.poll_event() { use Key::{DownArrow, LeftArrow, Return, RightArrow, UpArrow}; if let Event::Key(key, PressedState::Pressed) = event { @@ -139,11 +141,73 @@ impl Game { } } } - // reduce CPU usage + // reduce CPU usage by putting the thread to sleep + // The sleep time is small enough so the user won't notice input lag thread::sleep(Duration::from_millis(16)); } } + /// Handles player's turn + /// * Determine attack location + /// * Send attack coordinates to Opponent + /// * Receive attack outcome from Opponent + /// * Update state, render changes and change turn if necessary + fn my_chance(&mut self, drawing: &mut Drawing, turtle: &mut Turtle, last_bombed_location: &mut Option<(u8, u8)>) { + let attack_location = self.get_attack_location(drawing, turtle, *last_bombed_location); + *last_bombed_location = Some(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 => { + self.draw_cell(Cell::Missed, Position::AttackGrid(attack_location), turtle); + self.turn.flip(); + } + AttackOutcome::Hit => { + self.draw_cell(Cell::Bombed, Position::AttackGrid(attack_location), turtle); + } + AttackOutcome::Destroyed(ship) => { + ship.coordinates() + .into_iter() + .for_each(|pos| self.draw_cell(Cell::Destroyed, Position::AttackGrid(pos), turtle)); + self.turn.flip(); + } + } + } + _ => panic!("Expected Message of AttackResult from Opponent."), + } + } + + /// Handles opponent's turn + /// * Receive attack coordinates from Opponent + /// * Update state and send outcome of attack to Opponent + /// * Render changes and change turn if necessary + fn opponent_chance(&mut self, turtle: &mut Turtle) { + 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 => { + self.draw_cell(Cell::Missed, Position::ShipGrid(p), turtle); + self.turn.flip(); + } + AttackOutcome::Hit => { + self.draw_cell(Cell::Bombed, Position::ShipGrid(p), turtle); + } + AttackOutcome::Destroyed(ship) => { + ship.coordinates() + .into_iter() + .for_each(|pos| self.draw_cell(Cell::Destroyed, Position::ShipGrid(pos), turtle)); + self.turn.flip(); + } + } + } + _ => panic!("Expected Message of AttackCoordinates from Opponent"), + } + } + pub fn run(&mut self) { let mut drawing = Drawing::new(); let mut turtle = drawing.add_turtle(); @@ -153,58 +217,14 @@ impl Game { turtle.set_speed("instant"); self.draw_board(&mut turtle); + // Game loop loop { match self.turn { - Turn::Me => { - let attack_location = self.get_attack_location(&mut drawing, &mut turtle, last_bombed_location); - last_bombed_location = Some(attack_location); - self.channel.send_message(&Message::AttackCoordinates(attack_location)); - match self.channel.receive_message() { - Message::AttackResult(outcome) => match outcome { - AttackOutcome::Miss => { - self.state.set_attack_outcome(&attack_location, Cell::Missed); - self.draw_cell(Cell::Missed, Position::AttackGrid(attack_location), &mut turtle); - self.turn.flip(); - } - AttackOutcome::Hit => { - self.state.set_attack_outcome(&attack_location, Cell::Bombed); - self.draw_cell(Cell::Bombed, Position::AttackGrid(attack_location), &mut turtle); - } - AttackOutcome::Destroyed(ship) => { - self.state.set_destroyed_ship(&ship); - ship.coordinates() - .into_iter() - .for_each(|pos| self.draw_cell(Cell::Destroyed, Position::AttackGrid(pos), &mut turtle)); - 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 => { - self.draw_cell(Cell::Missed, Position::ShipGrid(p), &mut turtle); - self.turn.flip(); - } - AttackOutcome::Hit => { - self.draw_cell(Cell::Bombed, Position::ShipGrid(p), &mut turtle); - } - AttackOutcome::Destroyed(ship) => { - ship.coordinates() - .into_iter() - .for_each(|pos| self.draw_cell(Cell::Destroyed, Position::ShipGrid(pos), &mut turtle)); - self.turn.flip(); - } - } - } - _ => panic!("Expected Message of AttackCoordinates from Opponent"), - }, + Turn::Me => self.my_chance(&mut drawing, &mut turtle, &mut last_bombed_location), + Turn::Opponent => self.opponent_chance(&mut turtle), } + // Break Game loop (exit game), when either player destroys opponent's fleet or loses their fleet match (self.state.ships_lost, self.state.destroyed_rival_ships) { (5, _) => { println!("NT"); diff --git a/examples/battleship/grid.rs b/examples/battleship/grid.rs index 0ecb7ed1..ffb6c46b 100644 --- a/examples/battleship/grid.rs +++ b/examples/battleship/grid.rs @@ -8,10 +8,13 @@ pub enum Cell { Cruiser = 2, Submarine = 3, Destroyer = 4, + /// clear cell on ShipGrid Empty, + /// clear cell on AttackGrid Unattacked, Missed, Bombed, + /// Denotes a ship cell of a completely destroyed ship Destroyed, } diff --git a/examples/battleship/main.rs b/examples/battleship/main.rs index 08460cfc..97ebbe65 100644 --- a/examples/battleship/main.rs +++ b/examples/battleship/main.rs @@ -1,8 +1,25 @@ -//! Battleship board Game -//! Usage: -//! $> ./battleship # No arguments - outputs a TCP port waiting for the opponent to connect. -//! $> ./battleship # - connects to a server with the specified ip port -//! $> ./battleship bot # play with computer (single player). +//! Battleship is a two-player strategic guessing game. +//! There are two grids - let's call them - ShipGrid and AttackGrid. +//! ShipGrid, located on the left hand side, is where the player's fleet of ships is situated and marked. +//! AttackGrid, located on the right hand side, is where the opponent's fleet is situated but concealed. +//! Players alternate turns calling "shots" at the other player's ships. +//! You can use arrow keys (←, ↑, ↓, →) to move crosshair in AttackGrid and press `Enter ⏎` key to attack. +//! The objective of the game is to destroy the opposing player's fleet. +//! +//! This game can be played in single player mode as well as multiplayer. +//! To play in single player mode, you can pass `bot` as an argument to the program +//! $> ./battleship bot +//! +//! To play in multiplayer mode, one player needs to acts as a server and the other as client. +//! To act as a server, run the program without any arguments. +//! $> ./battleship # No arguments +//! This will output something like "Listening on port: , Waiting for connection..". +//! +//! If the other player is also within the same LAN, you can share your private IP address and +//! which they can use to connect with you by running the program with these arguments: +//! $> ./battleship # eg: ./battleship 192.168.0.120:35765 +//! +//! If not in same LAN, you can try DynamicDNS or a publicly routable IP address. // To run, use the command: cargo run --features unstable --example battleship #[cfg(all(not(feature = "unstable")))] @@ -16,50 +33,40 @@ mod crosshair; mod game; mod grid; mod ship; +mod utils; use bot::Bot; use channel::ChannelType; use game::Game; -use std::{net::TcpListener, thread, time::Duration}; - -pub fn get_available_tcp_port() -> u16 { - for port in 49152..=65535 { - if TcpListener::bind(&format!("127.0.0.1:{}", port)).is_ok() { - return port; - } - } - panic!("No ports available!"); -} +use std::{thread, time::Duration}; +use utils::*; fn main() { - let args: Vec = std::env::args().collect(); - let config = match args.len() { - 0 => unreachable!(), - 1 => "", - _ => &args[1], - }; - - match config { - "" => { + let opt = parse_args(); + match opt { + Opt::Server => { let mut game = Game::init(ChannelType::Server); game.run(); } - "bot" => { - // May fail due to TOCTOU - let port = get_available_tcp_port(); + Opt::Client(addr) => { + let mut game = Game::init(ChannelType::Client(addr)); + game.run(); + } + Opt::PlayWithBot => { + // Create a TCP listener on a free port and use it to make a game server. + // The game server will start listening on that port while we spawn + // a bot instance in a separate thread which would later connnect to the server. + let listener = get_tcp_listener(); + let port = listener.local_addr().unwrap().port(); let handle = thread::spawn(move || { - // delay to let game server bind to port + // delay to let the game server start listening on port thread::sleep(Duration::from_millis(10)); let mut bot = Bot::new(port); bot.play(); }); - let mut game = Game::init(ChannelType::ServeOnPort(port)); + let mut game = Game::init(ChannelType::UseListener(listener)); game.run(); handle.join().unwrap(); } - addr => { - let mut game = Game::init(ChannelType::Client(addr)); - game.run(); - } } } diff --git a/examples/battleship/ship.rs b/examples/battleship/ship.rs index 83134f26..10ced660 100644 --- a/examples/battleship/ship.rs +++ b/examples/battleship/ship.rs @@ -6,13 +6,8 @@ pub struct ShipPosition { pub bottom_right: (u8, u8), } -impl ShipPosition { - pub fn new(top_left: (u8, u8), bottom_right: (u8, u8)) -> Self { - Self { top_left, bottom_right } - } -} - -// Based on https://en.wikipedia.org/wiki/Battleship_(game)#Description +// This implementation is based on 1990 Milton Bradley version of Battleship +// https://en.wikipedia.org/wiki/Battleship_(game)#Description #[derive(Debug, Copy, Clone, PartialEq, Deserialize, Serialize)] pub enum ShipKind { Carrier, @@ -23,6 +18,7 @@ pub enum ShipKind { } impl ShipKind { + // returns the length of the ship pub fn size(&self) -> u8 { match self { Self::Carrier => 5, @@ -34,6 +30,7 @@ impl ShipKind { } } +// Specifies the alignment of a ship in the Grid #[derive(Copy, Clone, Debug, PartialEq)] pub enum Orientation { Horizontal, @@ -47,7 +44,15 @@ pub struct Ship { } impl Ship { - pub fn new(kind: ShipKind, position: ShipPosition) -> Self { + pub fn new(kind: ShipKind, top_left: (u8, u8), orientation: Orientation) -> Self { + let position = ShipPosition { + top_left, + bottom_right: match orientation { + Orientation::Horizontal => (top_left.0 + kind.size(), top_left.1), + Orientation::Veritcal => (top_left.0, top_left.1 + kind.size()), + }, + }; + Self { kind, position } } pub fn orientation(&self) -> Orientation { @@ -107,6 +112,12 @@ mod test { assert_eq!(carrier.orientation(), Orientation::Veritcal); assert_eq!(battleship.orientation(), Orientation::Horizontal); + + let cruiser = Ship::new(ShipKind::Cruiser, (3, 2), Orientation::Horizontal); + assert_eq!(cruiser.position.bottom_right, (6, 2)); + + let submarine = Ship::new(ShipKind::Submarine, (3, 2), Orientation::Veritcal); + assert_eq!(submarine.position.bottom_right, (3, 5)); } #[test] diff --git a/examples/battleship/utils.rs b/examples/battleship/utils.rs new file mode 100644 index 00000000..8c8cae2b --- /dev/null +++ b/examples/battleship/utils.rs @@ -0,0 +1,56 @@ +use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener}; + +/// Scans the private port range for an open port and creates a TCP listener +pub fn get_tcp_listener() -> TcpListener { + let localhost = IpAddr::V4(Ipv4Addr::LOCALHOST); + for port in 49152..=65535 { + if let Ok(listener) = TcpListener::bind(SocketAddr::new(localhost, port)) { + return listener; + } + } + panic!("No ports available!"); +} + +/// Command-line arguments config +pub enum Opt { + /// server - multiplayer mode + Server, + /// client - multiplayer mode + Client(SocketAddr), + /// single player mode + PlayWithBot, +} + +fn print_help() { + let help = r#" +battleship + +USAGE: + battleship # no arguments - acts as server + battleship bot # single player mode + battleship # connect to server eg: battleship 192.168.0.120:36354 + +FLAGS: + -h, --help Prints help information + +"#; + println!("{}", help); +} + +/// Parses command line arguments +pub fn parse_args() -> Opt { + let mut args = std::env::args().skip(1); + + match args.next().as_ref().map(String::as_ref) { + Some("-h") | Some("--help") => { + print_help(); + std::process::exit(0); + } + Some("bot") => Opt::PlayWithBot, + Some(addr) => { + let socket_addr = addr.parse().expect("Invalid IP:PORT, See help (-h, --help)"); + Opt::Client(socket_addr) + } + None => Opt::Server, + } +} From d3c5d3c3d026f642112757145440042349a23467 Mon Sep 17 00:00:00 2001 From: Sathwik Matsa Date: Wed, 28 Jul 2021 12:32:04 +0530 Subject: [PATCH 14/16] refactor ship.rs --- examples/battleship/battlestate.rs | 85 ++++++++------------------- examples/battleship/config.rs | 2 +- examples/battleship/grid.rs | 10 ++-- examples/battleship/ship.rs | 93 +++++------------------------- 4 files changed, 44 insertions(+), 146 deletions(-) diff --git a/examples/battleship/battlestate.rs b/examples/battleship/battlestate.rs index c1b7ead5..3d73e77f 100644 --- a/examples/battleship/battlestate.rs +++ b/examples/battleship/battlestate.rs @@ -4,7 +4,7 @@ use turtle::rand::{choose, random_range}; use crate::{ grid::{Cell, Grid}, - ship::*, + ship::{Orientation, Ship, ShipKind}, }; #[derive(Copy, Clone, Debug, PartialEq, Deserialize, Serialize)] @@ -60,22 +60,6 @@ impl Display for BattleState { } impl BattleState { - #[allow(dead_code)] - fn custom(ships: [Ship; 5]) -> Self { - 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(); - }) - }); - Self { - ships, - ship_grid, - attack_grid: Grid::new(Cell::Unattacked), - destroyed_rival_ships: 0, - ships_lost: 0, - } - } pub fn new() -> Self { let (ships, ship_grid) = Self::random_ship_grid(); Self { @@ -188,44 +172,31 @@ impl BattleState { #[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 { - kind: ShipKind::Carrier, - position: ShipPosition { - top_left: (2, 4), - bottom_right: (2, 8), - }, - }, - Ship { - kind: ShipKind::Battleship, - position: ShipPosition { - top_left: (1, 0), - bottom_right: (4, 0), - }, - }, - Ship { - kind: ShipKind::Cruiser, - position: ShipPosition { - top_left: (5, 2), - bottom_right: (7, 2), - }, - }, - Ship { - kind: ShipKind::Submarine, - position: ShipPosition { - top_left: (8, 4), - bottom_right: (8, 6), - }, - }, - Ship { - kind: ShipKind::Destroyer, - position: ShipPosition { - top_left: (6, 7), - bottom_right: (9, 7), - }, - }, + 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 @@ -239,7 +210,7 @@ mod test { // 7 . . C . . . D D . . 7 . . . R R R . . . . // 8 . . C . . . . . . . 8 . . . . . . . . . . // 9 . . . . . . . . . . 9 . . . . . . . . . . - let mut state = BattleState::custom(ships); + 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); @@ -259,13 +230,7 @@ mod test { // turn 5: player attacks (6, 2) - destroys D state.set_attack_outcome( &(6, 2), - AttackOutcome::Destroyed(Ship { - kind: ShipKind::Destroyer, - position: ShipPosition { - top_left: (6, 2), - bottom_right: (7, 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); diff --git a/examples/battleship/config.rs b/examples/battleship/config.rs index 03a94d9c..4123ed01 100644 --- a/examples/battleship/config.rs +++ b/examples/battleship/config.rs @@ -7,7 +7,7 @@ impl Config { pub const EMPTY_COLOR: &'static str = "#55dde0"; pub const UNATTACKED_COLOR: &'static str = "#55dde0"; pub const CARRIER_COLOR: &'static str = "#fde74c"; - pub const BATTLESHIP_COLOR: &'static str = "#f48923"; + pub const BATTLESHIP_COLOR: &'static str = "#f4d58d"; pub const CRUISER_COLOR: &'static str = "#947757"; pub const SUBMARINE_COLOR: &'static str = "#9bc53d"; pub const DESTROYER_COLOR: &'static str = "#238cf4"; diff --git a/examples/battleship/grid.rs b/examples/battleship/grid.rs index ffb6c46b..9a67025d 100644 --- a/examples/battleship/grid.rs +++ b/examples/battleship/grid.rs @@ -3,11 +3,11 @@ use std::ops::Deref; #[derive(Debug, Copy, Clone, PartialEq)] pub enum Cell { - Carrier = 0, - Battleship = 1, - Cruiser = 2, - Submarine = 3, - Destroyer = 4, + Carrier, + Battleship, + Cruiser, + Submarine, + Destroyer, /// clear cell on ShipGrid Empty, /// clear cell on AttackGrid diff --git a/examples/battleship/ship.rs b/examples/battleship/ship.rs index 10ced660..d31917ca 100644 --- a/examples/battleship/ship.rs +++ b/examples/battleship/ship.rs @@ -1,11 +1,5 @@ use serde::{Deserialize, Serialize}; -#[derive(Copy, Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct ShipPosition { - pub top_left: (u8, u8), - pub bottom_right: (u8, u8), -} - // This implementation is based on 1990 Milton Bradley version of Battleship // https://en.wikipedia.org/wiki/Battleship_(game)#Description #[derive(Debug, Copy, Clone, PartialEq, Deserialize, Serialize)] @@ -31,7 +25,7 @@ impl ShipKind { } // Specifies the alignment of a ship in the Grid -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum Orientation { Horizontal, Veritcal, @@ -40,104 +34,43 @@ pub enum Orientation { #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Ship { pub kind: ShipKind, - pub position: ShipPosition, + top_left: (u8, u8), + orientation: Orientation, } impl Ship { pub fn new(kind: ShipKind, top_left: (u8, u8), orientation: Orientation) -> Self { - let position = ShipPosition { + Self { + kind, top_left, - bottom_right: match orientation { - Orientation::Horizontal => (top_left.0 + kind.size(), top_left.1), - Orientation::Veritcal => (top_left.0, top_left.1 + kind.size()), - }, - }; - - Self { kind, position } - } - pub fn orientation(&self) -> Orientation { - let diff_x = self.position.top_left.0 as i32 - self.position.bottom_right.0 as i32; - let diff_y = self.position.top_left.1 as i32 - self.position.bottom_right.1 as i32; - match (diff_x, diff_y) { - (0, _) => Orientation::Veritcal, - (_, 0) => Orientation::Horizontal, - (_, _) => unreachable!(), + orientation, } } - pub fn is_located_over(&self, pos: &(u8, u8)) -> bool { - let collinear = { - (pos.0 as i32 - self.position.top_left.0 as i32) * (self.position.top_left.1 as i32 - self.position.bottom_right.1 as i32) - - (self.position.top_left.0 as i32 - self.position.bottom_right.0 as i32) * (pos.1 as i32 - self.position.top_left.1 as i32) - == 0 - }; - let x_within_bounds = (self.position.top_left.0..=self.position.bottom_right.0).contains(&pos.0); - let y_within_bounds = (self.position.top_left.1..=self.position.bottom_right.1).contains(&pos.1); - collinear && x_within_bounds && y_within_bounds - } 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; + let (x, y) = self.top_left; (0..self.kind.size()) - .map(|i| match orientation { + .map(|i| match self.orientation { Orientation::Horizontal => (x + i, y), Orientation::Veritcal => (x, y + i), }) .collect() } + pub fn is_located_over(&self, pos: &(u8, u8)) -> bool { + self.coordinates().contains(pos) + } } #[cfg(test)] mod test { use super::*; - #[test] - fn ship_orientation() { - let carrier = Ship { - kind: ShipKind::Carrier, - position: ShipPosition { - top_left: (1, 2), - bottom_right: (1, 6), - }, - }; - - let battleship = Ship { - kind: ShipKind::Battleship, - position: ShipPosition { - top_left: (3, 2), - bottom_right: (6, 2), - }, - }; - - assert_eq!(carrier.orientation(), Orientation::Veritcal); - assert_eq!(battleship.orientation(), Orientation::Horizontal); - - let cruiser = Ship::new(ShipKind::Cruiser, (3, 2), Orientation::Horizontal); - assert_eq!(cruiser.position.bottom_right, (6, 2)); - - let submarine = Ship::new(ShipKind::Submarine, (3, 2), Orientation::Veritcal); - assert_eq!(submarine.position.bottom_right, (3, 5)); - } - #[test] fn ship_intersection() { - let carrier = Ship { - kind: ShipKind::Carrier, - position: ShipPosition { - top_left: (1, 2), - bottom_right: (1, 6), - }, - }; + let carrier = Ship::new(ShipKind::Carrier, (1, 2), Orientation::Veritcal); let cspan: Vec<_> = (2..=6).map(|y| (1, y)).collect(); - let battleship = Ship { - kind: ShipKind::Battleship, - position: ShipPosition { - top_left: (3, 2), - bottom_right: (6, 2), - }, - }; + let battleship = Ship::new(ShipKind::Battleship, (3, 2), Orientation::Horizontal); let bspan: Vec<_> = (3..=6).map(|x| (x, 2)).collect(); for x in 0..10 { From 5acbc117d2637af80362289ac9a51d5faacf48cf Mon Sep 17 00:00:00 2001 From: Sathwik Matsa Date: Wed, 28 Jul 2021 12:38:22 +0530 Subject: [PATCH 15/16] Apply suggestions from code review Co-authored-by: Franz Dietrich --- examples/battleship/main.rs | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/examples/battleship/main.rs b/examples/battleship/main.rs index 97ebbe65..40a5d6e0 100644 --- a/examples/battleship/main.rs +++ b/examples/battleship/main.rs @@ -1,27 +1,37 @@ //! Battleship is a two-player strategic guessing game. -//! There are two grids - let's call them - ShipGrid and AttackGrid. -//! ShipGrid, located on the left hand side, is where the player's fleet of ships is situated and marked. -//! AttackGrid, located on the right hand side, is where the opponent's fleet is situated but concealed. -//! Players alternate turns calling "shots" at the other player's ships. -//! You can use arrow keys (←, ↑, ↓, →) to move crosshair in AttackGrid and press `Enter ⏎` key to attack. -//! The objective of the game is to destroy the opposing player's fleet. +//! There are two grids - let's call them - ShipGrid (left) and AttackGrid (right). +//! The ShipGrid, is where the player's fleet of ships is situated and marked. +//! The AttackGrid, is where the opponent's fleet is situated but concealed. +//! The goal is to destroy the opponents fleet by shooting(guessing all tiles) containing a ship of his fleet. +//! Whenever a shot was fired the player gets a feedback: either miss (•) or hit (a red ⚫). +//! That feedback enables some optimization strategies - to guess strategically optimal. +//! +//! You can use the arrow keys (←, ↑, ↓, →) to move the cross-hair in AttackGrid and press the `Enter ⏎` key to attack. //! -//! This game can be played in single player mode as well as multiplayer. //! To play in single player mode, you can pass `bot` as an argument to the program +//! //! $> ./battleship bot +//! $> cargo run --features unstable --example battleship bot # From within the turtle source-code. //! -//! To play in multiplayer mode, one player needs to acts as a server and the other as client. +//! To play in multiplayer mode, one player needs to act as the server and the other as client. //! To act as a server, run the program without any arguments. +//! //! $> ./battleship # No arguments +//! $> cargo run --features unstable --example battleship # From within the turtle source-code. +//! //! This will output something like "Listening on port: , Waiting for connection..". +//! As soon as an opponent connects the game starts. //! //! If the other player is also within the same LAN, you can share your private IP address and -//! which they can use to connect with you by running the program with these arguments: +//! which they can use to connect to your game by running the program with these arguments: +//! //! $> ./battleship # eg: ./battleship 192.168.0.120:35765 +//! $> cargo run --features unstable --example battleship //! -//! If not in same LAN, you can try DynamicDNS or a publicly routable IP address. +//! If not in same LAN, you can try DynamicDNS or a publicly routable IP address +//! but there are many possible problems that could arise. -// To run, use the command: cargo run --features unstable --example battleship +// To run, use the command: cargo run --features unstable --example battleship bot #[cfg(all(not(feature = "unstable")))] compile_error!("This example relies on unstable features. Run with `--features unstable`"); From 2451f9414b5f219e1dc2222e74be23df70de39a3 Mon Sep 17 00:00:00 2001 From: Sathwik Matsa Date: Wed, 28 Jul 2021 18:21:16 +0530 Subject: [PATCH 16/16] Apply suggestions from code review --- examples/battleship/battlestate.rs | 37 +++++++------------------ examples/battleship/config.rs | 12 ++++---- examples/battleship/game.rs | 4 +-- examples/battleship/grid.rs | 44 +++++++++++++++++++++--------- examples/battleship/main.rs | 9 +++--- 5 files changed, 54 insertions(+), 52 deletions(-) diff --git a/examples/battleship/battlestate.rs b/examples/battleship/battlestate.rs index 3d73e77f..1250f38d 100644 --- a/examples/battleship/battlestate.rs +++ b/examples/battleship/battlestate.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use std::{convert::TryInto, fmt::Display}; +use std::convert::TryInto; use turtle::rand::{choose, random_range}; use crate::{ @@ -37,28 +37,6 @@ pub struct BattleState { 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::() - }) - .collect::>(); - write!(f, "{}", output.join("\n")) - } -} - impl BattleState { pub fn new() -> Self { let (ships, ship_grid) = Self::random_ship_grid(); @@ -74,13 +52,18 @@ impl BattleState { 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 => { + 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[attacked_cell as usize]; + let lost_ship = self + .ships + .iter() + .find(|ship| ship.kind.to_cell() == attacked_cell) + .copied() + .unwrap(); lost_ship .coordinates() .into_iter() @@ -94,14 +77,14 @@ impl BattleState { } } } - _ => unreachable!(), + 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, - _ => unreachable!(), + Cell::Ship(_) | Cell::Empty => unreachable!(), } } pub fn set_attack_outcome(&mut self, attacked_pos: &(u8, u8), outcome: AttackOutcome) { diff --git a/examples/battleship/config.rs b/examples/battleship/config.rs index 4123ed01..8516080d 100644 --- a/examples/battleship/config.rs +++ b/examples/battleship/config.rs @@ -1,6 +1,6 @@ +use crate::{grid::Cell, ship::ShipKind}; use turtle::Color; -use crate::grid::Cell; pub struct Config {} impl Config { @@ -31,11 +31,11 @@ impl Config { pub fn cell_color(cell: &Cell) -> Color { match cell { - Cell::Carrier => Self::CARRIER_COLOR.into(), - Cell::Battleship => Self::BATTLESHIP_COLOR.into(), - Cell::Cruiser => Self::CRUISER_COLOR.into(), - Cell::Submarine => Self::SUBMARINE_COLOR.into(), - Cell::Destroyer => Self::DESTROYER_COLOR.into(), + 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(), Cell::Empty => Self::EMPTY_COLOR.into(), Cell::Unattacked => Self::UNATTACKED_COLOR.into(), Cell::Missed => Self::MISSED_COLOR.into(), diff --git a/examples/battleship/game.rs b/examples/battleship/game.rs index 32ce5078..d9535475 100644 --- a/examples/battleship/game.rs +++ b/examples/battleship/game.rs @@ -125,14 +125,14 @@ impl Game { // TODO: Replace poll_event with blocking next_event after Events API is stabilized // https://github.com/sunjay/turtle/issues/178 while let Some(event) = drawing.poll_event() { - use Key::{DownArrow, LeftArrow, Return, RightArrow, UpArrow}; + use Key::{DownArrow, LeftArrow, Return, RightArrow, Space, UpArrow}; if let Event::Key(key, PressedState::Pressed) = event { match key { LeftArrow => crosshair.move_left(self), RightArrow => crosshair.move_right(self), UpArrow => crosshair.move_up(self), DownArrow => crosshair.move_down(self), - Return => { + Return | Space => { if let Some(pos) = crosshair.try_bomb() { return pos; } diff --git a/examples/battleship/grid.rs b/examples/battleship/grid.rs index 9a67025d..87768f60 100644 --- a/examples/battleship/grid.rs +++ b/examples/battleship/grid.rs @@ -1,13 +1,9 @@ use crate::ship::ShipKind; -use std::ops::Deref; +use std::{fmt::Display, ops::Deref}; #[derive(Debug, Copy, Clone, PartialEq)] pub enum Cell { - Carrier, - Battleship, - Cruiser, - Submarine, - Destroyer, + Ship(ShipKind), /// clear cell on ShipGrid Empty, /// clear cell on AttackGrid @@ -18,18 +14,28 @@ pub enum Cell { Destroyed, } -impl ShipKind { - pub fn to_cell(self) -> Cell { +impl Display for Cell { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - ShipKind::Carrier => Cell::Carrier, - ShipKind::Battleship => Cell::Battleship, - ShipKind::Cruiser => Cell::Cruiser, - ShipKind::Submarine => Cell::Submarine, - ShipKind::Destroyer => Cell::Destroyer, + Cell::Ship(ShipKind::Carrier) => write!(f, "C"), + Cell::Ship(ShipKind::Battleship) => write!(f, "B"), + Cell::Ship(ShipKind::Cruiser) => write!(f, "R"), + Cell::Ship(ShipKind::Submarine) => write!(f, "S"), + Cell::Ship(ShipKind::Destroyer) => write!(f, "D"), + Cell::Empty | Cell::Unattacked => write!(f, "."), + Cell::Missed => write!(f, ","), + Cell::Bombed => write!(f, "*"), + Cell::Destroyed => write!(f, "#"), } } } +impl ShipKind { + pub fn to_cell(self) -> Cell { + Cell::Ship(self) + } +} + #[derive(Debug, Copy, Clone)] pub struct Grid([[Cell; 10]; 10]); @@ -40,6 +46,18 @@ impl Deref for Grid { } } +impl Display for Grid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for i in 0..10 { + for j in 0..10 { + write!(f, "{}", self.get(&(j, i)))? + } + writeln!(f)? + } + Ok(()) + } +} + impl Grid { pub fn new(cell: Cell) -> Self { Self { 0: [[cell; 10]; 10] } diff --git a/examples/battleship/main.rs b/examples/battleship/main.rs index 40a5d6e0..09dc1f36 100644 --- a/examples/battleship/main.rs +++ b/examples/battleship/main.rs @@ -6,19 +6,20 @@ //! Whenever a shot was fired the player gets a feedback: either miss (•) or hit (a red ⚫). //! That feedback enables some optimization strategies - to guess strategically optimal. //! -//! You can use the arrow keys (←, ↑, ↓, →) to move the cross-hair in AttackGrid and press the `Enter ⏎` key to attack. +//! You can use the arrow keys (←, ↑, ↓, →) to move the cross-hair in AttackGrid +//! and press the `Enter ⏎` or Space key to attack. //! //! To play in single player mode, you can pass `bot` as an argument to the program -//! +//! //! $> ./battleship bot //! $> cargo run --features unstable --example battleship bot # From within the turtle source-code. //! //! To play in multiplayer mode, one player needs to act as the server and the other as client. //! To act as a server, run the program without any arguments. -//! +//! //! $> ./battleship # No arguments //! $> cargo run --features unstable --example battleship # From within the turtle source-code. -//! +//! //! This will output something like "Listening on port: , Waiting for connection..". //! As soon as an opponent connects the game starts. //!