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

199
game.py
View File

@ -9,9 +9,6 @@ import time
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
now = time.time()
@ -87,10 +84,12 @@ class Flagger(asynchat.async_chat):
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()
asynchat.async_chat.handle_error(self)
def set_flag(self, team):
if not team:
team = 'dirtbags'
self.push(team.encode('utf-8') + b'\n')
self.flag = team
@ -108,19 +107,18 @@ class Manager:
"""
def __init__(self, nplayers, game_factory, flagger):
self.nplayers = nplayers
def __init__(self, game_factory, flagger, minplayers, maxplayers=None):
self.game_factory = game_factory
self.flagger = flagger
self.games = {}
self.minplayers = minplayers
self.maxplayers = maxplayers or minplayers
self.games = set()
self.lobby = set()
self.contestants = []
add_heart(self.heartbeat)
def heartbeat(self):
games = list(self.games)
for game in games:
print('heartbeat', game)
for game in list(self.games):
game.heartbeat()
def enter_lobby(self, player):
@ -142,58 +140,46 @@ class Manager:
self.flagger.set_flag(player.name)
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 = []
gone = set()
for player in self.lobby:
if player.connected:
self.contestants.append(player)
else:
gone.add(player)
self.lobby.difference_update(gone)
self.contestants = list(self.lobby)
def run_contest(self):
# Purge any disconnected players
self.contestants = [p for p in self.contestants 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)
clen = len(self.contestants)
glen = len(self.games)
if (((llen == 1) )):
if llen == 1:
# Give the flag to the only team connected
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
self.set_flag(self.contestants.pop())
self.start_contest()
if (((llen == 0) and (clen == 0) and (glen == 0)) or
((llen < self.nplayers) and (clen == 0) and (glen == 0)) or
( (clen < self.nplayers) and (glen >= 1))):
pass
elif (((llen >= self.nplayers) and (clen == 0) and (glen == 0))):
elif (llen >= self.minplayers) and (clen == 0) and (glen == 0):
# There are enough in the lobby to begin a contest now
self.start_contest()
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
while len(self.contestants) >= self.minplayers:
players = self.contestants[:self.maxplayers]
del self.contestants[:self.maxplayers]
game = self.game_factory(self, set(players))
self.games.add(game)
for player in players:
player.attach_game(game)
def declare_winner(self, game, winner):
print('winner', game)
players = self.games[game]
del self.games[game]
def declare_winner(self, game, winner=None):
print('winner', game, winner)
self.games.remove(game)
# Winner stays in the contest
winner.win()
if winner:
self.add_contestant(winner)
def player_cmd(self, args):
@ -201,7 +187,7 @@ class Manager:
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()]
return len(self.games)
elif cmd == 'flag':
return self.flagger.flag
else:
@ -209,7 +195,12 @@ class Manager:
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):
global now
asynchat.async_chat.__init__(self, sock=sock)
self.manager = manager
self.game = None
@ -218,14 +209,14 @@ class Player(asynchat.async_chat):
self.blocked = None
self.name = None
self.pending = None
self.last_activity = time.time()
self.last_activity = now
def readable(self):
global now, timeout
ret = (not self.blocked) and asynchat.async_chat.readable(self)
if ret:
if now - self.last_activity > timeout:
if now - self.last_activity > self.timeout:
# They waited too long.
self.err('idle timeout')
self.close()
@ -238,8 +229,10 @@ class Player(asynchat.async_chat):
def unblock(self):
"""Unblock reads"""
global now
self.blocked = False
self.last_activity = time.time()
self.last_activity = now
def attach_game(self, game):
self.game = game
@ -311,8 +304,9 @@ class Player(asynchat.async_chat):
self.err(str(err))
def close(self):
self.unblock()
if self.game:
self.game.disconnect(self)
self.game.player_died(self)
self.manager.disconnect(self)
asynchat.async_chat.close(self)
@ -333,6 +327,9 @@ class Game:
self.players = players
self.setup()
def setup(self):
pass
def heartbeat(self):
pass
@ -340,6 +337,7 @@ class Game:
self.manager.declare_winner(self, winner)
# Congratulate winner
if winner:
winner.win()
# Inform losers of their loss
@ -347,7 +345,6 @@ class Game:
for p in losers:
p.lose()
def handle(self, player, cmd, args):
"""Handle a command from player.
@ -363,24 +360,17 @@ class Game:
raise ValueError('Invalid command: %s' % cmd)
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
your own forfeit method.
self.remove(player)
"""
def remove(self, player):
"""Remove the player from the game."""
if len(self.players) == 2:
if player == self.players[0]:
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."""
self.players.remove(player)
player.detach_game()
def player_died(self, player):
self.forfeit(player)
@ -388,26 +378,45 @@ class TurnBasedGame(Game):
# How long you get to make a move (in seconds)
move_timeout = 2.0
# How long you get to complete the game (in seconds)
game_timeout = 6.0
def __init__(self, manager, players):
global now
self.ended_turn = set()
self.running = True
self.winner = None
self.lastmoved = dict([(p, now) for p in players])
self.began = now
Game.__init__(self, manager, players)
def heartbeat(self):
global now
for p, when in self.lastmoved.items():
if now - when > self.move_timeout:
self.disconnect(p)
if self.winner:
break
if now - self.began > self.game_timeout:
self.running = False
def disconnect(self, player):
Game.disconnect(self, player)
self.end_turn(player)
# Idle players forfeit. They're also booted, so we don't have
# to worry about the synchronous illusion.
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):
"""Declare winner.
@ -419,7 +428,7 @@ class TurnBasedGame(Game):
"""
self.manager.declare_winner(self, winner)
self.running = False
self.winner = winner
def calculate_moves(self):
@ -436,24 +445,38 @@ class TurnBasedGame(Game):
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.lastmoved[player] = now
player.block()
if len(self.ended_turn) == len(self.players):
if not self.players:
self.manager.declare_winner(self, None)
elif len(self.players) == 1:
winners = list(self.players)
self.declare_winner(winners[0])
elif len(self.ended_turn) >= len(self.players):
self.calculate_moves()
if self.running:
for p in self.players:
p.unblock()
self.calculate_moves()
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()
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():
global timeout, pulse, now
global pulse, now
my_timeout = min(timeout, pulse)
while True:
while asyncore.socket_map:
now = time.time()
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)
manager = Manager(2, game_factory, flagger)
manager = Manager(game_factory, flagger, minplayers, maxplayers)
listener = Listener(('', port), Player, manager)
loop()

View File

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

View File

@ -19,7 +19,7 @@ class Client:
def write(self, *val):
s = json.dumps(val)
if self.debug:
print('--> %s' % s)
print(self, '--> %s' % s)
self.wfile.write(s.encode('utf-8') + b'\n')
def read(self):
@ -48,6 +48,7 @@ class RandomBot(threading.Thread):
def run(self):
c = Client(('localhost', 5388))
c.debug = True
#print('lobby', c.command('^', 'lobby'))
c.command('login', self.team, 'furble')
while True:
@ -55,7 +56,10 @@ class RandomBot(threading.Thread):
ret = c.command(move)
if ret == ['WIN']:
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():
bots = []