Skip to content

Commit

Permalink
improvements matchmaking #2276
Browse files Browse the repository at this point in the history
  • Loading branch information
BastLast committed Jan 16, 2025
1 parent 2606124 commit 2982630
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 75 deletions.
111 changes: 42 additions & 69 deletions Core/src/commands/player/FightCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from "../../../../Lib/src/packets/commands/CommandFightPacket";
import {BlockingConstants} from "../../../../Lib/src/constants/BlockingConstants";
import {InventorySlots} from "../../core/database/game/models/InventorySlot";
import {LogsReadRequests} from "../../core/database/logs/LogsReadRequests";
import {LogsReadRequests, RankedFightResult} from "../../core/database/logs/LogsReadRequests";

type PlayerStats = {
classId: number,
Expand Down Expand Up @@ -54,94 +54,67 @@ async function getPlayerStats(player: Player): Promise<PlayerStats> {
};
}

/**
* Check if a BO3 is already finished (3 games played or 2 wins)
* @param bo3
*/
function bo3isAlreadyFinished(bo3: RankedFightResult) {

Check failure on line 61 in Core/src/commands/player/FightCommand.ts

View workflow job for this annotation

GitHub Actions / eslint-core-module

Missing return type on function
return bo3.won > 1 || bo3.lost > 1 || bo3.draw + bo3.won + bo3.lost >= 3;
}

