moth/game.py

501 lines
13 KiB
Python
Raw Normal View History

2009-08-25 18:04:42 -06:00
#! /usr/bin/env python3
import json
import asyncore
import asynchat
import socket
import traceback
2009-08-26 15:14:09 -06:00
import time
from errno import EPIPE
# The current time of day
now = time.time()
2009-08-25 18:04:42 -06:00
# Heartbeat frequency (in seconds)
pulse = 2.0
##
## Heartbeat stuff
##
hearts = set()
last_beat = 0
def add_heart(cb):
global hearts
hearts.add(cb)
def del_heart(cb):
global hearts
hearts.remove(cb)
def beat_heart():
global hearts, last_beat, now
if now - last_beat > pulse:
last_beat = now
for cb in hearts:
try:
cb()
except:
traceback.print_exc()
##
## Network stuff
##
2009-08-25 18:04:42 -06:00
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)
2009-08-26 15:14:09 -06:00
self.push(auth + b'\n')
2009-08-25 18:04:42 -06:00
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.
asyncore.close_all()
asynchat.async_chat.handle_error(self)
2009-08-25 18:04:42 -06:00
def set_flag(self, team):
if not team:
team = 'dirtbags'
2009-08-26 15:14:09 -06:00
self.push(team.encode('utf-8') + b'\n')
2009-08-25 18:04:42 -06:00
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, game_factory, flagger, minplayers, maxplayers=None):
2009-08-25 18:04:42 -06:00
self.game_factory = game_factory
self.flagger = flagger
self.minplayers = minplayers
self.maxplayers = maxplayers or minplayers
self.games = set()
2009-08-26 15:14:09 -06:00
self.lobby = set()
2009-08-25 18:04:42 -06:00
self.contestants = []
add_heart(self.heartbeat)
def heartbeat(self):
for game in list(self.games):
game.heartbeat()
2009-08-25 18:04:42 -06:00
def enter_lobby(self, player):
2009-08-26 15:14:09 -06:00
self.lobby.add(player)
self.run_contest()
2009-08-25 18:04:42 -06:00
def add_contestant(self, player):
self.contestants.append(player)
self.run_contest()
def disconnect(self, player):
"""Player has disconnected."""
2009-08-26 15:14:09 -06:00
pass
def set_flag(self, player):
"""Player has the flag."""
2009-08-26 15:14:09 -06:00
self.flagger.set_flag(player.name)
def start_contest(self):
"""Start a new contest."""
2009-08-26 15:14:09 -06:00
self.contestants = list(self.lobby)
2009-08-26 15:14:09 -06:00
2009-08-25 18:04:42 -06:00
def run_contest(self):
2009-08-26 15:14:09 -06:00
# 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])
llen = len(self.lobby)
clen = len(self.contestants)
glen = len(self.games)
if llen == 1:
2009-08-26 15:14:09 -06:00
# Give the flag to the only team connected
self.set_flag(list(self.lobby)[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):
2009-08-26 15:14:09 -06:00
# Give the flag to the last team standing, and start a new contest
self.set_flag(self.contestants.pop())
self.start_contest()
elif (llen >= self.minplayers) and (clen == 0) and (glen == 0):
# There are enough in the lobby to begin a contest now
2009-08-26 15:14:09 -06:00
self.start_contest()
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)
2009-08-25 18:04:42 -06:00
for player in players:
player.attach_game(game)
def declare_winner(self, game, winner=None):
print('winner', game, winner)
self.games.remove(game)
2009-08-26 15:14:09 -06:00
# Winner stays in the contest
if winner:
self.add_contestant(winner)
2009-08-25 18:04:42 -06:00
def player_cmd(self, args):
cmd = args[0].lower()
if cmd == 'lobby':
return [p.name for p in self.lobby]
elif cmd == 'games':
return len(self.games)
2009-08-25 18:04:42 -06:00
elif cmd == 'flag':
return self.flagger.flag
else:
raise ValueError('Unrecognized manager command')
class Player(asynchat.async_chat):
# How long can a connection not send anything at all (unless blocked)?
timeout = 10.0
2009-08-25 18:04:42 -06:00
def __init__(self, sock, manager):
global now
2009-08-25 18:04:42 -06:00
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
self.last_activity = now
2009-08-25 18:04:42 -06:00
def readable(self):
2009-08-26 15:14:09 -06:00
global now, timeout
ret = (not self.blocked) and asynchat.async_chat.readable(self)
if ret:
if now - self.last_activity > self.timeout:
2009-08-26 15:14:09 -06:00
# They waited too long.
self.err('idle timeout')
self.close()
return False
return ret
2009-08-25 18:04:42 -06:00
def block(self):
"""Block reads"""
self.blocked = True
def unblock(self):
"""Unblock reads"""
global now
2009-08-25 18:04:42 -06:00
self.blocked = False
self.last_activity = now
2009-08-25 18:04:42 -06:00
def attach_game(self, game):
self.game = game
if self.pending:
self.unblock()
self.game.handle(self, *self.pending)
2009-08-26 15:14:09 -06:00
self.pending = None
def detach_game(self):
self.game = None
2009-08-25 18:04:42 -06:00
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])
2009-08-26 16:33:07 -06:00
def win(self):
self.detach_game()
2009-08-26 16:33:07 -06:00
self._write_val(['WIN'])
2009-08-25 18:04:42 -06:00
self.unblock()
def lose(self):
self.detach_game()
2009-08-25 18:04:42 -06:00
self._write_val(['LOSE'])
self.unblock()
def collect_incoming_data(self, data):
self.inbuf.append(data)
2009-08-26 16:33:07 -06:00
if len(self.inbuf) > 10:
self.err('Too much data, punk.')
self.close()
2009-08-25 18:04:42 -06:00
def found_terminator(self):
2009-08-26 15:14:09 -06:00
self.last_activity = time.time()
2009-08-25 18:04:42 -06:00
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))
2009-08-26 15:14:09 -06:00
def close(self):
self.unblock()
2009-08-26 15:14:09 -06:00
if self.game:
self.game.player_died(self)
self.manager.disconnect(self)
2009-08-26 15:14:09 -06:00
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
2009-08-25 18:04:42 -06:00
class Game:
def __init__(self, manager, players):
self.manager = manager
self.players = players
self.setup()
def setup(self):
pass
def heartbeat(self):
pass
def declare_winner(self, winner):
self.manager.declare_winner(self, winner)
# Congratulate winner
if winner:
winner.win()
# Inform losers of their loss
losers = [p for p in players if p != winner]
for p in losers:
p.lose()
2009-08-26 15:14:09 -06:00
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(self, player):
"""Player forfeits the game."""
2009-08-26 15:14:09 -06:00
self.remove(player)
2009-08-26 15:14:09 -06:00
def remove(self, player):
"""Remove the player from the game."""
2009-08-26 15:14:09 -06:00
self.players.remove(player)
player.detach_game()
def player_died(self, player):
self.forfeit(player)
2009-08-26 15:14:09 -06:00
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
2009-08-26 15:14:09 -06:00
def __init__(self, manager, players):
global now
2009-08-26 15:14:09 -06:00
self.ended_turn = set()
self.running = True
self.winner = None
self.lastmoved = dict([(p, now) for p in players])
self.began = now
2009-08-26 15:14:09 -06:00
Game.__init__(self, manager, players)
def heartbeat(self):
global now
if now - self.began > self.game_timeout:
self.running = False
# 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.
In a turn-based game, you can't tell anyone that the game has
ended until they make a move. Otherwise, you ruin the illusion
of the game being synchronous. This only sets the winner variable,
which is checked in self.end_turn().
"""
self.running = False
self.winner = winner
2009-08-26 15:14:09 -06:00
def calculate_moves(self):
"""Calculate all moves at the end of a turn.
Override this to define what to do when every player has ended
their turn.
"""
2009-08-26 15:14:09 -06:00
pass
def end_turn(self, player):
"""End player's turn."""
global now
2009-08-26 15:14:09 -06:00
self.ended_turn.add(player)
self.lastmoved[player] = now
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):
2009-08-26 15:14:09 -06:00
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)
2009-08-26 15:14:09 -06:00
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)
2009-08-26 15:14:09 -06:00
##
## Running a game
##
2009-08-26 15:14:09 -06:00
def loop():
global pulse, now
2009-08-26 15:14:09 -06:00
while asyncore.socket_map:
2009-08-26 15:14:09 -06:00
now = time.time()
beat_heart()
asyncore.poll2(timeout=pulse, map=asyncore.socket_map)
2009-08-26 15:14:09 -06:00
2009-08-25 18:04:42 -06:00
def run(game_factory, port, auth, minplayers, maxplayers=None):
2009-08-25 18:04:42 -06:00
flagger = Flagger(('localhost', 6668), auth)
manager = Manager(game_factory, flagger, minplayers, maxplayers)
2009-08-25 18:04:42 -06:00
listener = Listener(('', port), Player, manager)
2009-08-26 15:14:09 -06:00
loop()
2009-08-25 18:04:42 -06:00