mirror of
git://git.savannah.nongnu.org/eliot.git
synced 2024-12-25 21:59:30 +01:00
Reports: initial version, with 2 reports.
The reports are already usable, but the statistics report could be improved.
This commit is contained in:
parent
50a2ab7996
commit
6fbd7febb0
6 changed files with 556 additions and 0 deletions
20
extras/reports/README.txt
Normal file
20
extras/reports/README.txt
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
This directory contains several Python scripts to generate reports from Eliot
|
||||||
|
save games (in XML format). To run them, you need python installed, and in the
|
||||||
|
case of stats.py, you also need the Cheetah template engine
|
||||||
|
(http://www.cheetahtemplate.org/, package python-cheetah on Debian systems).
|
||||||
|
|
||||||
|
At the moment, the following scripts are supported:
|
||||||
|
- stats.py generates an HTML page with information roughly equivalent to
|
||||||
|
the Statistics window in Eliot
|
||||||
|
- sigles.py generates a game summary, usable with the SIGLES program (used
|
||||||
|
in French Scrabble tournaments)
|
||||||
|
|
||||||
|
Each script gives usage information when called with a -h or --help argument.
|
||||||
|
|
||||||
|
Note that eliot.py is not directly usable: it contains utility functions
|
||||||
|
(mainly to extract data from the save game) that are called from the other
|
||||||
|
scripts.
|
||||||
|
|
||||||
|
If you write a new report that you find useful, please consider contributing
|
||||||
|
it!
|
||||||
|
|
249
extras/reports/eliot.py
Normal file
249
extras/reports/eliot.py
Normal file
|
@ -0,0 +1,249 @@
|
||||||
|
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)
|
||||||
|
|
36
extras/reports/sigles.py
Executable file
36
extras/reports/sigles.py
Executable file
|
@ -0,0 +1,36 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
# Eliot modules
|
||||||
|
import eliot
|
||||||
|
|
||||||
|
|
||||||
|
# Command-line parsing
|
||||||
|
parser = argparse.ArgumentParser(description="""Generate a game summary suitable for the SIGLES program
|
||||||
|
(used by the French Scrabble Federation for official tournaments)""")
|
||||||
|
parser.add_argument("-s", "--savegame", help="game saved with Eliot", type=file, required=True)
|
||||||
|
parser.add_argument("-o", "--output", help="output file in latin1 encoding (stdout by default)",
|
||||||
|
type=eliot.FileType(mode='w', encoding="latin1"), default=sys.stdout)
|
||||||
|
parser.add_argument("-n", "--no-empty", help="do not output lines for empty tables", action="store_true")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Do most of the work: open the save game file, parse it,
|
||||||
|
# and build easy to use data structures
|
||||||
|
gameData = eliot.readSaveGame(args.savegame)
|
||||||
|
|
||||||
|
# Output file
|
||||||
|
out = args.output
|
||||||
|
|
||||||
|
# Index the players by their table number
|
||||||
|
playerByTable = dict([(p.tableNb, p) for p in gameData.players])
|
||||||
|
# Generate the report
|
||||||
|
for num in range(1, 1 + max(playerByTable.keys())):
|
||||||
|
if num in playerByTable:
|
||||||
|
p = playerByTable[num]
|
||||||
|
out.write('"%s","%s","%s"\n' % (num, p.name, p.totalScore))
|
||||||
|
elif not args.no_empty:
|
||||||
|
out.write('"%s","%s","%s"\n' % (num, "--table vide--", -1))
|
||||||
|
|
112
extras/reports/stats.css
Normal file
112
extras/reports/stats.css
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
border: 1px solid white;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statsTable {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gameTitle, .colHeader {
|
||||||
|
background-color: #666699;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playerName {
|
||||||
|
background-color: #9999cc;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.master {
|
||||||
|
background-color: #b3b3b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topSolo {
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
/* Generated by Ultimate CSS Gradient Generator (http://www.colorzilla.com/gradient-editor/) */
|
||||||
|
background: #f50000; /* Old browsers */
|
||||||
|
/* IE9 SVG, needs conditional override of 'filter' to 'none' */
|
||||||
|
background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2Y1MDAwMCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNmNGJkYzAiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+);
|
||||||
|
background: -moz-linear-gradient(top, #f50000 0%, #f4bdc0 100%); /* FF3.6+ */
|
||||||
|
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f50000), color-stop(100%,#f4bdc0)); /* Chrome,Safari4+ */
|
||||||
|
background: -webkit-linear-gradient(top, #f50000 0%,#f4bdc0 100%); /* Chrome10+,Safari5.1+ */
|
||||||
|
background: -o-linear-gradient(top, #f50000 0%,#f4bdc0 100%); /* Opera 11.10+ */
|
||||||
|
background: -ms-linear-gradient(top, #f50000 0%,#f4bdc0 100%); /* IE10+ */
|
||||||
|
background: linear-gradient(to bottom, #f50000 0%,#f4bdc0 100%); /* W3C */
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f50000', endColorstr='#f4bdc0',GradientType=0 ); /* IE6-8 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.top {
|
||||||
|
/* Generated by Ultimate CSS Gradient Generator (http://www.colorzilla.com/gradient-editor/) */
|
||||||
|
background: #f2a675; /* Old browsers */
|
||||||
|
/* IE9 SVG, needs conditional override of 'filter' to 'none' */
|
||||||
|
background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIxJSIgc3RvcC1jb2xvcj0iI2YyYTY3NSIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNmMGUzZGEiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+);
|
||||||
|
background: -moz-linear-gradient(top, #f2a675 1%, #f0e3da 100%); /* FF3.6+ */
|
||||||
|
background: -webkit-gradient(linear, left top, left bottom, color-stop(1%,#f2a675), color-stop(100%,#f0e3da)); /* Chrome,Safari4+ */
|
||||||
|
background: -webkit-linear-gradient(top, #f2a675 1%,#f0e3da 100%); /* Chrome10+,Safari5.1+ */
|
||||||
|
background: -o-linear-gradient(top, #f2a675 1%,#f0e3da 100%); /* Opera 11.10+ */
|
||||||
|
background: -ms-linear-gradient(top, #f2a675 1%,#f0e3da 100%); /* IE10+ */
|
||||||
|
background: linear-gradient(to bottom, #f2a675 1%,#f0e3da 100%); /* W3C */
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f2a675', endColorstr='#f0e3da',GradientType=0 ); /* IE6-8 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.subTopSolo {
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
/* Generated by Ultimate CSS Gradient Generator (http://www.colorzilla.com/gradient-editor/) */
|
||||||
|
background: #0668c1; /* Old browsers */
|
||||||
|
/* IE9 SVG, needs conditional override of 'filter' to 'none' */
|
||||||
|
background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIxJSIgc3RvcC1jb2xvcj0iIzA2NjhjMSIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNjNGQ2ZWEiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+);
|
||||||
|
background: -moz-linear-gradient(top, #0668c1 1%, #c4d6ea 100%); /* FF3.6+ */
|
||||||
|
background: -webkit-gradient(linear, left top, left bottom, color-stop(1%,#0668c1), color-stop(100%,#c4d6ea)); /* Chrome,Safari4+ */
|
||||||
|
background: -webkit-linear-gradient(top, #0668c1 1%,#c4d6ea 100%); /* Chrome10+,Safari5.1+ */
|
||||||
|
background: -o-linear-gradient(top, #0668c1 1%,#c4d6ea 100%); /* Opera 11.10+ */
|
||||||
|
background: -ms-linear-gradient(top, #0668c1 1%,#c4d6ea 100%); /* IE10+ */
|
||||||
|
background: linear-gradient(to bottom, #0668c1 1%,#c4d6ea 100%); /* W3C */
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#0668c1', endColorstr='#c4d6ea',GradientType=0 ); /* IE6-8 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.subTop {
|
||||||
|
/* Generated by Ultimate CSS Gradient Generator (http://www.colorzilla.com/gradient-editor/) */
|
||||||
|
background: #b8cad8; /* Old browsers */
|
||||||
|
/* IE9 SVG, needs conditional override of 'filter' to 'none' */
|
||||||
|
background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIxJSIgc3RvcC1jb2xvcj0iI2I4Y2FkOCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNlN2ViZWUiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+);
|
||||||
|
background: -moz-linear-gradient(top, #b8cad8 1%, #e7ebee 100%); /* FF3.6+ */
|
||||||
|
background: -webkit-gradient(linear, left top, left bottom, color-stop(1%,#b8cad8), color-stop(100%,#e7ebee)); /* Chrome,Safari4+ */
|
||||||
|
background: -webkit-linear-gradient(top, #b8cad8 1%,#e7ebee 100%); /* Chrome10+,Safari5.1+ */
|
||||||
|
background: -o-linear-gradient(top, #b8cad8 1%,#e7ebee 100%); /* Opera 11.10+ */
|
||||||
|
background: -ms-linear-gradient(top, #b8cad8 1%,#e7ebee 100%); /* IE10+ */
|
||||||
|
background: linear-gradient(to bottom, #b8cad8 1%,#e7ebee 100%); /* W3C */
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#b8cad8', endColorstr='#e7ebee',GradientType=0 ); /* IE6-8 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.normal1 {
|
||||||
|
background-color: #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.normal2 {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legendTable {
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legendIcon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legendText {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
32
extras/reports/stats.py
Executable file
32
extras/reports/stats.py
Executable file
|
@ -0,0 +1,32 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
from Cheetah.Template import Template
|
||||||
|
|
||||||
|
# Eliot modules
|
||||||
|
import eliot
|
||||||
|
|
||||||
|
|
||||||
|
# Command-line parsing
|
||||||
|
parser = argparse.ArgumentParser(description="""Generate an HTML page roughly equivalent to the Statistics window in Eliot""")
|
||||||
|
parser.add_argument("-s", "--savegame", help="game saved with Eliot", type=file, required=True)
|
||||||
|
parser.add_argument("-o", "--output", help="output file (stdout by default)",
|
||||||
|
type=argparse.FileType('w'), default=sys.stdout)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Do most of the work: open the save game file, parse it,
|
||||||
|
# and build easy to use data structures
|
||||||
|
gameData = eliot.readSaveGame(args.savegame.name)
|
||||||
|
|
||||||
|
# Load the template
|
||||||
|
with file("stats.tmpl") as f:
|
||||||
|
templDef = f.read()
|
||||||
|
|
||||||
|
# Fill it with values
|
||||||
|
templ = Template(templDef, {'gameData': gameData})
|
||||||
|
|
||||||
|
# Print the generated document
|
||||||
|
args.output.write(str(templ));
|
||||||
|
|
107
extras/reports/stats.tmpl
Normal file
107
extras/reports/stats.tmpl
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0//EN" "http://www.w3.org/Math/DTD/mathml2/xhtml-math11-f.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="stats.css" />
|
||||||
|
<title>Page Title XXX</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<table class="statsTable">
|
||||||
|
<tr class="statsTable">
|
||||||
|
<td class="gameTitle" colspan="${gameData.turnNb + 9}">$gameData.name</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="statsTable">
|
||||||
|
<td class="colHeader">Player</td>
|
||||||
|
#for $turn in $gameData.turns.keys
|
||||||
|
<td class="colHeader">$turn</td>
|
||||||
|
#end for
|
||||||
|
<td class="colHeader">Sub-total</td>
|
||||||
|
<td class="colHeader">S</td>
|
||||||
|
<td class="colHeader">W</td>
|
||||||
|
<td class="colHeader">P</td>
|
||||||
|
<td class="colHeader">Total</td>
|
||||||
|
<td class="colHeader">Diff</td>
|
||||||
|
<td class="colHeader">Game %</td>
|
||||||
|
<td class="colHeader">Ranking</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="statsTable">
|
||||||
|
<td class="master playerName">Master</td>
|
||||||
|
#for $turn in $gameData.turns.values
|
||||||
|
<td class="master">$turn.move.points</td>
|
||||||
|
#end for
|
||||||
|
<td class="master">$gameData.totalScore</td>
|
||||||
|
<td class="master"></td>
|
||||||
|
<td class="master"></td>
|
||||||
|
<td class="master"></td>
|
||||||
|
<td class="master">$gameData.totalScore</td>
|
||||||
|
<td class="master"></td>
|
||||||
|
<td class="master">100%</td>
|
||||||
|
<td class="master"></td>
|
||||||
|
</tr>
|
||||||
|
#for $player in $gameData.players
|
||||||
|
<tr class="statsTable">
|
||||||
|
<td class="playerName">$player.name</td>
|
||||||
|
#for $turn in $player.turns.values
|
||||||
|
#if $gameData.isSolo($turn.num, $turn.move)
|
||||||
|
#if $gameData.isTop($turn.num, $turn.move)
|
||||||
|
<td class="topSolo">$turn.move.points</td>
|
||||||
|
#else
|
||||||
|
<td class="subTopSolo">$turn.move.points</td>
|
||||||
|
#end if
|
||||||
|
#else
|
||||||
|
#if $gameData.isTop($turn.num, $turn.move)
|
||||||
|
<td class="top">$turn.move.points</td>
|
||||||
|
#elif $gameData.isSubTop($turn.num, $turn.move)
|
||||||
|
<td class="subTop">$turn.move.points</td>
|
||||||
|
#elif $player.id % 2 == 1
|
||||||
|
<td class="normal1">$turn.move.points</td>
|
||||||
|
#else
|
||||||
|
<td class="normal2">$turn.move.points</td>
|
||||||
|
#end if
|
||||||
|
#end if
|
||||||
|
#end for
|
||||||
|
#if $player.id % 2 == 1
|
||||||
|
<td class="normal1">$player.rawScore</td>
|
||||||
|
<td class="normal1">$player.solosPoints</td>
|
||||||
|
<td class="normal1">$player.warningsNb</td>
|
||||||
|
<td class="normal1">$player.penaltiesPoints</td>
|
||||||
|
<td class="normal1">$player.totalScore</td>
|
||||||
|
<td class="normal1">$player.diffWithTop</td>
|
||||||
|
<td class="normal1">$player.percentTop</td>
|
||||||
|
<td class="normal1">$player.rank</td>
|
||||||
|
#else
|
||||||
|
<td class="normal2">$player.rawScore</td>
|
||||||
|
<td class="normal2">$player.solosPoints</td>
|
||||||
|
<td class="normal2">$player.warningsNb</td>
|
||||||
|
<td class="normal2">$player.penaltiesPoints</td>
|
||||||
|
<td class="normal2">$player.totalScore</td>
|
||||||
|
<td class="normal2">$player.diffWithTop</td>
|
||||||
|
<td class="normal2">$player.percentTop</td>
|
||||||
|
<td class="normal2">$player.rank</td>
|
||||||
|
#end if
|
||||||
|
</tr>
|
||||||
|
#end for
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table class="legendTable">
|
||||||
|
<tr>
|
||||||
|
<td class="legendIcon topSolo"></td>
|
||||||
|
<td class="legendText">Solo with maximal score</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="legendIcon subTopSolo"></td>
|
||||||
|
<td class="legendText">Solo with non-maximal score</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="legendIcon top"></td>
|
||||||
|
<td class="legendText">Move with maximal score (not a solo)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="legendIcon subTop"></td>
|
||||||
|
<td class="legendText">Move with non-maximal score (not a solo)</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
Loading…
Reference in a new issue