From c05f440b3753d47bf05deb7b7de6ff8689f60e18 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 25 Aug 2009 18:04:42 -0600 Subject: [PATCH] Game framework and roshambo game --- game.py | 232 +++++++++++++++++++++++++++++++++++++++++++++++++ roshambo.py | 42 +++++++++ roshambocli.py | 43 +++++++++ 3 files changed, 317 insertions(+) create mode 100755 game.py create mode 100755 roshambo.py create mode 100755 roshambocli.py diff --git a/game.py b/game.py new file mode 100755 index 0000000..a97dbde --- /dev/null +++ b/game.py @@ -0,0 +1,232 @@ +#! /usr/bin/env python3 + +## +## XXX: Add timeout for Player if not blocked +## XXX: What if someone disconnects? +## + +import json +import asyncore +import asynchat +import socket +import traceback + +class Listener(asyncore.dispatcher): + def __init__(self, addr, player_factory, manager): + asyncore.dispatcher.__init__(self) + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.set_reuse_addr() + self.bind(addr) + self.listen(4) + self.player_factory = player_factory + self.manager = manager + + def handle_accept(self): + conn, addr = self.accept() + player = self.player_factory(conn, self.manager) + # We don't need to keep the player, asyncore.socket_map already + # has a reference to it for as long as it's open. + + +class Flagger(asynchat.async_chat): + """Connection to flagd""" + + def __init__(self, addr, auth): + asynchat.async_chat.__init__(self) + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.connect(addr) + self.push(auth) + self.flag = None + + def handle_read(self): + msg = self.recv(4096) + raise ValueError("Flagger died: %r" % msg) + + def handle_error(self): + # If we lose the connection to flagd, nobody can score any + # points. Terminate everything. + asynchat.async_chat.handle_error(self) + asyncore.close_all() + + def set_flag(self, team): + self.push(b'%s\n' % (team.encode('utf-8'))) + self.flag = team + + +class Manager: + """Contest manager. + + When a player connects and registers, they enter the lobby. As soon + as there are enough players in the lobby to run a game, everyone in + the lobby becomes a contestant. Contestants are assigned to games. + When the game declares a winner, the winner is added back to the list + of contestants, and other players are sent back to the lobby. When + a winner is declared by the last running game, that winner gets the + flag. + + """ + + def __init__(self, nplayers, game_factory, flagger): + self.nplayers = nplayers + self.game_factory = game_factory + self.flagger = flagger + self.games = {} + self.lobby = [] + self.contestants = [] + + def enter_lobby(self, player): + if not player.connected: + return + self.lobby.append(player) + if (not self.contestants) and (len(self.lobby) >= self.nplayers): + # If there are no contestants, the current contest has ended + # and we're ready for a new one. + self.contestants = self.lobby[:] + self.run_contest() + + def add_contestant(self, player): + self.contestants.append(player) + self.run_contest() + + def run_contest(self): + while len(self.contestants) >= self.nplayers: + players = self.contestants[:self.nplayers] + del self.contestants[:self.nplayers] + game = self.game_factory(self, players) + self.games[game] = players + for player in players: + player.attach_game(game) + + def declare_winner(self, game, winner): + players = self.games[game] + del self.games[game] + players.remove(winner) + for p in players: + # Losers go back to the lobby + p.lose() + self.enter_lobby(p) + if not self.games: + # All games have ended and winner is the last player + # standing. They get the flag. + print('%r has the flag.' % winner) + winner.win(True) + self.flagger.set_flag(winner.name) + self.enter_lobby(winner) + else: + # Winner stays in the contest + winner.win() + self.add_contestant(winner) + + def player_cmd(self, args): + cmd = args[0].lower() + if cmd == 'lobby': + return [p.name for p in self.lobby] + elif cmd == 'games': + return [[p.name for p in ps] for ps in self.games.values()] + elif cmd == 'flag': + return self.flagger.flag + else: + raise ValueError('Unrecognized manager command') + + +class Player(asynchat.async_chat): + def __init__(self, sock, manager): + asynchat.async_chat.__init__(self, sock=sock) + self.manager = manager + self.game = None + self.set_terminator(b'\n') + self.inbuf = [] + self.blocked = None + self.name = None + self.pending = None + + def readable(self): + return (not self.blocked) and asynchat.async_chat.readable(self) + + def block(self): + """Block reads""" + self.blocked = True + + def unblock(self): + """Unblock reads""" + self.blocked = False + + def attach_game(self, game): + self.game = game + if self.pending: + self.unblock() + self.game.handle(self, *self.pending) + + def _write_val(self, val): + s = json.dumps(val) + '\n' + self.push(s.encode('utf-8')) + + def write(self, val): + self._write_val(['OK', val]) + + def err(self, msg): + self._write_val(['ERR', msg]) + + def win(self, flag=False): + val = ['WIN'] + if flag: + val.append('You have the flag') + self._write_val(val) + self.unblock() + + def lose(self): + self._write_val(['LOSE']) + self.unblock() + + def collect_incoming_data(self, data): + self.inbuf.append(data) + + def found_terminator(self): + try: + data = b''.join(self.inbuf) + self.inbuf = [] + val = json.loads(data.decode('utf-8')) + cmd, args = val[0].lower(), val[1:] + + if cmd == 'login': + if not self.name: + # XXX Check password + self.name = args[0] + self.write('Welcome to the fray, %s.' % self.name) + self.manager.enter_lobby(self) + else: + self.err('Already logged in.') + elif cmd == '^': + # Send to manager + ret = self.manager.player_cmd(args) + self.write(ret) + elif not self.name: + self.err('Log in first.') + else: + # Send to game + if not self.game: + self.pending = (cmd, args) + self.block() + else: + self.game.handle(self, cmd, args) + except Exception as err: + traceback.print_exc() + self.err(str(err)) + + +class Game: + def __init__(self, manager, players): + self.manager = manager + self.players = players + self.setup() + + def declare_winner(self, player): + self.manager.declare_winner(self, player) + + +def run(nplayers, game_factory, port, auth): + flagger = Flagger(('localhost', 6668), auth) + manager = Manager(2, game_factory, flagger) + listener = Listener(('', port), Player, manager) + asyncore.loop() + diff --git a/roshambo.py b/roshambo.py new file mode 100755 index 0000000..11772d7 --- /dev/null +++ b/roshambo.py @@ -0,0 +1,42 @@ +#! /usr/bin/env python3 + +## +## XXX: Move more of this into game +## + +import game + +class Roshambo(game.Game): + def setup(self): + print("Hello from setup") + self.other_move = None + + def make_move(self, player, move): + print(self.other_move, player, move) + if self.other_move: + other_player, other_move = self.other_move + moves = (move, other_move) + if move in (('rock', 'scissors'), + ('scissors', 'paper'), + ('paper', 'rock')): + # Player wins + self.declare_winner(player) + else: + self.declare_winner(other_player) + other_player.unblock() + else: + self.other_move = (player, move) + player.block() + + def handle(self, player, cmd, args): + if cmd in ('rock', 'scissors', 'paper'): + self.make_move(player, cmd) + else: + raise ValueError('Invalid command') + +def main(): + game.run(2, Roshambo, 5388, b'roshambo:::984233f357ecac03b3e38b9414cd262b') + +if __name__ == '__main__': + main() + diff --git a/roshambocli.py b/roshambocli.py new file mode 100755 index 0000000..e70b385 --- /dev/null +++ b/roshambocli.py @@ -0,0 +1,43 @@ +#! /usr/bin/env python3 + +import socket +import json + +class Client: + rbufsize = -1 + wbufsize = 0 + + def __init__(self, addr): + self.conn = socket.create_connection(addr) + self.wfile = self.conn.makefile('wb', self.wbufsize) + self.rfile = self.conn.makefile('rb', self.rbufsize) + + def write(self, *val): + s = json.dumps(val) + print('--> %s' % s) + self.wfile.write(s.encode('utf-8') + b'\n') + + def read(self): + line = self.rfile.readline().strip().decode('utf-8') + if not line: + return + print ('<-- %s' % line) + return json.loads(line) + + def command(self, *val): + self.write(*val) + ret = self.read() + if ret[0] == 'OK': + return ret[1] + elif ret[0] == 'ERR': + raise ValueError(ret[1]) + else: + print(ret) + +def main(): + c = Client(('localhost', 5388)) + c.command('^', 'lobby') + c.command('login', 'zebra', 'furble') + c.command('rock') + +main()