/**
* Find another player to fight the player that started the command
* @param player - player that wants to fight
* @param offset - offset to start the search in case the first try did not work
* @returns player opponent
*/
async function findOpponent(player: Player, offset: number): Promise<Player | null> {
const closestPlayers = await Players.findByDefenseGlory(
player.attackGloryPoints,
const validOpponents = await Players.findPotentialOpponent(
player,
FightConstants.PLAYER_PER_OPPONENT_SEARCH,
offset
);

// Remove the current player from the list (cannot fight itself)
const opponentCandidates = closestPlayers.filter(
(closestPlayer) => closestPlayer.id !== player.id
);

// Filter opponents based on level and ELO gap
const validOpponents = opponentCandidates.filter(
(opponent) =>
opponent.level >= FightConstants.REQUIRED_LEVEL &&
Math.abs(player.defenseGloryPoints - opponent.attackGloryPoints) <= FightConstants.ELO.MAX_ELO_GAP
);

if (validOpponents.length === 0) {
// No valid opponents found at this offset
if (offset > FightConstants.MAX_OFFSET_FOR_OPPONENT_SEARCH) {
return null;
}
// Recursively search with increased offset
return findOpponent(player, offset + 1);
}

// Shuffle array
validOpponents.sort(() => Math.random() - 0.5);

// Get the keycloak IDs of valid opponents
const opponentKeycloakIds = validOpponents.map((opponent) => opponent.keycloakId);

// Check if these players have been defenders recently
const haveBeenDefenderRecently = await LogsReadRequests.hasBeenADefenderInRankedFightSinceMinute(
opponentKeycloakIds,
FightConstants.DEFENDER_COOLDOWN_MINUTES
);

// Filter out opponents who have been defenders recently
const opponentsNotOnCooldown = validOpponents.filter(
(opponent) => !haveBeenDefenderRecently[opponent.keycloakId]
);
if (validOpponents.length !== 0) {
// Shuffle array
validOpponents.sort(() => Math.random() - 0.5);

if (opponentsNotOnCooldown.length === 0) {
// No valid opponents found after defender cooldown filter
if (offset > FightConstants.MAX_OFFSET_FOR_OPPONENT_SEARCH) {
return null;
}
// Recursively search with increased offset
return findOpponent(player, offset + 1);
}

// Now get the keycloak IDs of the remaining opponents
const remainingOpponentKeycloakIds = opponentsNotOnCooldown.map((opponent) => opponent.keycloakId);

// Fetch the fight results against all remaining valid opponents in one call
const bo3Map = await LogsReadRequests.getRankedFightsThisWeek(
player.keycloakId,
remainingOpponentKeycloakIds
);
// Check if these players have been defenders recently
const haveBeenDefenderRecently = await LogsReadRequests.hasBeenADefenderInRankedFightSinceMinute(
validOpponents.map((opponent) => opponent.keycloakId),
FightConstants.DEFENDER_COOLDOWN_MINUTES
);

// Now iterate over opponentsNotOnCooldown and find the first one that meets all conditions
for (const opponent of opponentsNotOnCooldown) {
// Get the fight result for this opponent from the map
const bo3 = bo3Map.get(opponent.keycloakId) || { won: 0, lost: 0, draw: 0 };
// Filter out opponents who have been defenders recently
const opponentsNotOnCooldown = validOpponents.filter(
(opponent) => !haveBeenDefenderRecently[opponent.keycloakId]
);

if (bo3.won > 1 || bo3.lost > 1 || bo3.draw + bo3.won + bo3.lost >= 3) {
// Max fights already played with this opponent
continue;
if (opponentsNotOnCooldown.length !== 0) {
// Now get the keycloak IDs of the remaining opponents
const remainingOpponentKeycloakIds = opponentsNotOnCooldown.map((opponent) => opponent.keycloakId);

// Fetch the fight results against all remaining valid opponents in one call
const bo3Map = await LogsReadRequests.getRankedFightsThisWeek(
player.keycloakId,
remainingOpponentKeycloakIds
);

// Now iterate over opponentsNotOnCooldown and find the first one that meets all conditions
for (const opponent of opponentsNotOnCooldown) {
// Get the fight result for this opponent from the map
if (bo3isAlreadyFinished( bo3Map.get(opponent.keycloakId) || {won: 0, lost: 0, draw: 0})) {
continue;
}
// Found a valid opponent
return opponent;
}
}

// Found a valid opponent
return opponent;
}

// No valid opponents found in this batch, recursively search with increased offset
if (offset > FightConstants.MAX_OFFSET_FOR_OPPONENT_SEARCH) {
return null;
}

return findOpponent(player, offset + 1);
}

Expand Down
17 changes: 12 additions & 5 deletions Core/src/core/database/game/models/Player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1424,18 +1424,25 @@ export class Players {

/**
* Find the X players that are the closest in defense glory to a specific value
* @param defenseGlory - the value to search for
* @param player - the value to search for
* @param amountOfPlayersToRetrieve - the X amount of players
* @param offset - offset in case the found players are not enough and an offset search is necessary
*/
static async findByDefenseGlory(defenseGlory: number, amountOfPlayersToRetrieve: number, offset: number): Promise<Player[]> {
static async findPotentialOpponent(player: Player, amountOfPlayersToRetrieve: number, offset: number): Promise<Player[]> {
return await Player.findAll({
where: {
defenseGlory: {[Op.ne]: null}
defenseGlory: {
[Op.ne]: null,
[Op.between]: [
player.attackGloryPoints - FightConstants.ELO.MAX_ELO_GAP,
player.attackGloryPoints + FightConstants.ELO.MAX_ELO_GAP
]
},
level: {[Op.gt]: FightConstants.REQUIRED_LEVEL}

Check warning on line 1441 in Core/src/core/database/game/models/Player.ts

View workflow job for this annotation

GitHub Actions / eslint-core-module

Extra space before value for key 'level'
},
order: [
// Trier par la différence absolue avec defenseGlory recherchée
[Sequelize.literal(`ABS(defenseGlory - ${defenseGlory})`), "ASC"]
// Sort using the difference with the attack elo of the player
[Sequelize.literal(`ABS(defenseGlory - ${player.attackGloryPoints})`), "ASC"]
],
limit: amountOfPlayersToRetrieve,
offset: offset
Expand Down
2 changes: 1 addition & 1 deletion Core/src/core/database/logs/LogsReadRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {LogsGuildsJoins} from "./models/LogsGuildJoins";
import {LogsGuilds} from "./models/LogsGuilds";
import {MapLocationDataController} from "../../../data/MapLocation";

type RankedFightResult = {
export type RankedFightResult = {
won: number,
lost: number,
draw: number
Expand Down

0 comments on commit 2982630

Please sign in to comment.