@slippi/slippi-js
Version:
Official Project Slippi Javascript SDK
202 lines (198 loc) • 9.65 kB
JavaScript
;
var types = require('../types.cjs');
var exists = require('./exists.cjs');
function getWinners(gameEnd, settings, finalPostFrameUpdates) {
var _a, _b;
const { placements, gameEndMethod, lrasInitiatorIndex } = gameEnd;
const { players, isTeams } = settings;
// ==========================================================================
// LRAS / Unresolved (NO_CONTEST or UNRESOLVED)
// ==========================================================================
// When a game ends via LRAS (Large Render Area Start) or is unresolved,
// the winner is simply the player who did NOT initiate the LRAS.
// Only applies to 2-player games.
if (gameEndMethod === types.GameEndMethod.NO_CONTEST || gameEndMethod === types.GameEndMethod.UNRESOLVED) {
if (exists.exists(lrasInitiatorIndex) && players.length === 2) {
const winnerIndex = (_a = players.find(({ playerIndex }) => playerIndex !== lrasInitiatorIndex)) === null || _a === void 0 ? void 0 : _a.playerIndex;
if (exists.exists(winnerIndex)) {
return [{ playerIndex: winnerIndex, position: 0 }];
}
}
return [];
}
// Filter out followers (Nana)
const activePlayers = finalPostFrameUpdates.filter((pfu) => !pfu.isFollower);
// ==========================================================================
// Double KO (all players have 0 stocks)
// ==========================================================================
// If everyone died at the same time (both players have 0 stocks remaining),
// there is no winner - this is a draw.
if (activePlayers.every((pfu) => pfu.stocksRemaining === 0)) {
return [];
}
// ==========================================================================
// Use last frame data to determine winners (fallback)
// ==========================================================================
// This path is used when:
// - No gameEnd block exists (gameEndMethod is undefined/null), OR
// - Game ended via TIME (timeout) with 2+ players
//
// Winner determination from last frame:
// - More stocks remaining = better (wins)
// - If tied on stocks, lower percent = better (wins)
// - If completely tied, no winner (draw)
//
// For Teams: The winning team is determined by total combined stocks/percent,
// then all players on that team are returned as winners.
// We should determine winners from last frame if:
// - No gameEndMethod exists (file lacks gameEnd block), OR
// - Game ended via TIME (timeout)
// - AND we have at least 2 players with frame data
const shouldDetermineFromLastFrame = (!gameEndMethod || gameEndMethod === types.GameEndMethod.TIME) && activePlayers.length >= 2;
if (shouldDetermineFromLastFrame) {
if (isTeams) {
return getTeamsWinners(players, activePlayers);
}
return getFFAWinners(activePlayers);
}
// ==========================================================================
// Use placements from gameEnd block
// ==========================================================================
// If the game has a valid gameEnd block with placements, use those.
// This is the preferred path when gameEnd data is available.
// - For FFA: Return the player(s) with position 0
// - For Teams: Return all players on the winning team
if (placements && placements.length > 0) {
const firstPlace = placements.find((p) => p.position === 0);
if (!firstPlace) {
return [];
}
// For teams, return all players on the winning team
if (isTeams) {
const winningTeamId = (_b = players.find((p) => p.playerIndex === firstPlace.playerIndex)) === null || _b === void 0 ? void 0 : _b.teamId;
if (exists.exists(winningTeamId)) {
return placements.filter((p) => {
var _a;
const playerTeamId = (_a = players.find((pl) => pl.playerIndex === p.playerIndex)) === null || _a === void 0 ? void 0 : _a.teamId;
return playerTeamId === winningTeamId;
});
}
}
return [firstPlace];
}
// ==========================================================================
// No determinable winner
// ==========================================================================
// Unable to determine winners from any available data
return [];
}
// ==========================================================================
// Helper Functions
// ==========================================================================
/**
* Determines winners for Free-For-All (non-teams) games using last frame data.
*
* Ranking algorithm:
* 1. Sort by stocks remaining (descending) - more stocks = higher rank
* 2. If tied on stocks, sort by percent (ascending) - lower percent = higher rank
*
* Winners are players that are tied for first place (same stocks AND same percent).
* Returns players sorted by playerIndex (port order).
*/
function getFFAWinners(activePlayers) {
// Sort all players by performance (stocks desc, percent asc)
const sortedByPerformance = sortByPerformance(activePlayers);
// Find the top player's stats to determine ties
const topStocks = sortedByPerformance[0].stocksRemaining;
const topPercent = sortedByPerformance[0].percent;
// Winners are anyone tied with the top player (same stocks AND same percent)
const winners = sortedByPerformance.filter((p) => p.stocksRemaining === topStocks && p.percent === topPercent);
// Return in playerIndex (port) order, all with position 0
return winners
.sort((a, b) => { var _a, _b; return ((_a = a.playerIndex) !== null && _a !== void 0 ? _a : 0) - ((_b = b.playerIndex) !== null && _b !== void 0 ? _b : 0); })
.map((p) => ({ playerIndex: p.playerIndex, position: 0 }));
}
/**
* Determines winners for Teams games using last frame data.
*
* Team ranking algorithm:
* 1. Sum stocks for each team - team with more stocks = higher rank
* 2. If tied on stocks, sum percent for each team - team with lower percent = higher rank
*
* Returns all players on the winning team, with positions based on individual
* performance within that team.
*/
function getTeamsWinners(gamePlayers, activePlayers) {
// Aggregate stats by team
const teamAggregates = aggregateTeams(gamePlayers, activePlayers);
if (teamAggregates.size === 0) {
return [];
}
// Sort teams by performance (stocks desc, percent asc)
const sortedTeams = [...teamAggregates.values()].sort((a, b) => {
const stocksDiff = b.totalStocks - a.totalStocks;
if (stocksDiff !== 0) {
return stocksDiff;
}
return a.totalPercent - b.totalPercent;
});
// Get all players on the winning team
const winningTeamPlayers = sortedTeams[0].players;
// Sort winning team players by individual performance to determine positions
const sortedByPerformance = sortByPerformance(winningTeamPlayers);
// Create a map from playerIndex to their position (rank) on the team
const positionMap = new Map(sortedByPerformance.map((p, idx) => [p.playerIndex, idx]));
// Return players in playerIndex (port) order, with their team position
return winningTeamPlayers
.sort((a, b) => { var _a, _b; return ((_a = a.playerIndex) !== null && _a !== void 0 ? _a : 0) - ((_b = b.playerIndex) !== null && _b !== void 0 ? _b : 0); })
.map((p) => {
var _a;
return ({
playerIndex: p.playerIndex,
position: (_a = positionMap.get(p.playerIndex)) !== null && _a !== void 0 ? _a : 0,
});
});
}
/**
* Aggregates player stats by team ID.
* Returns a Map where keys are team IDs and values are aggregate stats
* (totalStocks, totalPercent) plus the list of players on each team.
*/
function aggregateTeams(gamePlayers, activePlayers) {
var _a, _b, _c, _d;
const teamAggregates = new Map();
for (const pfu of activePlayers) {
const player = gamePlayers.find((p) => p.playerIndex === pfu.playerIndex);
if (!player) {
continue;
}
// Use -1 as default teamId for players without a team (shouldn't happen in teams mode)
const teamId = (_a = player.teamId) !== null && _a !== void 0 ? _a : -1;
const existing = (_b = teamAggregates.get(teamId)) !== null && _b !== void 0 ? _b : {
totalStocks: 0,
totalPercent: 0,
players: [],
};
teamAggregates.set(teamId, {
totalStocks: existing.totalStocks + ((_c = pfu.stocksRemaining) !== null && _c !== void 0 ? _c : 0),
totalPercent: existing.totalPercent + ((_d = pfu.percent) !== null && _d !== void 0 ? _d : 0),
players: [...existing.players, pfu],
});
}
return teamAggregates;
}
/**
* Sorts players by performance (stocks remaining desc, percent asc).
* Used to rank players for winner determination.
*/
function sortByPerformance(players) {
return [...players].sort((a, b) => {
var _a, _b, _c, _d;
const stocksDiff = ((_a = b.stocksRemaining) !== null && _a !== void 0 ? _a : 0) - ((_b = a.stocksRemaining) !== null && _b !== void 0 ? _b : 0);
if (stocksDiff !== 0) {
return stocksDiff;
}
return ((_c = a.percent) !== null && _c !== void 0 ? _c : 0) - ((_d = b.percent) !== null && _d !== void 0 ? _d : 0);
});
}
exports.getWinners = getWinners;