sea_battle_backend/
server.rs

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
11/// The default '/' route
12async fn index() -> impl Responder {
13    HttpResponse::Ok().json("Sea battle backend")
14}
15
16/// The default 404 route
17async fn not_found() -> impl Responder {
18    HttpResponse::NotFound().json("You missed your strike lol")
19}
20
21/// Get version information
22async fn version_information() -> impl Responder {
23    HttpResponse::Ok().json(VersionInfo::load_static())
24}
25
26/// Get game configuration
27async fn game_configuration() -> impl Responder {
28    HttpResponse::Ok().json(PlayConfiguration::default())
29}
30
31/// Get default game rules
32async fn default_game_rules() -> impl Responder {
33    HttpResponse::Ok().json(GameRules::random_players_rules())
34}
35
36/// Validate game rules
37async fn validate_game_rules(rules: web::Json<GameRules>) -> impl Responder {
38    HttpResponse::Ok().json(rules.get_errors())
39}
40
41/// Generate random boats layout
42async 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
67/// Start bot game
68async 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
97/// Start game by creating invite
98async 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
129/// Start game by creating invite
130async 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
154/// Start game, playing against a random person
155async 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}