bump
This commit is contained in:
parent
b1d59e3e18
commit
9c696fa89b
7 changed files with 908 additions and 0 deletions
0
backend/src/planetwars/mod.rs
Normal file
0
backend/src/planetwars/mod.rs
Normal file
11
backend/src/pw/mod.rs
Normal file
11
backend/src/pw/mod.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
|
||||
pub mod pw_protocol;
|
||||
mod pw_controller;
|
||||
mod pw_rules;
|
||||
mod pw_config;
|
||||
mod pw_serializer;
|
||||
|
||||
pub use self::pw_controller::PwMatch;
|
||||
pub use self::pw_rules::PlanetWars;
|
||||
pub use self::pw_config::Config;
|
||||
pub use self::pw_protocol::Map;
|
79
backend/src/pw/pw_config.rs
Normal file
79
backend/src/pw/pw_config.rs
Normal file
|
@ -0,0 +1,79 @@
|
|||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::io;
|
||||
|
||||
use serde_json;
|
||||
|
||||
use super::pw_protocol as proto;
|
||||
use super::pw_rules::*;
|
||||
|
||||
// TODO
|
||||
use server::ClientId;
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub map_file: String,
|
||||
pub max_turns: u64,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn create_game(&self, clients: Vec<ClientId>) -> PlanetWars {
|
||||
let planets = self.load_map(clients.len());
|
||||
let players = clients.into_iter()
|
||||
.map(|client_id| Player { id: client_id, alive: true })
|
||||
.collect();
|
||||
|
||||
PlanetWars {
|
||||
players: players,
|
||||
planets: planets,
|
||||
expeditions: Vec::new(),
|
||||
expedition_num: 0,
|
||||
turn_num: 0,
|
||||
max_turns: self.max_turns,
|
||||
}
|
||||
}
|
||||
|
||||
fn load_map(&self, num_players: usize) -> Vec<Planet> {
|
||||
let map = self.read_map().expect("[PLANET_WARS] reading map failed");
|
||||
|
||||
return map.planets
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(num, planet)| {
|
||||
let mut fleets = Vec::new();
|
||||
let owner = planet.owner.and_then(|owner_num| {
|
||||
// in the current map format, player numbers start at 1.
|
||||
// TODO: we might want to change this.
|
||||
let player_num = owner_num as usize - 1;
|
||||
// ignore players that are not in the game
|
||||
if player_num < num_players {
|
||||
Some(player_num)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if planet.ship_count > 0 {
|
||||
fleets.push(Fleet {
|
||||
owner: owner,
|
||||
ship_count: planet.ship_count,
|
||||
});
|
||||
}
|
||||
return Planet {
|
||||
id: num,
|
||||
name: planet.name,
|
||||
x: planet.x,
|
||||
y: planet.y,
|
||||
fleets: fleets,
|
||||
};
|
||||
}).collect();
|
||||
}
|
||||
|
||||
fn read_map(&self) -> io::Result<proto::Map> {
|
||||
let mut file = File::open(&self.map_file)?;
|
||||
let mut buf = String::new();
|
||||
file.read_to_string(&mut buf)?;
|
||||
let map = serde_json::from_str(&buf)?;
|
||||
return Ok(map);
|
||||
}
|
||||
}
|
452
backend/src/pw/pw_controller.rs
Normal file
452
backend/src/pw/pw_controller.rs
Normal file
|
@ -0,0 +1,452 @@
|
|||
use std::collections::{HashMap, HashSet};
|
||||
use std::time::{Duration, Instant};
|
||||
use std::mem;
|
||||
use std::io;
|
||||
|
||||
use events;
|
||||
use network::server::RegisteredHandle;
|
||||
use reactors::reactor::ReactorHandle;
|
||||
use reactors::{WireEvent, RequestHandler};
|
||||
use server::{ClientId, ConnectionManager};
|
||||
use sodiumoxide::crypto::sign::PublicKey;
|
||||
|
||||
use super::Config;
|
||||
use super::pw_rules::{PlanetWars, Dispatch};
|
||||
use super::pw_serializer::serialize_state;
|
||||
use super::pw_protocol::{
|
||||
self as proto,
|
||||
PlayerAction,
|
||||
PlayerCommand,
|
||||
CommandError,
|
||||
};
|
||||
|
||||
use serde_json;
|
||||
|
||||
pub struct Player {
|
||||
id: ClientId,
|
||||
num: usize,
|
||||
handle: RegisteredHandle,
|
||||
}
|
||||
|
||||
pub struct ClientHandler {
|
||||
client_id: u32,
|
||||
reactor_handle: ReactorHandle,
|
||||
}
|
||||
|
||||
impl ClientHandler {
|
||||
pub fn new(client_id: u32, reactor_handle: ReactorHandle) -> Self {
|
||||
ClientHandler {
|
||||
client_id,
|
||||
reactor_handle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_connect(&mut self, _event: &events::Connected)
|
||||
-> io::Result<WireEvent>
|
||||
{
|
||||
self.reactor_handle.dispatch(events::ClientConnected {
|
||||
client_id: self.client_id,
|
||||
});
|
||||
Ok(WireEvent::null())
|
||||
}
|
||||
|
||||
pub fn on_disconnect(&mut self, _event: &events::Disconnected)
|
||||
-> io::Result<WireEvent>
|
||||
{
|
||||
self.reactor_handle.dispatch(events::ClientDisconnected {
|
||||
client_id: self.client_id,
|
||||
});
|
||||
Ok(WireEvent::null())
|
||||
}
|
||||
|
||||
pub fn on_message(&mut self, event: &events::ClientSend)
|
||||
-> io::Result<WireEvent>
|
||||
{
|
||||
self.reactor_handle.dispatch(events::ClientMessage {
|
||||
client_id: self.client_id,
|
||||
data: event.data.clone(),
|
||||
});
|
||||
Ok(WireEvent::null())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PwMatch {
|
||||
state: PwMatchState,
|
||||
}
|
||||
|
||||
enum PwMatchState {
|
||||
Lobby(Lobby),
|
||||
Playing(PwController),
|
||||
Finished,
|
||||
}
|
||||
|
||||
impl PwMatch {
|
||||
pub fn new(match_uuid: Vec<u8>,
|
||||
reactor_handle: ReactorHandle,
|
||||
connection_manager: ConnectionManager)
|
||||
-> Self
|
||||
{
|
||||
let lobby = Lobby::new(
|
||||
match_uuid,
|
||||
connection_manager,
|
||||
reactor_handle
|
||||
);
|
||||
|
||||
return PwMatch {
|
||||
state: PwMatchState::Lobby(lobby),
|
||||
}
|
||||
}
|
||||
|
||||
fn take_state(&mut self) -> PwMatchState {
|
||||
mem::replace(&mut self.state, PwMatchState::Finished)
|
||||
}
|
||||
|
||||
pub fn register_client(&mut self, event: &events::RegisterClient) {
|
||||
if let &mut PwMatchState::Lobby(ref mut lobby) = &mut self.state {
|
||||
let client_id = ClientId::new(event.client_id);
|
||||
let key = PublicKey::from_slice(&event.public_key).unwrap();
|
||||
lobby.add_player(client_id, key);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_client(&mut self, event: &events::RemoveClient) {
|
||||
if let PwMatchState::Lobby(ref mut lobby) = self.state {
|
||||
let client_id = ClientId::new(event.client_id);
|
||||
lobby.remove_player(client_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_game(&mut self, event: &events::StartGame) {
|
||||
let state = self.take_state();
|
||||
|
||||
if let PwMatchState::Lobby(lobby) = state {
|
||||
let config = Config {
|
||||
map_file: event.map_path.clone(),
|
||||
max_turns: event.max_turns as u64,
|
||||
};
|
||||
self.state = PwMatchState::Playing(PwController::new(
|
||||
config,
|
||||
lobby.reactor_handle,
|
||||
lobby.connection_manager,
|
||||
lobby.players,
|
||||
));
|
||||
} else {
|
||||
self.state = state;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn game_step(&mut self, event: &events::GameStep) {
|
||||
if let PwMatchState::Playing(ref mut controller) = self.state {
|
||||
controller.on_step(event);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn client_message(&mut self, event: &events::ClientMessage) {
|
||||
if let PwMatchState::Playing(ref mut controller) = self.state {
|
||||
controller.on_client_message(event);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn game_finished(&mut self, event: &events::GameFinished) {
|
||||
let state = self.take_state();
|
||||
if let PwMatchState::Playing(mut controller) = state {
|
||||
controller.on_finished(event);
|
||||
}
|
||||
self.state = PwMatchState::Finished;
|
||||
}
|
||||
|
||||
pub fn timeout(&mut self, event: &events::TurnTimeout) {
|
||||
if let PwMatchState::Playing(ref mut controller) = self.state {
|
||||
controller.on_timeout(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Lobby {
|
||||
match_uuid: Vec<u8>,
|
||||
connection_manager: ConnectionManager,
|
||||
reactor_handle: ReactorHandle,
|
||||
|
||||
players: HashMap<ClientId, RegisteredHandle>,
|
||||
}
|
||||
|
||||
impl Lobby {
|
||||
fn new(match_uuid: Vec<u8>,
|
||||
connection_manager: ConnectionManager,
|
||||
reactor_handle: ReactorHandle)
|
||||
-> Self
|
||||
{
|
||||
return Lobby {
|
||||
match_uuid,
|
||||
connection_manager,
|
||||
reactor_handle,
|
||||
|
||||
players: HashMap::new(),
|
||||
// start counter at 1, because 0 is the control client
|
||||
}
|
||||
}
|
||||
|
||||
fn add_player(&mut self, client_id: ClientId, public_key: PublicKey) {
|
||||
let mut core = RequestHandler::new(
|
||||
ClientHandler::new(
|
||||
client_id.as_u32(),
|
||||
self.reactor_handle.clone(),
|
||||
),
|
||||
);
|
||||
|
||||
core.add_handler(ClientHandler::on_connect);
|
||||
core.add_handler(ClientHandler::on_disconnect);
|
||||
core.add_handler(ClientHandler::on_message);
|
||||
|
||||
let handle = self.connection_manager.create_client(
|
||||
self.match_uuid.clone(),
|
||||
client_id.as_u32(),
|
||||
public_key,
|
||||
core
|
||||
);
|
||||
|
||||
self.players.insert(client_id, handle);
|
||||
}
|
||||
|
||||
fn remove_player(&mut self, client_id: ClientId) {
|
||||
self.players.remove(&client_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PwController {
|
||||
state: PlanetWars,
|
||||
planet_map: HashMap<String, usize>,
|
||||
reactor_handle: ReactorHandle,
|
||||
connection_manager: ConnectionManager,
|
||||
|
||||
client_player: HashMap<ClientId, usize>,
|
||||
players: HashMap<ClientId, Player>,
|
||||
|
||||
waiting_for: HashSet<ClientId>,
|
||||
commands: HashMap<ClientId, String>,
|
||||
}
|
||||
|
||||
impl PwController {
|
||||
pub fn new(config: Config,
|
||||
reactor_handle: ReactorHandle,
|
||||
connection_manager: ConnectionManager,
|
||||
clients: HashMap<ClientId, RegisteredHandle>)
|
||||
-> Self
|
||||
{
|
||||
// TODO: we probably want a way to fixate player order
|
||||
let client_ids = clients.keys().cloned().collect();
|
||||
let state = config.create_game(client_ids);
|
||||
|
||||
let planet_map = state.planets.iter().map(|planet| {
|
||||
(planet.name.clone(), planet.id)
|
||||
}).collect();
|
||||
|
||||
let mut client_player = HashMap::new();
|
||||
let mut players = HashMap::new();
|
||||
|
||||
let clients_iter = clients.into_iter().enumerate();
|
||||
for (player_num, (client_id, client_handle)) in clients_iter {
|
||||
client_player.insert(client_id, player_num);
|
||||
players.insert(client_id, Player {
|
||||
id: client_id,
|
||||
num: player_num,
|
||||
handle: client_handle,
|
||||
});
|
||||
}
|
||||
|
||||
let mut controller = PwController {
|
||||
state,
|
||||
planet_map,
|
||||
players,
|
||||
client_player,
|
||||
reactor_handle,
|
||||
connection_manager,
|
||||
|
||||
waiting_for: HashSet::new(),
|
||||
commands: HashMap::new(),
|
||||
};
|
||||
// this initial dispatch starts the game
|
||||
controller.dispatch_state();
|
||||
return controller;
|
||||
}
|
||||
|
||||
|
||||
/// Advance the game by one turn.
|
||||
fn step(&mut self) {
|
||||
self.state.repopulate();
|
||||
self.execute_commands();
|
||||
self.state.step();
|
||||
|
||||
self.dispatch_state();
|
||||
}
|
||||
|
||||
fn dispatch_state(&mut self) {
|
||||
let turn_num = self.state.turn_num;
|
||||
// TODO: fix this
|
||||
let state = serde_json::to_string(&serialize_state(&self.state)).unwrap();
|
||||
|
||||
if self.state.is_finished() {
|
||||
let event = events::GameFinished { turn_num, state };
|
||||
self.reactor_handle.dispatch(event);
|
||||
} else {
|
||||
let event = events::GameStep { turn_num, state };
|
||||
self.reactor_handle.dispatch(event);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_step(&mut self, step: &events::GameStep) {
|
||||
let state = &self.state;
|
||||
let waiting_for = &mut self.waiting_for;
|
||||
|
||||
self.players.retain(|_, player| {
|
||||
if state.players[player.num].alive {
|
||||
waiting_for.insert(player.id);
|
||||
player.handle.dispatch(events::GameStep {
|
||||
turn_num: step.turn_num,
|
||||
state: step.state.clone(),
|
||||
});
|
||||
// keep this player in the game
|
||||
return true;
|
||||
} else {
|
||||
player.handle.dispatch(events::GameFinished {
|
||||
turn_num: step.turn_num,
|
||||
state: step.state.clone(),
|
||||
});
|
||||
// this player is dead, kick him!
|
||||
// TODO: shutdown the reactor
|
||||
// TODO: todo
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
let deadline = Instant::now() + Duration::from_secs(1);
|
||||
self.reactor_handle.dispatch_at(deadline, events::TurnTimeout {
|
||||
turn_num: state.turn_num,
|
||||
});
|
||||
}
|
||||
|
||||
fn on_client_message(&mut self, event: &events::ClientMessage) {
|
||||
let client_id = ClientId::new(event.client_id);
|
||||
self.waiting_for.remove(&client_id);
|
||||
self.commands.insert(client_id, event.data.clone());
|
||||
|
||||
if self.waiting_for.is_empty() {
|
||||
self.step();
|
||||
}
|
||||
}
|
||||
|
||||
fn on_finished(&mut self, event: &events::GameFinished) {
|
||||
self.players.retain(|_player_id, player| {
|
||||
player.handle.dispatch(events::GameFinished {
|
||||
turn_num: event.turn_num,
|
||||
state: event.state.clone(),
|
||||
});
|
||||
// game is over, kick everyone.
|
||||
false
|
||||
});
|
||||
println!("everybody has been kicked");
|
||||
self.reactor_handle.quit();
|
||||
}
|
||||
|
||||
fn on_timeout(&mut self, event: &events::TurnTimeout) {
|
||||
if self.state.turn_num == event.turn_num {
|
||||
self.step();
|
||||
}
|
||||
}
|
||||
|
||||
fn player_commands(&mut self) -> HashMap<ClientId, Option<String>> {
|
||||
let commands = &mut self.commands;
|
||||
return self.players.values().map(|player| {
|
||||
let command = commands.remove(&player.id);
|
||||
return (player.id, command);
|
||||
}).collect();
|
||||
}
|
||||
|
||||
fn execute_commands(&mut self) {
|
||||
let mut commands = self.player_commands();
|
||||
|
||||
for (player_id, command) in commands.drain() {
|
||||
let player_num = self.players[&player_id].num;
|
||||
let action = self.execute_action(player_num, command);
|
||||
let serialized_action = serde_json::to_string(&action).unwrap();
|
||||
self.reactor_handle.dispatch(events::PlayerAction {
|
||||
client_id: player_id.as_u32(),
|
||||
action: serialized_action.clone(),
|
||||
});
|
||||
self.players
|
||||
.get_mut(&player_id)
|
||||
.unwrap()
|
||||
.handle
|
||||
.dispatch(events::PlayerAction {
|
||||
client_id: player_id.as_u32(),
|
||||
action: serialized_action,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_action(&mut self, player_num: usize, response: Option<String>)
|
||||
-> PlayerAction
|
||||
{
|
||||
// TODO: it would be cool if this could be done with error_chain.
|
||||
|
||||
let message = match response {
|
||||
None => return PlayerAction::Timeout,
|
||||
Some(message) => message,
|
||||
};
|
||||
|
||||
let action: proto::Action = match serde_json::from_str(&message) {
|
||||
Err(err) => return PlayerAction::ParseError(err.to_string()),
|
||||
Ok(action) => action,
|
||||
};
|
||||
|
||||
let commands = action.commands.into_iter().map(|command| {
|
||||
match self.parse_command(player_num, &command) {
|
||||
Ok(dispatch) => {
|
||||
self.state.dispatch(&dispatch);
|
||||
PlayerCommand {
|
||||
command,
|
||||
error: None,
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
PlayerCommand {
|
||||
command,
|
||||
error: Some(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
}).collect();
|
||||
|
||||
return PlayerAction::Commands(commands);
|
||||
}
|
||||
|
||||
fn parse_command(&self, player_num: usize, mv: &proto::Command)
|
||||
-> Result<Dispatch, CommandError>
|
||||
{
|
||||
let origin_id = *self.planet_map
|
||||
.get(&mv.origin)
|
||||
.ok_or(CommandError::OriginDoesNotExist)?;
|
||||
|
||||
let target_id = *self.planet_map
|
||||
.get(&mv.destination)
|
||||
.ok_or(CommandError::DestinationDoesNotExist)?;
|
||||
|
||||
if self.state.planets[origin_id].owner() != Some(player_num) {
|
||||
return Err(CommandError::OriginNotOwned);
|
||||
}
|
||||
|
||||
if self.state.planets[origin_id].ship_count() < mv.ship_count {
|
||||
return Err(CommandError::NotEnoughShips);
|
||||
}
|
||||
|
||||
if mv.ship_count == 0 {
|
||||
return Err(CommandError::ZeroShipMove);
|
||||
}
|
||||
|
||||
Ok(Dispatch {
|
||||
origin: origin_id,
|
||||
target: target_id,
|
||||
ship_count: mv.ship_count,
|
||||
})
|
||||
}
|
||||
}
|
105
backend/src/pw/pw_protocol.rs
Normal file
105
backend/src/pw/pw_protocol.rs
Normal file
|
@ -0,0 +1,105 @@
|
|||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Expedition {
|
||||
pub id: u64,
|
||||
pub ship_count: u64,
|
||||
pub origin: String,
|
||||
pub destination: String,
|
||||
pub owner: u32,
|
||||
pub turns_remaining: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Planet {
|
||||
pub ship_count: u64,
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub owner: Option<u32>,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Action {
|
||||
#[serde(rename = "moves")]
|
||||
pub commands: Vec<Command>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Command {
|
||||
pub origin: String,
|
||||
pub destination: String,
|
||||
pub ship_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Map {
|
||||
pub planets: Vec<Planet>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct State {
|
||||
pub planets: Vec<Planet>,
|
||||
pub expeditions: Vec<Expedition>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GameInfo {
|
||||
pub players: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum CommandError {
|
||||
NotEnoughShips,
|
||||
OriginNotOwned,
|
||||
ZeroShipMove,
|
||||
OriginDoesNotExist,
|
||||
DestinationDoesNotExist,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PlayerCommand {
|
||||
pub command: Command,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<CommandError>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[serde(tag = "type", content = "value")]
|
||||
pub enum PlayerAction {
|
||||
Timeout,
|
||||
ParseError(String),
|
||||
Commands(Vec<PlayerCommand>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
pub enum ServerMessage {
|
||||
/// Game state in current turn
|
||||
GameState(State),
|
||||
/// The action that was performed
|
||||
PlayerAction(PlayerAction),
|
||||
/// The game is over, and this is the concluding state.
|
||||
FinalState(State),
|
||||
}
|
||||
|
||||
// lobby messages
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
pub enum ControlMessage {
|
||||
PlayerConnected {
|
||||
player_id: u64,
|
||||
},
|
||||
PlayerDisconnected {
|
||||
player_id: u64,
|
||||
},
|
||||
GameState(State),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
pub enum LobbyCommand {
|
||||
StartMatch,
|
||||
}
|
200
backend/src/pw/pw_rules.rs
Normal file
200
backend/src/pw/pw_rules.rs
Normal file
|
@ -0,0 +1,200 @@
|
|||
use server::ClientId;
|
||||
|
||||
/// The planet wars game rules.
|
||||
pub struct PlanetWars {
|
||||
pub players: Vec<Player>,
|
||||
pub planets: Vec<Planet>,
|
||||
pub expeditions: Vec<Expedition>,
|
||||
// How many expeditions were already dispatched.
|
||||
// This is needed for assigning expedition identifiers.
|
||||
pub expedition_num: u64,
|
||||
pub turn_num: u64,
|
||||
pub max_turns: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Player {
|
||||
pub id: ClientId,
|
||||
pub alive: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Fleet {
|
||||
pub owner: Option<usize>,
|
||||
pub ship_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Planet {
|
||||
pub id: usize,
|
||||
pub name: String,
|
||||
pub fleets: Vec<Fleet>,
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Expedition {
|
||||
pub id: u64,
|
||||
pub origin: usize,
|
||||
pub target: usize,
|
||||
pub fleet: Fleet,
|
||||
pub turns_remaining: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Dispatch {
|
||||
pub origin: usize,
|
||||
pub target: usize,
|
||||
pub ship_count: u64,
|
||||
}
|
||||
|
||||
impl PlanetWars {
|
||||
|
||||
pub fn dispatch(&mut self, dispatch: &Dispatch) {
|
||||
let distance = self.planets[dispatch.origin].distance(
|
||||
&self.planets[dispatch.target]
|
||||
);
|
||||
|
||||
let origin = &mut self.planets[dispatch.origin];
|
||||
origin.fleets[0].ship_count -= dispatch.ship_count;
|
||||
|
||||
|
||||
let expedition = Expedition {
|
||||
id: self.expedition_num,
|
||||
origin: dispatch.origin,
|
||||
target: dispatch.target,
|
||||
turns_remaining: distance,
|
||||
fleet: Fleet {
|
||||
owner: origin.owner(),
|
||||
ship_count: dispatch.ship_count,
|
||||
},
|
||||
};
|
||||
|
||||
// increment counter
|
||||
self.expedition_num += 1;
|
||||
self.expeditions.push(expedition);
|
||||
}
|
||||
|
||||
// Play one step of the game
|
||||
pub fn step(&mut self) {
|
||||
self.turn_num += 1;
|
||||
|
||||
// Initially mark all players dead, re-marking them as alive once we
|
||||
// encounter a sign of life.
|
||||
for player in self.players.iter_mut() {
|
||||
player.alive = false;
|
||||
}
|
||||
|
||||
self.step_expeditions();
|
||||
self.resolve_combat();
|
||||
}
|
||||
|
||||
pub fn repopulate(&mut self) {
|
||||
for planet in self.planets.iter_mut() {
|
||||
if planet.owner().is_some() {
|
||||
planet.fleets[0].ship_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn step_expeditions(&mut self) {
|
||||
let mut i = 0;
|
||||
let exps = &mut self.expeditions;
|
||||
while i < exps.len() {
|
||||
// compare with 1 to avoid issues with planet distance 0
|
||||
if exps[i].turns_remaining <= 1 {
|
||||
// remove expedition from expeditions, and add to fleet
|
||||
let exp = exps.swap_remove(i);
|
||||
let planet = &mut self.planets[exp.target];
|
||||
planet.orbit(exp.fleet);
|
||||
} else {
|
||||
exps[i].turns_remaining -= 1;
|
||||
if let Some(owner_num) = exps[i].fleet.owner {
|
||||
// owner has an expedition in progress; this is a sign of life.
|
||||
self.players[owner_num].alive = true;
|
||||
}
|
||||
|
||||
// proceed to next expedition
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_combat(&mut self) {
|
||||
for planet in self.planets.iter_mut() {
|
||||
planet.resolve_combat();
|
||||
if let Some(owner_num) = planet.owner() {
|
||||
// owner owns a planet; this is a sign of life.
|
||||
self.players[owner_num].alive = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_finished(&self) -> bool {
|
||||
let remaining = self.players.iter().filter(|p| p.alive).count();
|
||||
return remaining < 2 || self.turn_num >= self.max_turns;
|
||||
}
|
||||
|
||||
pub fn living_players(&self) -> Vec<ClientId> {
|
||||
self.players.iter().filter_map(|p| {
|
||||
if p.alive {
|
||||
Some(p.id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}).collect()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Planet {
|
||||
pub fn owner(&self) -> Option<usize> {
|
||||
self.fleets.first().and_then(|f| f.owner)
|
||||
}
|
||||
|
||||
pub fn ship_count(&self) -> u64 {
|
||||
self.fleets.first().map_or(0, |f| f.ship_count)
|
||||
}
|
||||
|
||||
/// Make a fleet orbit this planet.
|
||||
fn orbit(&mut self, fleet: Fleet) {
|
||||
// If owner already has a fleet present, merge
|
||||
for other in self.fleets.iter_mut() {
|
||||
if other.owner == fleet.owner {
|
||||
other.ship_count += fleet.ship_count;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// else, add fleet to fleets list
|
||||
self.fleets.push(fleet);
|
||||
}
|
||||
|
||||
fn resolve_combat(&mut self) {
|
||||
// The player owning the largest fleet present will win the combat.
|
||||
// Here, we resolve how many ships he will have left.
|
||||
// note: in the current implementation, we could resolve by doing
|
||||
// winner.ship_count -= second_largest.ship_count, but this does not
|
||||
// allow for simple customizations (such as changing combat balance).
|
||||
|
||||
self.fleets.sort_by(|a, b| a.ship_count.cmp(&b.ship_count).reverse());
|
||||
while self.fleets.len() > 1 {
|
||||
let fleet = self.fleets.pop().unwrap();
|
||||
// destroy some ships
|
||||
for other in self.fleets.iter_mut() {
|
||||
other.ship_count -= fleet.ship_count;
|
||||
}
|
||||
|
||||
// remove dead fleets
|
||||
while self.fleets.last().map(|f| f.ship_count) == Some(0) {
|
||||
self.fleets.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn distance(&self, other: &Planet) -> u64 {
|
||||
let dx = self.x - other.x;
|
||||
let dy = self.y - other.y;
|
||||
return (dx.powi(2) + dy.powi(2)).sqrt().ceil() as u64;
|
||||
}
|
||||
}
|
61
backend/src/pw/pw_serializer.rs
Normal file
61
backend/src/pw/pw_serializer.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
use super::pw_rules::{PlanetWars, Planet, Expedition};
|
||||
use super::pw_protocol as proto;
|
||||
|
||||
/// Serialize given gamestate
|
||||
pub fn serialize_state(state: &PlanetWars) -> proto::State {
|
||||
let serializer = Serializer::new(state);
|
||||
serializer.serialize_state()
|
||||
}
|
||||
|
||||
struct Serializer<'a> {
|
||||
state: &'a PlanetWars,
|
||||
}
|
||||
|
||||
impl<'a> Serializer<'a> {
|
||||
fn new(state: &'a PlanetWars) -> Self {
|
||||
Serializer {
|
||||
state: state,
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_state(&self) -> proto::State {
|
||||
proto::State {
|
||||
planets: self.state
|
||||
.planets
|
||||
.iter()
|
||||
.map(|planet| self.serialize_planet(planet))
|
||||
.collect(),
|
||||
expeditions: self.state
|
||||
.expeditions
|
||||
.iter()
|
||||
.map(|exp| self.serialize_expedition(exp))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
// gets the client id for a player number
|
||||
fn player_client_id(&self, player_num: usize) -> u32 {
|
||||
self.state.players[player_num].id.as_u32()
|
||||
}
|
||||
|
||||
fn serialize_planet(&self, planet: &Planet) -> proto::Planet {
|
||||
proto::Planet {
|
||||
name: planet.name.clone(),
|
||||
x: planet.x,
|
||||
y: planet.y,
|
||||
owner: planet.owner().map(|num| self.player_client_id(num)),
|
||||
ship_count: planet.ship_count(),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_expedition(&self, exp: &Expedition) -> proto::Expedition {
|
||||
proto::Expedition {
|
||||
id: exp.id,
|
||||
owner: self.player_client_id(exp.fleet.owner.unwrap()),
|
||||
ship_count: exp.fleet.ship_count,
|
||||
origin: self.state.planets[exp.origin].name.clone(),
|
||||
destination: self.state.planets[exp.target].name.clone(),
|
||||
turns_remaining: exp.turns_remaining,
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue