Skip to content

Instantly share code, notes, and snippets.

@dradtke
Last active August 29, 2015 14:07
Show Gist options
  • Save dradtke/ee1cb4a24e03186ba784 to your computer and use it in GitHub Desktop.
Save dradtke/ee1cb4a24e03186ba784 to your computer and use it in GitHub Desktop.
An experiment in designing a turn-based concurrent game architecture.
//! 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<()>,
}
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