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
This commit is contained in:
Martin Barksten
2016-06-07 20:04:57 +02:00
committed by Emil Breding
commit 01c87721ae
9 changed files with 828 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@ -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/

13
Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "snakebot_rust"
version = "0.1.0"
authors = ["Martin Barksten <martin.barksten@gmail.com>"]
[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"] }

32
log4rs.toml Normal file
View File

@ -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

27
readme.md Normal file
View File

@ -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: `<repo>/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.

229
src/main.rs Normal file
View File

@ -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<String>) {
from()
}
WebsocketChannel(err: mpsc::SendError<Arc<ws::Sender>>) {
from()
}
}
}
struct Client {
out: Arc<ws::Sender>,
snake: Snake,
out_sender: mpsc::Sender<Arc<ws::Sender>>,
id_sender: mpsc::Sender<String>,
}
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<String>,
out_sender: mpsc::Sender<Arc<ws::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<String>,
out_receiver: mpsc::Receiver<Arc<ws::Sender>>,
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();
}

195
src/maputil.rs Normal file
View File

@ -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));
}
}

204
src/messages.rs Normal file
View File

@ -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<String>
}
#[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<SnakeInfo>,
pub foodPositions: Vec<i32>,
pub obstaclePositions: Vec<i32>,
pub receivingPlayerId: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct SnakeInfo {
pub name: String,
pub points: i32,
pub positions: Vec<i32>,
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,
}
}

60
src/snake.rs Normal file
View File

@ -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) {
}
}

53
src/util.rs Normal file
View File

@ -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<i32>, 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
}