From 18a788398ad549550b2e36a35ded2d3acc60c0ce Mon Sep 17 00:00:00 2001 From: Vince Au Date: Mon, 10 May 2021 16:02:00 +1000 Subject: [PATCH 1/2] feat(stats): add doubles stats calculation This reverts commit ca5b5eef834363c2cd7e81b04dc2e6475fe7cf0d. --- src/stats/actions.ts | 27 +++++----- src/stats/combos.ts | 99 ++++++++++++++++++++---------------- src/stats/common.ts | 54 ++++++++------------ src/stats/conversions.ts | 107 ++++++++++++++++++++------------------- src/stats/inputs.ts | 34 +++++-------- src/stats/overall.ts | 106 +++++++++++++++++++++++--------------- src/stats/stocks.ts | 31 ++++++------ test/conversion.spec.ts | 14 ++--- test/stats.spec.ts | 20 +++----- 9 files changed, 253 insertions(+), 239 deletions(-) diff --git a/src/stats/actions.ts b/src/stats/actions.ts index 35ded9a5..4d569fc1 100644 --- a/src/stats/actions.ts +++ b/src/stats/actions.ts @@ -1,7 +1,7 @@ import _ from "lodash"; import { FrameEntryType, GameStartType } from "../types"; -import { ActionCountsType, getSinglesPlayerPermutationsFromSettings, PlayerIndexedType, State } from "./common"; +import { ActionCountsType, State } from "./common"; import { StatComputer } from "./stats"; // Frame pattern that indicates a dash dance turn was executed @@ -13,16 +13,17 @@ interface PlayerActionState { } export class ActionsComputer implements StatComputer { - private playerPermutations = new Array(); - private state = new Map(); + private playerIndices: number[] = []; + private state = new Map(); public setup(settings: GameStartType): void { + // Reset the state this.state = new Map(); - this.playerPermutations = getSinglesPlayerPermutationsFromSettings(settings); - this.playerPermutations.forEach((indices) => { + + this.playerIndices = settings.players.map((p) => p.playerIndex); + this.playerIndices.forEach((playerIndex) => { const playerCounts: ActionCountsType = { - playerIndex: indices.playerIndex, - opponentIndex: indices.opponentIndex, + playerIndex, wavedashCount: 0, wavelandCount: 0, airDodgeCount: 0, @@ -49,15 +50,15 @@ export class ActionsComputer implements StatComputer { playerCounts: playerCounts, animations: [], }; - this.state.set(indices, playerState); + this.state.set(playerIndex, playerState); }); } public processFrame(frame: FrameEntryType): void { - this.playerPermutations.forEach((indices) => { - const state = this.state.get(indices); + this.playerIndices.forEach((index) => { + const state = this.state.get(index); if (state) { - handleActionCompute(state, indices, frame); + handleActionCompute(state, index, frame); } }); } @@ -122,8 +123,8 @@ function didStartLedgegrab(currentAnimation: State, previousAnimation: State): b return isCurrentlyGrabbingLedge && !wasPreviouslyGrabbingLedge; } -function handleActionCompute(state: PlayerActionState, indices: PlayerIndexedType, frame: FrameEntryType): void { - const playerFrame = frame.players[indices.playerIndex]!.post; +function handleActionCompute(state: PlayerActionState, playerIndex: number, frame: FrameEntryType): void { + const playerFrame = frame.players[playerIndex]!.post; const incrementCount = (field: string, condition: boolean): void => { if (!condition) { return; diff --git a/src/stats/combos.ts b/src/stats/combos.ts index af1bcbff..bc869818 100644 --- a/src/stats/combos.ts +++ b/src/stats/combos.ts @@ -1,12 +1,11 @@ import { EventEmitter } from "events"; -import _ from "lodash"; +import { last } from "lodash"; import { FrameEntryType, FramesType, GameStartType, PostFrameUpdateType } from "../types"; import { calcDamageTaken, ComboType, didLoseStock, - getSinglesPlayerPermutationsFromSettings, isCommandGrabbed, isDamaged, isDead, @@ -14,7 +13,6 @@ import { isGrabbed, isTeching, MoveLandedType, - PlayerIndexedType, Timers, } from "./common"; import { StatComputer } from "./stats"; @@ -34,9 +32,9 @@ interface ComboState { } export class ComboComputer extends EventEmitter implements StatComputer { - private playerPermutations = new Array(); - private state = new Map(); - private combos = new Array(); + private playerIndices: number[] = []; + private combos: ComboType[] = []; + private state = new Map(); private settings: GameStartType | null = null; public setup(settings: GameStartType): void { @@ -44,9 +42,9 @@ export class ComboComputer extends EventEmitter implements StatComputer { + this.playerIndices = settings.players.map((p) => p.playerIndex); + this.playerIndices.forEach((indices) => { const playerState: ComboState = { combo: null, move: null, @@ -59,15 +57,15 @@ export class ComboComputer extends EventEmitter implements StatComputer { - const state = this.state.get(indices); + this.playerIndices.forEach((index) => { + const state = this.state.get(index); if (state) { - handleComboCompute(allFrames, state, indices, frame, this.combos); + handleComboCompute(allFrames, state, index, frame, this.combos); // Emit an event for the new combo if (state.event !== null) { this.emit(state.event, { - combo: _.last(this.combos), + combo: last(this.combos), settings: this.settings, }); state.event = null; @@ -84,29 +82,20 @@ export class ComboComputer extends EventEmitter implements StatComputer= 0 && lastHitBy <= 3; + if (playerDamageTaken && lastHitBy !== null && validLastHitBy) { + // Update who hit us last + state.combo.lastHitBy = lastHitBy; + // If animation of last hit has been cleared that means this is a new move. This // prevents counting multiple hits from the same move such as fox's drill if (state.lastHitAnimation === null) { state.move = { frame: currentFrameNumber, - moveId: playerFrame.lastAttackLanded!, + moveId: frame.players[lastHitBy]!.post!.lastAttackLanded!, hitCount: 0, damage: 0, + playerIndex: lastHitBy, }; state.combo.moves.push(state.move); @@ -165,7 +167,7 @@ function handleComboCompute( if (state.move) { state.move.hitCount += 1; - state.move.damage += opntDamageTaken; + state.move.damage += playerDamageTaken; } // Store previous frame animation to consider the case of a trade, the previous @@ -184,18 +186,25 @@ function handleComboCompute( return; } - const opntIsTeching = isTeching(oppActionStateId); - const opntIsDowned = isDown(oppActionStateId); - const opntDidLoseStock = prevOpponentFrame && didLoseStock(opponentFrame, prevOpponentFrame); - const opntIsDying = isDead(oppActionStateId); + const playerIsTeching = isTeching(playerActionStateId); + const playerIsDowned = isDown(playerActionStateId); + const playerDidLoseStock = prevPlayerFrame && didLoseStock(playerFrame, prevPlayerFrame); + const playerIsDying = isDead(playerActionStateId); - // Update percent if opponent didn't lose stock - if (!opntDidLoseStock) { - state.combo.currentPercent = opponentFrame.percent ?? 0; + // Update percent if the player didn't lose stock + if (!playerDidLoseStock) { + state.combo.currentPercent = playerFrame.percent ?? 0; } - if (opntIsDamaged || opntIsGrabbed || opntIsCommandGrabbed || opntIsTeching || opntIsDowned || opntIsDying) { - // If opponent got grabbed or damaged, reset the reset counter + if ( + playerIsDamaged || + playerIsGrabbed || + playerIsCommandGrabbed || + playerIsTeching || + playerIsDowned || + playerIsDying + ) { + // If the player got grabbed or damaged, reset the reset counter state.resetCounter = 0; } else { state.resetCounter += 1; @@ -203,8 +212,8 @@ function handleComboCompute( let shouldTerminate = false; - // Termination condition 1 - player kills opponent - if (opntDidLoseStock) { + // Termination condition 1 - player was killed + if (playerDidLoseStock) { state.combo.didKill = true; shouldTerminate = true; } @@ -217,7 +226,7 @@ function handleComboCompute( // If combo should terminate, mark the end states and add it to list if (shouldTerminate) { state.combo.endFrame = playerFrame.frame; - state.combo.endPercent = prevOpponentFrame ? prevOpponentFrame.percent ?? 0 : 0; + state.combo.endPercent = prevPlayerFrame ? prevPlayerFrame.percent ?? 0 : 0; state.event = ComboEvent.COMBO_END; state.combo = null; diff --git a/src/stats/common.ts b/src/stats/common.ts index 71c2eb9c..9d530d9d 100644 --- a/src/stats/common.ts +++ b/src/stats/common.ts @@ -1,6 +1,6 @@ import _ from "lodash"; -import { GameStartType, PostFrameUpdateType } from "../types"; +import { PostFrameUpdateType } from "../types"; export interface StatsType { gameComplete: boolean; @@ -19,11 +19,6 @@ export interface RatioType { ratio: number | null; } -export interface PlayerIndexedType { - playerIndex: number; - opponentIndex: number; -} - export interface DurationType { startFrame: number; endFrame?: number | null; @@ -35,7 +30,8 @@ export interface DamageType { endPercent?: number | null; } -export interface StockType extends PlayerIndexedType, DurationType, DamageType { +export interface StockType extends DurationType, DamageType { + playerIndex: number; count: number; deathAnimation?: number | null; } @@ -45,20 +41,22 @@ export interface MoveLandedType { moveId: number; hitCount: number; damage: number; + playerIndex: number; } -export interface ConversionType extends PlayerIndexedType, DurationType, DamageType { +export interface ComboType extends DurationType, DamageType { + playerIndex: number; moves: MoveLandedType[]; - openingType: string; didKill: boolean; + lastHitBy: number | null; } -export interface ComboType extends PlayerIndexedType, DurationType, DamageType { - moves: MoveLandedType[]; - didKill: boolean; +export interface ConversionType extends ComboType { + openingType: string; } -export interface ActionCountsType extends PlayerIndexedType { +export interface ActionCountsType { + playerIndex: number; wavedashCount: number; wavelandCount: number; airDodgeCount: number; @@ -90,7 +88,8 @@ export interface InputCountsType { total: number; } -export interface OverallType extends PlayerIndexedType { +export interface OverallType { + playerIndex: number; inputCounts: InputCountsType; conversionCount: number; totalDamage: number; @@ -176,30 +175,19 @@ export const Timers = { COMBO_STRING_RESET_FRAMES: 45, }; -export function getSinglesPlayerPermutationsFromSettings(settings: GameStartType): PlayerIndexedType[] { - if (!settings || settings.players.length !== 2) { - // Only return opponent indices for singles - return []; +export function didLoseStock( + frame: PostFrameUpdateType | undefined, + prevFrame: PostFrameUpdateType | undefined, +): boolean { + if (!frame || !prevFrame) { + return false; } - return [ - { - playerIndex: settings.players[0].playerIndex, - opponentIndex: settings.players[1].playerIndex, - }, - { - playerIndex: settings.players[1].playerIndex, - opponentIndex: settings.players[0].playerIndex, - }, - ]; -} - -export function didLoseStock(frame: PostFrameUpdateType, prevFrame: PostFrameUpdateType): boolean { - if (!frame || !prevFrame) { + if (prevFrame.stocksRemaining === null || frame.stocksRemaining === null) { return false; } - return prevFrame.stocksRemaining! - frame.stocksRemaining! > 0; + return prevFrame.stocksRemaining - frame.stocksRemaining > 0; } export function isInControl(state: number): boolean { diff --git a/src/stats/conversions.ts b/src/stats/conversions.ts index c031a54a..eab99811 100644 --- a/src/stats/conversions.ts +++ b/src/stats/conversions.ts @@ -6,13 +6,11 @@ import { calcDamageTaken, ConversionType, didLoseStock, - getSinglesPlayerPermutationsFromSettings, isCommandGrabbed, isDamaged, isGrabbed, isInControl, MoveLandedType, - PlayerIndexedType, Timers, } from "./common"; import { StatComputer } from "./stats"; @@ -31,9 +29,9 @@ interface MetadataType { } export class ConversionComputer extends EventEmitter implements StatComputer { - private playerPermutations = new Array(); - private conversions = new Array(); - private state = new Map(); + private playerIndices: number[] = []; + private conversions: ConversionType[] = []; + private state = new Map(); private metadata: MetadataType; private settings: GameStartType | null = null; @@ -45,31 +43,31 @@ export class ConversionComputer extends EventEmitter implements StatComputer p.playerIndex); this.conversions = []; - this.state = new Map(); + this.state = new Map(); this.metadata = { lastEndFrameByOppIdx: {}, }; - this.settings = settings; - this.playerPermutations.forEach((indices) => { + this.playerIndices.forEach((index) => { const playerState: PlayerConversionState = { conversion: null, move: null, resetCounter: 0, lastHitAnimation: null, }; - this.state.set(indices, playerState); + this.state.set(index, playerState); }); } public processFrame(frame: FrameEntryType, allFrames: FramesType): void { - this.playerPermutations.forEach((indices) => { - const state = this.state.get(indices); + this.playerIndices.forEach((index) => { + const state = this.state.get(index); if (state) { - const terminated = handleConversionCompute(allFrames, state, indices, frame, this.conversions); + const terminated = handleConversionCompute(allFrames, state, index, frame, this.conversions); if (terminated) { this.emit("CONVERSION", { combo: _.last(this.conversions), @@ -109,10 +107,12 @@ export class ConversionComputer extends EventEmitter implements StatComputer conversion.startFrame; + const lastMove = _.last(conversion.moves); + // If not trade, check the player endFrame + const playerEndFrame = this.metadata.lastEndFrameByOppIdx[ + lastMove ? lastMove.playerIndex : conversion.playerIndex + ]; + const isCounterAttack = playerEndFrame && playerEndFrame > conversion.startFrame; conversion.openingType = isCounterAttack ? "counter-attack" : "neutral-win"; }); }); @@ -122,29 +122,20 @@ export class ConversionComputer extends EventEmitter implements StatComputer= 0 && lastHitBy <= 3; + if (playerDamageTaken && lastHitBy !== null && validLastHitBy) { + // Update who hit us last + state.conversion.lastHitBy = lastHitBy; + // If animation of last hit has been cleared that means this is a new move. This // prevents counting multiple hits from the same move such as fox's drill if (state.lastHitAnimation === null) { state.move = { frame: currentFrameNumber, - moveId: playerFrame.lastAttackLanded!, + moveId: frame.players[lastHitBy]!.post!.lastAttackLanded!, hitCount: 0, damage: 0, + playerIndex: lastHitBy, }; state.conversion.moves.push(state.move); @@ -195,7 +198,7 @@ function handleConversionCompute( if (state.move) { state.move.hitCount += 1; - state.move.damage += opntDamageTaken; + state.move.damage += playerDamageTaken; } // Store previous frame animation to consider the case of a trade, the previous @@ -210,32 +213,32 @@ function handleConversionCompute( return false; } - const opntInControl = isInControl(oppActionStateId); - const opntDidLoseStock = prevOpponentFrame && didLoseStock(opponentFrame, prevOpponentFrame); + const playerInControl = isInControl(playerActionStateId); + const playerDidLoseStock = prevPlayerFrame && didLoseStock(playerFrame, prevPlayerFrame); - // Update percent if opponent didn't lose stock - if (!opntDidLoseStock) { - state.conversion.currentPercent = opponentFrame.percent ?? 0; + // Update percent if the player didn't lose stock + if (!playerDidLoseStock) { + state.conversion.currentPercent = playerFrame.percent ?? 0; } - if (opntIsDamaged || opntIsGrabbed || opntIsCommandGrabbed) { - // If opponent got grabbed or damaged, reset the reset counter + if (playerIsDamaged || playerIsGrabbed || playerIsCommandGrabbed) { + // If the player got grabbed or damaged, reset the reset counter state.resetCounter = 0; } - const shouldStartResetCounter = state.resetCounter === 0 && opntInControl; + const shouldStartResetCounter = state.resetCounter === 0 && playerInControl; const shouldContinueResetCounter = state.resetCounter > 0; if (shouldStartResetCounter || shouldContinueResetCounter) { // This will increment the reset timer under the following conditions: - // 1) if we were punishing opponent but they have now entered an actionable state - // 2) if counter has already started counting meaning opponent has entered actionable state + // 1) if the player is being punishing but they have now entered an actionable state + // 2) if counter has already started counting meaning the player has entered actionable state state.resetCounter += 1; } let shouldTerminate = false; - // Termination condition 1 - player kills opponent - if (opntDidLoseStock) { + // Termination condition 1 - player was killed + if (playerDidLoseStock) { state.conversion.didKill = true; shouldTerminate = true; } @@ -248,7 +251,7 @@ function handleConversionCompute( // If conversion should terminate, mark the end states and add it to list if (shouldTerminate) { state.conversion.endFrame = playerFrame.frame; - state.conversion.endPercent = prevOpponentFrame ? prevOpponentFrame.percent ?? 0 : 0; + state.conversion.endPercent = prevPlayerFrame ? prevPlayerFrame.percent ?? 0 : 0; state.conversion = null; state.move = null; diff --git a/src/stats/inputs.ts b/src/stats/inputs.ts index 8783497d..d6b411cf 100644 --- a/src/stats/inputs.ts +++ b/src/stats/inputs.ts @@ -1,7 +1,6 @@ import _ from "lodash"; import { FrameEntryType, Frames, FramesType, GameStartType } from "../types"; -import { getSinglesPlayerPermutationsFromSettings, PlayerIndexedType } from "./common"; import { StatComputer } from "./stats"; enum JoystickRegion { @@ -18,7 +17,6 @@ enum JoystickRegion { export interface PlayerInput { playerIndex: number; - opponentIndex: number; inputCount: number; joystickInputCount: number; cstickInputCount: number; @@ -27,33 +25,32 @@ export interface PlayerInput { } export class InputComputer implements StatComputer { - private state = new Map(); - private playerPermutations = new Array(); + private playerIndices: number[] = []; + private state = new Map(); public setup(settings: GameStartType): void { - // Reset the state + // Reset the state since it's a new game + this.playerIndices = settings.players.map((p) => p.playerIndex); this.state = new Map(); - this.playerPermutations = getSinglesPlayerPermutationsFromSettings(settings); - this.playerPermutations.forEach((indices) => { + this.playerIndices.forEach((index) => { const playerState: PlayerInput = { - playerIndex: indices.playerIndex, - opponentIndex: indices.opponentIndex, + playerIndex: index, inputCount: 0, joystickInputCount: 0, cstickInputCount: 0, buttonInputCount: 0, triggerInputCount: 0, }; - this.state.set(indices, playerState); + this.state.set(index, playerState); }); } public processFrame(frame: FrameEntryType, allFrames: FramesType): void { - this.playerPermutations.forEach((indices) => { - const state = this.state.get(indices); + this.playerIndices.forEach((index) => { + const state = this.state.get(index); if (state) { - handleInputCompute(allFrames, state, indices, frame); + handleInputCompute(allFrames, state, index, frame); } }); } @@ -63,16 +60,11 @@ export class InputComputer implements StatComputer { } } -function handleInputCompute( - frames: FramesType, - state: PlayerInput, - indices: PlayerIndexedType, - frame: FrameEntryType, -): void { - const playerFrame = frame.players[indices.playerIndex]!.pre; +function handleInputCompute(frames: FramesType, state: PlayerInput, playerIndex: number, frame: FrameEntryType): void { + const playerFrame = frame.players[playerIndex]!.pre; const currentFrameNumber = playerFrame.frame!; const prevFrameNumber = currentFrameNumber - 1; - const prevPlayerFrame = frames[prevFrameNumber] ? frames[prevFrameNumber].players[indices.playerIndex]!.pre : null; + const prevPlayerFrame = frames[prevFrameNumber] ? frames[prevFrameNumber].players[playerIndex]!.pre : null; if (currentFrameNumber < Frames.FIRST_PLAYABLE || !prevPlayerFrame) { // Don't count inputs until the game actually starts diff --git a/src/stats/overall.ts b/src/stats/overall.ts index 2d895a56..78819d98 100644 --- a/src/stats/overall.ts +++ b/src/stats/overall.ts @@ -1,14 +1,7 @@ import _ from "lodash"; import { GameStartType } from "../types"; -import { - ConversionType, - getSinglesPlayerPermutationsFromSettings, - InputCountsType, - OverallType, - RatioType, - StockType, -} from "./common"; +import { ConversionType, InputCountsType, OverallType, RatioType, StockType } from "./common"; import { PlayerInput } from "./inputs"; interface ConversionsByPlayerByOpening { @@ -25,18 +18,17 @@ export function generateOverallStats( playableFrameCount: number, ): OverallType[] { const inputsByPlayer = _.keyBy(inputs, "playerIndex"); - const stocksByPlayer = _.groupBy(stocks, "playerIndex"); - const conversionsByPlayer = _.groupBy(conversions, "playerIndex"); + const originalConversions = conversions; + const conversionsByPlayer = _.groupBy(conversions, (conv) => conv.moves[0]?.playerIndex); const conversionsByPlayerByOpening: ConversionsByPlayerByOpening = _.mapValues(conversionsByPlayer, (conversions) => _.groupBy(conversions, "openingType"), ); const gameMinutes = playableFrameCount / 3600; - const playerIndices = getSinglesPlayerPermutationsFromSettings(settings); - const overall = playerIndices.map((indices) => { - const playerIndex = indices.playerIndex; - const opponentIndex = indices.opponentIndex; + const overall = settings.players.map((player) => { + const playerIndex = player.playerIndex; + const playerInputs = _.get(inputsByPlayer, playerIndex) || {}; const inputCounts: InputCountsType = { buttons: _.get(playerInputs, "buttonInputCount"), @@ -45,21 +37,49 @@ export function generateOverallStats( joystick: _.get(playerInputs, "joystickInputCount"), total: _.get(playerInputs, "inputCount"), }; - - const conversions = _.get(conversionsByPlayer, playerIndex) || []; - const successfulConversions = conversions.filter((conversion) => conversion.moves.length > 1); - const opponentStocks = _.get(stocksByPlayer, opponentIndex) || []; - const opponentEndedStocks = _.filter(opponentStocks, "endFrame"); - - const conversionCount = conversions.length; - const successfulConversionCount = successfulConversions.length; - const totalDamage = - _.sumBy(conversions, (conversion) => conversion.moves.reduce((total, move) => total + move.damage, 0)) || 0; - const killCount = opponentEndedStocks.length; + // const conversions = _.get(conversionsByPlayer, playerIndex) || []; + // const successfulConversions = conversions.filter((conversion) => conversion.moves.length > 1); + let conversionCount = 0; + let successfulConversionCount = 0; + + const opponentIndices = settings.players + .filter((opp) => { + // We want players which aren't ourselves + if (opp.playerIndex === playerIndex) { + return false; + } + + // Make sure they're not on our team either + return !settings.isTeams || opp.teamId !== player.teamId; + }) + .map((opp) => opp.playerIndex); + + let totalDamage = 0; + let killCount = 0; + + // These are the conversions that we did on our opponents + originalConversions + // Filter down to conversions of our opponent + .filter((conversion) => conversion.playerIndex !== playerIndex) + .forEach((conversion) => { + conversionCount++; + + // We killed the opponent + if (conversion.didKill && conversion.lastHitBy === playerIndex) { + killCount += 1; + } + if (conversion.moves.length > 1 && conversion.moves[0].playerIndex === playerIndex) { + successfulConversionCount++; + } + conversion.moves.forEach((move) => { + if (move.playerIndex === playerIndex) { + totalDamage += move.damage; + } + }); + }); return { playerIndex: playerIndex, - opponentIndex: opponentIndex, inputCounts: inputCounts, conversionCount: conversionCount, totalDamage: totalDamage, @@ -70,9 +90,9 @@ export function generateOverallStats( digitalInputsPerMinute: getRatio(inputCounts.buttons, gameMinutes), openingsPerKill: getRatio(conversionCount, killCount), damagePerOpening: getRatio(totalDamage, conversionCount), - neutralWinRatio: getOpeningRatio(conversionsByPlayerByOpening, playerIndex, opponentIndex, "neutral-win"), - counterHitRatio: getOpeningRatio(conversionsByPlayerByOpening, playerIndex, opponentIndex, "counter-attack"), - beneficialTradeRatio: getBeneficialTradeRatio(conversionsByPlayerByOpening, playerIndex, opponentIndex), + neutralWinRatio: getOpeningRatio(conversionsByPlayerByOpening, playerIndex, opponentIndices, "neutral-win"), + counterHitRatio: getOpeningRatio(conversionsByPlayerByOpening, playerIndex, opponentIndices, "counter-attack"), + beneficialTradeRatio: getBeneficialTradeRatio(conversionsByPlayerByOpening, playerIndex, opponentIndices), }; }); @@ -90,12 +110,14 @@ function getRatio(count: number, total: number): RatioType { function getOpeningRatio( conversionsByPlayerByOpening: ConversionsByPlayerByOpening, playerIndex: number, - opponentIndex: number, + opponentIndices: number[], type: string, ): RatioType { const openings = _.get(conversionsByPlayerByOpening, [playerIndex, type]) || []; - const opponentOpenings = _.get(conversionsByPlayerByOpening, [opponentIndex, type]) || []; + const opponentOpenings = _.flatten( + opponentIndices.map((opponentIndex) => _.get(conversionsByPlayerByOpening, [opponentIndex, type]) || []), + ); return getRatio(openings.length, openings.length + opponentOpenings.length); } @@ -103,10 +125,12 @@ function getOpeningRatio( function getBeneficialTradeRatio( conversionsByPlayerByOpening: ConversionsByPlayerByOpening, playerIndex: number, - opponentIndex: number, + opponentIndices: number[], ): RatioType { const playerTrades = _.get(conversionsByPlayerByOpening, [playerIndex, "trade"]) || []; - const opponentTrades = _.get(conversionsByPlayerByOpening, [opponentIndex, "trade"]) || []; + const opponentTrades = _.flatten( + opponentIndices.map((opponentIndex) => _.get(conversionsByPlayerByOpening, [opponentIndex, "trade"]) || []), + ); const benefitsPlayer = []; @@ -115,13 +139,15 @@ function getBeneficialTradeRatio( zippedTrades.forEach((conversionPair) => { const playerConversion = _.first(conversionPair); const opponentConversion = _.last(conversionPair); - const playerDamage = playerConversion!.currentPercent - playerConversion!.startPercent; - const opponentDamage = opponentConversion!.currentPercent - opponentConversion!.startPercent; - - if (playerConversion!.didKill && !opponentConversion!.didKill) { - benefitsPlayer.push(playerConversion); - } else if (playerDamage > opponentDamage) { - benefitsPlayer.push(playerConversion); + if (playerConversion && opponentConversion) { + const playerDamage = playerConversion.currentPercent - playerConversion.startPercent; + const opponentDamage = opponentConversion.currentPercent - opponentConversion.startPercent; + + if (playerConversion!.didKill && !opponentConversion!.didKill) { + benefitsPlayer.push(playerConversion); + } else if (playerDamage > opponentDamage) { + benefitsPlayer.push(playerConversion); + } } }); diff --git a/src/stats/stocks.ts b/src/stats/stocks.ts index 95e775c3..006fc37d 100644 --- a/src/stats/stocks.ts +++ b/src/stats/stocks.ts @@ -1,7 +1,7 @@ import _ from "lodash"; import { FrameEntryType, FramesType, GameStartType } from "../types"; -import { didLoseStock, getSinglesPlayerPermutationsFromSettings, isDead, PlayerIndexedType, StockType } from "./common"; +import { didLoseStock, isDead, StockType } from "./common"; import { StatComputer } from "./stats"; interface StockState { @@ -9,29 +9,29 @@ interface StockState { } export class StockComputer implements StatComputer { - private state = new Map(); - private playerPermutations = new Array(); - private stocks = new Array(); + private state = new Map(); + private playerIndices: number[] = []; + private stocks: StockType[] = []; public setup(settings: GameStartType): void { - // Reset state + // Reset the state since it's a new game this.state = new Map(); - this.playerPermutations = getSinglesPlayerPermutationsFromSettings(settings); + this.playerIndices = settings.players.map((p) => p.playerIndex); this.stocks = []; - this.playerPermutations.forEach((indices) => { + this.playerIndices.forEach((index) => { const playerState: StockState = { stock: null, }; - this.state.set(indices, playerState); + this.state.set(index, playerState); }); } public processFrame(frame: FrameEntryType, allFrames: FramesType): void { - this.playerPermutations.forEach((indices) => { - const state = this.state.get(indices); + this.playerIndices.forEach((index) => { + const state = this.state.get(index); if (state) { - handleStockCompute(allFrames, state, indices, frame, this.stocks); + handleStockCompute(allFrames, state, index, frame, this.stocks); } }); } @@ -44,14 +44,14 @@ export class StockComputer implements StatComputer { function handleStockCompute( frames: FramesType, state: StockState, - indices: PlayerIndexedType, + playerIndex: number, frame: FrameEntryType, stocks: StockType[], ): void { - const playerFrame = frame.players[indices.playerIndex]!.post; + const playerFrame = frame.players[playerIndex]!.post; const currentFrameNumber = playerFrame.frame!; const prevFrameNumber = currentFrameNumber - 1; - const prevPlayerFrame = frames[prevFrameNumber] ? frames[prevFrameNumber].players[indices.playerIndex]!.post : null; + const prevPlayerFrame = frames[prevFrameNumber] ? frames[prevFrameNumber].players[playerIndex]!.post : null; // If there is currently no active stock, wait until the player is no longer spawning. // Once the player is no longer spawning, start the stock @@ -62,8 +62,7 @@ function handleStockCompute( } state.stock = { - playerIndex: indices.playerIndex, - opponentIndex: indices.opponentIndex, + playerIndex, startFrame: currentFrameNumber, endFrame: null, startPercent: 0, diff --git a/test/conversion.spec.ts b/test/conversion.spec.ts index 893b2d76..b3769a67 100644 --- a/test/conversion.spec.ts +++ b/test/conversion.spec.ts @@ -7,7 +7,7 @@ describe("when calculating conversions", () => { const puff = stats.overall[0]; let totalDamagePuffDealt = 0; stats.conversions.forEach((conversion) => { - if (conversion.playerIndex === puff.playerIndex) { + if (conversion.lastHitBy === puff.playerIndex) { totalDamagePuffDealt += conversion.moves.reduce((total, move) => total + move.damage, 0); } }); @@ -22,7 +22,7 @@ describe("when calculating conversions", () => { const bowser = stats.overall[0]; let totalDamageBowserDealt = 0; stats.conversions.forEach((conversion) => { - if (conversion.playerIndex === bowser.playerIndex) { + if (conversion.lastHitBy === bowser.playerIndex) { totalDamageBowserDealt += conversion.moves.reduce((total, move) => total + move.damage, 0); } }); @@ -39,7 +39,7 @@ describe("when calculating conversions", () => { const falcon = stats.overall[0]; let totalDamageFalconDealt = 0; stats.conversions.forEach((conversion) => { - if (conversion.playerIndex === falcon.playerIndex) { + if (conversion.lastHitBy === falcon.playerIndex) { totalDamageFalconDealt += conversion.moves.reduce((total, move) => total + move.damage, 0); } }); @@ -54,7 +54,7 @@ describe("when calculating conversions", () => { const ganon = stats.overall[0]; let totalDamageGanonDealt = 0; stats.conversions.forEach((conversion) => { - if (conversion.playerIndex === ganon.playerIndex) { + if (conversion.lastHitBy === ganon.playerIndex) { totalDamageGanonDealt += conversion.moves.reduce((total, move) => total + move.damage, 0); } }); @@ -69,7 +69,7 @@ describe("when calculating conversions", () => { const kirby = stats.overall[0]; let totalDamageKirbyDealt = 0; stats.conversions.forEach((conversion) => { - if (conversion.playerIndex === kirby.playerIndex) { + if (conversion.lastHitBy === kirby.playerIndex) { totalDamageKirbyDealt += conversion.moves.reduce((total, move) => total + move.damage, 0); } }); @@ -84,7 +84,7 @@ describe("when calculating conversions", () => { const yoshi = stats.overall[0]; let totalDamageYoshiDealt = 0; stats.conversions.forEach((conversion) => { - if (conversion.playerIndex === yoshi.playerIndex) { + if (conversion.lastHitBy === yoshi.playerIndex) { totalDamageYoshiDealt += conversion.moves.reduce((total, move) => total + move.damage, 0); } }); @@ -99,7 +99,7 @@ describe("when calculating conversions", () => { const mewTwo = stats.overall[0]; let totalDamageMewTwoDealt = 0; stats.conversions.forEach((conversion) => { - if (conversion.playerIndex === mewTwo.playerIndex) { + if (conversion.lastHitBy === mewTwo.playerIndex) { totalDamageMewTwoDealt += conversion.moves.reduce((total, move) => total + move.damage, 0); } }); diff --git a/test/stats.spec.ts b/test/stats.spec.ts index a46a09f9..2bd92951 100644 --- a/test/stats.spec.ts +++ b/test/stats.spec.ts @@ -67,7 +67,7 @@ describe("when calculating stats", () => { const yl = stats.overall[1]; let totalDamagePuffDealt = 0; stats.conversions.forEach((conversion) => { - if (conversion.playerIndex === puff.playerIndex) { + if (conversion.lastHitBy === puff.playerIndex) { totalDamagePuffDealt += conversion.moves.reduce((total, move) => total + move.damage, 0); } }); @@ -87,15 +87,11 @@ describe("when calculating stats", () => { let totalDamagePichuDealt = 0; let icsDamageDealt = 0; stats.conversions.forEach((conversion) => { - switch (conversion.playerIndex) { - case pichu.playerIndex: { - totalDamagePichuDealt += conversion.moves.reduce((total, move) => total + move.damage, 0); - break; - } - case ics.playerIndex: { - icsDamageDealt += conversion.moves.reduce((total, move) => total + move.damage, 0); - break; - } + if (conversion.playerIndex === pichu.playerIndex) { + icsDamageDealt += conversion.moves.reduce((total, move) => total + move.damage, 0); + } + if (conversion.playerIndex === ics.playerIndex) { + totalDamagePichuDealt += conversion.moves.reduce((total, move) => total + move.damage, 0); } }); // Pichu should have done at least 32% damage @@ -117,10 +113,10 @@ describe("when calculating stats", () => { let totalDamageNessDealt = 0; let totalDamageFoxDealt = 0; stats.conversions.forEach((conversion) => { - if (conversion.playerIndex === ness.playerIndex) { + if (conversion.lastHitBy === ness.playerIndex) { totalDamageNessDealt += conversion.moves.reduce((total, move) => total + move.damage, 0); } - if (conversion.playerIndex === fox.playerIndex) { + if (conversion.lastHitBy === fox.playerIndex) { totalDamageFoxDealt += conversion.moves.reduce((total, move) => total + move.damage, 0); } }); From cebeebac705595ad2d02dc027fcfd192b3d29d5c Mon Sep 17 00:00:00 2001 From: Vince Au Date: Sun, 23 May 2021 23:35:33 +1000 Subject: [PATCH 2/2] fix: duplicate identifier error --- src/stats/common.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/stats/common.ts b/src/stats/common.ts index 34d7deb9..0c53cdc2 100644 --- a/src/stats/common.ts +++ b/src/stats/common.ts @@ -42,7 +42,6 @@ export interface MoveLandedType { moveId: number; hitCount: number; damage: number; - playerIndex: number; } export interface ComboType extends DurationType, DamageType {