Skip to content

Instantly share code, notes, and snippets.

@markjenkins
Last active March 22, 2016 20:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save markjenkins/552b5bc94aef8c766522 to your computer and use it in GitHub Desktop.
Save markjenkins/552b5bc94aef8c766522 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python
# coding=UTF8
"""
Copyright (c) 2014 Rudolf Rocker Chess Club
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
@Author Mark Jenkins <mark@markjenkins.ca>
"""
# python imports
from sys import argv
from csv import reader, DictReader, DictWriter
# glick2 imports
from glicko2 import Player as Glicko2Player
RATING_FIELD = 'player_rating'
RATING_DEVIATION_FIELD = 'player_rd'
VOLATILITY_FIELD = 'player_vol'
STAT_FIELDS = (
RATING_FIELD, RATING_DEVIATION_FIELD, VOLATILITY_FIELD,
)
# important, this must match the order in new_player_result_history
PREV_GAME_RESULT_FIELDS = (
'white_wins', 'white_draws', 'white_losses',
'black_wins', 'black_draws', 'black_losses',
)
PLAYER_NAME_FIELD = 'player_name'
PLAYER_ID_FIELD = 'player_id'
OPPONENTS_FIELD = 'opponents'
RESULTS_FIELD = 'results'
INPUT_ONLY_FIELD_NAMES = (OPPONENTS_FIELD, RESULTS_FIELD)
FIELD_NAMES = (
(PLAYER_NAME_FIELD, PLAYER_ID_FIELD) +
STAT_FIELDS +
PREV_GAME_RESULT_FIELDS +
INPUT_ONLY_FIELD_NAMES
) # end tuple addition expression
WIN = 1
DRAW = 0.5
LOSS = 0
WHITE = 'w'
BLACK = 'b'
def unpack_opponent_and_colour(value):
return (value[0], value[1:])
def decode_player(player):
decoded_player = player.copy()
decoded_player.update(
(key, int(value) )
for key, value in player.iteritems()
if key in PREV_GAME_RESULT_FIELDS
)
decoded_player.update(
(key, float(value) )
for key, value in player.iteritems()
if key in STAT_FIELDS
)
decoded_player[OPPONENTS_FIELD] = tuple(
unpack_opponent_and_colour(p)
for p in player[OPPONENTS_FIELD].split(',')
if p != ''
)
decoded_player[RESULTS_FIELD] = tuple(
float(result) for result in player[RESULTS_FIELD].split(',')
if result != ''
)
return decoded_player
def count_for_pattern_match( opponents, result_tuples, w_or_b, result_match ):
return sum( 1
for i, colour_op in enumerate(opponents)
if w_or_b in colour_op[0] and result_tuples[i] == result_match )
def new_player_result_history(player):
# important to work on a copy, the original player needs to be
# left alone so that others can update their rating based on its previous
new_player = player.copy()
new_results = tuple(
count_for_pattern_match( player[OPPONENTS_FIELD], player[RESULTS_FIELD],
colour_match, result_match )
for colour_match, result_match in (
# important, this must match the order of PREV_GAME_RESULT_FIELDS
(WHITE, WIN),
(WHITE, DRAW),
(WHITE, LOSS),
(BLACK, WIN),
(BLACK, DRAW),
(BLACK, LOSS),
)
)
new_player.update(
(field_name, player[field_name] + new_results[i])
for i, field_name in enumerate(PREV_GAME_RESULT_FIELDS)
)
return new_player
def update_player(player, all_players):
# this makes a copy, which is important as we never want the updates
# that we make here to this player to affect the updates we make to
# the other players
p = new_player_result_history(player)
glicko_player = Glicko2Player(
*tuple(
p[field_name]
for field_name in STAT_FIELDS ) )
if len(p[OPPONENTS_FIELD]) > 0:
glicko_player.update_player(
# I can get away with passing a generator as these are
# only iterated over once
( all_players[op][RATING_FIELD]
for (colour, op) in p[OPPONENTS_FIELD] ),
( all_players[op][RATING_DEVIATION_FIELD]
for (colour, op) in p[OPPONENTS_FIELD] ),
p[RESULTS_FIELD]
)
else:
glicko_player.did_not_compete()
p[RATING_FIELD] = str(int( round(glicko_player.getRating(),0) ))
p[RATING_DEVIATION_FIELD] = '%.2f' % glicko_player.getRd()
p[VOLATILITY_FIELD] = '%.5f' % glicko_player.vol
# these fields are input only
for f in INPUT_ONLY_FIELD_NAMES:
del p[f]
return p
def read_db(csv_file_name):
with open(csv_file_name) as f:
csv_read = reader(f)
labels = csv_read.next()
with open(csv_file_name) as f:
csv_read = DictReader(f, FIELD_NAMES)
csv_read.next() # toss the labels
players = {
p[PLAYER_ID_FIELD]: decode_player(p)
for p in csv_read
}
return labels, players
def output_html(html_file, updated_players):
ONE_RD = '68.27% RD'
TWO_RD = '95.45% RD'
# copy all the players al we're modifying for the sake of html output
updated_players = {
player_id: player.copy()
for player_id, player in updated_players.iteritems() }
for player in updated_players.itervalues():
rating = int(player[RATING_FIELD])
rd = float(player[RATING_DEVIATION_FIELD])
player[ONE_RD] = "%s-%s" % (
rating - rd,
rating + rd,
)
player[TWO_RD] = "%s-%s" % (
rating - rd*2,
rating + rd*2,
)
with open(html_file, 'w') as f:
f.write("<table>\n")
f.write("""
<tr>
<th>Nickname</th>
<th>Rating</th>
<th>68.27% range</th>
<th>95.45% RD</th>
<th>Volatility</th>
<th>White wins</th>
<th>White draws</th>
<th>White losses</th>
<th>Black wins</th>
<th>Black draws</th>
<th>Black losses</th>
</tr>
""")
field_names = (
PLAYER_NAME_FIELD, RATING_FIELD,
ONE_RD, TWO_RD, VOLATILITY_FIELD) + PREV_GAME_RESULT_FIELDS
f.write( "\n\n".join(
"<tr>\n" + "\n".join(
"<td>"+ str(outer_player[field_name]) + "</td>"
for field_name in field_names
) + "\n</tr>"
for rating, player_outer_id, outer_player in
sorted( (
(player[RATING_FIELD], player_id, player)
for player_id, player in updated_players.iteritems()
), # end generator expression
reverse=True,
) # sorted
) # join
) # write
f.write("\n\n</table>\n")
def main():
# will crash if program not run with these three arguments
old_file, new_file, html_file = argv[1:]
labels, players = read_db(old_file)
# update all players
updated_players = { player_id: update_player(player, players)
for player_id, player in players.iteritems() }
with open(new_file, 'w') as f:
# not using the last two fields for output, they're input only
csv_write = DictWriter(f, FIELD_NAMES[:-2])
# write out the labels, exclude the last two fields
# which are input only
csv_write.writerow( dict(
zip( FIELD_NAMES[:-2], labels[:-2] ) ) )
csv_write.writerows(
updated_players[player_id]
for player_id in sorted(updated_players.iterkeys()) )
output_html(html_file, updated_players)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment