Skip to content

Commit

Permalink
Doubles Stats (#54)
Browse files Browse the repository at this point in the history
* Added tracking of grabs/throws, also can now reference player counts as an object

* Updated lint

* asdfasd

* output

* Doubles Output

* Added 'melee' and 'console' folder test cases

* Mid lint fix

* Add test files

* Added static stats comparison

* Remove NPM related changes

* Update combo to handle moveId of last hit

* Remove extra content from PR

* Remove more references to opponentIndex

* Rename opponentIndex to opponentIndices

* Update comments mentioning opponent

* Updated to merge multidimensional array of stats into a single, gamewide statistic

* Updated to merge multidimensional array of stats into a single, gamewide statistic

* Updated to exclude team members from stat analysis. Also added teamMembers to object

* Updated to exclude team members from stat analysis. Also added teamMembers to object

* refactor(stats): completely remove PlayerIndexedType

* style(eslint): allow object shorthand notation

* feat(stats): store lastHitBy in combos and conversions to track kills

* fix(stats): fix damage done and kill count calculation.

It's naive to think that we did all the damage and took all the kills in
a FFA/teams game. Fix the overall stats calculation accordingly.

* Got all the tests passing

* Got all the tests passing

* Got all the tests passing

* Got all the tests passing

* refactor(stats): handle nullable types

* test: cleanup tests

Co-authored-by: Vince Au <vinceau09@gmail.com>
  • Loading branch information
OGoodness and vinceau committed Apr 13, 2021
1 parent 57378da commit 3de9791
Show file tree
Hide file tree
Showing 12 changed files with 319 additions and 253 deletions.
1 change: 0 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ module.exports = {
varsIgnorePattern: "^_",
},
],
"object-shorthand": ["error", "never"],
"strict-booleans/no-nullable-numbers": "error",
},
};
14 changes: 8 additions & 6 deletions src/SlippiGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
InputComputer,
Stats,
StatsType,
getSinglesPlayerPermutationsFromSettings,
generateOverallStats,
StatOptions,
} from "./stats";
Expand Down Expand Up @@ -59,8 +58,7 @@ export class SlippiGame {
);
this.parser = new SlpParser();
this.parser.on(SlpParserEvent.SETTINGS, (settings) => {
const playerPermutations = getSinglesPlayerPermutationsFromSettings(settings);
this.statsComputer.setPlayerPermutations(playerPermutations);
this.statsComputer.setup(settings);
});
// Use finalized frames for stats computation
this.parser.on(SlpParserEvent.FINALIZED_FRAME, (frame: FrameEntryType) => {
Expand Down Expand Up @@ -115,21 +113,25 @@ export class SlippiGame {
return this.parser.getFrames();
}

public getStats(): StatsType {
public getStats(): StatsType | null {
if (this.finalStats) {
return this.finalStats;
}

this._process();

const settings = this.parser.getSettings();
if (settings === null) {
return null;
}

// Finish processing if we're not up to date
this.statsComputer.process();
const inputs = this.inputComputer.fetch();
const stocks = this.stockComputer.fetch();
const conversions = this.conversionComputer.fetch();
const indices = getSinglesPlayerPermutationsFromSettings(this.parser.getSettings()!);
const playableFrames = this.parser.getPlayableFrameCount();
const overall = generateOverallStats(indices, inputs, stocks, conversions, playableFrames);
const overall = generateOverallStats(settings, inputs, stocks, conversions, playableFrames);

const stats = {
lastFrame: this.parser.getLatestFrameNumber(),
Expand Down
32 changes: 17 additions & 15 deletions src/stats/actions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import _ from "lodash";
import { State, PlayerIndexedType, ActionCountsType } from "./common";
import { FrameEntryType } from "../types";
import { State, ActionCountsType } from "./common";
import { FrameEntryType, GameStartType } from "../types";
import { StatComputer } from "./stats";

// Frame pattern that indicates a dash dance turn was executed
Expand All @@ -12,15 +12,17 @@ interface PlayerActionState {
}

export class ActionsComputer implements StatComputer<ActionCountsType[]> {
private playerPermutations = new Array<PlayerIndexedType>();
private state = new Map<PlayerIndexedType, PlayerActionState>();
private playerIndices: number[] = [];
private state = new Map<number, PlayerActionState>();

public setPlayerPermutations(playerPermutations: PlayerIndexedType[]): void {
this.playerPermutations = playerPermutations;
this.playerPermutations.forEach((indices) => {
public setup(settings: GameStartType): void {
// Reset the state
this.state = new Map();

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,
Expand All @@ -35,15 +37,15 @@ export class ActionsComputer implements StatComputer<ActionCountsType[]> {
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);
}
});
}
Expand Down Expand Up @@ -101,8 +103,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;
Expand Down
105 changes: 61 additions & 44 deletions src/stats/combos.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import _ from "lodash";
import { FrameEntryType, FramesType, PostFrameUpdateType } from "../types";
import { MoveLandedType, ComboType, PlayerIndexedType } from "./common";
import { FrameEntryType, FramesType, PostFrameUpdateType, GameStartType } from "../types";
import { MoveLandedType, ComboType } from "./common";
import {
isDamaged,
isGrabbed,
Expand All @@ -22,13 +22,17 @@ interface ComboState {
}

export class ComboComputer implements StatComputer<ComboType[]> {
private playerPermutations = new Array<PlayerIndexedType>();
private state = new Map<PlayerIndexedType, ComboState>();
private combos = new Array<ComboType>();
private playerIndices: number[] = [];
private combos: ComboType[] = [];
private state = new Map<number, ComboState>();

public setPlayerPermutations(playerPermutations: PlayerIndexedType[]): void {
this.playerPermutations = playerPermutations;
this.playerPermutations.forEach((indices) => {
public setup(settings: GameStartType): void {
// Reset the state
this.state = new Map();
this.combos = [];

this.playerIndices = settings.players.map((p) => p.playerIndex);
this.playerIndices.forEach((indices) => {
const playerState: ComboState = {
combo: null,
move: null,
Expand All @@ -40,10 +44,10 @@ export class ComboComputer implements StatComputer<ComboType[]> {
}

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) {
handleComboCompute(allFrames, state, indices, frame, this.combos);
handleComboCompute(allFrames, state, index, frame, this.combos);
}
});
}
Expand All @@ -56,29 +60,20 @@ export class ComboComputer implements StatComputer<ComboType[]> {
function handleComboCompute(
frames: FramesType,
state: ComboState,
indices: PlayerIndexedType,
playerIndex: number,
frame: FrameEntryType,
combos: ComboType[],
): void {
const currentFrameNumber = frame.frame;
const playerFrame = frame.players[indices.playerIndex]!.post;
const opponentFrame = frame.players[indices.opponentIndex]!.post;
const playerFrame = frame.players[playerIndex]!.post;

const prevFrameNumber = currentFrameNumber - 1;
let prevPlayerFrame: PostFrameUpdateType | null = null;
let prevOpponentFrame: PostFrameUpdateType | null = null;

if (frames[prevFrameNumber]) {
prevPlayerFrame = frames[prevFrameNumber].players[indices.playerIndex]!.post;
prevOpponentFrame = frames[prevFrameNumber].players[indices.opponentIndex]!.post;
prevPlayerFrame = frames[prevFrameNumber].players[playerIndex]!.post;
}

const oppActionStateId = opponentFrame.actionStateId!;
const opntIsDamaged = isDamaged(oppActionStateId);
const opntIsGrabbed = isGrabbed(oppActionStateId);
const opntIsCommandGrabbed = isCommandGrabbed(oppActionStateId);
const opntDamageTaken = prevOpponentFrame ? calcDamageTaken(opponentFrame, prevOpponentFrame) : 0;

// Keep track of whether actionState changes after a hit. Used to compute move count
// When purely using action state there was a bug where if you did two of the same
// move really fast (such as ganon's jab), it would count as one move. Added
Expand All @@ -93,42 +88,57 @@ function handleComboCompute(
state.lastHitAnimation = null;
}

// If opponent took damage and was put in some kind of stun this frame, either
const playerActionStateId = playerFrame.actionStateId!;
const playerIsDamaged = isDamaged(playerActionStateId);
const playerIsGrabbed = isGrabbed(playerActionStateId);
const playerIsCommandGrabbed = isCommandGrabbed(playerActionStateId);

// If the player took damage and was put in some kind of stun this frame, either
// start a combo or count the moves for the existing combo
if (opntIsDamaged || opntIsGrabbed || opntIsCommandGrabbed) {
if (playerIsDamaged || playerIsGrabbed || playerIsCommandGrabbed) {
if (!state.combo) {
state.combo = {
playerIndex: indices.playerIndex,
opponentIndex: indices.opponentIndex,
playerIndex,
startFrame: currentFrameNumber,
endFrame: null,
startPercent: prevOpponentFrame ? prevOpponentFrame.percent ?? 0 : 0,
currentPercent: opponentFrame.percent ?? 0,
startPercent: prevPlayerFrame ? prevPlayerFrame.percent ?? 0 : 0,
currentPercent: playerFrame.percent ?? 0,
endPercent: null,
moves: [],
didKill: false,
lastHitBy: null,
};

combos.push(state.combo);
}

if (opntDamageTaken) {
const playerDamageTaken = prevPlayerFrame ? calcDamageTaken(playerFrame, prevPlayerFrame) : 0;
if (playerDamageTaken) {
// 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
let lastHitBy = playerFrame.lastHitBy ?? playerIndex;
if (playerFrame.lastHitBy === null || playerFrame.lastHitBy > 4) {
lastHitBy = playerIndex;
}

// Update who hit us last
state.combo.lastHitBy = lastHitBy;

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);
}

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
Expand All @@ -143,27 +153,34 @@ 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;
}

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;
}
Expand All @@ -176,7 +193,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.combo = null;
state.move = null;
Expand Down
Loading

0 comments on commit 3de9791

Please sign in to comment.