Create attractive VASL scenarios, with loads of useful information embedded to assist with game play. https://vasl-templates.org
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
vasl-templates/vasl_templates/tools/dump_log_file_analysis.py

268 lines
9.7 KiB

#!/usr/bin/env python3
"""Dump the log file analysis reports generated by the VASSAL shim."""
import os
import itertools
import click
import tabulate
from vasl_templates.webapp.lfa import parse_analysis_report, DEFAULT_LFA_DICE_HOTNESS_WEIGHTS
EXPECTED_DISTRIB = {
"DR": { 2: 2.8, 3: 5.6, 4: 8.3, 5: 11.1, 6: 13.9, 7: 16.7, 8: 13.9, 9: 11.1, 10: 8.3, 11: 5.6, 12: 2.8 },
"dr": { 1: 16.7, 2: 16.7, 3: 16.7, 4: 16.7, 5: 16.7, 6: 16.7 },
}
# ---------------------------------------------------------------------
@click.command()
@click.option( "--file","-f","fname", required=True, help="Log file analysis report." )
@click.option( "--players/--no-players","-p", help="Dump the players." )
@click.option( "--events/--no-events","-e", help="Dump the events." )
@click.option( "--roll-type","-r","roll_type", help="Roll type filter (e.g. IFT or MC)." )
@click.option( "--window","-w","window_size", default=1, help="Moving average window size." )
def main( fname, players, events, roll_type, window_size ):
"""Dump a Log File Analysis report (generated by the VASSAL shim)."""
# initialize
if not os.path.isfile( fname ):
raise RuntimeError( "Can't find the report file: {}".format( fname ) )
# parse the report
report = parse_analysis_report( fname )
# dump each log file
for log_file in report["logFiles"]:
# output a header for the next log file
print( "=== {} {}".format( log_file["filename"], 80*"=" )[ :80 ] )
print()
# dump the scenario details
scenario_name = log_file["scenario"].get( "scenarioName" )
if scenario_name:
print( "Scenario: {}".format( scenario_name ), end="" )
scenario_id = log_file["scenario"].get( "scenarioId" )
if scenario_id:
print( " ({})".format( scenario_id ), end="" )
print()
# dump the players
if players:
print( "Players:" )
max_id_len = max( len(k) for k in report["players"] )
fmt = "- {:%d} = {}" % max_id_len
for player_id,player_name in report["players"].items():
print( fmt.format( player_id, player_name ) )
# dump the DR/dr distributions
dump_distrib( report["players"], log_file, roll_type )
print()
# dump the time-plot
if events or roll_type:
dump_time_plot( report["players"], log_file, roll_type, window_size )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def dump_distrib( players, log_file, roll_type ): #pylint: disable=too-many-locals,too-many-branches,too-many-statements
"""Dump the DR/dr distributions."""
# initialize
stats = { p: {
"DR": { "nRolls": 0, "rollTotal": 0 },
"dr": { "nRolls": 0, "rollTotal": 0 },
} for p in players
}
distrib = { p: {
"DR": { k: 0 for k in range(2,12+1) },
"dr": { k: 0 for k in range(1,6+1) },
} for p in players
}
# process events
for evt in log_file["events"]:
# check if we should process the next event
if evt["eventType"] != "roll":
continue
if roll_type and evt["rollType"].lower() != roll_type.lower():
continue
# update the stats
player_id = evt["playerId"]
key = "DR" if isinstance( evt["rollValue"], list ) else "dr"
stats[ player_id ][ key ][ "nRolls" ] += 1
val = roll_total( evt["rollValue"] )
stats[ player_id ][ key ][ "rollTotal" ] += val
distrib[ player_id ][ key ][ val ] += 1
# calculate averages
avg = lambda x, y: x / y if y != 0 else 0
for player_id in players:
for key in ["DR","dr"]:
stats[ player_id ][ key ][ "rollAverage" ] = avg(
stats[player_id][key].pop("rollTotal"),
stats[player_id][key]["nRolls"]
)
# calculate chi-squared and hotness
for player_id in players:
for key in ["DR","dr"]:
stats[ player_id ][ key ][ "chiSquared" ] = chi_squared(
distrib[player_id][ key ],
EXPECTED_DISTRIB[ key ]
)
stats[ player_id ][ key ][ "hotness" ] = hotness(
distrib[player_id][ key ],
EXPECTED_DISTRIB[ key ],
DEFAULT_LFA_DICE_HOTNESS_WEIGHTS[ key ],
)
# output the results
for key in ["dr","DR"]:
print()
print( "=== {} distribution ===".format( key ) )
vals = range(2,12+1) if key == "DR" else range(1,6+1)
results = [ itertools.chain( [""], vals, ["total","average","chi2","hotness"] ) ]
total_rolls = sum( stats[p][key]["nRolls"] for p in players )
for player_id,player_name in players.items():
# add a row for the stats
row = [ player_name ]
has_vals = False
for val in vals:
nRolls = distrib[player_id][key][val]
if nRolls != 0:
row.append( nRolls )
has_vals = True
else:
row.append( "" )
val2 = stats[player_id][key]["nRolls"]
val2a = val2 / total_rolls if total_rolls != 0 else 0
row.append( "{} ({}%)".format( val2, int(100*val2a+0.5) ) )
row.append( fpfmt( stats[player_id][key]["rollAverage"], 1 ) )
row.append( fpfmt( stats[player_id][key]["chiSquared"], 3 ) )
results.append( row )
# add a row for the averages
if has_vals:
row = [ "" ]
for val in vals:
nRolls = distrib[player_id][key][val]
if nRolls:
val2 = avg( distrib[player_id][key][val], stats[player_id][key]["nRolls"] )
row.append( fpfmt( 100*val2, 1 ) )
else:
row.append( "" )
results.append( row )
# add a row for the dice hotness
row = [ "" ]
partials = stats[ player_id ][ key ][ "hotness" ]
if partials:
for val in partials:
row.append( fpfmt( val, 3 ) )
row.extend( [ "", "", "" ] )
row.append( fpfmt( sum(partials), 3 ) )
results.append( row )
print( tabulate.tabulate( results, headers="firstrow" ) )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def dump_time_plot( players, log_file, roll_type, window_size ):
"""Dump the time-plot values."""
# initialize
rolls = []
windows = { p: [] for p in players }
def dump_rolls():
"""Dump the buffered ROLL events."""
print( tabulate.tabulate( rolls, tablefmt="plain" ) )
def onTurnTrack( evt ): #pylint: disable=unused-variable,possibly-unused-variable
"""Process a TURN TRACK event."""
nonlocal rolls
if rolls:
dump_rolls()
rolls = []
print()
print( "--- {} Turn {} {} ---".format( evt["side"], evt["turnNo"], evt["phase"] ) )
print()
def onRoll( evt ) : #pylint: disable=unused-variable,possibly-unused-variable
"""Process a ROLL event"""
# check if we should process this ROLL event
if roll_type:
if evt["rollType"].lower() != roll_type.lower():
return
player_id = evt[ "playerId" ]
if window_size == 1:
# add the raw roll
if isinstance( evt["rollValue"], int ):
val = evt["rollValue"]
else:
val = ", ".join( str(v) for v in evt["rollValue"] )
rolls.append( [ players[player_id], evt["rollType"], val ] )
else:
# add the moving average
windows[ player_id ].append( roll_total( evt["rollValue"] ) )
if len(windows[player_id]) < window_size:
return
val = sum( windows[player_id] ) / len(windows[player_id])
del windows[player_id][0]
rolls.append( [ players[player_id], val ] )
# process events
print( "=== EVENTS ===" )
print()
for evt in log_file["events"]:
eventType = evt["eventType"]
locals()[ "on" + eventType[0].upper() + eventType[1:] ]( evt )
if rolls:
dump_rolls()
# ---------------------------------------------------------------------
def chi_squared( observed, expected ):
"""Calculate the chi-squared for a set of values."""
nRolls = sum( observed.values() )
if nRolls == 0:
return None
assert observed.keys() == expected.keys()
return sum(
( observed[val]/nRolls - expected[val]/100 ) ** 2 / (expected[val]/100)
for val in expected
)
def hotness( observed, expected, weights ):
"""Calculate the hotness for a set of values."""
nRolls = sum( observed.values() )
if nRolls == 0:
return None
assert observed.keys() == expected.keys() == weights.keys()
partials = []
sign = lambda val: -1 if val < 0 else +1
for val in expected:
diff = observed[val]/nRolls - expected[val]/100
partials.append( sign(diff) * diff**2 * weights[val] / (expected[val]/100) )
return partials
def roll_total( roll ):
"""Calculate the total of a roll."""
if isinstance( roll, list ):
assert all( isinstance(r,int) for r in roll )
return sum( roll )
else:
assert isinstance( roll, int )
return roll
def fpfmt( val, nDigits ):
"""Format a floating point number."""
if val is None:
return "-"
return ("{:.%df}" % nDigits).format( val )
# ---------------------------------------------------------------------
if __name__ == "__main__":
main() #pylint: disable=no-value-for-parameter