Get roshambo back to playable

It's more robust now, anyway.  Not perfect.  The trick seems to have been
to make Game dumber, checking for disconnected clients instead of getting
notices about disconnects.
This commit is contained in:
Neale Pickett 2009-08-28 15:52:56 -06:00
parent 0a07f40ff4
commit 2a9b46a662
3 changed files with 125 additions and 99 deletions

205
game.py
View File

@ -9,9 +9,6 @@ import time
from errno import EPIPE from errno import EPIPE
# Number of seconds (roughly) you can be idle before you pass your turn
timeout = 30.0
# The current time of day # The current time of day
now = time.time() now = time.time()
@ -87,10 +84,12 @@ class Flagger(asynchat.async_chat):
def handle_error(self): def handle_error(self):
# If we lose the connection to flagd, nobody can score any # If we lose the connection to flagd, nobody can score any
# points. Terminate everything. # points. Terminate everything.
asynchat.async_chat.handle_error(self)
asyncore.close_all() asyncore.close_all()
asynchat.async_chat.handle_error(self)
def set_flag(self, team): def set_flag(self, team):
if not team:
team = 'dirtbags'
self.push(team.encode('utf-8') + b'\n') self.push(team.encode('utf-8') + b'\n')
self.flag = team self.flag = team
@ -108,19 +107,18 @@ class Manager:
""" """
def __init__(self, nplayers, game_factory, flagger): def __init__(self, game_factory, flagger, minplayers, maxplayers=None):
self.nplayers = nplayers
self.game_factory = game_factory self.game_factory = game_factory
self.flagger = flagger self.flagger = flagger
self.games = {} self.minplayers = minplayers
self.maxplayers = maxplayers or minplayers
self.games = set()
self.lobby = set() self.lobby = set()
self.contestants = [] self.contestants = []
add_heart(self.heartbeat) add_heart(self.heartbeat)
def heartbeat(self): def heartbeat(self):
games = list(self.games) for game in list(self.games):
for game in games:
print('heartbeat', game)
game.heartbeat() game.heartbeat()
def enter_lobby(self, player): def enter_lobby(self, player):
@ -142,66 +140,54 @@ class Manager:
self.flagger.set_flag(player.name) self.flagger.set_flag(player.name)
def start_contest(self): def start_contest(self):
"""Start a new contest. """Start a new contest."""
This is where we purge any disconnected clients from the lobby. self.contestants = list(self.lobby)
"""
self.contestants = []
gone = set()
for player in self.lobby:
if player.connected:
self.contestants.append(player)
else:
gone.add(player)
self.lobby.difference_update(gone)
def run_contest(self): def run_contest(self):
# Purge any disconnected players # Purge any disconnected players
self.contestants = [p for p in self.contestants if p.connected] self.contestants = [p for p in self.contestants if p.connected]
self.lobby = set([p for p in self.lobby if p.connected]) self.lobby = set([p for p in self.lobby if p.connected])
# This is the closest thing we get to pattern matching in python
llen = len(self.lobby) llen = len(self.lobby)
clen = len(self.contestants) clen = len(self.contestants)
glen = len(self.games) glen = len(self.games)
if (((llen == 1) )): if llen == 1:
# Give the flag to the only team connected # Give the flag to the only team connected
self.set_flag(list(self.lobby)[0]) self.set_flag(list(self.lobby)[0])
elif (( (clen == 1) and (glen == 0))): elif llen < self.minplayers:
# More than one connected team, but still not enough to play
self.set_flag(None)
elif (clen == 1) and (glen == 0):
# Give the flag to the last team standing, and start a new contest # Give the flag to the last team standing, and start a new contest
self.set_flag(self.contestants.pop()) self.set_flag(self.contestants.pop())
self.start_contest() self.start_contest()
if (((llen == 0) and (clen == 0) and (glen == 0)) or elif (llen >= self.minplayers) and (clen == 0) and (glen == 0):
((llen < self.nplayers) and (clen == 0) and (glen == 0)) or # There are enough in the lobby to begin a contest now
( (clen < self.nplayers) and (glen >= 1))):
pass
elif (((llen >= self.nplayers) and (clen == 0) and (glen == 0))):
self.start_contest() self.start_contest()
while len(self.contestants) >= self.nplayers: while len(self.contestants) >= self.minplayers:
players = self.contestants[:self.nplayers] players = self.contestants[:self.maxplayers]
del self.contestants[:self.nplayers] del self.contestants[:self.maxplayers]
game = self.game_factory(self, players) game = self.game_factory(self, set(players))
self.games[game] = players self.games.add(game)
for player in players: for player in players:
player.attach_game(game) player.attach_game(game)
def declare_winner(self, game, winner): def declare_winner(self, game, winner=None):
print('winner', game) print('winner', game, winner)
players = self.games[game] self.games.remove(game)
del self.games[game]
# Winner stays in the contest # Winner stays in the contest
winner.win() if winner:
self.add_contestant(winner) self.add_contestant(winner)
def player_cmd(self, args): def player_cmd(self, args):
cmd = args[0].lower() cmd = args[0].lower()
if cmd == 'lobby': if cmd == 'lobby':
return [p.name for p in self.lobby] return [p.name for p in self.lobby]
elif cmd == 'games': elif cmd == 'games':
return [[p.name for p in ps] for ps in self.games.values()] return len(self.games)
elif cmd == 'flag': elif cmd == 'flag':
return self.flagger.flag return self.flagger.flag
else: else:
@ -209,7 +195,12 @@ class Manager:
class Player(asynchat.async_chat): class Player(asynchat.async_chat):
# How long can a connection not send anything at all (unless blocked)?
timeout = 10.0
def __init__(self, sock, manager): def __init__(self, sock, manager):
global now
asynchat.async_chat.__init__(self, sock=sock) asynchat.async_chat.__init__(self, sock=sock)
self.manager = manager self.manager = manager
self.game = None self.game = None
@ -218,14 +209,14 @@ class Player(asynchat.async_chat):
self.blocked = None self.blocked = None
self.name = None self.name = None
self.pending = None self.pending = None
self.last_activity = time.time() self.last_activity = now
def readable(self): def readable(self):
global now, timeout global now, timeout
ret = (not self.blocked) and asynchat.async_chat.readable(self) ret = (not self.blocked) and asynchat.async_chat.readable(self)
if ret: if ret:
if now - self.last_activity > timeout: if now - self.last_activity > self.timeout:
# They waited too long. # They waited too long.
self.err('idle timeout') self.err('idle timeout')
self.close() self.close()
@ -238,8 +229,10 @@ class Player(asynchat.async_chat):
def unblock(self): def unblock(self):
"""Unblock reads""" """Unblock reads"""
global now
self.blocked = False self.blocked = False
self.last_activity = time.time() self.last_activity = now
def attach_game(self, game): def attach_game(self, game):
self.game = game self.game = game
@ -311,8 +304,9 @@ class Player(asynchat.async_chat):
self.err(str(err)) self.err(str(err))
def close(self): def close(self):
self.unblock()
if self.game: if self.game:
self.game.disconnect(self) self.game.player_died(self)
self.manager.disconnect(self) self.manager.disconnect(self)
asynchat.async_chat.close(self) asynchat.async_chat.close(self)
@ -333,6 +327,9 @@ class Game:
self.players = players self.players = players
self.setup() self.setup()
def setup(self):
pass
def heartbeat(self): def heartbeat(self):
pass pass
@ -340,14 +337,14 @@ class Game:
self.manager.declare_winner(self, winner) self.manager.declare_winner(self, winner)
# Congratulate winner # Congratulate winner
winner.win() if winner:
winner.win()
# Inform losers of their loss # Inform losers of their loss
losers = [p for p in players if p != winner] losers = [p for p in players if p != winner]
for p in losers: for p in losers:
p.lose() p.lose()
def handle(self, player, cmd, args): def handle(self, player, cmd, args):
"""Handle a command from player. """Handle a command from player.
@ -363,24 +360,17 @@ class Game:
raise ValueError('Invalid command: %s' % cmd) raise ValueError('Invalid command: %s' % cmd)
def forfeit(self, player): def forfeit(self, player):
"""Player forfeits the game, in a 2-player game. """Player forfeits the game."""
If your game has more than 2 players, you need to define self.remove(player)
your own forfeit method.
""" def remove(self, player):
"""Remove the player from the game."""
if len(self.players) == 2: self.players.remove(player)
if player == self.players[0]: player.detach_game()
self.declare_winner(self.players[1])
else:
self.declare_winner(self.players[0])
else:
raise NotImplementedError('forfeit method undefined')
def disconnect(self, player):
"""Disconnect the player."""
def player_died(self, player):
self.forfeit(player) self.forfeit(player)
@ -388,26 +378,45 @@ class TurnBasedGame(Game):
# How long you get to make a move (in seconds) # How long you get to make a move (in seconds)
move_timeout = 2.0 move_timeout = 2.0
# How long you get to complete the game (in seconds)
game_timeout = 6.0
def __init__(self, manager, players): def __init__(self, manager, players):
global now global now
self.ended_turn = set() self.ended_turn = set()
self.running = True
self.winner = None self.winner = None
self.lastmoved = dict([(p, now) for p in players]) self.lastmoved = dict([(p, now) for p in players])
self.began = now
Game.__init__(self, manager, players) Game.__init__(self, manager, players)
def heartbeat(self): def heartbeat(self):
global now global now
for p, when in self.lastmoved.items(): if now - self.began > self.game_timeout:
if now - when > self.move_timeout: self.running = False
self.disconnect(p)
if self.winner:
break
def disconnect(self, player): # Idle players forfeit. They're also booted, so we don't have
Game.disconnect(self, player) # to worry about the synchronous illusion.
self.end_turn(player) for player in list(self.players):
if not player.connected:
self.remove(player)
continue
when = self.lastmoved[player]
if now - when > self.move_timeout:
player.err('Timeout waiting for a move')
player.close()
# If everyone left, nobody wins.
if not self.players:
self.manager.declare_winner(self, None)
def player_died(self, player):
Game.player_died(self, player)
if player in self.players:
# Update stuff
self.heartbeat()
def declare_winner(self, winner): def declare_winner(self, winner):
"""Declare winner. """Declare winner.
@ -419,7 +428,7 @@ class TurnBasedGame(Game):
""" """
self.manager.declare_winner(self, winner) self.running = False
self.winner = winner self.winner = winner
def calculate_moves(self): def calculate_moves(self):
@ -436,24 +445,38 @@ class TurnBasedGame(Game):
global now global now
# The player has ended their turn; it's okay to tell them now
# that the game has ended.
if self.winner:
if self.winner == player:
player.win()
else:
player.lose()
return
self.ended_turn.add(player) self.ended_turn.add(player)
self.lastmoved[player] = now self.lastmoved[player] = now
player.block() if not self.players:
if len(self.ended_turn) == len(self.players): self.manager.declare_winner(self, None)
for p in self.players: elif len(self.players) == 1:
p.unblock() winners = list(self.players)
self.declare_winner(winners[0])
elif len(self.ended_turn) >= len(self.players):
self.calculate_moves() self.calculate_moves()
if self.running:
for p in self.players:
p.unblock()
else:
# Game has ended, tell everyone how they did
for p in list(self.players):
if self.winner == p:
p.win()
else:
p.lose()
self.manager.declare_winner(self, self.winner)
self.ended_turn = set() self.ended_turn = set()
elif self.running:
player.block()
else:
# The game has ended, tell the player, now that they've made
# a move.
if self.winner == player:
player.win()
self.manager.declare_winner(self, self.winner)
else:
player.lose()
self.remove(player)
## ##
@ -461,19 +484,17 @@ class TurnBasedGame(Game):
## ##
def loop(): def loop():
global timeout, pulse, now global pulse, now
my_timeout = min(timeout, pulse) while asyncore.socket_map:
while True:
now = time.time() now = time.time()
beat_heart() beat_heart()
asyncore.poll2(timeout=my_timeout) asyncore.poll2(timeout=pulse, map=asyncore.socket_map)
def run(nplayers, game_factory, port, auth): def run(game_factory, port, auth, minplayers, maxplayers=None):
flagger = Flagger(('localhost', 6668), auth) flagger = Flagger(('localhost', 6668), auth)
manager = Manager(2, game_factory, flagger) manager = Manager(game_factory, flagger, minplayers, maxplayers)
listener = Listener(('', port), Player, manager) listener = Listener(('', port), Player, manager)
loop() loop()

View File

@ -7,18 +7,19 @@ class Roshambo(game.TurnBasedGame):
self.moves = [] self.moves = []
def calculate_moves(self): def calculate_moves(self):
players = [m[0] for m in self.moves]
moves = [m[1] for m in self.moves] moves = [m[1] for m in self.moves]
if moves[0] == moves[1]: if moves[0] == moves[1]:
self.moves[0][0].write('tie') players[0].write('tie')
self.moves[1][0].write('tie') players[1].write('tie')
self.moves = [] self.moves = []
elif moves in (('rock', 'scissors'), elif moves in (('rock', 'scissors'),
('scissors', 'paper'), ('scissors', 'paper'),
('paper', 'rock')): ('paper', 'rock')):
# First player wins # First player wins
self.declare_winner(self.moves[0][0]) self.declare_winner(players[0])
else: else:
self.declare_winner(self.moves[1][0]) self.declare_winner(players[1])
def make_move(self, player, move): def make_move(self, player, move):
self.moves.append((player, move)) self.moves.append((player, move))
@ -35,7 +36,7 @@ class Roshambo(game.TurnBasedGame):
def main(): def main():
game.run(2, Roshambo, 5388, b'roshambo:::984233f357ecac03b3e38b9414cd262b') game.run(Roshambo, 5388, b'roshambo:::984233f357ecac03b3e38b9414cd262b', 2)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@ -19,7 +19,7 @@ class Client:
def write(self, *val): def write(self, *val):
s = json.dumps(val) s = json.dumps(val)
if self.debug: if self.debug:
print('--> %s' % s) print(self, '--> %s' % s)
self.wfile.write(s.encode('utf-8') + b'\n') self.wfile.write(s.encode('utf-8') + b'\n')
def read(self): def read(self):
@ -48,6 +48,7 @@ class RandomBot(threading.Thread):
def run(self): def run(self):
c = Client(('localhost', 5388)) c = Client(('localhost', 5388))
c.debug = True
#print('lobby', c.command('^', 'lobby')) #print('lobby', c.command('^', 'lobby'))
c.command('login', self.team, 'furble') c.command('login', self.team, 'furble')
while True: while True:
@ -55,7 +56,10 @@ class RandomBot(threading.Thread):
ret = c.command(move) ret = c.command(move)
if ret == ['WIN']: if ret == ['WIN']:
print('%s wins' % self.team) print('%s wins' % self.team)
time.sleep(random.uniform(0.2, 3)) amt = random.uniform(0.2, 2.1)
if c.debug:
print(c, 'sleep %f' % amt)
time.sleep(amt)
def main(): def main():
bots = [] bots = []