Skip to content

Commit

Permalink
[PR] Correctly Calculate Winners for Timeout (#123)
Browse files Browse the repository at this point in the history
* fix: correct winner now indicated on timeout

* chore: remove logger and ensure file is closed

* chore: add placements test for timeout winner

* chore: add type to variable

* dont use lodash functions
  • Loading branch information
JLaferri authored Mar 15, 2023
1 parent cd81779 commit c5d403d
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 10 deletions.
Binary file added slp/placementsTest/incorrect-winner-timeout.slp
Binary file not shown.
43 changes: 34 additions & 9 deletions src/SlippiGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,23 @@ import type {
GeckoListType,
MetadataType,
PlacementType,
PostFrameUpdateType,
RollbackFrames,
} from "./types";
import { GameMode } from "./types";
import { GameMode, GameEndMethod } from "./types";
import { getWinners } from "./utils/getWinners";
import { extractDistanceInfoFromFrame } from "./utils/homeRunDistance";
import { SlpParser, SlpParserEvent } from "./utils/slpParser";
import type { SlpReadInput } from "./utils/slpReader";
import { closeSlpFile, getGameEnd, getMetadata, iterateEvents, openSlpFile, SlpInputSource } from "./utils/slpReader";
import type { SlpFileType, SlpReadInput } from "./utils/slpReader";
import {
closeSlpFile,
getGameEnd,
getMetadata,
iterateEvents,
openSlpFile,
SlpInputSource,
extractFinalPostFrameUpdates,
} from "./utils/slpReader";

/**
* Slippi Game class that wraps a file
Expand Down Expand Up @@ -87,11 +96,11 @@ export class SlippiGame {
});
}

private _process(shouldStop: EventCallbackFunc = () => false): void {
private _process(shouldStop: EventCallbackFunc = () => false, file?: SlpFileType): void {
if (this.parser.getGameEnd() !== null) {
return;
}
const slpfile = openSlpFile(this.input);
const slpfile = file ?? openSlpFile(this.input);
// Generate settings from iterating through file
this.readPosition = iterateEvents(
slpfile,
Expand All @@ -106,7 +115,9 @@ export class SlippiGame {
},
this.readPosition,
);
closeSlpFile(slpfile);
if (!file) {
closeSlpFile(slpfile);
}
}

/**
Expand Down Expand Up @@ -260,11 +271,25 @@ export class SlippiGame {
}

public getWinners(): PlacementType[] {
const gameEnd = this.getGameEnd({ skipProcessing: true });
const settings = this.getSettings();
// Read game end block directly
const slpfile = openSlpFile(this.input);
const gameEnd = getGameEnd(slpfile);
this._process(() => this.parser.getSettings() !== null, slpfile);
const settings = this.parser.getSettings();
if (!gameEnd || !settings) {
// Technically using the final post frame updates, it should be possible to compute winners for
// replays without a gameEnd message. But I'll leave this here anyway
closeSlpFile(slpfile);
return [];
}
return getWinners(gameEnd, settings);

// If we went to time, let's fetch the post frame updates to compute the winner
let finalPostFrameUpdates: PostFrameUpdateType[] = [];
if (gameEnd.gameEndMethod === GameEndMethod.TIME) {
finalPostFrameUpdates = extractFinalPostFrameUpdates(slpfile);
}

closeSlpFile(slpfile);
return getWinners(gameEnd, settings, finalPostFrameUpdates);
}
}
29 changes: 28 additions & 1 deletion src/utils/getWinners.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { GameEndType, GameStartType, PlacementType } from "../types";
import type { GameEndType, GameStartType, PlacementType, PostFrameUpdateType } from "../types";
import { GameEndMethod } from "../types";
import { exists } from "./exists";

export function getWinners(
gameEnd: GameEndType,
settings: Pick<GameStartType, "players" | "isTeams">,
finalPostFrameUpdates: PostFrameUpdateType[],
): PlacementType[] {
const { placements, gameEndMethod, lrasInitiatorIndex } = gameEnd;
const { players, isTeams } = settings;
Expand All @@ -26,6 +27,32 @@ export function getWinners(
return [];
}

if (gameEndMethod === GameEndMethod.TIME && players.length === 2) {
const nonFollowerUpdates = finalPostFrameUpdates.filter((pfu) => !pfu.isFollower);
if (nonFollowerUpdates.length !== players.length) {
return [];
}

const p1 = nonFollowerUpdates[0]!;
const p2 = nonFollowerUpdates[1]!;
if (p1.stocksRemaining! > p2.stocksRemaining!) {
return [{ playerIndex: p1.playerIndex!, position: 0 }];
} else if (p2.stocksRemaining! > p1.stocksRemaining!) {
return [{ playerIndex: p2.playerIndex!, position: 0 }];
}

const p1Health = Math.trunc(p1.percent!);
const p2Health = Math.trunc(p2.percent!);
if (p1Health < p2Health) {
return [{ playerIndex: p1.playerIndex!, position: 0 }];
} else if (p2Health < p1Health) {
return [{ playerIndex: p2.playerIndex!, position: 0 }];
}

// If stocks and percents were tied, no winner
return [];
}

const firstPosition = placements.find((placement) => placement.position === 0);
if (!firstPosition) {
return [];
Expand Down
47 changes: 47 additions & 0 deletions src/utils/slpReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
MetadataType,
PlacementType,
PlayerType,
PostFrameUpdateType,
SelfInducedSpeedsType,
} from "../types";
import { Command } from "../types";
Expand Down Expand Up @@ -684,3 +685,49 @@ export function getGameEnd(slpFile: SlpFileType): GameEndType | null {

return gameEndMessage as GameEndType;
}

export function extractFinalPostFrameUpdates(slpFile: SlpFileType): PostFrameUpdateType[] {
const { ref, rawDataPosition, rawDataLength, messageSizes } = slpFile;

// The following should exist on all replay versions
const postFramePayloadSize = messageSizes[Command.POST_FRAME_UPDATE];
const gameEndPayloadSize = messageSizes[Command.GAME_END];
const frameBookendPayloadSize = messageSizes[Command.FRAME_BOOKEND];

// Technically this should not be possible
if (!exists(postFramePayloadSize)) {
return [];
}

const gameEndSize = gameEndPayloadSize ? gameEndPayloadSize + 1 : 0;
const postFrameSize = postFramePayloadSize + 1;
const frameBookendSize = frameBookendPayloadSize ? frameBookendPayloadSize + 1 : 0;

let frameNum: number | null = null;
let postFramePosition = rawDataPosition + rawDataLength - gameEndSize - frameBookendSize - postFrameSize;
const postFrameUpdates: PostFrameUpdateType[] = [];
do {
const buffer = new Uint8Array(postFrameSize);
readRef(ref, buffer, 0, buffer.length, postFramePosition);
if (buffer[0] !== Command.POST_FRAME_UPDATE) {
break;
}

const postFrameMessage = parseMessage(Command.POST_FRAME_UPDATE, buffer) as PostFrameUpdateType | null;
if (!postFrameMessage) {
break;
}

if (frameNum === null) {
frameNum = postFrameMessage.frame;
} else if (frameNum !== postFrameMessage.frame) {
// If post frame message is found but the frame doesn't match, it's not part of the final frame
break;
}

postFrameUpdates.unshift(postFrameMessage);
postFramePosition -= postFrameSize;
} while (postFramePosition >= rawDataPosition);

return postFrameUpdates;
}
8 changes: 8 additions & 0 deletions test/placings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,13 @@ describe("when determining placings", () => {
expect(settings.players[2].teamId).toBe(0); // Expect player 3 to be on team red
expect(settings.players[3].teamId).toBe(2); // Expect player 4 to be on team green
});

it("should return correct winners in timeout", () => {
const game = new SlippiGame("slp/placementsTest/incorrect-winner-timeout.slp");
const winners = game.getWinners();
expect(winners).toHaveLength(1);
expect(winners[0].playerIndex).toBe(0);
expect(winners[0].position).toBe(0);
});
});
});

0 comments on commit c5d403d

Please sign in to comment.