From 061da11d43c1b71c8625327ae854a48f60af428c Mon Sep 17 00:00:00 2001 From: Rembane Date: Fri, 21 Apr 2017 15:49:42 +0200 Subject: [PATCH] Refactored the Rust Snakebot codebase. (#7) * Normal logging now logs to stdout; * Renamed Inbound::GameLinkEvent to Inbound::Gamelink; * Renamed the struct GameResultSnake to GameResult; * Added Inbound::GameResult; * Rewrote the message handling to become more succinct; * Turned the default_gamesettings function into a Default impl; * Made the snake smaller and prettier; * Replaced some direction checking code with less code; * Added logging messages to all callbacks; * Added as_movement_delta to the Direction impl; * Added nice error messages to the message parsing; * Moved some code in maputil to increase the DRY-factor. --- log4rs.toml | 2 +- src/main.rs | 70 ++++++++---------- src/maputil.rs | 39 +++++----- src/messages.rs | 186 +++++++++++++++++------------------------------- src/snake.rs | 37 ++++------ src/structs.rs | 83 ++++++--------------- 6 files changed, 156 insertions(+), 261 deletions(-) diff --git a/log4rs.toml b/log4rs.toml index 911e93a..56cd80e 100644 --- a/log4rs.toml +++ b/log4rs.toml @@ -23,5 +23,5 @@ additive = false [loggers.snake] level = "debug" -appenders = [ "snake" ] +appenders = [ "console" ] additive = false diff --git a/src/main.rs b/src/main.rs index a38cc14..e2b1dd5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,13 @@ +#![allow(non_snake_case)] #[macro_use] extern crate log; #[macro_use] extern crate quick_error; #[macro_use] extern crate serde_derive; +#[macro_use] extern crate serde_json; extern crate clap; extern crate config; extern crate log4rs; extern crate rustc_version; extern crate serde; -extern crate serde_json; extern crate target_info; extern crate ws; @@ -17,7 +18,7 @@ mod structs; mod util; use clap::{ Arg, App }; -use messages::{ Inbound }; +use messages::{ Inbound, Outbound, handle_inbound_msg, render_outbound_message }; use snake::{ Snake }; use std::path::Path; use std::string::{ String }; @@ -74,9 +75,8 @@ struct Client { fn route_msg(client: &mut Client, str_msg: &String) -> Result<(), ClientError> { let snake = &mut client.snake; - let inbound_msg = try!(messages::parse_inbound_msg(str_msg)); - match inbound_msg { + match try!(handle_inbound_msg(str_msg)) { Inbound::GameEnded(msg) => { snake.on_game_ended(&msg); if client.config.venue == "training" { @@ -88,10 +88,13 @@ fn route_msg(client: &mut Client, str_msg: &String) -> Result<(), ClientError> { try!(client.out.close(ws::CloseCode::Normal)); }, Inbound::MapUpdate(msg) => { - let direction = maputil::direction_as_string(&snake.get_next_move(&msg)); - let response = try!(messages::create_register_move_msg(direction, msg)); - debug!(target: LOG_TARGET, "Responding with RegisterMove {:?}", response); - try!(client.out.send(response)); + let m = render_outbound_message(Outbound::RegisterMove { + direction: snake.get_next_move(&msg), + gameTick: msg.gameTick, + receivingPlayerId: msg.receivingPlayerId, + gameId: msg.gameId }); + debug!(target: LOG_TARGET, "Responding with RegisterMove {:?}", m); + try!(client.out.send(m)); }, Inbound::SnakeDead(msg) => { snake.on_snake_dead(&msg); @@ -104,9 +107,9 @@ fn route_msg(client: &mut Client, str_msg: &String) -> Result<(), ClientError> { snake.on_player_registered(&msg); if msg.gameMode == "TRAINING" { - let response = try!(messages::create_start_game_msg()); - debug!(target: LOG_TARGET, "Requesting a game start {:?}", response); - try!(client.out.send(response)); + let m = render_outbound_message(Outbound::StartGame); + debug!(target: LOG_TARGET, "Requesting a game start {:?}", m); + try!(client.out.send(m)); }; info!(target: LOG_TARGET, "Starting heart beat"); @@ -119,9 +122,12 @@ fn route_msg(client: &mut Client, str_msg: &String) -> Result<(), ClientError> { Inbound::HeartBeatResponse(_) => { // do nothing }, - Inbound::GameLinkEvent(msg) => { + Inbound::GameLink(msg) => { info!(target: LOG_TARGET, "Watch game at {}", msg.url); }, + Inbound::GameResult(msg) => { + info!(target: LOG_TARGET, "We got some game result! {:?}", msg); + }, Inbound::UnrecognizedMessage => { error!(target: LOG_TARGET, "Received unrecognized message {:?}", str_msg); } @@ -130,28 +136,17 @@ fn route_msg(client: &mut Client, str_msg: &String) -> Result<(), ClientError> { Ok(()) } - impl ws::Handler for Client { fn on_open(&mut self, _: ws::Handshake) -> ws::Result<()> { debug!(target: LOG_TARGET, "Connection to Websocket opened"); - - let client_info = messages::create_client_info_msg(); - if let Ok(message) = client_info { - info!(target: LOG_TARGET, "Sending client info to server: {:?}", message); - try!(self.out.send(message)); - } else { - error!(target: LOG_TARGET, "Unable to create client info message {:?}", client_info); - try!(self.out.close(ws::CloseCode::Error)); - } - - let parse_msg = messages::create_play_registration_msg(self.config.snake_name.clone()); - if let Ok(response) = parse_msg { - info!(target: LOG_TARGET, "Registering player with message: {:?}", response); - self.out.send(response) - } else { - error!(target: LOG_TARGET, "Unable to create play registration message {:?}", parse_msg); - self.out.close(ws::CloseCode::Error) - } + let m = render_outbound_message(Outbound::ClientInfo); + info!(target: LOG_TARGET, "Sending client info to server: {:?}", m); + try!(self.out.send(m)); + let msg = render_outbound_message(Outbound::RegisterPlayer { + playerName: self.config.snake_name.clone(), + gameSettings: Default::default() }); + info!(target: LOG_TARGET, "Registering player with message: {:?}", msg); + self.out.send(msg) } fn on_message(&mut self, msg: ws::Message) -> ws::Result<()> { @@ -251,16 +246,9 @@ fn do_heart_beat(id: String, out: Arc, done_receiver: mpsc::Receiver } debug!(target: LOG_TARGET, "Sending heartbeat request"); - - let id = id.clone(); - let parsed_msg = messages::create_heart_beat_msg(id); - if let Ok(heart_beat) = parsed_msg { - let send_result = out.send(heart_beat); - if let Err(e) = send_result { - error!(target: LOG_TARGET, "Unable to send heartbeat, got error {:?}", e); - } - } else { - error!(target: LOG_TARGET, "Unable to parse heart beat message {:?}", parsed_msg); + let send_result = out.send(render_outbound_message(Outbound::HeartBeat { receivingPlayerId: id.clone() })); + if let Err(e) = send_result { + error!(target: LOG_TARGET, "Unable to send heartbeat, got error {:?}", e); } } } diff --git a/src/maputil.rs b/src/maputil.rs index 148c88e..e355e40 100644 --- a/src/maputil.rs +++ b/src/maputil.rs @@ -1,5 +1,6 @@ use structs::{ Map, SnakeInfo }; use util; +use serde::ser::{ Serialize, Serializer }; #[derive(PartialEq, Debug)] pub enum Tile<'a> { @@ -11,7 +12,7 @@ pub enum Tile<'a> { SnakeBody { coordinate: (i32,i32), snake: &'a SnakeInfo } } -#[derive(Debug)] +#[derive(Clone, Copy, Debug)] pub enum Direction { Down, Up, @@ -19,23 +20,27 @@ pub enum Direction { Right } -pub fn direction_as_string(direction: &Direction) -> String { - let s = match direction { - &Direction::Down => "DOWN", - &Direction::Up => "UP", - &Direction::Left => "LEFT", - &Direction::Right => "RIGHT" - }; - - String::from(s) +impl Serialize for Direction { + fn serialize(&self, serializer: S) -> Result + where S: Serializer + { + serializer.serialize_str(match *self { + Direction::Down => "DOWN", + Direction::Up => "UP", + Direction::Left => "LEFT", + Direction::Right => "RIGHT", + }) + } } -pub fn direction_as_movement_delta(direction: &Direction) -> (i32,i32) { - match direction { - &Direction::Down => (0, 1), - &Direction::Up => (0, -1), - &Direction::Left => (-1, 0), - &Direction::Right => (1, 0) +impl Direction { + pub fn as_movement_delta(&self) -> (i32,i32) { + match *self { + Direction::Down => ( 0, 1), + Direction::Up => ( 0, -1), + Direction::Left => (-1, 0), + Direction::Right => ( 1, 0) + } } } @@ -83,7 +88,7 @@ impl Map { } pub fn can_snake_move_in_direction(&self, snake: &SnakeInfo, direction: Direction) -> bool { - let (xd,yd) = direction_as_movement_delta(&direction); + let (xd,yd) = direction.as_movement_delta(); let (x,y) = util::translate_position(snake.positions[0], self.width); self.is_tile_available_for_movement((x+xd,y+yd)) diff --git a/src/messages.rs b/src/messages.rs index e91f6ba..66e9cdb 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -1,40 +1,12 @@ use structs; -use serde_json::{ from_str, to_string, Error }; use target_info::Target; -use rustc_version; - -// Inbound -pub const GAME_ENDED: &'static str = - "se.cygni.snake.api.event.GameEndedEvent"; -pub const TOURNAMENT_ENDED: &'static str = - "se.cygni.snake.api.event.TournamentEndedEvent"; -pub const MAP_UPDATE: &'static str = - "se.cygni.snake.api.event.MapUpdateEvent"; -pub const SNAKE_DEAD: &'static str = - "se.cygni.snake.api.event.SnakeDeadEvent"; -pub const GAME_STARTING: &'static str = - "se.cygni.snake.api.event.GameStartingEvent"; -pub const PLAYER_REGISTERED: &'static str = - "se.cygni.snake.api.response.PlayerRegistered"; -pub const INVALID_PLAYER_NAME: &'static str = - "se.cygni.snake.api.exception.InvalidPlayerName"; -pub const HEART_BEAT_RESPONSE: &'static str = - "se.cygni.snake.api.response.HeartBeatResponse"; -pub const GAME_LINK_EVENT: &'static str = - "se.cygni.snake.api.event.GameLinkEvent"; - -// Outbound -const REGISTER_PLAYER_MESSAGE_TYPE: &'static str = - "se.cygni.snake.api.request.RegisterPlayer"; -const START_GAME: &'static str = - "se.cygni.snake.api.request.StartGame"; -const REGISTER_MOVE: &'static str = - "se.cygni.snake.api.request.RegisterMove"; -const HEART_BEAT_REQUEST: &'static str = - "se.cygni.snake.api.request.HeartBeatRequest"; -const CLIENT_INFO: &'static str = - "se.cygni.snake.api.request.ClientInfo"; +use rustc_version::{version}; +use maputil::{Direction}; +use structs::{GameSettings}; +use serde_json::{ from_str, from_value, Error, Map, Value }; +use std::iter::FromIterator; +#[derive(Serialize, Deserialize, Debug)] pub enum Inbound { GameEnded(structs::GameEnded), TournamentEnded(structs::TournamentEnded), @@ -44,97 +16,73 @@ pub enum Inbound { PlayerRegistered(structs::PlayerRegistered), InvalidPlayerName(structs::InvalidPlayerName), HeartBeatResponse(structs::HeartBeatResponse), - GameLinkEvent(structs::GameLink), + GameLink(structs::GameLink), + GameResult(structs::GameResult), UnrecognizedMessage } -pub fn parse_inbound_msg(msg: &String) -> Result { - let msg: Inbound = - if msg.contains(GAME_ENDED) { - Inbound::GameEnded(try!(from_str(msg))) - } else if msg.contains(TOURNAMENT_ENDED) { - Inbound::TournamentEnded(try!(from_str(msg))) - } else if msg.contains(MAP_UPDATE) { - Inbound::MapUpdate(try!(from_str(msg))) - } else if msg.contains(SNAKE_DEAD) { - Inbound::SnakeDead(try!(from_str(msg))) - } else if msg.contains(GAME_STARTING) { - Inbound::GameStarting(try!(from_str(msg))) - } else if msg.contains(PLAYER_REGISTERED) { - Inbound::PlayerRegistered(try!(from_str(msg))) - } else if msg.contains(INVALID_PLAYER_NAME) { - Inbound::InvalidPlayerName(try!(from_str(msg))) - } else if msg.contains(HEART_BEAT_RESPONSE) { - Inbound::HeartBeatResponse(try!(from_str(msg))) - } else if msg.contains(GAME_LINK_EVENT) { - Inbound::GameLinkEvent(try!(from_str(msg))) - } else { - Inbound::UnrecognizedMessage - }; - - Ok(msg) +/// We turn the string into `Inbound` by converting the string into a +/// JSON object, extracting the type-field from the object, and using the +/// last part of the type-field to get the correct constructor in `Inbound`. +/// Then we let Serde do its magic and deserialize a constructed JSON object +/// with the constructor name as type. If the type has a Event suffix it is +/// removed since almost all `Inbound` messages are events. +/// +/// Example: +/// { type: "foo.bar.baz.GameResult", } +/// ---------- This is the part we extract and hand to serde. +/// Like this: {GameResult: } +/// +pub fn handle_inbound_msg(s: &str) -> Result { + let mut json_value = from_str::(s) + .expect(&format!("Couldn't parse string into JSON: {:?}", s)); + let mut map = json_value.as_object_mut() + .expect(&format!("Couldn't parse string into JSON object: {:?}", s)); + let type_value = map.remove("type").expect(&format!("Couldn't find key `type` in: {:?}", &map)); + let type_str = type_value.as_str().expect(&format!("Couldn't turn JSON Value into string: {:?}", &map)); + let typ = type_str.rsplit('.').next() + .expect(&format!("The type parser needs a dot-separated string, this string lacks dots: {:?}", type_str)) + .replace("Event", ""); + from_value(Value::Object(Map::from_iter(vec![(typ, Value::Object(map.clone()))]))) } - -pub fn create_play_registration_msg(name: String) -> Result { - to_string(&structs::PlayRegistration { - type_: String::from(REGISTER_PLAYER_MESSAGE_TYPE), - playerName: name, - gameSettings: default_gamesettings() - }) +pub enum Outbound { + RegisterPlayer{playerName: String, gameSettings: GameSettings}, + StartGame, + RegisterMove{direction: Direction, gameTick: u32, receivingPlayerId: String, gameId: String}, + HeartBeat{receivingPlayerId: String}, + ClientInfo, } -pub fn create_start_game_msg() -> Result { - to_string(&structs::StartGame { - type_: String::from(START_GAME) - }) +pub fn render_outbound_message(msg: Outbound) -> String { + (match msg { + Outbound::RegisterPlayer {playerName, gameSettings} => json!({ + "type": "se.cygni.snake.api.request.RegisterPlayer", + "playerName": playerName, + "gameSettings": gameSettings + }), + Outbound::StartGame => json!({ + "type": "se.cygni.snake.api.request.StartGame", + }), + Outbound::RegisterMove {direction, gameTick, receivingPlayerId, gameId} => json!({ + "type": "se.cygni.snake.api.request.RegisterMove", + "direction": direction, + "gameTick": gameTick, + "receivingPlayerId": receivingPlayerId, + "gameId": gameId, + }), + Outbound::HeartBeat {receivingPlayerId} => json!({ + "type": "se.cygni.snake.api.request.HeartBeatRequest", + "receivingPlayerId": receivingPlayerId, + }), + Outbound::ClientInfo => json!({ + "type": "se.cygni.snake.api.request.ClientInfo", + "language": "Rust", + "languageVersion": version().unwrap().to_string(), + "operatingSystem": Target::os(), + "operatingSystemVersion": "???", + "clientVersion": option_env!("CARGO_PKG_VERSION").unwrap_or("0.1337"), + }), + }).to_string() } -pub fn create_register_move_msg(direction: String, request: structs::MapUpdate) -> Result { - to_string(&structs::RegisterMove { - type_: String::from(REGISTER_MOVE), - direction: direction, - gameTick: request.gameTick, - receivingPlayerId: request.receivingPlayerId, - gameId: request.gameId - }) -} - -pub fn create_heart_beat_msg(id: String) -> Result { - to_string(&structs::HeartBeatRequest { - type_: String::from( HEART_BEAT_REQUEST ), - receivingPlayerId: id - }) -} - -pub fn create_client_info_msg() -> Result { - to_string(&structs::ClientInfo { - type_: String::from(CLIENT_INFO), - language: String::from("rust"), - languageVersion: format!("{}", rustc_version::version().unwrap()), - operatingSystem: String::from(Target::os()), - operatingSystemVersion: String::from(""), - clientVersion: String::from(option_env!("CARGO_PKG_VERSION").unwrap_or("")) - }) -} - -pub fn default_gamesettings() -> structs::GameSettings { - structs::GameSettings { - maxNoofPlayers: 5, - startSnakeLength: 1, - timeInMsPerTick: 250, - obstaclesEnabled: true, - foodEnabled: true, - headToTailConsumes: true, - tailConsumeGrows: false, - addFoodLikelihood: 15, - removeFoodLikelihood: 5, - spontaneousGrowthEveryNWorldTick: 3, - trainingGame: false, - pointsPerLength: 1, - pointsPerFood: 2, - pointsPerCausedDeath: 5, - pointsPerNibble: 10, - noofRoundsTailProtectedAfterNibble: 3, - } -} diff --git a/src/snake.rs b/src/snake.rs index fcbfa99..22e8bdd 100644 --- a/src/snake.rs +++ b/src/snake.rs @@ -1,4 +1,4 @@ -use structs::{ MapUpdate, GameEnded, TournamentEnded, SnakeDead, GameStarting, PlayerRegistered, InvalidPlayerName}; +use structs::{ MapUpdate, GameEnded, TournamentEnded, SnakeDead, GameStarting, PlayerRegistered, InvalidPlayerName }; use maputil::{ Direction }; use util::{ translate_positions }; @@ -10,28 +10,19 @@ impl Snake { pub fn get_next_move(&self, msg: &MapUpdate) -> Direction { debug!(target: LOG_TARGET, "Game map updated, tick: {}", msg.gameTick); - let ref map = msg.map; - let player_id = &msg.receivingPlayerId; - let snake = map.get_snake_by_id(player_id).unwrap(); + let map = &msg.map; + let snake = map.get_snake_by_id(&msg.receivingPlayerId).unwrap(); debug!(target: LOG_TARGET, "Food can be found at {:?}", translate_positions(&map.foodPositions, map.width)); debug!(target: LOG_TARGET, "My snake positions are {:?}", translate_positions(&snake.positions, map.width)); - - let direction = if map.can_snake_move_in_direction(snake, Direction::Down) { - Direction::Down - } else if map.can_snake_move_in_direction(snake, Direction::Left) { - Direction::Left - } else if map.can_snake_move_in_direction(snake, Direction::Right) { - Direction::Right - } else if map.can_snake_move_in_direction(snake, Direction::Up) { - Direction::Up - } else { - // this is bad - Direction::Down - }; - - debug!(target: LOG_TARGET, "Snake will move in direction {:?}", direction); - direction + for &d in [Direction::Down, Direction::Left, Direction::Right, Direction::Up].into_iter() { + if map.can_snake_move_in_direction(snake, d) { + debug!(target: LOG_TARGET, "Snake will move in direction {:?}", d); + return d; + } + } + debug!(target: LOG_TARGET, "Snake cannot but will move down."); + return Direction::Down; } pub fn on_game_ended(&self, msg: &GameEnded) { @@ -47,14 +38,14 @@ impl Snake { } pub fn on_game_starting(&self, _: &GameStarting) { - + debug!(target: LOG_TARGET, "All snakes are ready to rock. Game is starting."); } pub fn on_player_registered(&self, _: &PlayerRegistered) { - + debug!(target: LOG_TARGET, "Player has been registered."); } pub fn on_invalid_playername(&self, _: &InvalidPlayerName) { - + debug!(target: LOG_TARGET, "Player name invalid."); } } diff --git a/src/structs.rs b/src/structs.rs index a842974..c5bfdc8 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -20,52 +20,31 @@ pub struct GameSettings { pub noofRoundsTailProtectedAfterNibble: u32, } -#[derive(Serialize, Deserialize, Debug)] -pub struct PlayRegistration { - #[serde(rename="type")] - pub type_: String, - pub playerName: String, - pub gameSettings: GameSettings, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct ClientInfo { - #[serde(rename="type")] - pub type_: String, - pub language: String, - pub languageVersion: String, - pub operatingSystem: String, - pub operatingSystemVersion: String, - pub clientVersion: String -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct RegisterMove { - #[serde(rename="type")] - pub type_: String, - pub direction: String, - pub gameTick: u32, - pub receivingPlayerId: String, - pub gameId: String -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct StartGame { - #[serde(rename="type")] - pub type_: String, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct HeartBeatRequest { - #[serde(rename="type")] - pub type_: String, - pub receivingPlayerId: String +impl Default for GameSettings { + fn default() -> GameSettings { + GameSettings { + maxNoofPlayers: 5, + startSnakeLength: 1, + timeInMsPerTick: 250, + obstaclesEnabled: true, + foodEnabled: true, + headToTailConsumes: true, + tailConsumeGrows: false, + addFoodLikelihood: 15, + removeFoodLikelihood: 5, + spontaneousGrowthEveryNWorldTick: 3, + trainingGame: false, + pointsPerLength: 1, + pointsPerFood: 2, + pointsPerCausedDeath: 5, + pointsPerNibble: 10, + noofRoundsTailProtectedAfterNibble: 3, + } + } } #[derive(Serialize, Deserialize, Debug)] pub struct PlayerRegistered { - #[serde(rename="type")] - pub type_: String, pub gameId: String, pub gameMode: String, pub receivingPlayerId: String, @@ -75,8 +54,6 @@ pub struct PlayerRegistered { #[derive(Serialize, Deserialize, Debug)] pub struct MapUpdate { - #[serde(rename="type")] - pub type_: String, pub receivingPlayerId: String, pub gameId: String, pub gameTick: u32, @@ -85,15 +62,11 @@ pub struct MapUpdate { #[derive(Serialize, Deserialize, Debug)] pub struct InvalidPlayerName { - #[serde(rename="type")] - pub type_: String, pub reasonCode: u32, } #[derive(Serialize, Deserialize, Debug)] pub struct GameEnded { - #[serde(rename="type")] - pub type_: String, pub receivingPlayerId: String, pub playerWinnerId: String, pub gameId: String, @@ -103,8 +76,6 @@ pub struct GameEnded { #[derive(Serialize, Deserialize, Debug)] pub struct SnakeDead { - #[serde(rename="type")] - pub type_: String, pub playerId: String, pub x: u32, pub y: u32, @@ -115,8 +86,6 @@ pub struct SnakeDead { #[derive(Serialize, Deserialize, Debug)] pub struct GameStarting { - #[serde(rename="type")] - pub type_: String, pub receivingPlayerId: String, pub gameId: String, pub noofPlayers: u32, @@ -126,15 +95,11 @@ pub struct GameStarting { #[derive(Serialize, Deserialize, Debug)] pub struct HeartBeatResponse { - #[serde(rename="type")] - pub type_: String, pub receivingPlayerId: String } #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct GameLink { - #[serde(rename="type")] - pub type_: String, pub receivingPlayerId: String, pub gameId: String, pub url: String, @@ -142,18 +107,16 @@ pub struct GameLink { #[derive(Serialize, Deserialize, Debug)] pub struct TournamentEnded { - #[serde(rename="type")] - pub type_: String, pub receivingPlayerId: String, pub tournamentId: String, pub tournamentName: String, - pub gameResult: Vec, + pub gameResult: Vec, pub gameId: String, pub playerWinnerId: String, } #[derive(Serialize, Deserialize, Debug)] -pub struct GameResultSnake { +pub struct GameResult { pub points: i32, pub playerId: String, pub name: String