UNPKG

@slippi/slippi-js

Version:
202 lines (198 loc) 9.65 kB
'use strict'; 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;