Fix up game.py; no known bugs

This commit is contained in:
Neale Pickett 2009-08-26 15:14:09 -06:00
parent c05f440b37
commit 689bff487c
3 changed files with 223 additions and 66 deletions

199
game.py
View File

@ -1,15 +1,19 @@
#! /usr/bin/env python3 #! /usr/bin/env python3
##
## XXX: Add timeout for Player if not blocked
## XXX: What if someone disconnects?
##
import json import json
import asyncore import asyncore
import asynchat import asynchat
import socket import socket
import traceback import traceback
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()
class Listener(asyncore.dispatcher): class Listener(asyncore.dispatcher):
def __init__(self, addr, player_factory, manager): def __init__(self, addr, player_factory, manager):
@ -35,7 +39,7 @@ class Flagger(asynchat.async_chat):
asynchat.async_chat.__init__(self) asynchat.async_chat.__init__(self)
self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.connect(addr) self.connect(addr)
self.push(auth) self.push(auth + b'\n')
self.flag = None self.flag = None
def handle_read(self): def handle_read(self):
@ -49,7 +53,7 @@ class Flagger(asynchat.async_chat):
asyncore.close_all() asyncore.close_all()
def set_flag(self, team): def set_flag(self, team):
self.push(b'%s\n' % (team.encode('utf-8'))) self.push(team.encode('utf-8') + b'\n')
self.flag = team self.flag = team
@ -71,24 +75,65 @@ class Manager:
self.game_factory = game_factory self.game_factory = game_factory
self.flagger = flagger self.flagger = flagger
self.games = {} self.games = {}
self.lobby = [] self.lobby = set()
self.contestants = [] self.contestants = []
def enter_lobby(self, player): def enter_lobby(self, player):
if not player.connected: self.lobby.add(player)
return self.run_contest()
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): def add_contestant(self, player):
self.contestants.append(player) self.contestants.append(player)
self.run_contest() self.run_contest()
def leave(self, player):
"""Player has left the tournament"""
pass
def set_flag(self, player):
"""Player has the flag"""
self.flagger.set_flag(player.name)
def start_contest(self):
"""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)
def run_contest(self): 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) )):
# Give the flag to the only team connected
self.set_flag(list(self.lobby)[0])
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))):
self.start_contest()
while len(self.contestants) >= self.nplayers: while len(self.contestants) >= self.nplayers:
players = self.contestants[:self.nplayers] players = self.contestants[:self.nplayers]
del self.contestants[:self.nplayers] del self.contestants[:self.nplayers]
@ -100,22 +145,19 @@ class Manager:
def declare_winner(self, game, winner): def declare_winner(self, game, winner):
players = self.games[game] players = self.games[game]
del self.games[game] del self.games[game]
players.remove(winner)
# Game is over, detach all players
for p in players: for p in players:
# Losers go back to the lobby p.detach_game()
# Inform losers of their loss
losers = [p for p in players if p != winner]
for p in losers:
p.lose() p.lose()
self.enter_lobby(p)
if not self.games: # Winner stays in the contest
# All games have ended and winner is the last player winner.win()
# standing. They get the flag. self.add_contestant(winner)
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): def player_cmd(self, args):
cmd = args[0].lower() cmd = args[0].lower()
@ -139,9 +181,19 @@ 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()
def readable(self): def readable(self):
return (not self.blocked) and asynchat.async_chat.readable(self) global now, timeout
ret = (not self.blocked) and asynchat.async_chat.readable(self)
if ret:
if now - self.last_activity > timeout:
# They waited too long.
self.err('idle timeout')
self.close()
return False
return ret
def block(self): def block(self):
"""Block reads""" """Block reads"""
@ -150,12 +202,17 @@ class Player(asynchat.async_chat):
def unblock(self): def unblock(self):
"""Unblock reads""" """Unblock reads"""
self.blocked = False self.blocked = False
self.last_activity = time.time()
def attach_game(self, game): def attach_game(self, game):
self.game = game self.game = game
if self.pending: if self.pending:
self.unblock() self.unblock()
self.game.handle(self, *self.pending) self.game.handle(self, *self.pending)
self.pending = None
def detach_game(self):
self.game = None
def _write_val(self, val): def _write_val(self, val):
s = json.dumps(val) + '\n' s = json.dumps(val) + '\n'
@ -182,6 +239,7 @@ class Player(asynchat.async_chat):
self.inbuf.append(data) self.inbuf.append(data)
def found_terminator(self): def found_terminator(self):
self.last_activity = time.time()
try: try:
data = b''.join(self.inbuf) data = b''.join(self.inbuf)
self.inbuf = [] self.inbuf = []
@ -213,20 +271,97 @@ class Player(asynchat.async_chat):
traceback.print_exc() traceback.print_exc()
self.err(str(err)) self.err(str(err))
def close(self):
if self.game:
self.game.forfeit(self)
self.manager.leave(self)
asynchat.async_chat.close(self)
def send(self, data):
try:
return asynchat.async_chat.send(self, data)
except socket.error as why:
if why.args[0] == EPIPE:
# Broken pipe, shut down.
self.close()
else:
raise
class Game: class Game:
def __init__(self, manager, players): def __init__(self, manager, players):
self.manager = manager self.manager = manager
self.players = players self.players = players
self.setup() self.setup()
if not hasattr(self, 'forfeit'):
if len(self.players) == 2:
self.forfeit = self.forfeit_2p
else:
raise NotImplementedError('forfeit method undefined')
def declare_winner(self, player): def declare_winner(self, player):
self.manager.declare_winner(self, player) self.manager.declare_winner(self, player)
def handle(self, player, cmd, args):
"""Handle a command from player.
This just dispatches to 'self.do_[cmd]'.
"""
method_name = 'do_%s' % cmd
try:
method = getattr(self, method_name)
method(player, args)
except AttributeError:
raise ValueError('Invalid command: %s' % cmd)
def forfeit_2p(self, player):
"""Player forfeits the game, in a 2-player game.
If your game has more than 2 players, you need to define
your own forfeit method.
"""
if player == self.players[0]:
self.declare_winner(self.players[1])
else:
self.declare_winner(self.players[0])
class TurnBasedGame(Game):
def __init__(self, manager, players):
self.ended_turn = set()
Game.__init__(self, manager, players)
def calculate_moves(self):
"""Override this to define what to do when the turn is over"""
pass
def end_turn(self, player):
"""End player's turn"""
self.ended_turn.add(player)
player.block()
if len(self.ended_turn) == len(self.players):
for p in self.players:
p.unblock()
self.calculate_moves()
self.ended_turn = set()
def loop():
global timeout, now
while True:
now = time.time()
asyncore.poll2(timeout=timeout)
def run(nplayers, game_factory, port, auth): def run(nplayers, game_factory, port, auth):
flagger = Flagger(('localhost', 6668), auth) flagger = Flagger(('localhost', 6668), auth)
manager = Manager(2, game_factory, flagger) manager = Manager(2, game_factory, flagger)
listener = Listener(('', port), Player, manager) listener = Listener(('', port), Player, manager)
asyncore.loop() loop()

