1use actix::{Actor, Addr};
2use actix_cors::Cors;
3use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer, Responder};
4use actix_web_actors::ws;
5
6use crate::args::Args;
7use crate::data::{BoatsLayout, GameRules, PlayConfiguration, VersionInfo};
8use crate::dispatcher_actor::DispatcherActor;
9use crate::human_player_ws::{HumanPlayerWS, StartMode};
10
11async fn index() -> impl Responder {
13 HttpResponse::Ok().json("Sea battle backend")
14}
15
16async fn not_found() -> impl Responder {
18 HttpResponse::NotFound().json("You missed your strike lol")
19}
20
21async fn version_information() -> impl Responder {
23 HttpResponse::Ok().json(VersionInfo::load_static())
24}
25
26async fn game_configuration() -> impl Responder {
28 HttpResponse::Ok().json(PlayConfiguration::default())
29}
30
31async fn default_game_rules() -> impl Responder {
33 HttpResponse::Ok().json(GameRules::random_players_rules())
34}
35
36async fn validate_game_rules(rules: web::Json<GameRules>) -> impl Responder {
38 HttpResponse::Ok().json(rules.get_errors())
39}
40
41async fn gen_boats_layout(rules: web::Json<GameRules>) -> impl Responder {
43 let errors = rules.get_errors();
44 if !errors.is_empty() {
45 return HttpResponse::BadRequest().json(errors);
46 }
47 match BoatsLayout::gen_random_for_rules(&rules) {
48 Ok(l) => HttpResponse::Ok().json(l),
49 Err(e) => {
50 log::error!(
51 "Failed to generate boats layout for valid game rules: {} ! / Rules: {:?}",
52 e,
53 rules
54 );
55 HttpResponse::InternalServerError().json("Failed to generate random layout!")
56 }
57 }
58}
59
60#[derive(serde::Serialize, serde::Deserialize, Eq, PartialEq, Debug)]
61pub struct BotPlayQuery {
62 #[serde(flatten)]
63 pub rules: GameRules,
64 pub player_name: String,
65}
66
67async fn start_bot_play(
69 req: HttpRequest,
70 stream: web::Payload,
71 query: web::Query<BotPlayQuery>,
72 dispatcher: web::Data<Addr<DispatcherActor>>,
73) -> Result<HttpResponse, actix_web::Error> {
74 let errors = query.rules.get_errors();
75 if !errors.is_empty() {
76 return Ok(HttpResponse::BadRequest().json(errors));
77 }
78
79 let player_ws = HumanPlayerWS::new(
80 StartMode::Bot(query.rules.clone()),
81 &dispatcher,
82 query.player_name.clone(),
83 );
84
85 let resp = ws::start(player_ws, &req, stream);
86 log::info!("New bot play with configuration: {:?}", &query.rules);
87 resp
88}
89
90#[derive(serde::Serialize, serde::Deserialize)]
91pub struct CreateInviteQuery {
92 #[serde(flatten)]
93 pub rules: GameRules,
94 pub player_name: String,
95}
96
97async fn start_create_invite(
99 req: HttpRequest,
100 stream: web::Payload,
101 query: web::Query<CreateInviteQuery>,
102 dispatcher: web::Data<Addr<DispatcherActor>>,
103) -> Result<HttpResponse, actix_web::Error> {
104 let errors = query.rules.get_errors();
105 if !errors.is_empty() {
106 return Ok(HttpResponse::BadRequest().json(errors));
107 }
108
109 let player_ws = HumanPlayerWS::new(
110 StartMode::CreateInvite(query.rules.clone()),
111 &dispatcher,
112 query.0.player_name,
113 );
114
115 let resp = ws::start(player_ws, &req, stream);
116 log::info!(
117 "New create invite play with configuration: {:?}",
118 &query.0.rules
119 );
120 resp
121}
122
123#[derive(serde::Serialize, serde::Deserialize)]
124pub struct AcceptInviteQuery {
125 pub code: String,
126 pub player_name: String,
127}
128
129async fn start_accept_invite(
131 req: HttpRequest,
132 stream: web::Payload,
133 query: web::Query<AcceptInviteQuery>,
134 dispatcher: web::Data<Addr<DispatcherActor>>,
135) -> Result<HttpResponse, actix_web::Error> {
136 let player_ws = HumanPlayerWS::new(
137 StartMode::AcceptInvite {
138 code: query.code.clone(),
139 },
140 &dispatcher,
141 query.0.player_name,
142 );
143
144 let resp = ws::start(player_ws, &req, stream);
145 log::info!("New accept invite: {:?}", &query.0.code);
146 resp
147}
148
149#[derive(serde::Serialize, serde::Deserialize)]
150pub struct PlayRandomQuery {
151 pub player_name: String,
152}
153
154async fn start_random(
156 req: HttpRequest,
157 stream: web::Payload,
158 query: web::Query<PlayRandomQuery>,
159 dispatcher: web::Data<Addr<DispatcherActor>>,
160) -> Result<HttpResponse, actix_web::Error> {
161 let player_ws = HumanPlayerWS::new(StartMode::PlayRandom, &dispatcher, query.0.player_name);
162
163 let resp = ws::start(player_ws, &req, stream);
164 log::info!("New random play");
165 resp
166}
167
168pub async fn start_server(args: Args) -> std::io::Result<()> {
169 log::info!("Start to listen on {}", args.listen_address);
170
171 let args_clone = args.clone();
172
173 let dispatcher_actor = DispatcherActor::default().start();
174
175 HttpServer::new(move || {
176 let mut cors = Cors::default();
177 match args_clone.cors.as_deref() {
178 Some("*") => cors = cors.allow_any_origin(),
179 Some(orig) => cors = cors.allowed_origin(orig),
180 None => {}
181 }
182
183 App::new()
184 .app_data(web::Data::new(dispatcher_actor.clone()))
185 .wrap(cors)
186 .route("/version", web::get().to(version_information))
187 .route("/config", web::get().to(game_configuration))
188 .route("/game_rules/default", web::get().to(default_game_rules))
189 .route("/game_rules/validate", web::post().to(validate_game_rules))
190 .route("/generate_boats_layout", web::post().to(gen_boats_layout))
191 .route("/play/bot", web::get().to(start_bot_play))
192 .route("/play/create_invite", web::get().to(start_create_invite))
193 .route("/play/accept_invite", web::get().to(start_accept_invite))
194 .route("/play/random", web::get().to(start_random))
195 .route("/", web::get().to(index))
196 .route("{tail:.*}", web::get().to(not_found))
197 })
198 .bind(&args.listen_address)?
199 .run()
200 .await
201}
202
203#[cfg(test)]
204mod test {
205 use crate::data::GameRules;
206 use crate::server::BotPlayQuery;
207
208 #[test]
209 fn simple_bot_request_serialize_deserialize() {
210 let query = BotPlayQuery {
211 rules: Default::default(),
212 player_name: "Player".to_string(),
213 };
214
215 let string = serde_urlencoded::to_string(&query).unwrap();
216 let des = serde_urlencoded::from_str(&string).unwrap();
217
218 assert_eq!(query, des)
219 }
220
221 #[test]
222 fn simple_bot_request_serialize_deserialize_no_timeout() {
223 let query = BotPlayQuery {
224 rules: GameRules {
225 strike_timeout: None,
226 ..Default::default()
227 },
228 player_name: "Player".to_string(),
229 };
230
231 let string = serde_urlencoded::to_string(&query).unwrap();
232 let des = serde_urlencoded::from_str(&string).unwrap();
233
234 assert_eq!(query, des)
235 }
236}