@ -0,0 +1,268 @@ |
||||
#!/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 |
||||
"""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 |
||||
"""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 |
@ -0,0 +1,154 @@ |
||||
""" Webapp handlers. """ |
||||
|
||||
import os |
||||
import time |
||||
import base64 |
||||
import logging |
||||
import xml.etree.cElementTree as ET |
||||
|
||||
from flask import request, jsonify |
||||
|
||||
from vasl_templates.webapp import app |
||||
from vasl_templates.webapp.vassal import VassalShim |
||||
from vasl_templates.webapp.utils import SimpleError, TempFile |
||||
|
||||
# weights for each possible roll value |
||||
DEFAULT_LFA_DICE_HOTNESS_WEIGHTS = { |
||||
"DR": { 2: 30, 3: 18, 4: 10, 5: 5, 6: 2, 7: 0, 8: -2, 9: -5, 10: -10, 11: -18, 12: -30 }, |
||||
"dr": { 1: 5, 2: 2.5, 3: 1, 4: -1, 5: -2.5, 6: -5 } |
||||
} |
||||
|
||||
# minimum number of rolls for dice hotness to be considered reasonable |
||||
DEFAULT_LFA_DICE_HOTNESS_THRESHOLDS = { |
||||
"DR": 100, "dr": 50 |
||||
} |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@app.route( "/analyze-vlogs", methods=["POST"] ) |
||||
def analyze_vlogs(): #pylint: disable=too-many-locals |
||||
"""Analyze VASL log file(s).""" |
||||
|
||||
# parse the request |
||||
start_time = time.time() |
||||
vlog_data = request.json |
||||
|
||||
# initialize |
||||
logger = logging.getLogger( "analyze_vlogs" ) |
||||
temp_files = [] |
||||
|
||||
try: |
||||
|
||||
# save each VLOG file in a temp file |
||||
if not vlog_data: |
||||
raise SimpleError( "No log files were submitted." ) |
||||
for vlog_no, vlog in enumerate( vlog_data ): |
||||
fname, data = vlog |
||||
data = base64.b64decode( data ) |
||||
logger.info( "Analyzing VLOG (#bytes=%d): %s", len(data), fname ) |
||||
temp_file = TempFile() |
||||
temp_file.open() |
||||
temp_file.write( data ) |
||||
temp_file.close( delete=False ) |
||||
save_fname = app.config.get( "ANALYZE_VLOG_INPUT" ) |
||||
if save_fname: |
||||
if len(vlog_data) == 1: |
||||
temp_file.save_copy( save_fname, logger, "VLOG data" ) |
||||
else: |
||||
parts = os.path.splitext( save_fname ) |
||||
temp_file.save_copy( "{}-{}".format(parts[0],1+vlog_no) + parts[1], logger, "VLOG data" ) |
||||
temp_files.append( temp_file ) |
||||
|
||||
# run the VASSAL shim to analyze the VLOG file(s) |
||||
with TempFile() as report_file: |
||||
report_file.close( delete=False ) |
||||
vassal_shim = VassalShim() |
||||
fnames = [ tf.name for tf in temp_files ] |
||||
fnames.append( report_file.name ) |
||||
vassal_shim.analyze_logfiles( *fnames ) |
||||
report_file.save_copy( app.config.get("ANALYZE_VLOG_REPORT"), logger, "analysis report" ) |
||||
report = parse_analysis_report( report_file.name, logger ) |
||||
|
||||
except Exception as ex: #pylint: disable=broad-except |
||||
|
||||
return VassalShim.translate_vassal_shim_exception( ex, logger ) |
||||
|
||||
finally: |
||||
|
||||
# clean up |
||||
for tf in temp_files: |
||||
tf.close( delete=True ) |
||||
|
||||
# insert the filenames for each log file, as they were passed in to us |
||||
for vlog_no,vlog in enumerate( vlog_data ): |
||||
report["logFiles"][ vlog_no ]["filename"] = vlog[0] |
||||
|
||||
# return the results |
||||
logger.info( "Analyzed the VLOG file(s) OK: elapsed=%.3fs", time.time()-start_time ) |
||||
return jsonify( report ) |
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||
|
||||
def parse_analysis_report( fname, logger=None ): |
||||
"""Parse the analysis report generated by the VASSAL shim.""" |
||||
|
||||
# initialize |
||||
doc = ET.parse( fname ) |
||||
|
||||
# get the complete list of players across all the log files |
||||
players = {} |
||||
for elem in doc.findall( ".//diceEvent" ): |
||||
player_name = elem.attrib[ "player" ] |
||||
if player_name not in players: |
||||
# NOTE: ChartJS (in the frontend Javascript) identifies datasets using a 0-based index, |
||||
# so to avoid accidentally mixing these up with player ID's, we generate non-numeric player ID's. |
||||
player_id = "p:{}".format( len(players) + 1 ) |
||||
players[ player_name ] = player_id |
||||
|
||||
# generate the results for each log file |
||||
log_files = [] |
||||
for logFileElem in doc.findall( ".//logFile" ): |
||||
|
||||
# process the events for the next log file |
||||
events, scenario = [], {} |
||||
for elem in logFileElem.find( ".//events" ): |
||||
|
||||
if elem.tag == "diceEvent": |
||||
# found a DICE ROLL event |
||||
player_id = players[ elem.attrib["player"] ] |
||||
values = [ int(v) for v in elem.text.split(",") ] |
||||
events.append( { |
||||
"eventType": "roll", |
||||
"playerId": player_id, |
||||
"rollType": elem.attrib[ "rollType" ], |
||||
"rollValue": values[0] if len(values) == 1 else values |
||||
} ) |
||||
elif elem.tag == "turnTrackEvent": |
||||
# found a TURN TRACK event |
||||
events.append( { |
||||
"eventType": "turnTrack", |
||||
"side": elem.attrib[ "side" ], |
||||
"turnNo": elem.attrib[ "turnNo" ], |
||||
"phase": elem.attrib[ "phase" ] |
||||
} ) |
||||
else: |
||||
if logger: |
||||
logger.warn( "Found an unknown analysis event: %s", elem.tag ) |
||||
|
||||
# extract the scenario details |
||||
elem = logFileElem.find( ".//scenario" ) |
||||
if elem is not None: |
||||
scenario[ "scenarioName" ] = elem.text |
||||
if "id" in elem.attrib: |
||||
scenario[ "scenarioId" ] = elem.attrib["id"] |
||||
|
||||
log_files.append( { |
||||
"filename": logFileElem.attrib[ "filename" ], |
||||
"scenario": scenario, |
||||
"events": events, |
||||
} ) |
||||
|
||||
return { |
||||
"players": { v: k for k,v in players.items() }, |
||||
"logFiles": log_files |
||||
} |
@ -0,0 +1,284 @@ |
||||
/* jshint esnext: true */ |
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
// Wrapper around the results of a log file analysis.
|
||||
class LogFileAnalysis |
||||
{ |
||||
|
||||
constructor( data, logFileNo ) { |
||||
|
||||
// initialize
|
||||
var logFiles=[], playersSeen={}, events=[], title=null, title2=null ; |
||||
|
||||
// process each log file
|
||||
data.logFiles.forEach( function( logFile, index ) { |
||||
if ( logFileNo >= 0 && logFileNo != index ) |
||||
return ; |
||||
// record the next log file and generate an event for it
|
||||
logFiles.push( logFile.filename ? logFile.filename : "file #"+(1+index) ) ; |
||||
events.push( { eventType: "logFile", filename: logFile.filename } ) ; |
||||
// take a copy of each event
|
||||
logFile.events.forEach( function( evt ) { |
||||
events.push( evt ) ; |
||||
playersSeen[ evt.playerId ] = true ; |
||||
} ) ; |
||||
// check if we found a scenario name in the log file
|
||||
if ( logFile.scenario.scenarioName ) { |
||||
// NOTE: We prefer the last (i.e. probably the most recent) scenario name/ID.
|
||||
title = logFile.scenario.scenarioName ; |
||||
title2 = logFile.scenario.scenarioId ; |
||||
} |
||||
} ) ; |
||||
|
||||
// check if we found a scenario name
|
||||
if ( ! title ) { |
||||
// nope - just use the log filename
|
||||
title = logFiles[0] ; |
||||
if ( logFiles.length > 1 ) |
||||
title2 = "and " + (logFiles.length-1) + " " + pluralString(logFiles.length-1,"other","others") ; |
||||
} |
||||
|
||||
// keep only the players seen in the events
|
||||
var players={}, playerIds=[] ; |
||||
for ( var playerId in data.players ) { |
||||
if ( ! playersSeen[playerId] ) |
||||
continue ; |
||||
players[ playerId ] = data.players[ playerId ] ; |
||||
playerIds.push( playerId ) ; |
||||
} |
||||
|
||||
// check if the user is one of the scenario players
|
||||
if ( gUserSettings[ "vasl-username" ] ) { |
||||
var vaslUserName = gUserSettings[ "vasl-username" ].toLowerCase() ; |
||||
for ( var i=0 ; i < playerIds.length ; ++i ) { |
||||
playerId = playerIds[ i ] ; |
||||
if ( players[ playerId ].toLowerCase() === vaslUserName ) { |
||||
// yup - change their name to "Me" and put them first
|
||||
players[ playerId ] = "Me" ; |
||||
var tmp = playerIds[0] ; |
||||
playerIds[0] = playerId ; |
||||
playerIds[i] = tmp ; |
||||
break ; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// save the extracted results
|
||||
this._players = players ; // nb: maps player ID's to names
|
||||
this._playerIds = playerIds ; // nb: ordered list of player ID's
|
||||
this.logFiles = logFiles ; |
||||
this.logFileNo = logFileNo ; |
||||
this.events = events ; |
||||
this.title = title ; |
||||
this.title2 = title2 ; |
||||
} |
||||
|
||||
extractStats( filter ) { |
||||
|
||||
// initialize
|
||||
var events = this.events ; |
||||
|
||||
function doExtractStats( playerId, singleDie ) { |
||||
|
||||
// initialize
|
||||
var stats2 = { nRolls: 0, distrib: {} } ; |
||||
var rollTotal = 0 ; |
||||
|
||||
// process each event
|
||||
events.forEach( function( evt ) { |
||||
if ( evt.eventType !== "roll" || playerId != evt.playerId ) |
||||
return ; |
||||
if ( filter && ! filter(evt) ) |
||||
return ; |
||||
if ( singleDie && ! LogFileAnalysis.isSingleDie( evt.rollValue ) ) |
||||
return ; |
||||
else if ( ! singleDie && LogFileAnalysis.isSingleDie( evt.rollValue ) ) |
||||
return ; |
||||
stats2.nRolls += 1 ; |
||||
var evtRollTotal = LogFileAnalysis.rollTotal( evt.rollValue ) ; |
||||
rollTotal += evtRollTotal ; |
||||
if ( stats2.distrib[ evtRollTotal ] ) |
||||
stats2.distrib[ evtRollTotal ] += 1 ; |
||||
else |
||||
stats2.distrib[ evtRollTotal ] = 1 ; |
||||
} ) ; |
||||
|
||||
stats2.rollAverage = rollTotal / stats2.nRolls ; |
||||
return stats2 ; |
||||
} |
||||
|
||||
// extract the stats for each player
|
||||
var stats = { totalRolls: { DR: 0, dr: 0 } } ; |
||||
this.forEachPlayer( function( playerId ) { |
||||
stats[ playerId ] = { |
||||
DR: doExtractStats( playerId, false ), |
||||
dr: doExtractStats( playerId, true ), |
||||
} ; |
||||
stats.totalRolls.DR += stats[playerId].DR.nRolls ; |
||||
stats.totalRolls.dr += stats[playerId].dr.nRolls ; |
||||
} ) ; |
||||
|
||||
return stats ; |
||||
} |
||||
|
||||
extractEvents( windowSize, handlers ) { |
||||
|
||||
// initialize
|
||||
var windowVals={}, events=[], nRolls={} ; |
||||
this.forEachPlayer( function( playerId ) { |
||||
windowVals[playerId] = [] ; |
||||
nRolls[playerId] = 0 ; |
||||
} ) ; |
||||
|
||||
function callHandler( handlerName, evt ) { |
||||
// invoke the specified handler
|
||||
if ( handlerName[0] != "_" ) |
||||
handlerName = "on" + handlerName[0].toUpperCase() + handlerName.substring(1) + "Event" ; |
||||
var handler = handlers[ handlerName ] ; |
||||
if ( ! handler ) |
||||
return null ; |
||||
return handler( evt ) ; |
||||
} |
||||
|
||||
// process each event
|
||||
this.events.forEach( function( evt ) { |
||||
|
||||
// notify the caller
|
||||
var rc = callHandler( evt.eventType, evt ) ; |
||||
if ( rc === false ) |
||||
return ; // nb: the caller wants to ignore this event
|
||||
|
||||
// check if this is a DR/dr roll
|
||||
if ( evt.eventType === "roll" ) { |
||||
// yup - update the values in the window buffer
|
||||
var playerId = evt.playerId ; |
||||
++ nRolls[ playerId ] ; |
||||
windowVals[playerId].push( evt.rollValue ) ; |
||||
if ( windowVals[playerId].length < windowSize ) { |
||||
return ; |
||||
} |
||||
// calculate the next moving average from the buffered values
|
||||
var rollTotal = windowVals[playerId].reduce( function( total, v ) { |
||||
return total + LogFileAnalysis.rollTotal( v ) ; |
||||
}, 0 ) ; |
||||
var movingAverage = rollTotal / windowVals[playerId].length ; |
||||
windowVals[playerId].shift() ; |
||||
var newEvent = { |
||||
eventType: evt.eventType, |
||||
playerId: playerId, |
||||
rollType: evt.rollType, |
||||
rollValue: evt.rollValue, |
||||
movingAverage: movingAverage, |
||||
rollNo: nRolls[ playerId ], |
||||
} ; |
||||
// add the new value to the results
|
||||
callHandler( "_onAddEvent", newEvent ) ; |
||||
events.push( newEvent ) ; |
||||
} |
||||
} ) ; |
||||
|
||||
return { |
||||
events: events, |
||||
nRolls: nRolls, |
||||
windowSize: windowSize |
||||
} ; |
||||
} |
||||
|
||||
calcHotness( stats ) { |
||||
|
||||
// Dice "hotness" is a metric that tries to capture how good a set of rolls are. Chi-squared is the metric
|
||||
// usually used to determine how far a set of observed values is from the expected distribution, but it doesn't
|
||||
// distinguish from the rolls tending towards high or low values.
|
||||
//
|
||||
// So, we modify the chi-squared calculation as follows:
|
||||
// - take the square of the difference between the observed and expected values (as normal)
|
||||
// - however, we preserve the sign, then multiply it by a weight
|
||||
// - the values are summed
|
||||
//
|
||||
// These changes have the following effect:
|
||||
// - Weighting the columns means that if we roll more than the expected number of 5's and 6's, that will
|
||||
// increase the score, but it we roll more 2's and 3's, that will increase the score by even more.
|
||||
// - Also, because the weights are negative for rolls >= 8, rolling more of these makes the score go down.
|
||||
// - Preserving the sign means that if we roll more 2's than expected, the score increases, but if we roll
|
||||
// fewer than expected, the score will decrease. Similarly, if we roll more 10's than expected, the score
|
||||
// will go down (because while the squared difference is positive, the weight is negative).
|
||||
|
||||
function doCalcHotness( stats, expected, weights ) { |
||||
if ( stats.nRolls === 0 ) |
||||
return null ; |
||||
var total = 0 ; |
||||
for ( var val in weights ) { |
||||
var observed = stats.distrib[val] || 0 ; |
||||
var diff = observed/stats.nRolls - expected[val]/100 ; |
||||
var sign = diff < 0 ? -1 : +1 ; |
||||
var delta = sign * Math.pow(diff,2) * weights[val] / (expected[val]/100) ; |
||||
total += delta ; |
||||
} |
||||
return total ; |
||||
} |
||||
|
||||
// calculate how hot the dice were
|
||||
var results = {} ; |
||||
this.forEachPlayer( function( playerId ) { |
||||
var hotness = {} ; |
||||
for ( var key in LogFileAnalysis.EXPECTED_DISTRIB ) { |
||||
hotness[ key ] = doCalcHotness( |
||||
stats[ playerId ][ key ], |
||||
LogFileAnalysis.EXPECTED_DISTRIB[ key ], |
||||
gAppConfig.LFA_DICE_HOTNESS_WEIGHTS[ key ] |
||||
) ; |
||||
} |
||||
// NOTE: Dice hotness (and chi-squared) aren't particularly meaningful for small datasets,
|
||||
// and can have very skewed results. However, if there are enough DR's, we don't want to let
|
||||
// there only being a few dr's from stopping us from showing a result. It's tricky to handle
|
||||
// this case (we can't just add in the dr score if there are enough dr's, since that would
|
||||
// benefit someone over another player who didn't have enough dr's), so we just ignore dr's :-/
|
||||
var rollRatio = stats[playerId].DR.nRolls / gAppConfig.LFA_DICE_HOTNESS_THRESHOLDS.DR ; |
||||
results[ playerId ] = [ hotness.DR, rollRatio ] ; |
||||
} ) ; |
||||
|
||||
return results ; |
||||
} |
||||
|
||||
getRollTypes() { |
||||
// return the roll types
|
||||
var rollTypes = {} ; |
||||
this.events.forEach( function( evt ) { |
||||
if ( evt.eventType === "roll" && ! rollTypes[ evt.rollType ] ) |
||||
rollTypes[ evt.rollType ] = true ; |
||||
} ); |
||||
return Object.keys( rollTypes ) ; |
||||
} |
||||
|
||||
forEachPlayer( func ) { |
||||
// call the specified function for each player
|
||||
var playerIds = this.playerIds() ; |
||||
for ( var i=0 ; i < playerIds.length ; ++i ) |
||||
func( playerIds[i], i ) ; |
||||
} |
||||
|
||||
playerIds() { return this._playerIds ; } |
||||
playerName( playerId ) { return this._players[ playerId ] ; } |
||||
|
||||
static rollTotal( roll ) { |
||||
// return the total of a DR/dr
|
||||
if ( LogFileAnalysis.isSingleDie( roll ) ) |
||||
return roll ; |
||||
else |
||||
return roll.reduce( function (total,n) { return total + n ; }, 0 ) ; |
||||
} |
||||
|
||||
static isSingleDie( roll ) { |
||||
// check if a roll is a DR or dr
|
||||
return ! Array.isArray( roll ) ; |
||||
} |
||||
|
||||
} |
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
LogFileAnalysis.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 }, |
||||
} ; |
@ -0,0 +1 @@ |
||||
@keyframes chartjs-render-animation{from{opacity:.99}to{opacity:1}}.chartjs-render-monitor{animation:chartjs-render-animation 1ms}.chartjs-size-monitor,.chartjs-size-monitor-expand,.chartjs-size-monitor-shrink{position:absolute;direction:ltr;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1}.chartjs-size-monitor-expand>div{position:absolute;width:1000000px;height:1000000px;left:0;top:0}.chartjs-size-monitor-shrink>div{position:absolute;width:200%;height:200%;left:0;top:0} |
@ -0,0 +1,25 @@ |
||||
/** |
||||
* [chartjs-plugin-labels]{@link https://github.com/emn178/chartjs-plugin-labels}
|
||||
* |
||||
* @version 1.1.0 |
||||
* @author Chen, Yi-Cyuan [emn178@gmail.com] |
||||
* @copyright Chen, Yi-Cyuan 2017-2018 |
||||
* @license MIT |
||||
*/ |
||||
(function(){function f(){this.renderToDataset=this.renderToDataset.bind(this)}if("undefined"===typeof Chart)console.error("Can not find Chart object.");else{"function"!=typeof Object.assign&&(Object.assign=function(a,c){if(null==a)throw new TypeError("Cannot convert undefined or null to object");for(var b=Object(a),e=1;e<arguments.length;e++){var d=arguments[e];if(null!=d)for(var g in d)Object.prototype.hasOwnProperty.call(d,g)&&(b[g]=d[g])}return b});var k={};["pie","doughnut","polarArea","bar"].forEach(function(a){k[a]= |
||||
!0});f.prototype.setup=function(a,c){this.chart=a;this.ctx=a.ctx;this.args={};this.barTotal={};var b=a.config.options;this.options=Object.assign({position:"default",precision:0,fontSize:b.defaultFontSize,fontColor:b.defaultFontColor,fontStyle:b.defaultFontStyle,fontFamily:b.defaultFontFamily,shadowOffsetX:3,shadowOffsetY:3,shadowColor:"rgba(0,0,0,0.3)",shadowBlur:6,images:[],outsidePadding:2,textMargin:2,overlap:!0},c);"bar"===a.config.type&&(this.options.position="default",this.options.arc=!1,this.options.overlap= |
||||
!0)};f.prototype.render=function(){this.labelBounds=[];this.chart.data.datasets.forEach(this.renderToDataset)};f.prototype.renderToDataset=function(a,c){this.totalPercentage=0;this.total=null;var b=this.args[c];b.meta.data.forEach(function(c,d){this.renderToElement(a,b,c,d)}.bind(this))};f.prototype.renderToElement=function(a,c,b,e){if(this.shouldRenderToElement(c.meta,b)&&(this.percentage=null,c=this.getLabel(a,b,e))){var d=this.ctx;d.save();d.font=Chart.helpers.fontString(this.options.fontSize, |
||||
this.options.fontStyle,this.options.fontFamily);var g=this.getRenderInfo(b,c);this.drawable(b,c,g)&&(d.beginPath(),d.fillStyle=this.getFontColor(a,b,e),this.renderLabel(c,g));d.restore()}};f.prototype.renderLabel=function(a,c){return this.options.arc?this.renderArcLabel(a,c):this.renderBaseLabel(a,c)};f.prototype.renderBaseLabel=function(a,c){var b=this.ctx;if("object"===typeof a)b.drawImage(a,c.x-a.width/2,c.y-a.height/2,a.width,a.height);else{b.save();b.textBaseline="top";b.textAlign="center";this.options.textShadow&& |
||||
(b.shadowOffsetX=this.options.shadowOffsetX,b.shadowOffsetY=this.options.shadowOffsetY,b.shadowColor=this.options.shadowColor,b.shadowBlur=this.options.shadowBlur);for(var e=a.split("\n"),d=0;d<e.length;d++)b.fillText(e[d],c.x,c.y-this.options.fontSize/2*e.length+this.options.fontSize*d);b.restore()}};f.prototype.renderArcLabel=function(a,c){var b=this.ctx,e=c.radius,d=c.view;b.save();b.translate(d.x,d.y);if("string"===typeof a){b.rotate(c.startAngle);b.textBaseline="middle";b.textAlign="left";d= |
||||
a.split("\n");var g=0,l=[],f=0;"border"===this.options.position&&(f=(d.length-1)*this.options.fontSize/2);for(var h=0;h<d.length;++h){var m=b.measureText(d[h]);m.width>g&&(g=m.width);l.push(m.width)}for(h=0;h<d.length;++h){var n=d[h],k=(d.length-1-h)*-this.options.fontSize+f;b.save();b.rotate((g-l[h])/2/e);for(var p=0;p<n.length;p++){var q=n.charAt(p);m=b.measureText(q);b.save();b.translate(0,-1*e);b.fillText(q,0,k);b.restore();b.rotate(m.width/e)}b.restore()}}else b.rotate((d.startAngle+Math.PI/ |
||||
2+c.endAngle)/2),b.translate(0,-1*e),this.renderLabel(a,{x:0,y:0});b.restore()};f.prototype.shouldRenderToElement=function(a,c){return!a.hidden&&!c.hidden&&(this.options.showZero||"polarArea"===this.chart.config.type?0!==c._view.outerRadius:0!==c._view.circumference)};f.prototype.getLabel=function(a,c,b){if("function"===typeof this.options.render)a=this.options.render({label:this.chart.config.data.labels[b],value:a.data[b],percentage:this.getPercentage(a,c,b),dataset:a,index:b});else switch(this.options.render){case "value":a= |
||||
a.data[b];break;case "label":a=this.chart.config.data.labels[b];break;case "image":a=this.options.images[b]?this.loadImage(this.options.images[b]):"";break;default:a=this.getPercentage(a,c,b)+"%"}"object"===typeof a?a=this.loadImage(a):null!==a&&void 0!==a&&(a=a.toString());return a};f.prototype.getFontColor=function(a,c,b){var e=this.options.fontColor;"function"===typeof e?e=e({label:this.chart.config.data.labels[b],value:a.data[b],percentage:this.getPercentage(a,c,b),backgroundColor:a.backgroundColor[b], |
||||
dataset:a,index:b}):"string"!==typeof e&&(e=e[b]||this.chart.config.options.defaultFontColor);return e};f.prototype.getPercentage=function(a,c,b){if(null!==this.percentage)return this.percentage;if("polarArea"===this.chart.config.type){if(null===this.total)for(c=this.total=0;c<a.data.length;++c)this.total+=a.data[c];a=a.data[b]/this.total*100}else if("bar"===this.chart.config.type){if(void 0===this.barTotal[b])for(c=this.barTotal[b]=0;c<this.chart.data.datasets.length;++c)this.barTotal[b]+=this.chart.data.datasets[c].data[b]; |
||||
a=a.data[b]/this.barTotal[b]*100}else a=c._view.circumference/this.chart.config.options.circumference*100;a=parseFloat(a.toFixed(this.options.precision));this.options.showActualPercentages||("bar"===this.chart.config.type&&(this.totalPercentage=this.barTotalPercentage[b]||0),this.totalPercentage+=a,100<this.totalPercentage&&(a-=this.totalPercentage-100,a=parseFloat(a.toFixed(this.options.precision))),"bar"===this.chart.config.type&&(this.barTotalPercentage[b]=this.totalPercentage));return this.percentage= |
||||
a};f.prototype.getRenderInfo=function(a,c){return"bar"===this.chart.config.type?this.getBarRenderInfo(a,c):this.options.arc?this.getArcRenderInfo(a,c):this.getBaseRenderInfo(a,c)};f.prototype.getBaseRenderInfo=function(a,c){if("outside"===this.options.position||"border"===this.options.position){var b,e=a._view,d=e.startAngle+(e.endAngle-e.startAngle)/2,g=e.outerRadius/2;"border"===this.options.position?b=(e.outerRadius-g)/2+g:"outside"===this.options.position&&(b=e.outerRadius-g+g+this.options.textMargin); |
||||
b={x:e.x+Math.cos(d)*b,y:e.y+Math.sin(d)*b};"outside"===this.options.position&&(d=this.options.textMargin+this.measureLabel(c).width/2,b.x+=b.x<e.x?-d:d);return b}return a.tooltipPosition()};f.prototype.getArcRenderInfo=function(a,c){var b=a._view;var e="outside"===this.options.position?b.outerRadius+this.options.fontSize+this.options.textMargin:"border"===this.options.position?(b.outerRadius/2+b.outerRadius)/2:(b.innerRadius+b.outerRadius)/2;var d=b.startAngle,g=b.endAngle,l=g-d;d+=Math.PI/2;g+= |
||||
Math.PI/2;var f=this.measureLabel(c);d+=(g-(f.width/e+d))/2;return{radius:e,startAngle:d,endAngle:g,totalAngle:l,view:b}};f.prototype.getBarRenderInfo=function(a,c){var b=a.tooltipPosition();b.y-=this.measureLabel(c).height/2+this.options.textMargin;return b};f.prototype.drawable=function(a,c,b){if(this.options.overlap)return!0;if(this.options.arc)return b.endAngle-b.startAngle<=b.totalAngle;var e=this.measureLabel(c);c=b.x-e.width/2;var d=b.x+e.width/2,g=b.y-e.height/2;b=b.y+e.height/2;return"outside"=== |
||||
this.options.renderInfo?this.outsideInRange(c,d,g,b):a.inRange(c,g)&&a.inRange(c,b)&&a.inRange(d,g)&&a.inRange(d,b)};f.prototype.outsideInRange=function(a,c,b,e){for(var d=this.labelBounds,g=0;g<d.length;++g){for(var f=d[g],k=[[a,b],[a,e],[c,b],[c,e]],h=0;h<k.length;++h){var m=k[h][0],n=k[h][1];if(m>=f.left&&m<=f.right&&n>=f.top&&n<=f.bottom)return!1}k=[[f.left,f.top],[f.left,f.bottom],[f.right,f.top],[f.right,f.bottom]];for(h=0;h<k.length;++h)if(m=k[h][0],n=k[h][1],m>=a&&m<=c&&n>=b&&n<=e)return!1}d.push({left:a, |
||||
right:c,top:b,bottom:e});return!0};f.prototype.measureLabel=function(a){if("object"===typeof a)return{width:a.width,height:a.height};var c=0;a=a.split("\n");for(var b=0;b<a.length;++b){var e=this.ctx.measureText(a[b]);e.width>c&&(c=e.width)}return{width:c,height:this.options.fontSize*a.length}};f.prototype.loadImage=function(a){var c=new Image;c.src=a.src;c.width=a.width;c.height=a.height;return c};Chart.plugins.register({id:"labels",beforeDatasetsUpdate:function(a,c){if(k[a.config.type]){Array.isArray(c)|| |
||||
(c=[c]);var b=c.length;a._labels&&b===a._labels.length||(a._labels=c.map(function(){return new f}));for(var e=!1,d=0,g=0;g<b;++g){var l=a._labels[g];l.setup(a,c[g]);"outside"===l.options.position&&(e=!0,l=1.5*l.options.fontSize+l.options.outsidePadding,l>d&&(d=l))}e&&(a.chartArea.top+=d,a.chartArea.bottom-=d)}},afterDatasetUpdate:function(a,c,b){k[a.config.type]&&a._labels.forEach(function(a){a.args[c.index]=c})},beforeDraw:function(a){k[a.config.type]&&a._labels.forEach(function(a){a.barTotalPercentage= |
||||
{}})},afterDatasetsDraw:function(a){k[a.config.type]&&a._labels.forEach(function(a){a.render()})}})}})(); |
@ -0,0 +1,37 @@ |
||||
.ui-dialog.lfa-upload .ui-dialog-titlebar { background: #e0c090 ; } |
||||
.ui-dialog.lfa-upload .ui-dialog-buttonpane { border: none ; margin-top: 0 !important ; padding-top: 0 !important ; } |
||||
.ui-dialog.lfa-upload .ui-dialog-buttonpane button.add { display: inline-flex ; align-items: center ; margin-left: 0 !important ; height: 25px ; padding: 2px 8px 2px 6px ; } |
||||
.ui-dialog.lfa-upload .ui-dialog-buttonpane button.add img { height: 15px ; margin-right: 0.5em ; } |
||||
|
||||
#lfa-upload { |
||||
overflow-x: hidden ; overflow-y: hidden ; |
||||
} |
||||
|
||||
#lfa-upload .files { |
||||
width: 100% ; height: 99% ; |
||||
margin: 0 ; |
||||
border: 1px solid #ccc ; border-radius: 2px ; |
||||
overflow-x: hidden ; |
||||
} |
||||
#lfa-upload .files li { |
||||
display: flex ; |
||||
margin: 2px ; |
||||
border: 1px solid #ccc ; border-radius: 2px ; |
||||
background: #f0e0c0 ; |
||||
padding: 8px 5px 2px 8px ; |
||||
list-style-type: none ; |
||||
cursor: pointer ; |
||||
} |
||||
#lfa-upload .files li img.file { height: 1.5em ; margin-right: 0.5em ; } |
||||
#lfa-upload .files li .filename { flex: 1 ; height: 1.5em ; padding: 4px 3px ; } |
||||
#lfa-upload .files li.dragOutside { background: #f0d0d0 ; text-decoration: line-through ; } |
||||
#lfa-upload .files li .delete { float: right ; height: 10px ; margin: -2px ; } |
||||
|
||||
#lfa-upload .hint { |
||||
position: absolute ; top: 20px ; left: 20px ; bottom: 20px ; right: 20px ; |
||||
display: flex ; flex-direction: column ; justify-content: center ; |
||||
font-style: italic ; color: #666 ; |
||||
z-index: 1 ; |
||||
} |
||||
#lfa-upload .hint .content { display: flex ; justify-content: center ; } |
||||
#lfa-upload .hint .content img { float: left ; margin-right: 0.75em ; opacity: 0.6 } |
@ -0,0 +1,113 @@ |
||||
/* jQuery dialog */ |
||||
.ui-dialog.lfa { |
||||
width: calc(100% - 10px) !important ; |
||||
height: calc(100% - 10px) !important ; |
||||
left: 5px ; |
||||
top: 5px ; |
||||
border-radius: 5px ; |
||||
} |
||||
.ui-dialog.lfa .ui-dialog-titlebar { display: none ; } |
||||
|
||||
#lfa { |
||||
height: 100% !important ; |
||||
display: flex ; flex-direction: column ; |
||||
overflow-y: auto ; |
||||
/* style the HTML elements to match the charts */ |
||||
font-size: 80% ; font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif ; color: #333 ; |
||||
} |
||||
#lfa ::selection {} |
||||
#lfa ::-moz-selection {} |
||||
#lfa { user-select: none ; } |
||||
|
||||
/* splitter */ |
||||
#lfa .split.top-pane { display: flex ; min-height: 350px ; } |
||||
#lfa .split.top-pane .right { position: relative ; } |
||||
#lfa .split.bottom-pane { min-height: 200px ; position: relative ; } |
||||
#lfa .gutter { position: relative ; min-height: 3px ; background-color: #f0f0f0 ; cursor: row-resize ; z-index: 49 ; } |
||||
#lfa .gutter img { position: absolute ; left: 40% ; top: -2px ; } |
||||
|
||||
/* top pane: dr/DR distribution & pie charts */ |
||||
#lfa .distrib { position: relative ; } |
||||
#lfa .pie { display: inline-block ; width: 200px ; height: 90px ; } |
||||
#lfa .pie canvas { margin: -20px 0 0 -40px ; } |
||||
#lfa .hotness { position: absolute ; bottom: 20px ; right: 5px ; } |
||||
#lfa .banner { position: relative ; display: inline-block ; } |
||||
#lfa .options { position: absolute ; top: 0 ; right: 25px ; } |
||||
|
||||
/* bottom pane: time-plot chart */ |
||||
#lfa .time-plot-options { |
||||
display: inline-block ; position: absolute ; top: -14px ; right: 0 ; |
||||
z-index: 50 ; |
||||
padding-left: 0.5em ; |
||||
background: white ; |
||||
} |
||||
#lfa .time-plot { overflow-x: auto ; } |
||||
|
||||
/* banner */ |
||||
#lfa .banner { |
||||
min-width: 20em ; |
||||
margin: 0 1em 0.5em 0 ; |
||||
border: 1px solid #aaa ; border-radius: 5px ; |
||||
padding: 0.5em 1em ; |
||||
background: #ffffe0 ; color: #444 ; |
||||
} |
||||
#lfa .banner .title { font-size: 150% ; } |
||||
#lfa .banner .title2 { font-style: italic ; } |
||||
#lfa .banner .roll-type { font-size: 110% ; font-style: italic ; color: #666 ; } |
||||
#lfa .banner .select-file { position: absolute ; bottom: 2px ; right: 2px ; font-style: italic ; color: #444 ; cursor: pointer ; } |
||||
#lfa .banner .select-file .caption { line-height: 18px ; vertical-align: middle ; margin-right: 0.15em ; } |
||||
|
||||
/* dice hotness */ |
||||
#lfa .hotness { |
||||
width: 300px ; |
||||
border: 1px solid #ffc030 ; border-radius: 5px ; background: #fffcfc ; |
||||
padding: 2px 5px ; |
||||
} |
||||
#lfa .hotness img.dice { position: absolute ; left: -10px ; top : -25px ; height: 50px ; } |
||||
|
||||
/* options panel */ |
||||
#lfa .options { |
||||
border: 1px solid #ccc ; border-radius: 5px ; padding: 0.5em ; |
||||
background: #fffff8 ; |
||||
} |
||||
#lfa .options input[type=checkbox] { vertical-align: middle ; } |
||||
#lfa .options button.download { position: absolute ; right: 5px ; bottom: 5px ; padding: 3px !important ; } |
||||
#lfa .options button.player-colors { padding: 3px 6px 2px 5px !important ; } |
||||
|
||||
/* time-plot options */ |
||||
#lfa .time-plot-options button.zoom-in, #lfa .time-plot-options button.zoom-out { padding: 4px 6px 2px 6px !important ; } |
||||
|
||||
/* player colors popup */ |
||||
#lfa .player-colors-popup, #lfa .select-file-popup { |
||||
border: 1px solid #aaa ; border-radius: 5px ; |
||||
background: #f8f8f8 ; |
||||
z-index: 100 ; |
||||
} |
||||
#lfa .player-colors-popup .row { margin: 0.5em ; padding-top: 3px ; } |
||||
#lfa .player-colors-popup .sp-replacer { width: 40px ; height: 17px ; margin: -3px 0.5em 0 0 ; } |
||||
#lfa .player-colors-popup .sp-preview { width: 20px ; height: 15px ; } |
||||
|
||||
/* file selection popup */ |
||||
#lfa .select-file-popup .row { margin: 6px 8px ; } |
||||
#lfa .select-file-popup input[type="radio"] { margin: -2px 0.5em 0 0 ; vertical-align: middle ; } |
||||
|
||||
/* "no data" marker */ |
||||
#lfa .no-data { |
||||
width: 5em ; |
||||
border: 1px dotted #aaa ; border-radius: 5px ; |
||||
padding: 0.15em 0.25em 0.1em 0.25em ; |
||||
background: rgba(128,128,128,0.1) ; |
||||
color: #888 ; font-size: 150% ; text-align: center ; |
||||
} |
||||
|
||||
/* tabular chart data (for testing porpoises) */ |
||||
#lfa table.chart-data { border: 1px solid #888 ; font-size: 80% ; } |
||||
#lfa table.chart-data th { padding: 2px 5px ; border-bottom: 1px dotted #888 ; } |
||||
#lfa table.chart-data td { padding: 2px 5px ; text-align: center ; } |
||||
#lfa table.chart-data td.label { border-right: 1px dotted #888 ; min-width: 1em ; } |
||||
|
||||
/* miscellaneous */ |
||||
#lfa button.ui-dialog-titlebar-close { position: fixed ; top: 15px ; right: 5px ; } |
||||
#lfa .ui-selectmenu-button { padding: 2px 5px ; } |
||||
#lfa input[type="checkbox"] { margin-bottom: 0.2em ; } |
||||
#lfa label[disabled] { color: #aaa ; } |
After Width: | Height: | Size: 139 B |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 810 B |
After Width: | Height: | Size: 938 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 3.5 KiB |
@ -0,0 +1,26 @@ |
||||
/* jshint esnext: true */ |
||||
|
||||
// Manage jQuery event handlers.
|
||||
class jQueryHandlers |
||||
{ |
||||
|
||||
constructor() { |
||||
// initialize
|
||||
this.events = [] ; |
||||
} |
||||
|
||||
addHandler( $elem, evtType, handler ) { |
||||
// add an event handler
|
||||
$elem.on( evtType, handler ) ; |
||||
this.events.push( [ $elem, evtType, handler ] ) ; |
||||
} |
||||
|
||||
cleanUp() { |
||||
// clean up event handlers
|
||||
for ( var i=this.events.length-1 ; i >= 0 ; --i ) { |
||||
var evt = this.events[ i ] ; |
||||
evt[0].off( evt[1], evt[2] ) ; |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,288 @@ |
||||
( function() { // nb: put the entire file into its own local namespace, global stuff gets added to window.
|
||||
|
||||
var $gLogFilesToUpload ; |
||||
var gEventHandlers ; |
||||
var gDlgSizeAndPosition = {} ; |
||||
var gDisableClickToAddTimestamp = new Date() ; |
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
window.on_analyze_vlog = function() |
||||
{ |
||||
// initialize
|
||||
var $dlg ; |
||||
|
||||
function onAddFile() { |
||||
// FUDGE! Files can be removed from the upload list by using the mouse (e.g. Ctrl-Click,
|
||||
// or clicking on the "delete" icon), so we don't want to also trigger an "add" dialog.
|
||||
var delta = (new Date()).getTime() - gDisableClickToAddTimestamp.getTime() ; |
||||
if ( delta <= 5 ) |
||||
return ; |
||||
// add a file to the list of files to be analyzed
|
||||
if ( getUrlParam( "vlog_persistence" ) ) { |
||||
// FOR TESTING PORPOISES! We can't control a file upload from Selenium (since
|
||||
// the browser will use native controls), so we get the data from a <textarea>).
|
||||
var $elem = $( "#_vlog-persistence_" ) ; |
||||
var data = $elem.val() ; |
||||
var pos = data.indexOf( "|" ) ; |
||||
var fname = data.substring( 0, pos ) ; |
||||
var vlog_data = data.substring( pos+1 ) ; |
||||
$elem.val( "" ) ; // nb: let the test suite know we've received the data
|
||||
addFileToUploadList( fname, vlog_data ) ; |
||||
} else { |
||||
$("#load-vlog").trigger( "click" ) ; // nb: will call on_load_vlog_file_selected() when done
|
||||
} |
||||
} |
||||
|
||||
// handle drag events for items already in the upload list
|
||||
var isDraggedOutside = null ; |
||||
function onSortStart( evt, ui ) { |
||||
isDraggedOutside = false ; |
||||
} |
||||
function onDragOutside( evt, ui ) { // nb: we get one of these even after a drag has ended :-/
|
||||
if ( isDraggedOutside === null ) |
||||
return ; |
||||
isDraggedOutside = true ; |
||||
ui.item.addClass( "dragOutside" ) ; |
||||
} |
||||
function onDragInside( evt, ui ) { |
||||
isDraggedOutside = false ; |
||||
ui.item.removeClass( "dragOutside" ) ; |
||||
} |
||||
function onDragEnd( evt, ui ) { |
||||
if ( isDraggedOutside ) |
||||
removeFileFromUploadList( ui.item ) ; |
||||
isDraggedOutside = null ; |
||||
} |
||||
|
||||
// handle events for files being dragged in from outside the browser
|
||||
function initExternalDragDrop() { |
||||
[ $gLogFilesToUpload, $dlg.find(".hint") ].forEach( function( $elem ) { |
||||
gEventHandlers.addHandler( $elem, "dragenter", stopEvent ) ; |
||||
gEventHandlers.addHandler( $elem, "dragleave", stopEvent ) ; |
||||
gEventHandlers.addHandler( $elem, "dragover", stopEvent ) ; |
||||
gEventHandlers.addHandler( $elem, "drop", function( evt ) { |
||||
// add the files dragged in to the upload list
|
||||
addFilesToUploadList( evt.originalEvent.dataTransfer.files ) ; |
||||
stopEvent( evt ) ; |
||||
} ) ; |
||||
} ) ; |
||||
} |
||||
|
||||
// NOTE: We can't use the normal mechanism for handling Ctrl-Enter, since there are no input elements.
|
||||
// We do things using a document-level keydown event handler.
|
||||
function onKeyDown( evt ) { |
||||
if ( $gLogFilesToUpload.find( "li" ).length === 0 ) { |
||||
evt.preventDefault() ; |
||||
return false ; |
||||
} |
||||
auto_dismiss_dialog( $dlg, evt, "OK" ) ; |
||||
} |
||||
|
||||
// show the dialog
|
||||
gEventHandlers = new jQueryHandlers() ; |
||||
$( "#lfa-upload" ).dialog( { |
||||
title: "Analyze log files", |
||||
dialogClass: "lfa-upload", |
||||
modal: true, |
||||
width: Math.min( gDlgSizeAndPosition.width || 400, $(window).innerWidth() ), |
||||
minWidth: 400, |
||||
height: Math.min( gDlgSizeAndPosition.height || 300, $(window).innerHeight() ), |
||||
minHeight: 300, |
||||
position: { my: "center", at: "center", of: window }, |
||||
create: function() { |
||||
// initialize the dialog
|
||||
init_dialog( $(this), "OK", false ) ; |
||||
var $btnPane = $( ".ui-dialog.lfa-upload .ui-dialog-buttonpane" ) ; |
||||
var $btn = $( "<button class='add'> <img src='" + gImagesBaseUrl+"/sortable-add.png'> Add </button>" ) ; |
||||
$btnPane.prepend( $btn ) ; |
||||
}, |
||||
open: function() { |
||||
// initialize the dialog
|
||||
$dlg = $(this) ; |
||||
on_dialog_open( $(this) ) ; |
||||
gEventHandlers.addHandler( $(document), "keydown", onKeyDown ) ; |
||||
$gLogFilesToUpload = $( "#lfa-upload .files" ) ; |
||||
$gLogFilesToUpload.sortable( { |
||||
start: onSortStart, |
||||
out: onDragOutside, |
||||
over: onDragInside, |
||||
beforeStop: onDragEnd, |
||||
} ).empty() ; |
||||
initExternalDragDrop() ; |
||||
var $addBtn = $( ".ui-dialog.lfa-upload button.add" ) ; |
||||
gEventHandlers.addHandler( $addBtn, "click", onAddFile ) ; |
||||
gEventHandlers.addHandler( $gLogFilesToUpload, "click", onAddFile ) ; |
||||
gEventHandlers.addHandler( $dlg.find(".hint"), "click", onAddFile ) ; |
||||
updateUi() ; |
||||
}, |
||||
beforeClose: function() { |
||||
// save the current size and position
|
||||
gDlgSizeAndPosition = getElemSizeAndPosition( $(".ui-dialog.lfa-upload") ) ; |
||||
}, |
||||
close: function() { |
||||
// clean up handlers
|
||||
gEventHandlers.cleanUp() ; |
||||
}, |
||||
buttons: { |
||||
OK: function() { |
||||
// unload the files to be analyzed
|
||||
var vlog_data = [] ; |
||||
$gLogFilesToUpload.children( "li" ).each( function() { |
||||
vlog_data.push( [ $(this).attr("data-filename"), $(this).attr("data-vlog") ] ) ; |
||||
} ) ; |
||||
// analyze the log files
|
||||
$(this).dialog( "close" ) ; |
||||
analyzeLogFiles( vlog_data ) ; |
||||
}, |
||||
Cancel: function() { $(this).dialog( "close" ) ; }, |
||||
}, |
||||
} ) ; |
||||
} ; |
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
window.on_load_vlog_file_selected = function() { |
||||
// add the selected files to the upload list
|
||||
addFilesToUploadList( $( "#load-vlog" ).prop( "files" ) ) ; |
||||
} ; |
||||
|
||||
function addFilesToUploadList( files ) |
||||
{ |
||||
// initialize
|
||||
var currFileNo = 0 ; |
||||
var fileReader = new FileReader() ; |
||||
|
||||
// add each log file to the list
|
||||
function loadNextFile() { |
||||
if ( currFileNo >= files.length ) |
||||
return ; |
||||
var currFile = files[ currFileNo ] ; |
||||
fileReader.onload = function() { |
||||
// get the file data
|
||||
vlog_data = fileReader.result ; |
||||
if ( vlog_data.substring(0,5) === "data:" ) |
||||
vlog_data = vlog_data.split( "," )[1] ; |
||||
// add the file to the list
|
||||
addFileToUploadList( currFile.name, vlog_data ) ; |
||||
// read the next file
|
||||
++ currFileNo ; |
||||
loadNextFile() ; |
||||
} ; |
||||
fileReader.readAsDataURL( currFile ) ; |
||||
} |
||||
loadNextFile() ; |
||||
} |
||||
|
||||
function addFileToUploadList( fname, vlog_data ) |
||||
{ |
||||
// add the file to the upload list
|
||||
var buf = [ "<li>", |
||||
"<img src='" + gImagesBaseUrl+"/lfa/file.png" + "' class='file'>", |
||||
"<span class='filename'>", fname, "</span>", |
||||
"<img src='" + gImagesBaseUrl+"/cross.png" + "' class='delete'>", |
||||
"</li>" |
||||
] ; |
||||
var $item = $( buf.join("") ) ; |
||||
$item.attr( "data-filename", fname ) ; |
||||
$item.attr( "data-vlog", vlog_data ) ; |
||||
$gLogFilesToUpload.append( $item ) ; |
||||
updateUi() ; |
||||
|
||||
// add click handler to remove the file from the list
|
||||
gEventHandlers.addHandler( $item.children( ".delete" ), "click", function() { |
||||
gDisableClickToAddTimestamp = new Date() ; |
||||
removeFileFromUploadList( $(this).parent() ) ; |
||||
} ) ; |
||||
gEventHandlers.addHandler( $item, "click", function( evt ) { |
||||
if ( evt.ctrlKey ) { |
||||
gDisableClickToAddTimestamp = new Date() ; |
||||
removeFileFromUploadList( $(this) ) ; |
||||
} |
||||
} ) ; |
||||
} |
||||
|
||||
function removeFileFromUploadList( $item ) { |
||||
// remove the file from the upload list
|
||||
$item.remove() ; |
||||
setTimeout( updateUi, 100 ) ; // nb: we need this after a drag-out :-/
|
||||
} |
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
function updateUi() |
||||
{ |
||||
// update the UI
|
||||
var nFiles = $gLogFilesToUpload.find( "li" ).length ; |
||||
var $btn = $( ".ui-dialog.lfa-upload button.ok" ) ; |
||||
var $hint = $( ".ui-dialog.lfa-upload .hint" ) ; |
||||
if ( nFiles > 0 ) { |
||||
$btn.button( "enable" ) ; |
||||
$hint.hide() ; |
||||
} else { |
||||
$btn.button( "disable" ) ; |
||||
$hint.show() ; |
||||
} |
||||
} |
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
function analyzeLogFiles( vlog_data ) |
||||
{ |
||||
// send a request to analyze the log files
|
||||
var objName = pluralString( vlog_data.length, "log file", "log files" ) ; |
||||
var $dlg = _show_vassal_shim_progress_dlg( |
||||
"Analyzing your " + objName + "..." |
||||
) ; |
||||
$.ajax( { |
||||
url: gAnalyzeVlogsUrl, |
||||
type: "POST", |
||||
data: JSON.stringify( vlog_data ), |
||||
contentType: "application/json", |
||||
} ).done( function( data ) { |
||||
$dlg.dialog( "close" ) ; |
||||
resp = checkResponse( data, objName ) ; |
||||
if ( ! resp ) |
||||
return ; |
||||
show_lfa_dialog( resp ) ; |
||||
} ).fail( function( xhr, status, errorMsg ) { |
||||
$dlg.dialog( "close" ) ; |
||||
showErrorMsg( "Can't analyze the " + objName + ":<div class='pre'>" + escapeHTML(errorMsg) + "</div>" ) ; |
||||
} ) ; |
||||
} |
||||
|
||||
function checkResponse( resp, objName ) |
||||
{ |
||||
// check if there was an error
|
||||
if ( resp.error ) { |
||||
// yup - report it
|
||||
if ( getUrlParam( "vlog_persistence" ) ) { |
||||
$( "#_vlog-persistence_" ).val( |
||||
"ERROR: " + resp.error + "\n\n=== STDOUT ===\n" + resp.stdout + "\n=== STDERR ===\n" + resp.stderr |
||||
) ; |
||||
} else { |
||||
show_vassal_shim_error_dlg( resp, "Can't analyze the " + objName + "." ) ; |
||||
} |
||||
return null ; |
||||
} |
||||
|
||||
// check if anything was extracted
|
||||
if ( resp.logFiles ) { |
||||
var totalEvents = 0 ; |
||||
resp.logFiles.forEach( function( logFile ) { |
||||
totalEvents += logFile.events.length ; |
||||
} ) ; |
||||
if ( totalEvents === 0 ) { |
||||
showWarningMsg( "Couldn't find anything in the " + objName + "." + |
||||
"<p> " + pluralString(resp.logFiles.length,"It's","They're") + " probably either not a log file, or from an old version of VASL." |
||||
) ; |
||||
return null ; |
||||
} |
||||
} |
||||
|
||||
return resp ; |
||||
} |
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
} )() ; // end local namespace
|
@ -0,0 +1,507 @@ |
||||
/*** |
||||
Spectrum Colorpicker v1.8.1 |
||||
https://github.com/bgrins/spectrum |
||||
Author: Brian Grinstead |
||||
License: MIT |
||||
***/ |
||||
|
||||
.sp-container { |
||||
position:absolute; |
||||
top:0; |
||||
left:0; |
||||
display:inline-block; |
||||
*display: inline; |
||||
*zoom: 1; |
||||
/* https://github.com/bgrins/spectrum/issues/40 */ |
||||
z-index: 9999994; |
||||
overflow: hidden; |
||||
} |
||||
.sp-container.sp-flat { |
||||
position: relative; |
||||
} |
||||
|
||||
/* Fix for * { box-sizing: border-box; } */ |
||||
.sp-container, |
||||
.sp-container * { |
||||
-webkit-box-sizing: content-box; |
||||
-moz-box-sizing: content-box; |
||||
box-sizing: content-box; |
||||
} |
||||
|
||||
/* http://ansciath.tumblr.com/post/7347495869/css-aspect-ratio */ |
||||
.sp-top { |
||||
position:relative; |
||||
width: 100%; |
||||
display:inline-block; |
||||
} |
||||
.sp-top-inner { |
||||
position:absolute; |
||||
top:0; |
||||
left:0; |
||||
bottom:0; |
||||
right:0; |
||||
} |
||||
.sp-color { |
||||
position: absolute; |
||||
top:0; |
||||
left:0; |
||||
bottom:0; |
||||
right:20%; |
||||
} |
||||
.sp-hue { |
||||
position: absolute; |
||||
top:0; |
||||
right:0; |
||||
bottom:0; |
||||
left:84%; |
||||
height: 100%; |
||||
} |
||||
|
||||
.sp-clear-enabled .sp-hue { |
||||
top:33px; |
||||
height: 77.5%; |
||||
} |
||||
|
||||
.sp-fill { |
||||
padding-top: 80%; |
||||
} |
||||
.sp-sat, .sp-val { |
||||
position: absolute; |
||||
top:0; |
||||
left:0; |
||||
right:0; |
||||
bottom:0; |
||||
} |
||||
|
||||
.sp-alpha-enabled .sp-top { |
||||
margin-bottom: 18px; |
||||
} |
||||
.sp-alpha-enabled .sp-alpha { |
||||
display: block; |
||||
} |
||||
.sp-alpha-handle { |
||||
position:absolute; |
||||
top:-4px; |
||||
bottom: -4px; |
||||
width: 6px; |
||||
left: 50%; |
||||
cursor: pointer; |
||||
border: 1px solid black; |
||||
background: white; |
||||
opacity: .8; |
||||
} |
||||
.sp-alpha { |
||||
display: none; |
||||
position: absolute; |
||||
bottom: -14px; |
||||
right: 0; |
||||
left: 0; |
||||
height: 8px; |
||||
} |
||||
.sp-alpha-inner { |
||||
border: solid 1px #333; |
||||
} |
||||
|
||||
.sp-clear { |
||||
display: none; |
||||
} |
||||
|
||||
.sp-clear.sp-clear-display { |
||||
background-position: center; |
||||
} |
||||
|
||||
.sp-clear-enabled .sp-clear { |
||||
display: block; |
||||
position:absolute; |
||||
top:0px; |
||||
right:0; |
||||
bottom:0; |
||||
left:84%; |
||||
height: 28px; |
||||
} |
||||
|
||||
/* Don't allow text selection */ |
||||
.sp-container, .sp-replacer, .sp-preview, .sp-dragger, .sp-slider, .sp-alpha, .sp-clear, .sp-alpha-handle, .sp-container.sp-dragging .sp-input, .sp-container button { |
||||
-webkit-user-select:none; |
||||
-moz-user-select: -moz-none; |
||||
-o-user-select:none; |
||||
user-select: none; |
||||
} |
||||
|
||||
.sp-container.sp-input-disabled .sp-input-container { |
||||
display: none; |
||||
} |
||||
.sp-container.sp-buttons-disabled .sp-button-container { |
||||
display: none; |
||||
} |
||||
.sp-container.sp-palette-buttons-disabled .sp-palette-button-container { |
||||
display: none; |
||||
} |
||||
.sp-palette-only .sp-picker-container { |
||||
display: none; |
||||
} |
||||
.sp-palette-disabled .sp-palette-container { |
||||
display: none; |
||||
} |
||||
|
||||
.sp-initial-disabled .sp-initial { |
||||
display: none; |
||||
} |
||||
|
||||
|
||||
/* Gradients for hue, saturation and value instead of images. Not pretty... but it works */ |
||||
.sp-sat { |
||||
background-image: -webkit-gradient(linear, 0 0, 100% 0, from(#FFF), to(rgba(204, 154, 129, 0))); |
||||
background-image: -webkit-linear-gradient(left, #FFF, rgba(204, 154, 129, 0)); |
||||
background-image: -moz-linear-gradient(left, #fff, rgba(204, 154, 129, 0)); |
||||
background-image: -o-linear-gradient(left, #fff, rgba(204, 154, 129, 0)); |
||||
background-image: -ms-linear-gradient(left, #fff, rgba(204, 154, 129, 0)); |
||||
background-image: linear-gradient(to right, #fff, rgba(204, 154, 129, 0)); |
||||
-ms-filter: "progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr=#FFFFFFFF, endColorstr=#00CC9A81)"; |
||||
filter : progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr='#FFFFFFFF', endColorstr='#00CC9A81'); |
||||
} |
||||
.sp-val { |
||||
background-image: -webkit-gradient(linear, 0 100%, 0 0, from(#000000), to(rgba(204, 154, 129, 0))); |
||||
background-image: -webkit-linear-gradient(bottom, #000000, rgba(204, 154, 129, 0)); |
||||
background-image: -moz-linear-gradient(bottom, #000, rgba(204, 154, 129, 0)); |
||||
background-image: -o-linear-gradient(bottom, #000, rgba(204, 154, 129, 0)); |
||||
background-image: -ms-linear-gradient(bottom, #000, rgba(204, 154, 129, 0)); |
||||
background-image: linear-gradient(to top, #000, rgba(204, 154, 129, 0)); |
||||
-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#00CC9A81, endColorstr=#FF000000)"; |
||||
filter : progid:DXImageTransform.Microsoft.gradient(startColorstr='#00CC9A81', endColorstr='#FF000000'); |
||||
} |
||||
|
||||
.sp-hue { |
||||
background: -moz-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); |
||||
background: -ms-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); |
||||
background: -o-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); |
||||
background: -webkit-gradient(linear, left top, left bottom, from(#ff0000), color-stop(0.17, #ffff00), color-stop(0.33, #00ff00), color-stop(0.5, #00ffff), color-stop(0.67, #0000ff), color-stop(0.83, #ff00ff), to(#ff0000)); |
||||
background: -webkit-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); |
||||
background: linear-gradient(to bottom, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); |
||||
} |
||||
|
||||
/* IE filters do not support multiple color stops. |
||||
Generate 6 divs, line them up, and do two color gradients for each. |
||||
Yes, really. |
||||
*/ |
||||
.sp-1 { |
||||
height:17%; |
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0000', endColorstr='#ffff00'); |
||||
} |
||||
.sp-2 { |
||||
height:16%; |
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffff00', endColorstr='#00ff00'); |
||||
} |
||||
.sp-3 { |
||||
height:17%; |
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ff00', endColorstr='#00ffff'); |
||||
} |
||||
.sp-4 { |
||||
height:17%; |
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffff', endColorstr='#0000ff'); |
||||
} |
||||
.sp-5 { |
||||
height:16%; |
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0000ff', endColorstr='#ff00ff'); |
||||
} |
||||
.sp-6 { |
||||
height:17%; |
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff00ff', endColorstr='#ff0000'); |
||||
} |
||||
|
||||
.sp-hidden { |
||||
display: none !important; |
||||
} |
||||
|
||||
/* Clearfix hack */ |
||||
.sp-cf:before, .sp-cf:after { content: ""; display: table; } |
||||
.sp-cf:after { clear: both; } |
||||
.sp-cf { *zoom: 1; } |
||||
|
||||
/* Mobile devices, make hue slider bigger so it is easier to slide */ |
||||
@media (max-device-width: 480px) { |
||||
.sp-color { right: 40%; } |
||||
.sp-hue { left: 63%; } |
||||
.sp-fill { padding-top: 60%; } |
||||
} |
||||
.sp-dragger { |
||||
border-radius: 5px; |
||||
height: 5px; |
||||
width: 5px; |
||||
border: 1px solid #fff; |
||||
background: #000; |
||||
cursor: pointer; |
||||
position:absolute; |
||||
top:0; |
||||
left: 0; |
||||
} |
||||
.sp-slider { |
||||
position: absolute; |
||||
top:0; |
||||
cursor:pointer; |
||||
height: 3px; |
||||
left: -1px; |
||||
right: -1px; |
||||
border: 1px solid #000; |
||||
background: white; |
||||
opacity: .8; |
||||
} |
||||
|
||||
/* |
||||
Theme authors: |
||||
Here are the basic themeable display options (colors, fonts, global widths). |
||||
See http://bgrins.github.io/spectrum/themes/ for instructions. |
||||
*/ |
||||
|
||||
.sp-container { |
||||
border-radius: 0; |
||||
background-color: #ECECEC; |
||||
border: solid 1px #f0c49B; |
||||
padding: 0; |
||||
} |
||||
.sp-container, .sp-container button, .sp-container input, .sp-color, .sp-hue, .sp-clear { |
||||
font: normal 12px "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif; |
||||
-webkit-box-sizing: border-box; |
||||
-moz-box-sizing: border-box; |
||||
-ms-box-sizing: border-box; |
||||
box-sizing: border-box; |
||||
} |
||||
.sp-top { |
||||
margin-bottom: 3px; |
||||
} |
||||
.sp-color, .sp-hue, .sp-clear { |
||||
border: solid 1px #666; |
||||
} |
||||
|
||||
/* Input */ |
||||
.sp-input-container { |
||||
float:right; |
||||
width: 100px; |
||||
margin-bottom: 4px; |
||||
} |
||||
.sp-initial-disabled .sp-input-container { |
||||
width: 100%; |
||||
} |
||||
.sp-input { |
||||
font-size: 12px !important; |
||||
border: 1px inset; |
||||
padding: 4px 5px; |
||||
margin: 0; |
||||
width: 100%; |
||||
background:transparent; |
||||
border-radius: 3px; |
||||
color: #222; |
||||
} |
||||
.sp-input:focus { |
||||
border: 1px solid orange; |
||||
} |
||||
.sp-input.sp-validation-error { |
||||
border: 1px solid red; |
||||
background: #fdd; |
||||
} |
||||
.sp-picker-container , .sp-palette-container { |
||||
float:left; |
||||
position: relative; |
||||
padding: 10px; |
||||
padding-bottom: 300px; |
||||
margin-bottom: -290px; |
||||
} |
||||
.sp-picker-container { |
||||
width: 172px; |
||||
border-left: solid 1px #fff; |
||||
} |
||||
|
||||
/* Palettes */ |
||||
.sp-palette-container { |
||||
border-right: solid 1px #ccc; |
||||
} |
||||
|
||||
.sp-palette-only .sp-palette-container { |
||||
border: 0; |
||||
} |
||||
|
||||
.sp-palette .sp-thumb-el { |
||||
display: block; |
||||
position:relative; |
||||
float:left; |
||||
width: 24px; |
||||
height: 15px; |
||||
margin: 3px; |
||||
cursor: pointer; |
||||
border:solid 2px transparent; |
||||
} |
||||
.sp-palette .sp-thumb-el:hover, .sp-palette .sp-thumb-el.sp-thumb-active { |
||||
border-color: orange; |
||||
} |
||||
.sp-thumb-el { |
||||
position:relative; |
||||
} |
||||
|
||||
/* Initial */ |
||||
.sp-initial { |
||||
float: left; |
||||
border: solid 1px #333; |
||||
} |
||||
.sp-initial span { |
||||
width: 30px; |
||||
height: 25px; |
||||
border:none; |
||||
display:block; |
||||
float:left; |
||||
margin:0; |
||||
} |
||||
|
||||
.sp-initial .sp-clear-display { |
||||
background-position: center; |
||||
} |
||||
|
||||
/* Buttons */ |
||||
.sp-palette-button-container, |
||||
.sp-button-container { |
||||
float: right; |
||||
} |
||||
|
||||
/* Replacer (the little preview div that shows up instead of the <input>) */ |
||||
.sp-replacer { |
||||
margin:0; |
||||
overflow:hidden; |
||||
cursor:pointer; |
||||
padding: 4px; |
||||
display:inline-block; |
||||
*zoom: 1; |
||||
*display: inline; |
||||
border: solid 1px #91765d; |
||||
background: #eee; |
||||
color: #333; |
||||
vertical-align: middle; |
||||
} |
||||
.sp-replacer:hover, .sp-replacer.sp-active { |
||||
border-color: #F0C49B; |
||||
color: #111; |
||||
} |
||||
.sp-replacer.sp-disabled { |
||||
cursor:default; |
||||
border-color: silver; |
||||
color: silver; |
||||
} |
||||
.sp-dd { |
||||
padding: 2px 0; |
||||
height: 16px; |
||||
line-height: 16px; |
||||
float:left; |
||||
font-size:10px; |
||||
} |
||||
.sp-preview { |
||||
position:relative; |
||||
width:25px; |
||||
height: 20px; |
||||
border: solid 1px #222; |
||||
margin-right: 5px; |
||||
float:left; |
||||
z-index: 0; |
||||
} |
||||
|
||||
.sp-palette { |
||||
*width: 220px; |
||||
max-width: 220px; |
||||
} |
||||
.sp-palette .sp-thumb-el { |
||||
width:16px; |
||||
height: 16px; |
||||
margin:2px 1px; |
||||
border: solid 1px #d0d0d0; |
||||
} |
||||
|
||||
.sp-container { |
||||
padding-bottom:0; |
||||
} |
||||
|
||||
|
||||
/* Buttons: http://hellohappy.org/css3-buttons/ */ |
||||
.sp-container button { |
||||
background-color: #eeeeee; |
||||
background-image: -webkit-linear-gradient(top, #eeeeee, #cccccc); |
||||
background-image: -moz-linear-gradient(top, #eeeeee, #cccccc); |
||||
background-image: -ms-linear-gradient(top, #eeeeee, #cccccc); |
||||
background-image: -o-linear-gradient(top, #eeeeee, #cccccc); |
||||
background-image: linear-gradient(to bottom, #eeeeee, #cccccc); |
||||
border: 1px solid #ccc; |
||||
border-bottom: 1px solid #bbb; |
||||
border-radius: 3px; |
||||
color: #333; |
||||
font-size: 14px; |
||||
line-height: 1; |
||||
padding: 5px 4px; |
||||
text-align: center; |
||||
text-shadow: 0 1px 0 #eee; |
||||
vertical-align: middle; |
||||
} |
||||
.sp-container button:hover { |
||||
background-color: #dddddd; |
||||
background-image: -webkit-linear-gradient(top, #dddddd, #bbbbbb); |
||||
background-image: -moz-linear-gradient(top, #dddddd, #bbbbbb); |
||||
background-image: -ms-linear-gradient(top, #dddddd, #bbbbbb); |
||||
background-image: -o-linear-gradient(top, #dddddd, #bbbbbb); |
||||
background-image: linear-gradient(to bottom, #dddddd, #bbbbbb); |
||||
border: 1px solid #bbb; |
||||
border-bottom: 1px solid #999; |
||||
cursor: pointer; |
||||
text-shadow: 0 1px 0 #ddd; |
||||
} |
||||
.sp-container button:active { |
||||
border: 1px solid #aaa; |
||||
border-bottom: 1px solid #888; |
||||
-webkit-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; |
||||
-moz-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; |
||||
-ms-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; |
||||
-o-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; |
||||
box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee; |
||||
} |
||||
.sp-cancel { |
||||
font-size: 11px; |
||||
color: #d93f3f !important; |
||||
margin:0; |
||||
padding:2px; |
||||
margin-right: 5px; |
||||
vertical-align: middle; |
||||
text-decoration:none; |
||||
|
||||
} |
||||
.sp-cancel:hover { |
||||
color: #d93f3f !important; |
||||
text-decoration: underline; |
||||
} |
||||
|
||||
|
||||
.sp-palette span:hover, .sp-palette span.sp-thumb-active { |
||||
border-color: #000; |
||||
} |
||||
|
||||
.sp-preview, .sp-alpha, .sp-thumb-el { |
||||
position:relative; |
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==); |
||||
} |
||||
.sp-preview-inner, .sp-alpha-inner, .sp-thumb-inner { |
||||
display:block; |
||||
position:absolute; |
||||
top:0;left:0;bottom:0;right:0; |
||||
} |
||||
|
||||
.sp-palette .sp-thumb-inner { |
||||
background-position: 50% 50%; |
||||
background-repeat: no-repeat; |
||||
} |
||||
|
||||
.sp-palette .sp-thumb-light.sp-thumb-active .sp-thumb-inner { |
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAIVJREFUeNpiYBhsgJFMffxAXABlN5JruT4Q3wfi/0DsT64h8UD8HmpIPCWG/KemIfOJCUB+Aoacx6EGBZyHBqI+WsDCwuQ9mhxeg2A210Ntfo8klk9sOMijaURm7yc1UP2RNCMbKE9ODK1HM6iegYLkfx8pligC9lCD7KmRof0ZhjQACDAAceovrtpVBRkAAAAASUVORK5CYII=); |
||||
} |
||||
|
||||
.sp-palette .sp-thumb-dark.sp-thumb-active .sp-thumb-inner { |
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAMdJREFUOE+tkgsNwzAMRMugEAahEAahEAZhEAqlEAZhEAohEAYh81X2dIm8fKpEspLGvudPOsUYpxE2BIJCroJmEW9qJ+MKaBFhEMNabSy9oIcIPwrB+afvAUFoK4H0tMaQ3XtlrggDhOVVMuT4E5MMG0FBbCEYzjYT7OxLEvIHQLY2zWwQ3D+9luyOQTfKDiFD3iUIfPk8VqrKjgAiSfGFPecrg6HN6m/iBcwiDAo7WiBeawa+Kwh7tZoSCGLMqwlSAzVDhoK+6vH4G0P5wdkAAAAASUVORK5CYII=); |
||||
} |
||||
|
||||
.sp-clear-display { |
||||
background-repeat:no-repeat; |
||||
background-position: center; |
||||
background-image: url(data:image/gif;base64,R0lGODlhFAAUAPcAAAAAAJmZmZ2dnZ6enqKioqOjo6SkpKWlpaampqenp6ioqKmpqaqqqqurq/Hx8fLy8vT09PX19ff39/j4+Pn5+fr6+vv7+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAP8ALAAAAAAUABQAAAihAP9FoPCvoMGDBy08+EdhQAIJCCMybCDAAYUEARBAlFiQQoMABQhKUJBxY0SPICEYHBnggEmDKAuoPMjS5cGYMxHW3IiT478JJA8M/CjTZ0GgLRekNGpwAsYABHIypcAgQMsITDtWJYBR6NSqMico9cqR6tKfY7GeBCuVwlipDNmefAtTrkSzB1RaIAoXodsABiZAEFB06gIBWC1mLVgBa0AAOw==); |
||||
} |
@ -0,0 +1,13 @@ |
||||
<div id="lfa-upload" style="display:none;"> |
||||
|
||||
<ul class="files"> </ul> |
||||
|
||||
<div class="hint"> |
||||
<div class="content"> |
||||
<img src="{{url_for('static',filename='images/lfa/file.png')}}"> |
||||
Click here to add your VASL log files, <br> |
||||
or just drag them in. |
||||
</div> |
||||
</div> |
||||
|
||||
</div> |
@ -0,0 +1,76 @@ |
||||
<div id="lfa" style="display:none;"> |
||||
|
||||
<div class="split top-pane"> <!-- top pane --> |
||||
|
||||
<div class="left"> <!-- left column --> |
||||
|
||||
<div class="banner"> |
||||
<div> <span class="title"></span> <span class="title2"></span> </div> |
||||
<div class="roll-type"> </div> |
||||
<div class="select-file"></div> |
||||
</div> |
||||
|
||||
<div class="distrib d6x1"> <canvas></canvas> </div> |
||||
|
||||
<div class="pie d6x1"> <canvas></canvas> </div> |
||||
|
||||
</div> <!-- end left column --> |
||||
|
||||
<div class="right"> <!-- right column --> |
||||
|
||||
<div class="distrib d6x2"> <canvas></canvas> </div> |
||||
|
||||
<div class="pie d6x2"> <canvas></canvas> </div> |
||||
|
||||
<div class="hotness" title="How hot were the dice...?" style="display:none;"> |
||||
<img src="{{url_for('static',filename='images/lfa/hotness.png')}}" class="dice"> |
||||
<canvas></canvas> |
||||
</div> |
||||
|
||||
</div> <!-- end right column --> |
||||
|
||||
</div> <!-- end top pane --> |
||||
|
||||
<div class="split bottom-pane"> <!-- bottom pane --> |
||||
|
||||
<div class="time-plot-options" style="display:none;text-align:right;"> |
||||
<label for="moving-average"> Moving avera<u>g</u>e:</label> <select name="moving-average"></select> |
||||
|
||||
<button class="zoom-in"> <img src="{{url_for('static',filename='images/lfa/plus.png')}}" style="height:1em;" title="Zoom in"> </button> |
||||
<button class="zoom-out"> <img src="{{url_for('static',filename='images/lfa/minus.png')}}" style="height:1em;" title="Zoom out"> </button> |
||||
</div> |
||||
|
||||
<div class="time-plot"> |
||||
<div class="wrapper"> <canvas></canvas> </div> |
||||
</div> |
||||
|
||||
</div> <!-- end bottom pane --> |
||||
|
||||
<div class="options"> |
||||
<div> |
||||
<label name="roll-type"> <u>R</u>oll type: </label> |
||||
<select name="roll-type"> </select> |
||||
</div> |
||||
<input type="checkbox" name="stack-bar-graphs"> Stack bar graphs <br> |
||||
<input type="checkbox" name="disable-animations"> No animations <br> |
||||
<button class="player-colors" style="display:flex;"> |
||||
<img src="{{url_for('static',filename='images/lfa/player-colors.png')}}" style="height:1em;"> <span style="display:inline-block;margin-left:0.25em;">Colors</span> |
||||
</button> |
||||
<button class="download" style="display:flex;" title="Download the data"> |
||||
<img src="{{url_for('static',filename='images/lfa/download.png')}}" style="height:1.5em;"> |
||||
</button> |
||||
</div> |
||||
|
||||
<div class="player-colors-popup" style="display:none;"> </div> |
||||
|
||||
<div class="select-file-popup" style="display:none;"> </div> |
||||
|
||||
<div class="timePlot-tooltip" style="border-radius:5px;z-index:99;"> |
||||
<table style="font-size:100%;"> </table> |
||||
</div> |
||||
|
||||
<button type="button" class="ui-button ui-corner-all ui-widget ui-button-icon-only ui-dialog-titlebar-close" title="Close" style="z-index:99;"> |
||||
<span class="ui-button-icon ui-icon ui-icon-closethick"></span> |
||||
</button> |
||||
|
||||
</div> |
@ -0,0 +1,589 @@ |
||||
""" Test log file analysis. """ |
||||
|
||||
import os |
||||
import base64 |
||||
import csv |
||||
|
||||
import pytest |
||||
from selenium.webdriver.support.ui import Select |
||||
|
||||
from vasl_templates.webapp.tests.utils import init_webapp, select_menu_option, \ |
||||
wait_for, wait_for_elem, find_child, find_children, set_stored_msg, set_stored_msg_marker, get_stored_msg, \ |
||||
get_droplist_vals, select_droplist_val, unload_table |
||||
from vasl_templates.webapp.tests.test_vassal import _run_tests |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@pytest.mark.skipif( not pytest.config.option.vasl_mods, reason="--vasl-mods not specified" ) #pylint: disable=no-member |
||||
@pytest.mark.skipif( not pytest.config.option.vassal, reason="--vassal not specified" ) #pylint: disable=no-member |
||||
def test_full( webapp, webdriver ): |
||||
"""Test a full log file analysis.""" |
||||
|
||||
# initialize |
||||
control_tests = init_webapp( webapp, webdriver, vlog_persistence=1, lfa_tables=1 ) |
||||
|
||||
def do_test(): #pylint: disable=missing-docstring |
||||
|
||||
# analyze the log file |
||||
# === RPh === === PFPh === === MPh === === DFPh === |
||||
# A1: Other 5 4 A6: IFT 3 1 B5: IFT 4 2 |
||||
# A2: Rally 4 1 B3: MC 5 2 b5: sa 2 |
||||
# A3: Rally 3 1 A7: IFT 5 3 A12: MC 1 2 |
||||
# B1: Rally 6 2 b3: sa 4 |
||||
# A4: Rally 6 4 b4: rs 6 |
||||
# a1: dr 6 B4: MC 1 6 |
||||
# A5: Rally 3 4 A8: TH 5 5 |
||||
# B2: Rally 5 3 A9: TK 2 3 |
||||
# b1: dr 2 A10: IFT 3 3 |
||||
# b2: dr 2 A11: IFT 4 4 |
||||
_analyze_vlogs( "full.vlog" ) |
||||
|
||||
# check the results |
||||
lfa = _get_chart_data( 1 ) |
||||
assert lfa["distrib"]["dr"] == [ |
||||
[ "Alice (6.0)", "Bob (3.2)" ], |
||||
["",""], ["","60"], ["",""], ["","20"], ["",""], ["100","20"] |
||||
] |
||||
assert lfa["distrib"]["DR"] == [ |
||||
[ "Alice (6.6)", "Bob (7.2)" ], |
||||
["",""], ["8.3",""], ["16.7",""], ["16.7",""], ["8.3","20"], |
||||
["8.3","40"], ["16.7","40"], ["8.3",""], ["16.7",""], ["",""], ["",""] |
||||
] |
||||
|
||||
# check the results |
||||
assert lfa["pie"]["dr"] == [ ["Bob","5"], ["Alice","1"] ] |
||||
assert lfa["pie"]["DR"] == [ ["Bob","5"], ["Alice","12"] ] |
||||
|
||||
# check the results |
||||
_check_time_plot_window_sizes( [ 1, 5 ] ) |
||||
assert lfa["timePlot"] == [ |
||||
[ "", "Alice (12)", "Bob (5)" ], |
||||
["","9",""], ["","5",""], ["","4",""], ["","","8"], ["","10",""], |
||||
["","7",""], ["","","8"], |
||||
[ "Axis 1 PFPh", "4", "" ], |
||||
["","","7"], ["","8",""], ["","","7"], ["","10",""], ["","5",""], |
||||
["","6",""], ["","8",""], |
||||
[ "Axis 1 DFPh", "","6" ], |
||||
["","3",""] |
||||
] |
||||
_check_time_plot_values( [1,5], "5", [ |
||||
[ "", "Alice (12)", "Bob (5)" ], |
||||
["","7",""], ["Axis 1 PFPh","6",""], ["","6.6",""], ["","7.8",""], ["","6.8",""], |
||||
["","6.6",""], ["","7.4",""], |
||||
[ "Axis 1 DFPh", "", "7.2" ], |
||||
["","6.4",""] |
||||
] ) |
||||
|
||||
# check the results |
||||
assert lfa["hotness"] == [ ["Alice","1.367"], ["Bob","-0.927"] ] |
||||
|
||||
# switch to showing the Morale Check DR's and check the results |
||||
_select_roll_type( "MC" ) |
||||
lfa = _get_chart_data() |
||||
assert lfa["distrib"]["dr"] == [] |
||||
assert lfa["distrib"]["DR"] == [ |
||||
[ "Alice (3.0)", "Bob (7.0)" ], |
||||
["",""], ["100",""], ["",""], ["",""], ["",""], |
||||
["","100"], ["",""], ["",""], ["",""], ["",""], ["",""] |
||||
] |
||||
assert lfa["pie"]["dr"] == [] |
||||
assert lfa["pie"]["DR"] == [ ["Bob","2"], ["Alice","1"] ] |
||||
_check_time_plot_values( [1], "1", [ |
||||
[ "", "Alice (1)", "Bob (2)" ], |
||||
[ "Axis 1 PFPh", "", "7" ], |
||||
["","","7"], |
||||
[ "Axis 1 DFPh", "3", "" ], |
||||
] ) |
||||
assert lfa["hotness"] == [ ["Alice","287.445"], ["Bob","0.000"] ] |
||||
|
||||
# switch to showing the Sniper Activation DR's and check the results |
||||
_select_roll_type( "SA" ) |
||||
lfa = _get_chart_data() |
||||
assert lfa["distrib"]["dr"] == [ |
||||
[ "Bob (3.0)" ], |
||||
[""], ["50"], [""], ["50"], [""], [""] |
||||
] |
||||
assert lfa["distrib"]["DR"] == [] |
||||
assert lfa["pie"]["dr"] == [ ["Bob","2"] ] |
||||
assert lfa["pie"]["DR"] == [] |
||||
_check_time_plot_values( [1], 1, [ |
||||
[ "", "Bob (2)" ], |
||||
[ "Axis 1 PFPh", "4" ], |
||||
[ "Axis 1 DFPh", "2" ], |
||||
] ) |
||||
assert lfa["hotness"] == [ ["Alice",""], ["Bob",""] ] |
||||
|
||||
# switch to showing the Close Combat DR's and check the results |
||||
_select_roll_type( "CC" ) |
||||
lfa = _get_chart_data() |
||||
assert lfa["distrib"]["dr"] == [] |
||||
assert lfa["distrib"]["DR"] == [] |
||||
assert lfa["pie"]["dr"] == [] |
||||
assert lfa["pie"]["DR"] == [] |
||||
_check_time_plot_values( [1], 1, [] ) |
||||
assert lfa["hotness"] == [ ["Alice",""], ["Bob",""] ] |
||||
|
||||
# close the analysis window |
||||
find_child( "#lfa button.ui-dialog-titlebar-close" ).click() |
||||
|
||||
# run the tests |
||||
_run_tests( control_tests, do_test, not pytest.config.option.short_tests ) #pylint: disable=no-member |
||||
|
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@pytest.mark.skipif( not pytest.config.option.vasl_mods, reason="--vasl-mods not specified" ) #pylint: disable=no-member |
||||
@pytest.mark.skipif( not pytest.config.option.vassal, reason="--vassal not specified" ) #pylint: disable=no-member |
||||
def test_4players( webapp, webdriver ): |
||||
"""Test a file log file analysis with 4 players.""" |
||||
|
||||
# initialize |
||||
control_tests = init_webapp( webapp, webdriver, vlog_persistence=1, lfa_tables=1 ) |
||||
|
||||
def do_test(): #pylint: disable=missing-docstring |
||||
|
||||
# analyze the log file |
||||
# RPh PFPh MPh DFPh |
||||
# --- ----- ------ ------ |
||||
# A1: 3 C1: 4 D3: 1 C6: 2 |
||||
# B1: 3 C2: 4 D4: 3 D7: 3 |
||||
# A2: 6 C3: 2 D5: 1 D8: 6 |
||||
# A3: 1 B5: 5 A6: 4 A11: 2 |
||||
# B2: 6 B6: 5 A7: 1 D9: 1 |
||||
# A4: 5 D2: 1 A8: 4 D10: 2 |
||||
# B3: 2 C4: 3 A9: 1 |
||||
# B4: 4 C5: 4 A10: 1 |
||||
# A5: 2 B7: 6 |
||||
# D1: 4 B8: 5 |
||||
# D6: 6 |
||||
_analyze_vlogs( "4players.vlog" ) |
||||
|
||||
# check the results |
||||
lfa = _get_chart_data( 1 ) |
||||
assert lfa["distrib"]["dr"] == [ |
||||
[ "Alice (2.7)", "Bob (4.5)", "Dave (2.8)", "Chuck (3.2)" ], |
||||
[ "36.4", "", "40", "" ], |
||||
[ "18.2", "12.5", "10", "33.3" ], |
||||
[ "9.1", "12.5", "20", "16.7" ], |
||||
[ "18.2", "12.5", "10", "50" ], |
||||
[ "9.1", "37.5", "", "" ], |
||||
[ "9.1", "25", "20", "" ] |
||||
] |
||||
assert lfa["distrib"]["DR"] == [] |
||||
|
||||
# check the results |
||||
assert lfa["pie"]["dr"] == [ ["Chuck","6"], ["Dave","10"], ["Bob","8"], ["Alice","11"] ] |
||||
assert lfa["pie"]["DR"] == [] |
||||
|
||||
# check the results |
||||
assert lfa["timePlot"] == [] |
||||
|
||||
# switch to showing the Random Selection dr's and check the results |
||||
_select_roll_type( "RS" ) |
||||
lfa = _get_chart_data( 1 ) |
||||
_check_time_plot_window_sizes( [ 1, 5 ] ) |
||||
assert lfa["timePlot"] == [ |
||||
[ "", "Alice (11)", "Bob (8)", "Dave (10)", "Chuck (6)" ], |
||||
["","3","","",""], ["","","3","",""], ["","6","","",""], ["","1","","",""], ["","","6","",""], |
||||
["","5","","",""], ["","","2","",""], ["","","4","",""], ["","2","","",""], ["","","","4",""], |
||||
[ "Allied 1 PFPh", "", "", "", "4" ], |
||||
["","","","","4"], ["","","","","2"], ["","","5","",""], ["","","5","",""], ["","","","1",""], |
||||
["","","","","3"], ["","","","","4"], |
||||
[ "Allied 1 MPh", "", "", "1", "" ], |
||||
["","","","3",""], ["","","","1",""], ["","4","","",""], ["","1","","",""], ["","4","","",""], |
||||
["","1","","",""], ["","1","","",""], ["","","6","",""], ["","","5","",""], ["","","","6",""], |
||||
[ "Allied 1 DFPh", "", "", "", "2" ], |
||||
["","","","3",""], ["","","","6",""], ["","2","","",""], ["","","","1",""], ["","","","2",""] |
||||
] |
||||
lfa = _get_chart_data( 5 ) |
||||
assert lfa["timePlot"] == [ |
||||
[ "", "Alice (11)", "Bob (8)", "Dave (10)", "Chuck (6)" ], |
||||
["","3.4","","",""], |
||||
["Allied 1 PFPh","","4","",""], |
||||
["","","4.4","",""], ["","","","","3.4"], |
||||
["Allied 1 MPh","","","2",""], |
||||
["","3.6","","",""], ["","2.6","","",""], ["","3.2","","",""], ["","2.4","","",""], ["","2.2","","",""], |
||||
["","","4.4","",""], ["","","5","",""], ["","","","2.4",""], |
||||
["Allied 1 DFPh","","","","3"], |
||||
["","","","2.8",""], ["","","","3.8",""], ["","1.8","","",""], ["","","","3.4",""], ["","","","3.6",""] |
||||
] |
||||
|
||||
# close the analysis window |
||||
find_child( "#lfa button.ui-dialog-titlebar-close" ).click() |
||||
|
||||
# run the tests |
||||
_run_tests( control_tests, do_test, not pytest.config.option.short_tests ) #pylint: disable=no-member |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@pytest.mark.skipif( not pytest.config.option.vasl_mods, reason="--vasl-mods not specified" ) #pylint: disable=no-member |
||||
@pytest.mark.skipif( not pytest.config.option.vassal, reason="--vassal not specified" ) #pylint: disable=no-member |
||||
def test_multiple_files( webapp, webdriver ): |
||||
"""Test analyzing multiple log files.""" |
||||
|
||||
# initialize |
||||
control_tests = init_webapp( webapp, webdriver, vlog_persistence=1, lfa_tables=1 ) |
||||
|
||||
def check_color_pickers( expected ): |
||||
"""Check which color pickers are being presented to the user.""" |
||||
find_child( "#lfa .options button.player-colors" ).click() |
||||
popup = wait_for_elem( 2, "#lfa .player-colors-popup" ) |
||||
player_names = [ e.text for e in find_children( ".row .caption", popup ) if e.text ] |
||||
assert player_names.pop() == "expected results" |
||||
assert player_names == expected |
||||
|
||||
def select_file( fname ): |
||||
"""Select one of the files being analyzed.""" |
||||
find_child( "#lfa .banner .select-file" ).click() |
||||
popup = wait_for_elem( 2, "#lfa .select-file-popup" ) |
||||
for row in find_children( ".row", popup ): |
||||
if find_child( "label", row ).text == fname: |
||||
find_child( "input[type='radio']", row ).click() |
||||
return |
||||
assert False, "Couldn't find file: "+fname |
||||
|
||||
def do_test(): #pylint: disable=missing-docstring |
||||
|
||||
# NOTE: The "1a" and "1b" log files have the same players (Alice and Bob), but the "2" log file |
||||
# has Bob and Chuck. |
||||
# multiple-1a multiple-1b multiple-2 |
||||
# ----------- ----------- ---------- |
||||
# A: IFT 5 2 A: IFT 3 6 B: IFT 5 5 |
||||
# B: IFT 2 6 B: IFT 4 2 C: IFT 6 5 |
||||
# A: rs 6 A: rs 2 B: sa 4 |
||||
# A: IFT 4 1 Turn Track Turn Track |
||||
# B: IFT 4 4 A: IFT 4 3 B: IFT 2 2 |
||||
# B: rs 1 B: IFT 6 4 C: IFT 5 4 |
||||
# Turn Track C: rs 5 |
||||
# A: IFT 2 1 |
||||
# B: IFT 4 4 |
||||
# A: rs 2 |
||||
# B: sa 5 |
||||
|
||||
# load 2 log files that have the same players |
||||
_analyze_vlogs( [ "multiple-1a.vlog", "multiple-1b.vlog" ] ) |
||||
|
||||
# check the results |
||||
lfa = _get_chart_data( 1 ) |
||||
assert lfa["timePlot"] == [ |
||||
[ "", "Alice (5)", "Bob (5)" ], |
||||
["","7",""], ["","","8"], ["","5",""], ["","","8"], |
||||
[ "Allied 1 PFPh", "3", "" ], |
||||
["","","8"], ["","9",""], ["","","6"], |
||||
[ "Allied 1 MPh", "7", "" ], |
||||
["","","10"], |
||||
] |
||||
assert lfa["hotness"] == [ ["Alice","7.673"], ["Bob","-5.484"] ] |
||||
_select_roll_type( "RS" ) |
||||
lfa = _get_chart_data( 1 ) |
||||
assert lfa["timePlot"] == [ |
||||
[ "", "Alice (3)", "Bob (1)" ], |
||||
["","6",""], ["","","1"], |
||||
[ "Allied 1 PFPh", "2", "" ], |
||||
["","2",""], |
||||
] |
||||
_select_roll_type( "SA" ) |
||||
lfa = _get_chart_data( 1 ) |
||||
assert lfa["timePlot"] == [ |
||||
[ "", "Bob (1)" ], |
||||
[ "Allied 1 PFPh", "5" ], |
||||
] |
||||
|
||||
# close the analysis window |
||||
find_child( "#lfa button.ui-dialog-titlebar-close" ).click() |
||||
|
||||
# load 2 log files that have different players |
||||
_analyze_vlogs( [ "multiple-1a.vlog", "multiple-2.vlog" ] ) |
||||
def check_all_files(): |
||||
"""Check the results for all files.""" |
||||
lfa = _get_chart_data( 1 ) |
||||
assert lfa["timePlot"] == [ |
||||
[ "", "Alice (3)", "Bob (5)", "Chuck (2)" ], |
||||
["","7","",""], ["","","8",""], ["","5","",""], ["","","8",""], |
||||
[ "Allied 1 PFPh", "3", "", "" ], |
||||
["","","8",""], ["","","10",""], ["","","","11"], |
||||
[ "UN 1 PFPh", "", "4", "" ], |
||||
["","","","9"], |
||||
] |
||||
assert lfa["hotness"] == [ ["Alice","28.512"], ["Bob","-3.336"], ["Chuck","-71.744"] ] |
||||
_select_roll_type( "RS" ) |
||||
lfa = _get_chart_data( 1 ) |
||||
assert lfa["timePlot"] == [ |
||||
[ "", "Alice (2)", "Bob (1)", "Chuck (1)" ], |
||||
["","6","",""], ["","","1",""], |
||||
[ "Allied 1 PFPh", "2", "", "" ], |
||||
["UN 1 PFPh","","","5"], |
||||
] |
||||
_select_roll_type( "SA" ) |
||||
lfa = _get_chart_data( 1 ) |
||||
assert lfa["timePlot"] == [ |
||||
[ "", "Bob (2)" ], |
||||
[ "Allied 1 PFPh", "5" ], |
||||
["","4"], |
||||
] |
||||
check_all_files() |
||||
check_color_pickers( [ "Alice", "Bob", "Chuck" ] ) |
||||
|
||||
# select a file and check the results |
||||
select_file( "multiple-1a.vlog" ) |
||||
_select_roll_type( "" ) |
||||
lfa = _get_chart_data( 1 ) |
||||
assert lfa["timePlot"] == [ |
||||
[ "", "Alice (3)", "Bob (3)" ], |
||||
["","7",""], ["","","8"], ["","5",""], ["","","8"], |
||||
[ "Allied 1 PFPh", "3", "" ], |
||||
["","","8"], |
||||
] |
||||
assert lfa["hotness"] == [ ["Alice","28.512"], ["Bob","-10.944"] ] |
||||
_select_roll_type( "RS" ) |
||||
lfa = _get_chart_data( 1 ) |
||||
assert lfa["timePlot"] == [ |
||||
[ "", "Alice (2)", "Bob (1)" ], |
||||
["","6",""], ["","","1"], |
||||
[ "Allied 1 PFPh", "2", "" ], |
||||
] |
||||
_select_roll_type( "SA" ) |
||||
lfa = _get_chart_data( 1 ) |
||||
assert lfa["timePlot"] == [ |
||||
[ "", "Bob (1)" ], |
||||
[ "Allied 1 PFPh", "5" ], |
||||
] |
||||
check_color_pickers( [ "Alice", "Bob" ] ) |
||||
|
||||
# select another file and check the results |
||||
select_file( "multiple-2.vlog" ) |
||||
_select_roll_type( "" ) |
||||
lfa = _get_chart_data( 1 ) |
||||
assert lfa["timePlot"] == [ |
||||
[ "", "Bob (2)", "Chuck (2)" ], |
||||
["","10",""], ["","","11"], |
||||
[ "UN 1 PFPh", "4", "" ], |
||||
["","","9"], |
||||
] |
||||
assert lfa["hotness"] == [ ["Bob","0.000"], ["Chuck","-71.744"] ] |
||||
_select_roll_type( "RS" ) |
||||
lfa = _get_chart_data( 1 ) |
||||
assert lfa["timePlot"] == [ |
||||
[ "", "Chuck (1)" ], |
||||
[ "UN 1 PFPh", "5" ], |
||||
] |
||||
_select_roll_type( "SA" ) |
||||
lfa = _get_chart_data( 1 ) |
||||
assert lfa["timePlot"] == [ |
||||
[ "", "Bob (1)" ], |
||||
["","4"], |
||||
] |
||||
check_color_pickers( [ "Bob", "Chuck" ] ) |
||||
|
||||
# select all files and check the results |
||||
select_file( "All files" ) |
||||
_select_roll_type( "" ) |
||||
check_all_files() |
||||
check_color_pickers( [ "Alice", "Bob", "Chuck" ] ) |
||||
|
||||
# close the analysis window |
||||
find_child( "#lfa button.ui-dialog-titlebar-close" ).click() |
||||
|
||||
# run the tests |
||||
_run_tests( control_tests, do_test, not pytest.config.option.short_tests ) #pylint: disable=no-member |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@pytest.mark.skipif( not pytest.config.option.vasl_mods, reason="--vasl-mods not specified" ) #pylint: disable=no-member |
||||
@pytest.mark.skipif( not pytest.config.option.vassal, reason="--vassal not specified" ) #pylint: disable=no-member |
||||
def test_3d6( webapp, webdriver ): |
||||
"""Test scenarios that use the 3d6 extension.""" |
||||
|
||||
# initialize |
||||
control_tests = init_webapp( webapp, webdriver, vlog_persistence=1, lfa_tables=1 ) |
||||
|
||||
def do_test(): #pylint: disable=missing-docstring |
||||
|
||||
# analyze the log file |
||||
_analyze_vlogs( "3d6.vlog" ) |
||||
|
||||
# check the results |
||||
# IFT 6,6 |
||||
# RS 2 |
||||
# 3d6 3,4,1 |
||||
# IFT 6,5 |
||||
# TH 6,2 |
||||
# 3d6 2,4,2 |
||||
lfa = _get_chart_data( 1 ) |
||||
assert lfa["timePlot"] == [ |
||||
[ "", "test (5)" ], |
||||
["","12"], ["","7"], ["","11"], ["","8"], ["","6"] |
||||
] |
||||
_select_roll_type( "3d6 (DR)" ) |
||||
lfa = _get_chart_data() |
||||
assert lfa["timePlot"] == [ |
||||
[ "", "test (2)" ], |
||||
["","7"], ["","6"] |
||||
] |
||||
_select_roll_type( "3d6 (dr)" ) |
||||
lfa = _get_chart_data() |
||||
assert lfa["timePlot"] == [ |
||||
[ "", "test (2)" ], |
||||
["","1"], ["","2"] |
||||
] |
||||
|
||||
# close the analysis window |
||||
find_child( "#lfa button.ui-dialog-titlebar-close" ).click() |
||||
|
||||
# run the tests |
||||
_run_tests( control_tests, do_test, not pytest.config.option.short_tests ) #pylint: disable=no-member |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@pytest.mark.skipif( not pytest.config.option.vasl_mods, reason="--vasl-mods not specified" ) #pylint: disable=no-member |
||||
@pytest.mark.skipif( not pytest.config.option.vassal, reason="--vassal not specified" ) #pylint: disable=no-member |
||||
def test_banner_updates( webapp, webdriver ): |
||||
"""Test updating the banner.""" |
||||
|
||||
# initialize |
||||
control_tests = init_webapp( webapp, webdriver, vlog_persistence=1 ) |
||||
|
||||
def check_banner( roll_type ): |
||||
"""Check the banner.""" |
||||
assert find_child( "#lfa .banner .title" ).text == "Log File Analysis test" |
||||
assert find_child( "#lfa .banner .title2" ).text == "(LFA-1)" |
||||
assert find_child( "#lfa .banner .roll-type" ).text == roll_type |
||||
|
||||
def do_test(): #pylint: disable=missing-docstring |
||||
|
||||
# analyze the log file |
||||
_analyze_vlogs( "banner-updates.vlog" ) |
||||
|
||||
# check the banner as the roll type is changed |
||||
check_banner( "Showing all rolls." ) |
||||
_select_roll_type( "MC" ) |
||||
check_banner( "Showing Morale Check rolls." ) |
||||
_select_roll_type( "RS" ) |
||||
check_banner( "Showing Random Selection rolls." ) |
||||
|
||||
# close the analysis window |
||||
find_child( "#lfa button.ui-dialog-titlebar-close" ).click() |
||||
|
||||
# run the tests |
||||
_run_tests( control_tests, do_test, not pytest.config.option.short_tests ) #pylint: disable=no-member |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@pytest.mark.skipif( not pytest.config.option.vasl_mods, reason="--vasl-mods not specified" ) #pylint: disable=no-member |
||||
@pytest.mark.skipif( not pytest.config.option.vassal, reason="--vassal not specified" ) #pylint: disable=no-member |
||||
def test_download_data( webapp, webdriver ): |
||||
"""Test downloading the data.""" |
||||
|
||||
# initialize |
||||
control_tests = init_webapp( webapp, webdriver, vlog_persistence=1, lfa_persistence=1 ) |
||||
|
||||
def do_test(): #pylint: disable=missing-docstring |
||||
|
||||
# analyze the log file |
||||
_analyze_vlogs( "download-test.vlog" ) |
||||
|
||||
# download the data |
||||
marker = set_stored_msg_marker( "_lfa-download_" ) |
||||
find_child( "#lfa button.download" ).click() |
||||
wait_for( 2, lambda: get_stored_msg("_lfa-download_") != marker ) |
||||
data = get_stored_msg( "_lfa-download_" ) |
||||
|
||||
# check the results |
||||
data = data.split( "\n" ) |
||||
rows = list( csv.reader( data, quoting=csv.QUOTE_NONNUMERIC ) ) |
||||
assert rows == [ |
||||
[ "Log file", "Phase", "Player", "Type", "Die 1", "Die 2" ], |
||||
[ "download-test.vlog", "", 'Joey "The Lips" Blow', "IFT", 4, 1 ], |
||||
[ "", "", 'Joey "The Lips" Blow', "IFT", 2, 5 ], |
||||
[ "", "", 'Joey "The Lips" Blow', "RS", 2, "" ], |
||||
[ "", "UN 1 PFPh", "\u65e5\u672c Guy", "IFT", 4, 6 ], |
||||
[ "", "", "\u65e5\u672c Guy", "IFT", 2, 6 ], |
||||
[ "", "", "\u65e5\u672c Guy", "RS", 3, "" ], |
||||
[ "", "UN 1 MPh", 'Joey "The Lips" Blow', "IFT", 2, 6 ], |
||||
[ "", "", 'Joey "The Lips" Blow', "IFT", 2, 3 ], |
||||
[ "", "", 'Joey "The Lips" Blow', "RS", 3, "" ] |
||||
] |
||||
|
||||
# run the test |
||||
_run_tests( control_tests, do_test, False ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def _analyze_vlogs( fnames ): |
||||
"""Analyze log file(s).""" |
||||
|
||||
# initialize |
||||
if isinstance( fnames, str ): |
||||
fnames = [ fnames ] |
||||
select_menu_option( "analyze_vlog" ) |
||||
dlg = wait_for_elem( 2, ".ui-dialog.lfa-upload" ) |
||||
|
||||
# add each log file |
||||
for fname in fnames: |
||||
fname = os.path.join( os.path.split(__file__)[0], "fixtures/analyze-vlog/"+fname ) |
||||
vlog_data = open( fname, "rb" ).read() |
||||
set_stored_msg( "_vlog-persistence_", "{}|{}".format( |
||||
os.path.split( fname )[1], |
||||
base64.b64encode( vlog_data ).decode( "utf-8" ) |
||||
) ) |
||||
find_child( "button.add", dlg ).click() |
||||
wait_for( 2, lambda: get_stored_msg( "_vlog-persistence_" ) == "" ) |
||||
|
||||
# start the analysis |
||||
find_child( "button.ok", dlg ).click() |
||||
wait_for_elem( 30, "#lfa" ) |
||||
|
||||
def _get_chart_data( window_size=None ): |
||||
"""Unload the chart data from the page.""" |
||||
# set the time-plot window size |
||||
if window_size is not None: |
||||
_set_time_plot_window_size( window_size ) |
||||
# unload the chart data |
||||
remove_first_col = lambda data: [ row[1:] for row in data ] |
||||
remove_last_col = lambda data: [ row[:-1] for row in data ] |
||||
remove_first_row = lambda data: data[1:] |
||||
return { |
||||
"distrib": { |
||||
"dr": remove_first_col( remove_last_col( _unload_table( "distrib d6x1" ) ) ), |
||||
"DR": remove_first_col( remove_last_col( _unload_table( "distrib d6x2" ) ) ), |
||||
}, |
||||
"pie": { |
||||
"dr": remove_first_row( _unload_table( "pie d6x1" ) ), |
||||
"DR": remove_first_row( _unload_table( "pie d6x2" ) ), |
||||
}, |
||||
"timePlot": _unload_table( "time-plot" ), |
||||
"hotness": remove_first_row( _unload_table( "hotness" ) ), |
||||
} |
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||
|
||||
def _select_roll_type( roll_type ): |
||||
"""Select the roll type.""" |
||||
elem = find_child( "select[name='roll-type']" ) |
||||
select_droplist_val( Select(elem), roll_type, isSelectMenu=True ) |
||||
|
||||
def _check_time_plot_window_sizes( expected ): |
||||
"""Check the available time-plot window sizes.""" |
||||
elem = find_child( "select[name='moving-average']" ) |
||||
vals = get_droplist_vals( Select(elem) ) |
||||
assert [ int(v[0]) for v in vals ] == expected |
||||
|
||||
def _set_time_plot_window_size( window_size ): |
||||
"""Select the specified time-plot moving average window size.""" |
||||
elem = find_child( "select[name='moving-average']" ) |
||||
select_droplist_val( Select(elem), window_size, isSelectMenu=True ) |
||||
|
||||
def _check_time_plot_values( expected_window_sizes, window_size, expected ): |
||||
"""Check the time-plot values.""" |
||||
# set the window size |
||||
assert int(window_size) in expected_window_sizes |
||||
_set_time_plot_window_size( window_size ) |
||||
# unload and check the time plot values |
||||
vals = _unload_table( "time-plot" ) |
||||
assert vals == expected |
||||
|
||||
def _unload_table( sel ): |
||||
"""Unload chart data from an HTML table.""" |
||||
return unload_table( |
||||
"//*[@class='{}']//table[@class='chart-data']//tr".format( sel ) |
||||
) |
@ -0,0 +1,37 @@ |
||||
package vassal_shim.lfa ; |
||||
|
||||
import org.w3c.dom.Document ; |
||||
import org.w3c.dom.Element ; |
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
public class DiceEvent implements Event |
||||
{ |
||||
String playerName ; |
||||
String rollType ; |
||||
String rollValues ; |
||||
|
||||
public DiceEvent( String playerName, String rollType, String rollValues ) |
||||
{ |
||||
// initialize the DiceEvent
|
||||
this.playerName = playerName ; |
||||
this.rollType = rollType ; |
||||
this.rollValues = rollValues ; |
||||
} |
||||
|
||||
public Element makeXmlElement( Document doc ) |
||||
{ |
||||
// create an XML element for the DiceEvent
|
||||
Element elem = doc.createElement( "diceEvent" ) ; |
||||
elem.setAttribute( "player", playerName ) ; |
||||
elem.setAttribute( "rollType", rollType ) ; |
||||
elem.setTextContent( rollValues ) ; |
||||
return elem ; |
||||
} |
||||
|
||||
public String toString() |
||||
{ |
||||
// return the DiceEvent as a string
|
||||
return "<DiceEvent:" + playerName + ":" + rollType + ":" + rollValues + ">" ; |
||||
} |
||||
} |
@ -0,0 +1,12 @@ |
||||
package vassal_shim.lfa ; |
||||
|
||||
import org.w3c.dom.Document ; |
||||
import org.w3c.dom.Element ; |
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
public interface Event |
||||
{ |
||||
public Element makeXmlElement( Document doc ) ; |
||||
} |
||||
|
@ -0,0 +1,20 @@ |
||||
package vassal_shim.lfa ; |
||||
|
||||
import java.util.ArrayList ; |
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
public class LogFileAnalysis |
||||
{ |
||||
public String logFilename ; |
||||
public String scenarioName ; |
||||
public String scenarioId ; |
||||
public ArrayList<Event> events ; |
||||
|
||||
public LogFileAnalysis( String logFilename, String scenarioName, String scenarioId, ArrayList<Event> events ) { |
||||
this.logFilename = logFilename ; |
||||
this.scenarioName = scenarioName ; |
||||
this.scenarioId = scenarioId ; |
||||
this.events = events ; |
||||
} |
||||
} |
@ -0,0 +1,37 @@ |
||||
package vassal_shim.lfa ; |
||||
|
||||
import org.w3c.dom.Document ; |
||||
import org.w3c.dom.Element ; |
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
public class TurnTrackEvent implements Event |
||||
{ |
||||
String playerSide ; |
||||
String turnNo ; |
||||
String phaseName ; |
||||
|
||||
public TurnTrackEvent( String playerSide, String turnNo, String phaseName ) |
||||
{ |
||||
// initialize the TurnTrackEvent
|
||||
this.playerSide = playerSide ; |
||||
this.turnNo = turnNo ; |
||||
this.phaseName = phaseName ; |
||||
} |
||||
|
||||
public Element makeXmlElement( Document doc ) |
||||
{ |
||||
// create an XML element for the TurnTrackEvent
|
||||
Element elem = doc.createElement( "turnTrackEvent" ) ; |
||||
elem.setAttribute( "side", playerSide ) ; |
||||
elem.setAttribute( "turnNo", turnNo ) ; |
||||
elem.setAttribute( "phase", phaseName ) ; |
||||
return elem ; |
||||
} |
||||
|
||||
public String toString() |
||||
{ |
||||
// return the TurnTrackEvent as a string
|
||||
return "<TurnTrackEvent:" + playerSide + ":" + turnNo + ":" + phaseName + ">" ; |
||||
} |
||||
} |