commit 01c87721ae73e364981f0b7564851cedee5c9de5 Author: Martin Barksten Date: Tue Jun 7 20:04:57 2016 +0200 A Rust client (#18) * Initial commit The client is currently only capable of registering for play * Switch to serde for json serialization * Parse all JSON messages Or rather all messages sent in the game * Switch to rust nightly for serde This mainly solves the whole json parsing problem reasonably well * Move move logic to snake module * Add util functions * Improve error handling in main * Implement utility functions on map struct And keep those that do not deal with the map in the util module * Refactor and improve the maputils * Add test cases to maputil functions Also fix the snake panicking due to an incorrect unwrap * Fix snake panicking due to bad unwrap Missed staging these in the last commit... * Add logging * Add a heart beat to the client Needs some proper error handling however * Handle errors properly in main.rs Also refactor to improve readability * Print what is happening to console * Add readme file Also lock the package versions used and update to latest nightly diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..261c02a --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ + +# Created by https://www.gitignore.io/api/rust + +### Rust ### +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock + +# Ignore logfiles +/log/ +/src/log/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..af48ce5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "snakebot_rust" +version = "0.1.0" +authors = ["Martin Barksten "] + +[dependencies] +ws = "0.4.8" +serde = "0.7.7" +serde_json = "0.7.1" +serde_macros = "0.7.7" +quick-error = "1.1.0" +log = "0.3.6" +log4rs = { version = "0.4.6", features = ["toml"] } diff --git a/log4rs.toml b/log4rs.toml new file mode 100644 index 0000000..25cf060 --- /dev/null +++ b/log4rs.toml @@ -0,0 +1,32 @@ +[appenders.console] +level = "info" +kind = "console" + +[appenders.console.encoder] +pattern = "{d(%+)(local)} [{t}] {h({l})} {M}:{m}{n}" + +[appenders.client] +kind = "file" +path = "log/client.log" +pattern = "{d} [{t}] {l} {M}:{m}{n}" +level = "debug" + +[appenders.snake] +kind = "file" +path = "log/snake.log" +pattern = "{d} [{t}] {l} {M}:{m}{n}" +level = "debug" + +[root] +level = "info" +appenders = [ "console" ] + +[logger.client] +level = "debug" +appenders = [ "client" ] +additive = false + +[logger.snake] +level = "debug" +appenders = [ "snake", "console" ] +additive = false diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..76f1a8b --- /dev/null +++ b/readme.md @@ -0,0 +1,27 @@ +# SNAKE CLIENT + +Do you want the most annoying compiler ever? +Do you want to constantly think of what is owning what variable? +Do you want to stare angrily at the screen and wonder what the hell it means that some dumb value can't be moved? +Then here is the ultimate snake client for you, written for the beautiful language Rust. + +## Requirements + +* Rust nightly (I recommend using [rustup](https://github.com/rust-lang-nursery/rustup.rs) to install it) +* Cargo (should automatically be installed by rustup) +* Snake Server (local or remote) + +The packages used have a tendency to sometimes break due to using the nightly build, +so if it doesn't work try to install specifically: *rustc 1.11.0-nightly (12238b984 2016-06-04)*. + +## Setup + +A. Clone the repository: `git clone https://github.com/cygni/snakebot-clients.git`; + +B. Open: `/snakebot-rust`; + +C. Run the snake: `cargo run`; + +D. Improve the snake: edit `src/snake.rs`, and more specifically `get_next_move`. + +E. Debugging: see `log/snake.log` for all log output from the snake. diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c2031a0 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,229 @@ +#![feature(custom_derive, plugin)] +#![plugin(serde_macros)] + +extern crate serde_json; +extern crate ws; +extern crate serde; +#[macro_use] extern crate quick_error; +#[macro_use] extern crate log; +extern crate log4rs; + +mod messages; +mod snake; +mod util; +mod maputil; + +use snake::{ Snake }; +use std::string::{ String }; +use std::thread; +use std::time::Duration; +use std::sync::mpsc; +use std::sync::Arc; + +const HOST: &'static str = "snake.cygni.se"; +const PORT: i32 = 80; +const MODE: &'static str = "training"; + +const HEART_BEAT_S: u64 = 20; +const LOG_TARGET: &'static str = "client"; + +quick_error! { + #[derive(Debug)] + pub enum ClientError { + Message(err: serde_json::Error) { + from() + } + + Websocket(err: ws::Error) { + from() + } + + StringChannel(err: mpsc::SendError) { + from() + } + + WebsocketChannel(err: mpsc::SendError>) { + from() + } + } +} + +struct Client { + out: Arc, + snake: Snake, + out_sender: mpsc::Sender>, + id_sender: mpsc::Sender, +} + +fn route_msg(client: &mut Client, msg: &String) -> Result<(), ClientError> { + let snake = &mut client.snake; + + if msg.contains(messages::GAME_ENDED) { + let json_msg: messages::GameEnded = try!(serde_json::from_str(msg)); + snake.on_game_ended(&json_msg); + } else if msg.contains(messages::MAP_UPDATE) { + let json_msg: messages::MapUpdate = try!(serde_json::from_str(msg)); + let direction = snake.get_next_move(&json_msg); + + let response = messages::RegisterMove { + type_: String::from(messages::REGISTER_MOVE), + direction: maputil::direction_as_string(&direction), + gameTick: json_msg.gameTick, + receivingPlayerId: json_msg.receivingPlayerId, + gameId: json_msg.gameId + }; + debug!(target: LOG_TARGET, "Responding with RegisterMove {:?}", response); + + let response = try!(serde_json::to_string(&response)); + try!(client.out.send(response)); + } else if msg.contains(messages::SNAKE_DEAD) { + let json_msg: messages::SnakeDead = try!(serde_json::from_str(msg)); + snake.on_snake_dead(&json_msg); + } else if msg.contains(messages::GAME_STARTING) { + let json_msg: messages::GameStarting = try!(serde_json::from_str(msg)); + snake.on_game_starting(&json_msg); + } else if msg.contains(messages::PLAYER_REGISTERED) { + let json_msg: messages::PlayerRegistered = try!(serde_json::from_str(msg)); + info!(target: LOG_TARGET, "Successfully registered player"); + + snake.on_player_registered(&json_msg); + + if json_msg.gameMode == "TRAINING" { + let start_msg = messages::StartGame { + type_: String::from(messages::START_GAME) + }; + debug!(target: LOG_TARGET, "Requesting a game start {:?}", start_msg); + + let response = try!(serde_json::to_string(&start_msg)); + try!(client.out.send(response)); + }; + + try!(client.out_sender.send(client.out.clone())); + try!(client.id_sender.send(json_msg.receivingPlayerId)); + } else if msg.contains(messages::INVALID_PLAYER_NAME) { + let json_msg: messages::InvalidPlayerName = try!(serde_json::from_str(msg)); + snake.on_invalid_playername(&json_msg); + } else if msg.contains(messages::HEART_BEAT_RESPONSE) { + // do nothing + let _: messages::InvalidPlayerName = try!(serde_json::from_str(msg)); + } + + Ok(()) +} + + +impl ws::Handler for Client { + fn on_open(&mut self, _: ws::Handshake) -> ws::Result<()> { + debug!(target: LOG_TARGET, "Connection to Websocket opened"); + + let message = messages::PlayRegistration { + type_: String::from(messages::REGISTER_PLAYER_MESSAGE_TYPE), + playerName: self.snake.get_name(), + gameSettings: messages::default_gamesettings() + }; + + info!(target: LOG_TARGET, "Registering player with message: {:?}", message); + + let encoded_message = serde_json::to_string(&message).unwrap(); + self.out.send(encoded_message) + } + + fn on_message(&mut self, msg: ws::Message) -> ws::Result<()> { + if let ws::Message::Text(text) = msg { + let route_result = route_msg(self, &text); + match route_result { + Err(e) => error!(target: LOG_TARGET, "Got error {} when routing message: {}", e, text), + Ok(_) => debug!(target: LOG_TARGET, "Succeeded in routing message {}", text) + } + } else { + warn!(target: LOG_TARGET, "Unexpectedly received non-string message: {}", msg) + } + + Ok(()) + } +} + +fn start_websocket_thread(id_sender: mpsc::Sender, + out_sender: mpsc::Sender>) -> thread::JoinHandle<()> { + thread::spawn(move || { + let connection_url = format!("ws://{}:{}/{}", HOST, PORT, MODE); + info!(target: LOG_TARGET, "Connecting to {:?}", connection_url); + let result = ws::connect(connection_url, |out| { + Client { + out: Arc::from(out), + snake: snake::Snake, + out_sender: out_sender.clone(), + id_sender: id_sender.clone(), + } + }); + debug!(target: LOG_TARGET, "Websocket is done, result {:?}", result); + }) +} + +fn start_heart_beat_thread(id_receiver: mpsc::Receiver, + out_receiver: mpsc::Receiver>, + done_receiver: mpsc::Receiver<()>) -> thread::JoinHandle<()> { + thread::spawn(move || { + let id = id_receiver.recv().unwrap(); + let out = out_receiver.recv().unwrap(); + + debug!(target: LOG_TARGET, "Starting heartbeat"); + + loop { + thread::sleep(Duration::from_secs(HEART_BEAT_S)); + let rec = done_receiver.try_recv(); + + // if the channel is disconnected or a done message is sent, break the loop + if let Err(e) = rec { + if e == mpsc::TryRecvError::Disconnected { + debug!(target: LOG_TARGET, "Stopping heartbeat due to channel disconnecting"); + break; + } + } else { + debug!(target: LOG_TARGET, "Stopping heartbeat due to finished execution"); + break; + } + + let id = id.clone(); + let heart_beat = messages::HeartBeatRequest { + type_: String::from( messages::HEART_BEAT_REQUEST ), + receivingPlayerId: id + }; + + debug!(target: LOG_TARGET, "Sending heartbeat request"); + let request = serde_json::to_string(&heart_beat).unwrap(); + let send_result = out.send(request); + if let Err(e) = send_result { + error!(target: LOG_TARGET, "Unable to send heartbeat, got error {:?}", e); + } + } + }) +} + +fn start_client() { + let (id_sender,id_receiver) = mpsc::channel(); + let (out_sender,out_receiver) = mpsc::channel(); + let (done_sender,done_receiver) = mpsc::channel(); + + let websocket = start_websocket_thread(id_sender, out_sender); + let heartbeat = start_heart_beat_thread(id_receiver, out_receiver, done_receiver); + + let websocket_res = websocket.join(); + debug!(target: LOG_TARGET, "Joining Websocket thread gave result {:?}", websocket_res); + + let send_res = done_sender.send(()); + if let Err(e) = send_res { + error!(target: LOG_TARGET, "Unable to send done message, got error {:?}", e); + } + + let heartbeat_res = heartbeat.join(); + debug!(target: LOG_TARGET, "Joining heartbeat thread gave result {:?}", heartbeat_res); + +} + +fn main() { + if let Err(_) = log4rs::init_file("log4rs.toml", Default::default()) { + log4rs::init_file("../log4rs.toml", Default::default()).unwrap(); + } + start_client(); +} diff --git a/src/maputil.rs b/src/maputil.rs new file mode 100644 index 0000000..1024b6d --- /dev/null +++ b/src/maputil.rs @@ -0,0 +1,195 @@ +use messages::{ Map, SnakeInfo }; +use util; + +#[derive(PartialEq, Debug)] +pub enum Tile<'a> { + Food { coordinate: (i32,i32) }, + Obstacle { coordinate: (i32,i32) }, + Empty { coordinate: (i32,i32) }, + SnakeHead { coordinate: (i32,i32), snake: &'a SnakeInfo }, + SnakeBody { coordinate: (i32,i32), snake: &'a SnakeInfo } +} + +#[derive(Debug)] +pub enum Direction { + Down, + Up, + Left, + 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) +} + +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 Map { + pub fn get_snake_by_id<'a>(&'a self, id: &String) -> Option<&'a SnakeInfo> { + self.snakeInfos.iter().find(|s| &s.id == id) + } + + pub fn get_tile_at(&self, coordinate: (i32,i32)) -> Tile { + let position = util::translate_coordinate(coordinate, self.width); + let snake_at_tile = self.snakeInfos.iter().find(|s| s.positions.contains(&position)); + + if self.obstaclePositions.contains(&position) { + Tile::Obstacle { coordinate: coordinate } + } else if self.foodPositions.contains(&position) { + Tile::Food { coordinate: coordinate } + } else if snake_at_tile.is_some() { + let s = snake_at_tile.unwrap(); + if s.positions[0] == position { + Tile::SnakeHead { coordinate: coordinate, snake: s } + } else { + Tile::SnakeBody { coordinate: coordinate, snake: s } + } + } else { + Tile::Empty { coordinate: coordinate } + } + } + + pub fn is_tile_available_for_movement(&self, coordinate: (i32,i32)) -> bool { + let tile = self.get_tile_at(coordinate); + match tile { + Tile::Empty { coordinate: _ } => true, + Tile::Food { coordinate: _ } => true, + _ => false + } + } + + pub fn can_snake_move_in_direction(&self, snake: &SnakeInfo, direction: Direction) -> bool { + let (xd,yd) = direction_as_movement_delta(&direction); + let (x,y) = util::translate_position(snake.positions[0], self.width); + + self.is_tile_available_for_movement((x+xd,y+yd)) + } + + #[allow(dead_code)] + pub fn is_coordinate_out_of_bounds(&self, coordinate: (i32,i32)) -> bool { + let (x,y) = coordinate; + x < 0 || x >= self.width || y < 0 || y >= self.height + } +} + +#[cfg(test)] +mod test { + use util::{ translate_coordinate }; + use maputil::{ Direction, Tile }; + use messages::{ Map, SnakeInfo }; + + const MAP_WIDTH: i32 = 3; + + fn get_snake_one() -> SnakeInfo { + SnakeInfo { + name: String::from("1"), + points: 0, + tailProtectedForGameTicks: 0, + positions: vec![translate_coordinate((1,1), MAP_WIDTH), + translate_coordinate((0,1), MAP_WIDTH)], + id: String::from("1") + } + } + + fn get_snake_two() -> SnakeInfo { + SnakeInfo { + name: String::from("2"), + points: 0, + tailProtectedForGameTicks: 0, + positions: vec![translate_coordinate((1,2), MAP_WIDTH)], + id: String::from("2") + } + } + + // The map used for testing, 1 and 2 represents the snakes + //yx012 + //0 F + //1 11# + //2 2 + fn get_test_map() -> Map { + Map { + type_: String::from("type"), + width: MAP_WIDTH, + height: MAP_WIDTH, + worldTick: 0, + snakeInfos: vec![get_snake_one(), get_snake_two()], + foodPositions: vec![translate_coordinate((1,0), MAP_WIDTH)], + obstaclePositions: vec![translate_coordinate((2,1), MAP_WIDTH)], + receivingPlayerId: Some(String::from("1")) + } + } + + #[test] + fn snake_can_be_found_by_id() { + let map = get_test_map(); + let id = map.receivingPlayerId.as_ref().unwrap(); + let s = map.get_snake_by_id(id); + let found_id = &s.unwrap().id; + assert_eq!(id, found_id); + } + + #[test] + fn tile_is_correctly_found() { + let map = get_test_map(); + let snake_one = get_snake_one(); + let snake_two = get_snake_two(); + let tiles = + vec![ + vec![Tile::Empty{ coordinate: (0,0) }, + Tile::Food{ coordinate: (1,0) }, + Tile::Empty{ coordinate: (2,0) }], + vec![Tile::SnakeBody{ coordinate: (0,1), snake: &snake_one }, + Tile::SnakeHead{ coordinate: (1,1), snake: &snake_one }, + Tile::Obstacle{ coordinate: (2,1)}], + vec![Tile::Empty{ coordinate: (0,2) }, + Tile::SnakeHead{ coordinate: (1,2), snake: &snake_two }, + Tile::Empty{ coordinate:(2,2) }]]; + for y in 0..map.width { + for x in 0..map.height { + assert_eq!(tiles[y as usize][x as usize], + map.get_tile_at((x,y))); + } + } + } + + #[test] + fn tile_is_correctly_marked_as_movable() { + let map = get_test_map(); + let tiles = vec![vec![true, true, true], + vec![false, false, false], + vec![true, false, true]]; + + for y in 0..map.height { + for x in 0..map.width { + assert_eq!(tiles[y as usize][x as usize], + map.is_tile_available_for_movement((x,y))); + } + } + } + + #[test] + fn can_snake_move_identifies_correctly() { + let map = get_test_map(); + let id = map.receivingPlayerId.as_ref().unwrap(); + let snake = map.get_snake_by_id(id).unwrap(); + + assert_eq!(true, map.can_snake_move_in_direction(&snake, Direction::Up)); + assert_eq!(false, map.can_snake_move_in_direction(&snake, Direction::Down)); + assert_eq!(false, map.can_snake_move_in_direction(&snake, Direction::Left)); + assert_eq!(false, map.can_snake_move_in_direction(&snake, Direction::Right)); + } +} diff --git a/src/messages.rs b/src/messages.rs new file mode 100644 index 0000000..2a535fa --- /dev/null +++ b/src/messages.rs @@ -0,0 +1,204 @@ +#![allow(non_snake_case)] +//inbound messages +pub const GAME_ENDED: &'static str = + "se.cygni.snake.api.event.GameEndedEvent"; +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_REQUEST: &'static str = + "se.cygni.snake.api.request.HeartBeatRequest"; + +//outbound messages +pub const REGISTER_PLAYER_MESSAGE_TYPE: &'static str = + "se.cygni.snake.api.request.RegisterPlayer"; +pub const START_GAME: &'static str = "se.cygni.snake.api.request.StartGame"; +pub const REGISTER_MOVE: &'static str = + "se.cygni.snake.api.request.RegisterMove"; +pub const HEART_BEAT_RESPONSE: &'static str = + "se.cygni.snake.api.request.HeartBeatResponse"; + +// Outbound messages +#[derive(Serialize, Deserialize, Debug)] +pub struct GameSettings { + pub width: String, + pub height: String, + pub maxNoofPlayers: u32, + pub startSnakeLength: u32, + pub timeInMsPerTick: u32, + pub obstaclesEnabled: bool, + pub foodEnabled: bool, + pub edgeWrapsAround: bool, + pub headToTailConsumes: bool, + pub tailConsumeGrows: bool, + pub addFoodLikelihood: u32, + pub removeFoodLikelihood: u32, + pub addObstacleLikelihood: u32, + pub removeObstacleLikelihood: u32, + pub spontaneousGrowthEveryNWorldTick: u32, + pub trainingGame: bool, + pub pointsPerLength: u32, + pub pointsPerFood: u32, + pub pointsPerCausedDeath: u32, + pub pointsPerNibble: u32, + pub pointsLastSnakeLiving: u32, + pub noofRoundsTailProtectedAfterNibble: u32, + pub pointsSuicide: i32, +} + +#[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 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 +} + +//Inbound messages +#[derive(Serialize, Deserialize, Debug)] +pub struct PlayerRegistered { + #[serde(rename="type")] + pub type_: String, + pub gameId: String, + pub gameMode: String, + pub receivingPlayerId: String, + pub name: String, + pub gameSettings: GameSettings +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct MapUpdate { + #[serde(rename="type")] + pub type_: String, + pub receivingPlayerId: String, + pub gameId: String, + pub gameTick: u32, + pub map: Map, +} + +#[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, + pub gameTick: u32, + pub map: Map, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct SnakeDead { + #[serde(rename="type")] + pub type_: String, + pub playerId: String, + pub x: u32, + pub y: u32, + pub gameId: String, + pub gameTick: u32, + pub deathReason: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct GameStarting { + #[serde(rename="type")] + pub type_: String, + pub receivingPlayerId: String, + pub gameId: String, + pub noofPlayers: u32, + pub width: u32, + pub height: u32, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct HeartBeatResponse { + #[serde(rename="type")] + pub type_: String, + pub receivingPlayerId: Option +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Map { + #[serde(rename="type")] + pub type_: String, + pub width: i32, + pub height: i32, + pub worldTick: u32, + pub snakeInfos: Vec, + pub foodPositions: Vec, + pub obstaclePositions: Vec, + pub receivingPlayerId: Option, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct SnakeInfo { + pub name: String, + pub points: i32, + pub positions: Vec, + pub tailProtectedForGameTicks: u32, + pub id: String +} + +pub fn default_gamesettings() -> GameSettings { + GameSettings { + width: String::from("MEDIUM"), + height: String::from("MEDIUM"), + maxNoofPlayers: 5, + startSnakeLength: 1, + timeInMsPerTick: 250, + obstaclesEnabled: true, + foodEnabled: true, + edgeWrapsAround: false, + headToTailConsumes: true, + tailConsumeGrows: false, + addFoodLikelihood: 15, + removeFoodLikelihood: 5, + addObstacleLikelihood: 15, + removeObstacleLikelihood: 15, + spontaneousGrowthEveryNWorldTick: 3, + trainingGame: false, + pointsPerLength: 1, + pointsPerFood: 2, + pointsPerCausedDeath: 5, + pointsPerNibble: 10, + pointsLastSnakeLiving: 10, + noofRoundsTailProtectedAfterNibble: 3, + pointsSuicide: -10, + } +} diff --git a/src/snake.rs b/src/snake.rs new file mode 100644 index 0000000..f3fa150 --- /dev/null +++ b/src/snake.rs @@ -0,0 +1,60 @@ +use messages; +use maputil::{ Direction }; +use util::{ translate_positions }; + +const LOG_TARGET: &'static str = "snake"; + +pub struct Snake; + +impl Snake { + pub fn get_name(&self) -> String { + String::from("rusty-snake") + } + + pub fn get_next_move(&self, msg: &messages::MapUpdate) -> Direction { + info!(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(); + + info!(target: LOG_TARGET, "Food can be found at {:?}", translate_positions(&map.foodPositions, map.width)); + info!(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 + } + + pub fn on_game_ended(&self, msg: &messages::GameEnded) { + info!(target: LOG_TARGET, "Game ended, the winner is: {:?}", msg.playerWinnerId); + } + + pub fn on_snake_dead(&self, msg: &messages::SnakeDead) { + info!(target: LOG_TARGET, "The snake died, reason was: {:?}", msg.deathReason); + } + + pub fn on_game_starting(&self, _: &messages::GameStarting) { + + } + + pub fn on_player_registered(&self, _: &messages::PlayerRegistered) { + + } + + pub fn on_invalid_playername(&self, _: &messages::InvalidPlayerName) { + + } +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..8b73fbb --- /dev/null +++ b/src/util.rs @@ -0,0 +1,53 @@ +#[allow(dead_code)] +pub fn translate_position(position: i32, map_width: i32) -> (i32,i32) { + let pos = position as f64; + let width = map_width as f64; + + let y = (pos / width).floor(); + let x = (pos - y * width).abs(); + + (x as i32, y as i32) +} + +#[allow(dead_code)] +pub fn translate_positions(positions: &Vec, map_width: i32) -> Vec<(i32,i32)> { + positions.into_iter().map(|pos| translate_position(*pos, map_width)).collect() +} + +#[allow(dead_code)] +pub fn translate_coordinate(coordinates: (i32,i32), map_width: i32) -> i32 { + let (x,y) = coordinates; + x + y * map_width +} + +#[allow(dead_code)] +pub fn get_manhattan_distance(start: (i32,i32), goal: (i32,i32)) -> i32 { + let (x1,y1) = start; + let (x2,y2) = goal; + + let x = ( x1 - x2 ).abs(); + let y = ( y1 - y2 ).abs(); + + x+y +} + +#[allow(dead_code)] +pub fn get_euclidian_distance(start: (i32,i32), goal: (i32,i32)) -> f64 { + let (x1,y1) = start; + let (x2,y2) = goal; + + let x = ( x1 - x2 ).pow(2); + let y = ( y1 - y2 ).pow(2); + let d = ( x + y ) as f64; + + d.sqrt().floor() +} + +#[allow(dead_code)] +pub fn is_within_square(coord: (i32,i32), ne_coord: (i32,i32), sw_coord: (i32,i32)) -> bool { + let (x,y) = coord; + let (ne_x, ne_y) = ne_coord; + let (sw_x, sw_y) = sw_coord; + + x < ne_x || x > sw_x || y < sw_y || y > ne_y +}