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

284 lines
11 KiB

/* 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 },
} ;