From 5c55aa8cc9919085f6266dcd0114d55823fc699c Mon Sep 17 00:00:00 2001 From: Vince Au Date: Mon, 10 May 2021 12:14:43 +1000 Subject: [PATCH] Revert "Doubles Stats (#54)" This reverts commit 3de9791b4d1bf5497f085da353366c65daf4d221. --- src/SlippiGame.ts | 4 +- 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 | 101 ++++++++++++------------------------ src/stats/stocks.ts | 31 ++++++------ test/conversion.spec.ts | 14 ++--- test/stats.spec.ts | 20 +++++--- 10 files changed, 235 insertions(+), 256 deletions(-) diff --git a/src/SlippiGame.ts b/src/SlippiGame.ts index 83e8791e..82437791 100644 --- a/src/SlippiGame.ts +++ b/src/SlippiGame.ts @@ -5,6 +5,7 @@ import { ComboComputer, ConversionComputer, generateOverallStats, + getSinglesPlayerPermutationsFromSettings, InputComputer, StatOptions, Stats, @@ -130,8 +131,9 @@ export class SlippiGame { const inputs = this.inputComputer.fetch(); const stocks = this.stockComputer.fetch(); const conversions = this.conversionComputer.fetch(); + const indices = getSinglesPlayerPermutationsFromSettings(settings); const playableFrames = this.parser.getPlayableFrameCount(); - const overall = generateOverallStats(settings, inputs, stocks, conversions, playableFrames); + const overall = generateOverallStats(indices, inputs, stocks, conversions, playableFrames); const stats = { lastFrame: this.parser.getLatestFrameNumber(), diff --git a/src/stats/actions.ts b/src/stats/actions.ts index 4d569fc1..35ded9a5 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, State } from "./common"; +import { ActionCountsType, getSinglesPlayerPermutationsFromSettings, PlayerIndexedType, State } from "./common"; import { StatComputer } from "./stats"; // Frame pattern that indicates a dash dance turn was executed @@ -13,17 +13,16 @@ interface PlayerActionState { } export class ActionsComputer implements StatComputer { - private playerIndices: number[] = []; - private state = new Map(); + private playerPermutations = new Array(); + private state = new Map(); public setup(settings: GameStartType): void { - // Reset the state this.state = new Map(); - - this.playerIndices = settings.players.map((p) => p.playerIndex); - this.playerIndices.forEach((playerIndex) => { + this.playerPermutations = getSinglesPlayerPermutationsFromSettings(settings); + this.playerPermutations.forEach((indices) => { const playerCounts: ActionCountsType = { - playerIndex, + playerIndex: indices.playerIndex, + opponentIndex: indices.opponentIndex, wavedashCount: 0, wavelandCount: 0, airDodgeCount: 0, @@ -50,15 +49,15 @@ export class ActionsComputer implements StatComputer { playerCounts: playerCounts, animations: [], }; - this.state.set(playerIndex, playerState); + this.state.set(indices, playerState); }); } public processFrame(frame: FrameEntryType): void { - this.playerIndices.forEach((index) => { - const state = this.state.get(index); + this.playerPermutations.forEach((indices) => { + const state = this.state.get(indices); if (state) { - handleActionCompute(state, index, frame); + handleActionCompute(state, indices, frame); } }); } @@ -123,8 +122,8 @@ function didStartLedgegrab(currentAnimation: State, previousAnimation: State): b return isCurrentlyGrabbingLedge && !wasPreviouslyGrabbingLedge; } -function handleActionCompute(state: PlayerActionState, playerIndex: number, frame: FrameEntryType): void { - const playerFrame = frame.players[playerIndex]!.post; +function handleActionCompute(state: PlayerActionState, indices: PlayerIndexedType, frame: FrameEntryType): void { + const playerFrame = frame.players[indices.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 bc869818..af1bcbff 100644 --- a/src/stats/combos.ts +++ b/src/stats/combos.ts @@ -1,11 +1,12 @@ import { EventEmitter } from "events"; -import { last } from "lodash"; +import _ from "lodash"; import { FrameEntryType, FramesType, GameStartType, PostFrameUpdateType } from "../types"; import { calcDamageTaken, ComboType, didLoseStock, + getSinglesPlayerPermutationsFromSettings, isCommandGrabbed, isDamaged, isDead, @@ -13,6 +14,7 @@ import { isGrabbed, isTeching, MoveLandedType, + PlayerIndexedType, Timers, } from "./common"; import { StatComputer } from "./stats"; @@ -32,9 +34,9 @@ interface ComboState { } export class ComboComputer extends EventEmitter implements StatComputer { - private playerIndices: number[] = []; - private combos: ComboType[] = []; - private state = new Map(); + private playerPermutations = new Array(); + private state = new Map(); + private combos = new Array(); private settings: GameStartType | null = null; public setup(settings: GameStartType): void { @@ -42,9 +44,9 @@ export class ComboComputer extends EventEmitter implements StatComputer p.playerIndex); - this.playerIndices.forEach((indices) => { + this.playerPermutations.forEach((indices) => { const playerState: ComboState = { combo: null, move: null, @@ -57,15 +59,15 @@ export class ComboComputer extends EventEmitter implements StatComputer { - const state = this.state.get(index); + this.playerPermutations.forEach((indices) => { + const state = this.state.get(indices); if (state) { - handleComboCompute(allFrames, state, index, frame, this.combos); + handleComboCompute(allFrames, state, indices, 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; @@ -82,20 +84,29 @@ 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 (opntDamageTaken) { // 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: frame.players[lastHitBy]!.post!.lastAttackLanded!, + moveId: playerFrame.lastAttackLanded!, hitCount: 0, damage: 0, - playerIndex: lastHitBy, }; state.combo.moves.push(state.move); @@ -167,7 +165,7 @@ function handleComboCompute( if (state.move) { state.move.hitCount += 1; - state.move.damage += playerDamageTaken; + state.move.damage += opntDamageTaken; } // Store previous frame animation to consider the case of a trade, the previous @@ -186,25 +184,18 @@ function handleComboCompute( return; } - const playerIsTeching = isTeching(playerActionStateId); - const playerIsDowned = isDown(playerActionStateId); - const playerDidLoseStock = prevPlayerFrame && didLoseStock(playerFrame, prevPlayerFrame); - const playerIsDying = isDead(playerActionStateId); + const opntIsTeching = isTeching(oppActionStateId); + const opntIsDowned = isDown(oppActionStateId); + const opntDidLoseStock = prevOpponentFrame && didLoseStock(opponentFrame, prevOpponentFrame); + const opntIsDying = isDead(oppActionStateId); - // Update percent if the player didn't lose stock - if (!playerDidLoseStock) { - state.combo.currentPercent = playerFrame.percent ?? 0; + // Update percent if opponent didn't lose stock + if (!opntDidLoseStock) { + state.combo.currentPercent = opponentFrame.percent ?? 0; } - if ( - playerIsDamaged || - playerIsGrabbed || - playerIsCommandGrabbed || - playerIsTeching || - playerIsDowned || - playerIsDying - ) { - // If the player got grabbed or damaged, reset the reset counter + if (opntIsDamaged || opntIsGrabbed || opntIsCommandGrabbed || opntIsTeching || opntIsDowned || opntIsDying) { + // If opponent got grabbed or damaged, reset the reset counter state.resetCounter = 0; } else { state.resetCounter += 1; @@ -212,8 +203,8 @@ function handleComboCompute( let shouldTerminate = false; - // Termination condition 1 - player was killed - if (playerDidLoseStock) { + // Termination condition 1 - player kills opponent + if (opntDidLoseStock) { state.combo.didKill = true; shouldTerminate = true; } @@ -226,7 +217,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 = prevPlayerFrame ? prevPlayerFrame.percent ?? 0 : 0; + state.combo.endPercent = prevOpponentFrame ? prevOpponentFrame.percent ?? 0 : 0; state.event = ComboEvent.COMBO_END; state.combo = null; diff --git a/src/stats/common.ts b/src/stats/common.ts index 9d530d9d..71c2eb9c 100644 --- a/src/stats/common.ts +++ b/src/stats/common.ts @@ -1,6 +1,6 @@ import _ from "lodash"; -import { PostFrameUpdateType } from "../types"; +import { GameStartType, PostFrameUpdateType } from "../types"; export interface StatsType { gameComplete: boolean; @@ -19,6 +19,11 @@ export interface RatioType { ratio: number | null; } +export interface PlayerIndexedType { + playerIndex: number; + opponentIndex: number; +} + export interface DurationType { startFrame: number; endFrame?: number | null; @@ -30,8 +35,7 @@ export interface DamageType { endPercent?: number | null; } -export interface StockType extends DurationType, DamageType { - playerIndex: number; +export interface StockType extends PlayerIndexedType, DurationType, DamageType { count: number; deathAnimation?: number | null; } @@ -41,22 +45,20 @@ export interface MoveLandedType { moveId: number; hitCount: number; damage: number; - playerIndex: number; } -export interface ComboType extends DurationType, DamageType { - playerIndex: number; +export interface ConversionType extends PlayerIndexedType, DurationType, DamageType { moves: MoveLandedType[]; + openingType: string; didKill: boolean; - lastHitBy: number | null; } -export interface ConversionType extends ComboType { - openingType: string; +export interface ComboType extends PlayerIndexedType, DurationType, DamageType { + moves: MoveLandedType[]; + didKill: boolean; } -export interface ActionCountsType { - playerIndex: number; +export interface ActionCountsType extends PlayerIndexedType { wavedashCount: number; wavelandCount: number; airDodgeCount: number; @@ -88,8 +90,7 @@ export interface InputCountsType { total: number; } -export interface OverallType { - playerIndex: number; +export interface OverallType extends PlayerIndexedType { inputCounts: InputCountsType; conversionCount: number; totalDamage: number; @@ -175,19 +176,30 @@ export const Timers = { COMBO_STRING_RESET_FRAMES: 45, }; -export function didLoseStock( - frame: PostFrameUpdateType | undefined, - prevFrame: PostFrameUpdateType | undefined, -): boolean { - if (!frame || !prevFrame) { - return false; +export function getSinglesPlayerPermutationsFromSettings(settings: GameStartType): PlayerIndexedType[] { + if (!settings || settings.players.length !== 2) { + // Only return opponent indices for singles + return []; } - if (prevFrame.stocksRemaining === null || frame.stocksRemaining === null) { + 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) { 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 eab99811..c031a54a 100644 --- a/src/stats/conversions.ts +++ b/src/stats/conversions.ts @@ -6,11 +6,13 @@ import { calcDamageTaken, ConversionType, didLoseStock, + getSinglesPlayerPermutationsFromSettings, isCommandGrabbed, isDamaged, isGrabbed, isInControl, MoveLandedType, + PlayerIndexedType, Timers, } from "./common"; import { StatComputer } from "./stats"; @@ -29,9 +31,9 @@ interface MetadataType { } export class ConversionComputer extends EventEmitter implements StatComputer { - private playerIndices: number[] = []; - private conversions: ConversionType[] = []; - private state = new Map(); + private playerPermutations = new Array(); + private conversions = new Array(); + private state = new Map(); private metadata: MetadataType; private settings: GameStartType | null = null; @@ -43,31 +45,31 @@ export class ConversionComputer extends EventEmitter implements StatComputer p.playerIndex); + // Reset the state + this.playerPermutations = getSinglesPlayerPermutationsFromSettings(settings); this.conversions = []; - this.state = new Map(); + this.state = new Map(); this.metadata = { lastEndFrameByOppIdx: {}, }; + this.settings = settings; - this.playerIndices.forEach((index) => { + this.playerPermutations.forEach((indices) => { const playerState: PlayerConversionState = { conversion: null, move: null, resetCounter: 0, lastHitAnimation: null, }; - this.state.set(index, playerState); + this.state.set(indices, playerState); }); } public processFrame(frame: FrameEntryType, allFrames: FramesType): void { - this.playerIndices.forEach((index) => { - const state = this.state.get(index); + this.playerPermutations.forEach((indices) => { + const state = this.state.get(indices); if (state) { - const terminated = handleConversionCompute(allFrames, state, index, frame, this.conversions); + const terminated = handleConversionCompute(allFrames, state, indices, frame, this.conversions); if (terminated) { this.emit("CONVERSION", { combo: _.last(this.conversions), @@ -107,12 +109,10 @@ export class ConversionComputer extends EventEmitter implements StatComputer conversion.startFrame; + + // If not trade, check the opponent endFrame + const oppEndFrame = this.metadata.lastEndFrameByOppIdx[conversion.opponentIndex]; + const isCounterAttack = oppEndFrame && oppEndFrame > conversion.startFrame; conversion.openingType = isCounterAttack ? "counter-attack" : "neutral-win"; }); }); @@ -122,20 +122,29 @@ 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 (opntDamageTaken) { // 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: frame.players[lastHitBy]!.post!.lastAttackLanded!, + moveId: playerFrame.lastAttackLanded!, hitCount: 0, damage: 0, - playerIndex: lastHitBy, }; state.conversion.moves.push(state.move); @@ -198,7 +195,7 @@ function handleConversionCompute( if (state.move) { state.move.hitCount += 1; - state.move.damage += playerDamageTaken; + state.move.damage += opntDamageTaken; } // Store previous frame animation to consider the case of a trade, the previous @@ -213,32 +210,32 @@ function handleConversionCompute( return false; } - const playerInControl = isInControl(playerActionStateId); - const playerDidLoseStock = prevPlayerFrame && didLoseStock(playerFrame, prevPlayerFrame); + const opntInControl = isInControl(oppActionStateId); + const opntDidLoseStock = prevOpponentFrame && didLoseStock(opponentFrame, prevOpponentFrame); - // Update percent if the player didn't lose stock - if (!playerDidLoseStock) { - state.conversion.currentPercent = playerFrame.percent ?? 0; + // Update percent if opponent didn't lose stock + if (!opntDidLoseStock) { + state.conversion.currentPercent = opponentFrame.percent ?? 0; } - if (playerIsDamaged || playerIsGrabbed || playerIsCommandGrabbed) { - // If the player got grabbed or damaged, reset the reset counter + if (opntIsDamaged || opntIsGrabbed || opntIsCommandGrabbed) { + // If opponent got grabbed or damaged, reset the reset counter state.resetCounter = 0; } - const shouldStartResetCounter = state.resetCounter === 0 && playerInControl; + const shouldStartResetCounter = state.resetCounter === 0 && opntInControl; const shouldContinueResetCounter = state.resetCounter > 0; if (shouldStartResetCounter || shouldContinueResetCounter) { // This will increment the reset timer under the following conditions: - // 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 + // 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 state.resetCounter += 1; } let shouldTerminate = false; - // Termination condition 1 - player was killed - if (playerDidLoseStock) { + // Termination condition 1 - player kills opponent + if (opntDidLoseStock) { state.conversion.didKill = true; shouldTerminate = true; } @@ -251,7 +248,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 = prevPlayerFrame ? prevPlayerFrame.percent ?? 0 : 0; + state.conversion.endPercent = prevOpponentFrame ? prevOpponentFrame.percent ?? 0 : 0; state.conversion = null; state.move = null; diff --git a/src/stats/inputs.ts b/src/stats/inputs.ts index d6b411cf..8783497d 100644 --- a/src/stats/inputs.ts +++ b/src/stats/inputs.ts @@ -1,6 +1,7 @@ import _ from "lodash"; import { FrameEntryType, Frames, FramesType, GameStartType } from "../types"; +import { getSinglesPlayerPermutationsFromSettings, PlayerIndexedType } from "./common"; import { StatComputer } from "./stats"; enum JoystickRegion { @@ -17,6 +18,7 @@ enum JoystickRegion { export interface PlayerInput { playerIndex: number; + opponentIndex: number; inputCount: number; joystickInputCount: number; cstickInputCount: number; @@ -25,32 +27,33 @@ export interface PlayerInput { } export class InputComputer implements StatComputer { - private playerIndices: number[] = []; - private state = new Map(); + private state = new Map(); + private playerPermutations = new Array(); public setup(settings: GameStartType): void { - // Reset the state since it's a new game - this.playerIndices = settings.players.map((p) => p.playerIndex); + // Reset the state this.state = new Map(); + this.playerPermutations = getSinglesPlayerPermutationsFromSettings(settings); - this.playerIndices.forEach((index) => { + this.playerPermutations.forEach((indices) => { const playerState: PlayerInput = { - playerIndex: index, + playerIndex: indices.playerIndex, + opponentIndex: indices.opponentIndex, inputCount: 0, joystickInputCount: 0, cstickInputCount: 0, buttonInputCount: 0, triggerInputCount: 0, }; - this.state.set(index, playerState); + this.state.set(indices, playerState); }); } public processFrame(frame: FrameEntryType, allFrames: FramesType): void { - this.playerIndices.forEach((index) => { - const state = this.state.get(index); + this.playerPermutations.forEach((indices) => { + const state = this.state.get(indices); if (state) { - handleInputCompute(allFrames, state, index, frame); + handleInputCompute(allFrames, state, indices, frame); } }); } @@ -60,11 +63,16 @@ export class InputComputer implements StatComputer { } } -function handleInputCompute(frames: FramesType, state: PlayerInput, playerIndex: number, frame: FrameEntryType): void { - const playerFrame = frame.players[playerIndex]!.pre; +function handleInputCompute( + frames: FramesType, + state: PlayerInput, + indices: PlayerIndexedType, + frame: FrameEntryType, +): void { + const playerFrame = frame.players[indices.playerIndex]!.pre; const currentFrameNumber = playerFrame.frame!; const prevFrameNumber = currentFrameNumber - 1; - const prevPlayerFrame = frames[prevFrameNumber] ? frames[prevFrameNumber].players[playerIndex]!.pre : null; + const prevPlayerFrame = frames[prevFrameNumber] ? frames[prevFrameNumber].players[indices.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 78819d98..744caded 100644 --- a/src/stats/overall.ts +++ b/src/stats/overall.ts @@ -1,7 +1,6 @@ import _ from "lodash"; -import { GameStartType } from "../types"; -import { ConversionType, InputCountsType, OverallType, RatioType, StockType } from "./common"; +import { ConversionType, InputCountsType, OverallType, PlayerIndexedType, RatioType, StockType } from "./common"; import { PlayerInput } from "./inputs"; interface ConversionsByPlayerByOpening { @@ -11,24 +10,24 @@ interface ConversionsByPlayerByOpening { } export function generateOverallStats( - settings: GameStartType, + playerIndices: PlayerIndexedType[], inputs: PlayerInput[], stocks: StockType[], conversions: ConversionType[], playableFrameCount: number, ): OverallType[] { const inputsByPlayer = _.keyBy(inputs, "playerIndex"); - const originalConversions = conversions; - const conversionsByPlayer = _.groupBy(conversions, (conv) => conv.moves[0]?.playerIndex); + const stocksByPlayer = _.groupBy(stocks, "playerIndex"); + const conversionsByPlayer = _.groupBy(conversions, "playerIndex"); const conversionsByPlayerByOpening: ConversionsByPlayerByOpening = _.mapValues(conversionsByPlayer, (conversions) => _.groupBy(conversions, "openingType"), ); const gameMinutes = playableFrameCount / 3600; - const overall = settings.players.map((player) => { - const playerIndex = player.playerIndex; - + const overall = playerIndices.map((indices) => { + const playerIndex = indices.playerIndex; + const opponentIndex = indices.opponentIndex; const playerInputs = _.get(inputsByPlayer, playerIndex) || {}; const inputCounts: InputCountsType = { buttons: _.get(playerInputs, "buttonInputCount"), @@ -37,49 +36,21 @@ 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); - 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; - } - }); - }); + + 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; return { playerIndex: playerIndex, + opponentIndex: opponentIndex, inputCounts: inputCounts, conversionCount: conversionCount, totalDamage: totalDamage, @@ -90,9 +61,9 @@ export function generateOverallStats( digitalInputsPerMinute: getRatio(inputCounts.buttons, gameMinutes), openingsPerKill: getRatio(conversionCount, killCount), damagePerOpening: getRatio(totalDamage, conversionCount), - neutralWinRatio: getOpeningRatio(conversionsByPlayerByOpening, playerIndex, opponentIndices, "neutral-win"), - counterHitRatio: getOpeningRatio(conversionsByPlayerByOpening, playerIndex, opponentIndices, "counter-attack"), - beneficialTradeRatio: getBeneficialTradeRatio(conversionsByPlayerByOpening, playerIndex, opponentIndices), + neutralWinRatio: getOpeningRatio(conversionsByPlayerByOpening, playerIndex, opponentIndex, "neutral-win"), + counterHitRatio: getOpeningRatio(conversionsByPlayerByOpening, playerIndex, opponentIndex, "counter-attack"), + beneficialTradeRatio: getBeneficialTradeRatio(conversionsByPlayerByOpening, playerIndex, opponentIndex), }; }); @@ -110,14 +81,12 @@ function getRatio(count: number, total: number): RatioType { function getOpeningRatio( conversionsByPlayerByOpening: ConversionsByPlayerByOpening, playerIndex: number, - opponentIndices: number[], + opponentIndex: number, type: string, ): RatioType { const openings = _.get(conversionsByPlayerByOpening, [playerIndex, type]) || []; - const opponentOpenings = _.flatten( - opponentIndices.map((opponentIndex) => _.get(conversionsByPlayerByOpening, [opponentIndex, type]) || []), - ); + const opponentOpenings = _.get(conversionsByPlayerByOpening, [opponentIndex, type]) || []; return getRatio(openings.length, openings.length + opponentOpenings.length); } @@ -125,12 +94,10 @@ function getOpeningRatio( function getBeneficialTradeRatio( conversionsByPlayerByOpening: ConversionsByPlayerByOpening, playerIndex: number, - opponentIndices: number[], + opponentIndex: number, ): RatioType { const playerTrades = _.get(conversionsByPlayerByOpening, [playerIndex, "trade"]) || []; - const opponentTrades = _.flatten( - opponentIndices.map((opponentIndex) => _.get(conversionsByPlayerByOpening, [opponentIndex, "trade"]) || []), - ); + const opponentTrades = _.get(conversionsByPlayerByOpening, [opponentIndex, "trade"]) || []; const benefitsPlayer = []; @@ -139,15 +106,13 @@ function getBeneficialTradeRatio( zippedTrades.forEach((conversionPair) => { const playerConversion = _.first(conversionPair); const opponentConversion = _.last(conversionPair); - 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); - } + 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 006fc37d..95e775c3 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, isDead, StockType } from "./common"; +import { didLoseStock, getSinglesPlayerPermutationsFromSettings, isDead, PlayerIndexedType, 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 playerIndices: number[] = []; - private stocks: StockType[] = []; + private state = new Map(); + private playerPermutations = new Array(); + private stocks = new Array(); public setup(settings: GameStartType): void { - // Reset the state since it's a new game + // Reset state this.state = new Map(); - this.playerIndices = settings.players.map((p) => p.playerIndex); + this.playerPermutations = getSinglesPlayerPermutationsFromSettings(settings); this.stocks = []; - this.playerIndices.forEach((index) => { + this.playerPermutations.forEach((indices) => { const playerState: StockState = { stock: null, }; - this.state.set(index, playerState); + this.state.set(indices, playerState); }); } public processFrame(frame: FrameEntryType, allFrames: FramesType): void { - this.playerIndices.forEach((index) => { - const state = this.state.get(index); + this.playerPermutations.forEach((indices) => { + const state = this.state.get(indices); if (state) { - handleStockCompute(allFrames, state, index, frame, this.stocks); + handleStockCompute(allFrames, state, indices, frame, this.stocks); } }); } @@ -44,14 +44,14 @@ export class StockComputer implements StatComputer { function handleStockCompute( frames: FramesType, state: StockState, - playerIndex: number, + indices: PlayerIndexedType, frame: FrameEntryType, stocks: StockType[], ): void { - const playerFrame = frame.players[playerIndex]!.post; + const playerFrame = frame.players[indices.playerIndex]!.post; const currentFrameNumber = playerFrame.frame!; const prevFrameNumber = currentFrameNumber - 1; - const prevPlayerFrame = frames[prevFrameNumber] ? frames[prevFrameNumber].players[playerIndex]!.post : null; + const prevPlayerFrame = frames[prevFrameNumber] ? frames[prevFrameNumber].players[indices.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,7 +62,8 @@ function handleStockCompute( } state.stock = { - playerIndex, + playerIndex: indices.playerIndex, + opponentIndex: indices.opponentIndex, startFrame: currentFrameNumber, endFrame: null, startPercent: 0, diff --git a/test/conversion.spec.ts b/test/conversion.spec.ts index b3769a67..893b2d76 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.lastHitBy === puff.playerIndex) { + if (conversion.playerIndex === 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.lastHitBy === bowser.playerIndex) { + if (conversion.playerIndex === 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.lastHitBy === falcon.playerIndex) { + if (conversion.playerIndex === 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.lastHitBy === ganon.playerIndex) { + if (conversion.playerIndex === 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.lastHitBy === kirby.playerIndex) { + if (conversion.playerIndex === 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.lastHitBy === yoshi.playerIndex) { + if (conversion.playerIndex === 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.lastHitBy === mewTwo.playerIndex) { + if (conversion.playerIndex === 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 2bd92951..a46a09f9 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.lastHitBy === puff.playerIndex) { + if (conversion.playerIndex === puff.playerIndex) { totalDamagePuffDealt += conversion.moves.reduce((total, move) => total + move.damage, 0); } }); @@ -87,11 +87,15 @@ describe("when calculating stats", () => { let totalDamagePichuDealt = 0; let icsDamageDealt = 0; stats.conversions.forEach((conversion) => { - 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); + 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; + } } }); // Pichu should have done at least 32% damage @@ -113,10 +117,10 @@ describe("when calculating stats", () => { let totalDamageNessDealt = 0; let totalDamageFoxDealt = 0; stats.conversions.forEach((conversion) => { - if (conversion.lastHitBy === ness.playerIndex) { + if (conversion.playerIndex === ness.playerIndex) { totalDamageNessDealt += conversion.moves.reduce((total, move) => total + move.damage, 0); } - if (conversion.lastHitBy === fox.playerIndex) { + if (conversion.playerIndex === fox.playerIndex) { totalDamageFoxDealt += conversion.moves.reduce((total, move) => total + move.damage, 0); } });