mirror of https://github.com/dirtbags/moth.git
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:
parent
0a07f40ff4
commit
2a9b46a662
199
game.py
199
game.py
|
@ -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,58 +140,46 @@ 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):
|
||||||
|
@ -201,7 +187,7 @@ class Manager:
|
||||||
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,6 +337,7 @@ class Game:
|
||||||
self.manager.declare_winner(self, winner)
|
self.manager.declare_winner(self, winner)
|
||||||
|
|
||||||
# Congratulate winner
|
# Congratulate winner
|
||||||
|
if winner:
|
||||||
winner.win()
|
winner.win()
|
||||||
|
|
||||||
# Inform losers of their loss
|
# Inform losers of their loss
|
||||||
|
@ -347,7 +345,6 @@ class Game:
|
||||||
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)
|
||||||
|
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:
|
for p in self.players:
|
||||||
p.unblock()
|
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()
|
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()
|
||||||
|
|
||||||
|
|
11
roshambo.py
11
roshambo.py
|
@ -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()
|
||||||
|
|
|
@ -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 = []
|
||||||
|
|
Loading…
Reference in New Issue