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:
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal 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
13
Cargo.toml
Normal 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
32
log4rs.toml
Normal 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
27
readme.md
Normal 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
229
src/main.rs
Normal 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
195
src/maputil.rs
Normal 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
204
src/messages.rs
Normal 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
60
src/snake.rs
Normal 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
53
src/util.rs
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user