View File

@ -1,38 +1,38 @@
#! /usr/bin/env python3 #! /usr/bin/env python3
##
## XXX: Move more of this into game
##
import game import game
class Roshambo(game.Game): class Roshambo(game.TurnBasedGame):
def setup(self): def setup(self):
print("Hello from setup") self.moves = []
self.other_move = None
def calculate_moves(self):
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')
self.moves = []
elif moves in (('rock', 'scissors'),
('scissors', 'paper'),
('paper', 'rock')):
# First player wins
self.declare_winner(self.moves[0][0])
else:
self.declare_winner(self.moves[1][0])
def make_move(self, player, move): def make_move(self, player, move):
print(self.other_move, player, move) self.moves.append((player, move))
if self.other_move: self.end_turn(player)
other_player, other_move = self.other_move
moves = (move, other_move) def do_rock(self, player, args):
if move in (('rock', 'scissors'), self.make_move(player, 'rock')
('scissors', 'paper'),
('paper', 'rock')): def do_scissors(self, player, args):
# Player wins self.make_move(player, 'scissors')
self.declare_winner(player)
else: def do_paper(self, player, args):
self.declare_winner(other_player) self.make_move(player, 'paper')
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(): def main():
game.run(2, Roshambo, 5388, b'roshambo:::984233f357ecac03b3e38b9414cd262b') game.run(2, Roshambo, 5388, b'roshambo:::984233f357ecac03b3e38b9414cd262b')

View File

@ -2,10 +2,14 @@
import socket import socket
import json import json
import random
import time
import threading
class Client: class Client:
rbufsize = -1 rbufsize = -1
wbufsize = 0 wbufsize = 0
debug = False
def __init__(self, addr): def __init__(self, addr):
self.conn = socket.create_connection(addr) self.conn = socket.create_connection(addr)
@ -14,14 +18,16 @@ class Client:
def write(self, *val): def write(self, *val):
s = json.dumps(val) s = json.dumps(val)
print('--> %s' % s) if self.debug:
print('--> %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):
line = self.rfile.readline().strip().decode('utf-8') line = self.rfile.readline().strip().decode('utf-8')
if not line: if not line:
return return
print ('<-- %s' % line) if self.debug:
print ('<-- %s' % line)
return json.loads(line) return json.loads(line)
def command(self, *val): def command(self, *val):
@ -32,12 +38,28 @@ class Client:
elif ret[0] == 'ERR': elif ret[0] == 'ERR':
raise ValueError(ret[1]) raise ValueError(ret[1])
else: else:
print(ret) return ret
class RandomBot(threading.Thread):
def __init__(self, team):
threading.Thread.__init__(self)
self.team = team
def run(self):
c = Client(('localhost', 5388))
#print('lobby', c.command('^', 'lobby'))
c.command('login', self.team, 'furble')
while True:
move = random.choice(['rock', 'scissors', 'paper'])
ret = c.command(move)
if ret == ['WIN']:
print('%s wins' % self.team)
time.sleep(random.uniform(0.2, 2))
def main(): def main():
c = Client(('localhost', 5388)) bots = []
c.command('^', 'lobby') for i in ['zebra', 'aardvark', 'wembly']:
c.command('login', 'zebra', 'furble') bots.append(RandomBot(i).start())
c.command('rock')
main() main()