Last active
August 29, 2015 14:07
-
-
Save dradtke/ee1cb4a24e03186ba784 to your computer and use it in GitHub Desktop.
An experiment in designing a turn-based concurrent game architecture.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//! Source file for the `game` crate. | |
#![allow(dead_code)] | |
/// The `Player` trait. | |
pub trait Player { | |
/// Gets called when it's time for this player to take their turn. | |
/// | |
/// The game is played by sending commands on `pipe` and checking | |
/// the responses that are returned. | |
fn take_turn(&self, pipe: &PlayerActionPipe, round: uint); | |
} | |
/// The `PlayerActionPipe` contains the channels that need | |
/// to be passed to the player for actions to be taken. | |
pub struct PlayerActionPipe { | |
cmd_send: SyncSender<Command>, | |
resp_recv: Receiver<Response>, | |
done_send: SyncSender<()>, | |
} | |
impl PlayerActionPipe { | |
/// Send a command and wait for the response. | |
pub fn send(&self, cmd: Command) -> Response { | |
self.cmd_send.send(cmd); self.resp_recv.recv() | |
} | |
} | |
#[deriving(Eq, PartialEq, Show)] | |
/// Player command as an enum. | |
pub enum Command { | |
NoCommand, | |
// ...a lot more | |
} | |
#[deriving(Eq, PartialEq, Show)] | |
/// Game response as an enum. | |
pub enum Response { | |
NoResponse, | |
// ...a lot more | |
} | |
pub struct Game { | |
playing: bool, // could potentially use a status enum here instead | |
players: Vec<PlayerHandle>, | |
} | |
impl Game { | |
/// Initialize a new game object. | |
pub fn new() -> Game { | |
Game{playing: false, players: Vec::new()} | |
} | |
/// Initialize a new game object with enough room for `capacity` | |
/// players.. | |
pub fn with_capacity(capacity: uint) -> Game { | |
Game{playing: false, players: Vec::with_capacity(capacity)} | |
} | |
/// Add a player. | |
/// | |
/// The player object needs to be `Send` so that it can be run in | |
/// its own task. | |
pub fn add_player<T: Player + Send>(&mut self, def: T) { | |
use std::comm; | |
// So many channels! | |
let (cmd_send, cmd_recv) = comm::sync_channel(0); | |
let (resp_send, resp_recv) = comm::sync_channel(0); | |
let (done_send, done_recv) = comm::sync_channel(0); | |
let (start_send, start_recv) = comm::sync_channel(0); | |
let (quit_send, quit_recv) = comm::channel(); | |
self.players.push(PlayerHandle{ | |
def: box def, | |
action_pipe: PlayerActionPipe{ | |
cmd_send: cmd_send, | |
resp_recv: resp_recv, | |
done_send: done_send, | |
}, | |
puppet_pipe: PlayerPuppetPipe { | |
start_recv: start_recv, | |
quit_recv: quit_recv, | |
}, | |
game_pipe: GamePipe { | |
cmd_recv: cmd_recv, | |
resp_send: resp_send, | |
done_recv: done_recv, | |
start_send: start_send, | |
quit_send: quit_send, | |
} | |
}); | |
} | |
/// Play the game. It loops forever until the game is over. | |
pub fn play(mut self) { | |
use std::collections::{Deque, DList}; | |
use std::sync::Arc; | |
use std::sync::atomic::{AtomicUint, SeqCst}; | |
use std::task; | |
self.playing = true; | |
let finished = Arc::new(AtomicUint::new(0)); | |
let num_players = self.players.len(); | |
let mut game_pipes = DList::new(); | |
for p in self.players.into_iter() { | |
let def = p.def; | |
let action_pipe = p.action_pipe; | |
let puppet_pipe = p.puppet_pipe; | |
game_pipes.push(p.game_pipe); | |
let finished = finished.clone(); | |
// Spawn each player in a new thread. It'll loop continuously, | |
// taking a turn whenever a signal is sent on `start_recv`, and | |
// exiting the loop when a signal is sent on `quit_recv`. | |
spawn(proc() { | |
let quit_recv = puppet_pipe.quit_recv; | |
let start_recv = puppet_pipe.start_recv; | |
loop { | |
select! { | |
() = quit_recv.recv() => break, | |
round = start_recv.recv() => { | |
def.take_turn(&action_pipe, round); | |
action_pipe.done_send.send(()); | |
} | |
} | |
} | |
// Notify the game handler that this task is done. | |
finished.fetch_add(1, SeqCst); | |
}); | |
} | |
let mut turn = 0u; | |
let mut round = 1u; | |
'game: loop { | |
let mut pipe = game_pipes.pop_front().expect("no players found!"); | |
// This is a hack needed because select!() only supports one identifier, | |
// which means that `() = pipe.done_recv.recv() => { ... }` doesn't work. | |
// The alternative is to use `std::comm::Select` manually, but this is | |
// easier and shorter. | |
let cmd_recv = pipe.cmd_recv; | |
let done_recv = pipe.done_recv; | |
// Signal the player that it's their turn. | |
pipe.start_send.send(round); | |
'player: loop { | |
// Wait for either a player command, or a signal that their turn | |
// has ended. | |
select! { | |
cmd = cmd_recv.recv() => { | |
// Act on the player's request. | |
match cmd { | |
NoCommand => {}, | |
} | |
// Send them a response. | |
pipe.resp_send.send(NoResponse); | |
}, | |
() = done_recv.recv() => { | |
break 'player; | |
} | |
} | |
} | |
// Need to put them back when we're done, since they were moved out. | |
pipe.cmd_recv = cmd_recv; | |
pipe.done_recv = done_recv; | |
// Add the player to the end of the list. | |
game_pipes.push(pipe); | |
// Keep track of the turn. Once the turn number hits the number of | |
// players, we've gone full circle and begun a new round. | |
turn += 1; | |
if turn == num_players { | |
turn = 0u; | |
round += 1; | |
} | |
// Play for ten rounds. | |
if round > 10 { | |
break 'game; | |
} | |
} | |
// Tell everyone to quit. | |
for pipe in game_pipes.iter() { pipe.quit_send.send(()); } | |
// Spin until everyone has finished listening on channels. | |
while finished.load(SeqCst) < num_players { | |
task::deschedule(); | |
} | |
// Game is done. | |
} | |
} | |
/// Player handle. It contains a definition of the player trait, | |
/// as well as several "pipes" that act as two-way communication | |
/// channels. | |
struct PlayerHandle { | |
def: Box<Player + Send>, | |
action_pipe: PlayerActionPipe, | |
puppet_pipe: PlayerPuppetPipe, | |
game_pipe: GamePipe, | |
} | |
/// The `PlayerPuppetPipe` contains the channels that are used | |
/// to tell a player when to start their turn, and when the | |
/// game is over. | |
struct PlayerPuppetPipe { | |
start_recv: Receiver<uint>, | |
quit_recv: Receiver<()>, | |
} | |
/// The `GamePipe` contains the channels that are used to listen | |
/// to player commands, send responses, and tell players when | |
/// to start and quit. | |
struct GamePipe { | |
cmd_recv: Receiver<Command>, | |
resp_send: SyncSender<Response>, | |
done_recv: Receiver<()>, | |
start_send: SyncSender<uint>, | |
quit_send: Sender<()>, | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extern crate game; | |
use game::{Game, Player, PlayerActionPipe, NoCommand, NoResponse}; | |
struct Me; | |
impl Player for Me { | |
fn take_turn(&self, pipe: &PlayerActionPipe, round: uint) { | |
println!("[{}] taking my turn", round); | |
let resp = pipe.send(NoCommand); | |
assert_eq!(resp, NoResponse); | |
} | |
} | |
struct You; | |
impl Player for You { | |
fn take_turn(&self, pipe: &PlayerActionPipe, round: uint) { | |
println!("[{}] taking your turn", round); | |
let resp = pipe.send(NoCommand); | |
assert_eq!(resp, NoResponse); | |
} | |
} | |
fn main() { | |
let mut g = Game::new(); | |
g.add_player(Me); | |
g.add_player(You); | |
g.play(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment