eliot/extras/reports/eliot.py
Olivier Teulière 6fbd7febb0 Reports: initial version, with 2 reports.
The reports are already usable, but the statistics report could be
improved.
2012-12-11 21:33:47 +01:00

249 lines
8.8 KiB
Python

import xml.etree.ElementTree as ET
import codecs
# Define some classes to store parsed data in a structured way
class Move(object):
"""Data for a move. The 'type' and 'points' members are always valid.
Other members depend on the type of move:
- for valid and invalid moves, word and coords can be used
- for a move changing letters, word can be used (it contains the changed letters)
- otherwise, no other field is valid"""
def __init__(self, type, points, word, coords):
self.type = type
self.points = int(points)
self.word = word
self.coords = coords
def __str__(self):
return "Move (type=%s): %s" % (self.type, self.points)
def isValid(self):
return self.type == "valid"
def isInvalid(self):
return self.type == "invalid"
def isChange(self):
return self.type == "change"
def isPass(self):
return self.type == "pass"
def isNone(self):
return self.type == "none"
class Turn(object):
"""Data for a player turn (or game turn)"""
def __init__(self, num, rack, move, events):
self.num = num
self.rack = rack
self.move = move
self.events = events
class Player(object):
"""Statistics of a player"""
def __init__(self, id, name):
self.id = id
self.name = name
self.tableNb = None
self.turns = {}
class GameData(object):
"""Entry point for all the game data"""
def __init__(self, name):
self.players = []
self.turns = {}
self.turnNb = 0
self.name = name
self.mode = None
def isTop(self, turnNb, move):
if move is None:
return False
movePoints = move.points
masterPoints = self.turns[turnNb].move.points
return masterPoints <= movePoints
def isSubTop(self, turnNb, move):
if move is None:
return False
playerPoints = [p.turns[turnNb].move.points for p in self.players]
return move.points >= max(playerPoints)
def isSolo(self, turnNb, move):
if move is None:
return False
playerPoints = [p.turns[turnNb].move.points for p in self.players]
return self.isSubTop(turnNb, move) and playerPoints.count(move.points) == 1
def readSaveGame(xmlFileName):
# Entry point for all the data
gameData = GameData(xmlFileName)
# XML tree
tree = ET.parse(xmlFileName)
gameData.mode = tree.findtext("Game/Mode")
# Parse players
for playerElem in tree.findall("Game/Player"):
player = Player(
int(playerElem.get("id")),
playerElem.findtext("Name"))
# Priori to Eliot 2.1, there was no table number in the file
tableNb = playerElem.findtext("TableNb");
if tableNb is None:
player.tableNb = player.id
else:
player.tableNb = int(tableNb)
gameData.players.append(player)
# Parse turns
turnNum = 0
for turnElem in tree.findall("History/Turn"):
turnNum += 1
# Get players data for this turn
for p in gameData.players:
# Get the player rack
allRacks = turnElem.findall("PlayerRack[@playerId='%s']" % p.id)
# Before Eliot 2.1 (version 2.0 only), the attribute was called "playerid"
allRacks.extend(turnElem.findall("PlayerRack[@playerid='%s']" % p.id))
if len(allRacks) == 0 and turnNum > 1:
rack = p.turns[turnNum - 1].rack
else:
# FIXME: does not work well in freegame mode,
# because we take the next rack instead of the current one
rack = allRacks[-1].text
# Get the player move
allMoves = turnElem.findall("PlayerMove[@playerId='%s']" % p.id)
# Before Eliot 2.1 (version 2.0 only), the attribute was called "playerid"
allMoves.extend(turnElem.findall("PlayerMove[@playerid='%s']" % p.id))
if len(allMoves) == 0:
move = Move("none", "0", "", "")
else:
lastMove = allMoves[-1]
move = Move(lastMove.get("type"),
lastMove.get("points"),
lastMove.get("word"),
lastMove.get("coord"))
if move.isChange():
move.word = lastMove.get("letters")
# Create turn
# TODO: handle events
turn = Turn(turnNum, rack, move, None)
p.turns[turnNum] = turn
# Get game data for this turn
rack = turnElem.findtext("GameRack")
gameMove = turnElem.find("GameMove")
if gameMove is None:
move = Move("none", "0", "", "")
else:
move = Move(gameMove.get("type"),
gameMove.get("points"),
gameMove.get("word"),
gameMove.get("coord"))
if move.isChange():
move.word = lastMove.get("letters")
gameData.turns[turnNum] = Turn(turnNum, rack, move, None);
gameData.turnNb = turnNum
# Parse statistics (introduced in version 2.1: version 2.0 didn't have that)
if tree.find("Statistics/GameStats") is None:
# For Eliot 2.0 only (version 2.1 introduced the tag)
gameData.totalScore = sum([t.move.points for t in gameData.turns.values()])
for p in gameData.players:
p.rawScore = sum([t.move.points for t in p.turns.values()])
# FIXME: should be computed from events
p.warningsNb = 0
p.penaltiesPoints = 0
p.solosPoints = 0
p.totalScore = p.rawScore + p.warningsNb + p.penaltiesPoints + p.solosPoints
p.diffWithTop = gameData.totalScore - p.totalScore
# TODO: percent
p.percentTop = ""
# TODO: rank
p.rank = 0
else:
playersById = dict([(p.id, p) for p in gameData.players])
for playerStat in tree.findall("Statistics/PlayerStats"):
playerId = int(playerStat.get("playerId"))
player = playersById[playerId]
assert player != None, \
"No player found with id %s" % playerId
player.rawScore = int(playerStat.get("rawScore"))
player.warningsNb = int(playerStat.get("warningsNb"))
player.penaltiesPoints = int(playerStat.get("penaltiesPoints"))
player.solosPoints = int(playerStat.get("solosPoints"))
player.totalScore = int(playerStat.get("totalScore"))
player.diffWithTop = int(playerStat.get("diffWithTop"))
player.percentTop = playerStat.get("percentTop")
player.rank = int(playerStat.get("rank"))
gameData.totalScore = tree.find("Statistics/GameStats").get("totalScore")
return gameData
# Utility class, to replace argparse.FileType until Issue 11175 is solved (http://bugs.python.org/issue11175)
class FileType(object):
"""Factory for creating file object types
Instances of FileType are typically passed as type= arguments to the
ArgumentParser add_argument() method.
Keyword Arguments:
- mode -- A string indicating how the file is to be opened. Accepts the
same values as the builtin open() function.
- bufsize -- The file's desired buffer size. Accepts the same values as
the builtin open() function.
- encoding -- The file's encoding. Accepts the same values as the
the builtin open() function.
- errors -- A string indicating how encoding and decoding errors are to
be handled. Accepts the same value as the builtin open() function.
"""
def __init__(self, mode='r', bufsize=-1, encoding=None, errors=None):
self._mode = mode
self._bufsize = bufsize
self._encoding = encoding
self._errors = errors
def __call__(self, string):
# the special argument "-" means sys.std{in,out}
if string == '-':
if 'r' in self._mode:
return _sys.stdin
elif 'w' in self._mode:
return _sys.stdout
else:
msg = _('argument "-" with mode %r') % self._mode
raise ValueError(msg)
# all other arguments are used as file names
try:
return codecs.open(string, self._mode, self._encoding, self._errors, self._bufsize)
except IOError as e:
message = _("can't open '%s': %s")
raise ArgumentTypeError(message % (string, e))
def __repr__(self):
args = self._mode, self._bufsize
kwargs = [('encoding', self._encoding), ('errors', self._errors)]
args_str = ', '.join([repr(arg) for arg in args if arg != -1] +
['%s=%r' % (kw, arg) for kw, arg in kwargs
if arg is not None])
return '%s(%s)' % (type(self).__name__, args_str)