mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 08:00:43 +00:00
Put friends on the same team (#3994)
Fixes #3911 ## Description: - Server captures `publicId` and `friends` from `getUserMe()` and includes each player's in-game friend `clientID`s in `PlayerSchema` on game start - Team assignment treats friends as a **soft preference** (best-effort): a non-clan player goes to the team where the most of their friends already are; if that team is full they spill to the next-emptiest team rather than getting kicked - Clans remain strict (kick overflow) since clan membership is an explicit opt-in; friends are implicit, so a friend-of-friend chain that doesn't fit shouldn't bench anyone - Friendship is symmetric — an edge from either direction counts, which keeps things working when one side's `getUserMe` is stale - Lobby preview unchanged — friend grouping only takes effect once the game actually starts (avoids exposing friend lists in the lobby payload) ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: evan
This commit is contained in:
@@ -117,6 +117,7 @@ export const UserMeResponseSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
|
friends: z.array(z.string()),
|
||||||
subscription: z
|
subscription: z
|
||||||
.object({
|
.object({
|
||||||
tier: z.string(),
|
tier: z.string(),
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export async function createGameRunner(
|
|||||||
random.nextID(),
|
random.nextID(),
|
||||||
p.isLobbyCreator ?? false,
|
p.isLobbyCreator ?? false,
|
||||||
p.clanTag,
|
p.clanTag,
|
||||||
|
p.friends ?? [],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -549,6 +549,7 @@ export const PlayerSchema = z.object({
|
|||||||
clanTag: ClanTagSchema,
|
clanTag: ClanTagSchema,
|
||||||
cosmetics: PlayerCosmeticsSchema.optional(),
|
cosmetics: PlayerCosmeticsSchema.optional(),
|
||||||
isLobbyCreator: z.boolean().optional(),
|
isLobbyCreator: z.boolean().optional(),
|
||||||
|
friends: z.array(ID).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const GameStartInfoSchema = z.object({
|
export const GameStartInfoSchema = z.object({
|
||||||
|
|||||||
@@ -580,6 +580,7 @@ export class PlayerInfo {
|
|||||||
public readonly id: PlayerID,
|
public readonly id: PlayerID,
|
||||||
public readonly isLobbyCreator: boolean = false,
|
public readonly isLobbyCreator: boolean = false,
|
||||||
public readonly clanTag: string | null = null,
|
public readonly clanTag: string | null = null,
|
||||||
|
public readonly friends: ClientID[] = [],
|
||||||
) {
|
) {
|
||||||
this.displayName = formatPlayerDisplayName(this.name, this.clanTag);
|
this.displayName = formatPlayerDisplayName(this.name, this.clanTag);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { PseudoRandom } from "../PseudoRandom";
|
import { PseudoRandom } from "../PseudoRandom";
|
||||||
|
import { ClientID } from "../Schemas";
|
||||||
import { simpleHash } from "../Util";
|
import { simpleHash } from "../Util";
|
||||||
import { PlayerInfo, PlayerType, Team } from "./Game";
|
import { PlayerInfo, PlayerType, Team } from "./Game";
|
||||||
|
|
||||||
@@ -10,31 +11,24 @@ export function assignTeams(
|
|||||||
const result = new Map<PlayerInfo, Team | "kicked">();
|
const result = new Map<PlayerInfo, Team | "kicked">();
|
||||||
const teamPlayerCount = new Map<Team, number>();
|
const teamPlayerCount = new Map<Team, number>();
|
||||||
|
|
||||||
// Group players by clan
|
// Clans are strict: a clan goes to one team together, and any overflow
|
||||||
|
// members get kicked. (You opted into the clan, so we honor "all or
|
||||||
|
// nothing" for placement.)
|
||||||
const clanGroups = new Map<string, PlayerInfo[]>();
|
const clanGroups = new Map<string, PlayerInfo[]>();
|
||||||
const noClanPlayers: PlayerInfo[] = [];
|
const nonClanPlayers: PlayerInfo[] = [];
|
||||||
|
for (const p of players) {
|
||||||
// Sort players into clan groups or no-clan list
|
if (p.clanTag) {
|
||||||
for (const player of players) {
|
if (!clanGroups.has(p.clanTag)) clanGroups.set(p.clanTag, []);
|
||||||
const clanTag = player.clanTag;
|
clanGroups.get(p.clanTag)!.push(p);
|
||||||
if (clanTag) {
|
|
||||||
if (!clanGroups.has(clanTag)) {
|
|
||||||
clanGroups.set(clanTag, []);
|
|
||||||
}
|
|
||||||
clanGroups.get(clanTag)!.push(player);
|
|
||||||
} else {
|
} else {
|
||||||
noClanPlayers.push(player);
|
nonClanPlayers.push(p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort clans by size (largest first)
|
const sortedClans = Array.from(clanGroups.values()).sort(
|
||||||
const sortedClanPlayers = Array.from(clanGroups.values()).sort(
|
|
||||||
(a, b) => b.length - a.length,
|
(a, b) => b.length - a.length,
|
||||||
);
|
);
|
||||||
|
for (const clan of sortedClans) {
|
||||||
// First, assign clan players
|
|
||||||
for (const clanPlayers of sortedClanPlayers) {
|
|
||||||
// Try to keep the clan together on the team with fewer players
|
|
||||||
let team: Team | null = null;
|
let team: Team | null = null;
|
||||||
let teamSize = 0;
|
let teamSize = 0;
|
||||||
for (const t of teams) {
|
for (const t of teams) {
|
||||||
@@ -43,10 +37,8 @@ export function assignTeams(
|
|||||||
teamSize = p;
|
teamSize = p;
|
||||||
team = t;
|
team = t;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (team === null) continue;
|
if (team === null) continue;
|
||||||
|
for (const player of clan) {
|
||||||
for (const player of clanPlayers) {
|
|
||||||
if (teamSize < maxTeamSize) {
|
if (teamSize < maxTeamSize) {
|
||||||
teamSize++;
|
teamSize++;
|
||||||
result.set(player, team);
|
result.set(player, team);
|
||||||
@@ -57,31 +49,85 @@ export function assignTeams(
|
|||||||
teamPlayerCount.set(team, teamSize);
|
teamPlayerCount.set(team, teamSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then, assign non-clan players to balance teams
|
// Friend edges are a soft preference: when placing a player, prefer the
|
||||||
let nationPlayers = noClanPlayers.filter(
|
// team where the most of their friends already are. If that team is full
|
||||||
(player) => player.playerType === PlayerType.Nation,
|
// we spill onto the next-emptiest non-full team rather than kicking — you
|
||||||
|
// didn't opt into being grouped with friend-of-friend, so a chain that
|
||||||
|
// doesn't fit shouldn't bench anyone.
|
||||||
|
const presentClientIDs = new Set<ClientID>();
|
||||||
|
for (const p of players) {
|
||||||
|
if (p.clientID !== null) presentClientIDs.add(p.clientID);
|
||||||
|
}
|
||||||
|
const friendGraph = new Map<ClientID, Set<ClientID>>();
|
||||||
|
const addEdge = (a: ClientID, b: ClientID) => {
|
||||||
|
let s = friendGraph.get(a);
|
||||||
|
if (s === undefined) {
|
||||||
|
s = new Set();
|
||||||
|
friendGraph.set(a, s);
|
||||||
|
}
|
||||||
|
s.add(b);
|
||||||
|
};
|
||||||
|
for (const p of players) {
|
||||||
|
if (p.clientID === null) continue;
|
||||||
|
for (const friendID of p.friends) {
|
||||||
|
if (!presentClientIDs.has(friendID)) continue;
|
||||||
|
addEdge(p.clientID, friendID);
|
||||||
|
addEdge(friendID, p.clientID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const teamByClientID = new Map<ClientID, Team>();
|
||||||
|
for (const [player, team] of result.entries()) {
|
||||||
|
if (player.clientID !== null && team !== "kicked") {
|
||||||
|
teamByClientID.set(player.clientID, team);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const placePlayer = (p: PlayerInfo) => {
|
||||||
|
const myFriends =
|
||||||
|
p.clientID !== null ? friendGraph.get(p.clientID) : undefined;
|
||||||
|
let bestTeam: Team | null = null;
|
||||||
|
let bestFriendCount = -1;
|
||||||
|
let bestSize = Infinity;
|
||||||
|
for (const t of teams) {
|
||||||
|
const size = teamPlayerCount.get(t) ?? 0;
|
||||||
|
if (size >= maxTeamSize) continue;
|
||||||
|
let friendsOnTeam = 0;
|
||||||
|
if (myFriends !== undefined) {
|
||||||
|
for (const friendID of myFriends) {
|
||||||
|
if (teamByClientID.get(friendID) === t) friendsOnTeam++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
friendsOnTeam > bestFriendCount ||
|
||||||
|
(friendsOnTeam === bestFriendCount && size < bestSize)
|
||||||
|
) {
|
||||||
|
bestFriendCount = friendsOnTeam;
|
||||||
|
bestSize = size;
|
||||||
|
bestTeam = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bestTeam === null) {
|
||||||
|
result.set(p, "kicked");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
teamPlayerCount.set(bestTeam, (teamPlayerCount.get(bestTeam) ?? 0) + 1);
|
||||||
|
result.set(p, bestTeam);
|
||||||
|
if (p.clientID !== null) teamByClientID.set(p.clientID, bestTeam);
|
||||||
|
};
|
||||||
|
|
||||||
|
let nationPlayers = nonClanPlayers.filter(
|
||||||
|
(p) => p.playerType === PlayerType.Nation,
|
||||||
);
|
);
|
||||||
if (nationPlayers.length > 0) {
|
if (nationPlayers.length > 0) {
|
||||||
// Shuffle only nations to randomize their team assignment
|
|
||||||
const random = new PseudoRandom(simpleHash(nationPlayers[0].id));
|
const random = new PseudoRandom(simpleHash(nationPlayers[0].id));
|
||||||
nationPlayers = random.shuffleArray(nationPlayers);
|
nationPlayers = random.shuffleArray(nationPlayers);
|
||||||
}
|
}
|
||||||
const otherPlayers = noClanPlayers.filter(
|
const otherPlayers = nonClanPlayers.filter(
|
||||||
(player) => player.playerType !== PlayerType.Nation,
|
(p) => p.playerType !== PlayerType.Nation,
|
||||||
);
|
);
|
||||||
|
for (const p of otherPlayers.concat(nationPlayers)) {
|
||||||
for (const player of otherPlayers.concat(nationPlayers)) {
|
placePlayer(p);
|
||||||
let team: Team | null = null;
|
|
||||||
let teamSize = 0;
|
|
||||||
for (const t of teams) {
|
|
||||||
const p = teamPlayerCount.get(t) ?? 0;
|
|
||||||
if (team !== null && teamSize <= p) continue;
|
|
||||||
teamSize = p;
|
|
||||||
team = t;
|
|
||||||
}
|
|
||||||
if (team === null) continue;
|
|
||||||
teamPlayerCount.set(team, teamSize + 1);
|
|
||||||
result.set(player, team);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -21,5 +21,7 @@ export class Client {
|
|||||||
public clanTag: string | null,
|
public clanTag: string | null,
|
||||||
public ws: WebSocket,
|
public ws: WebSocket,
|
||||||
public readonly cosmetics: PlayerCosmetics | undefined,
|
public readonly cosmetics: PlayerCosmetics | undefined,
|
||||||
|
public readonly publicId: string | undefined,
|
||||||
|
public readonly friends: string[],
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -731,18 +731,29 @@ export class GameServer {
|
|||||||
// if no client connects/pings.
|
// if no client connects/pings.
|
||||||
this.lastPingUpdate = Date.now();
|
this.lastPingUpdate = Date.now();
|
||||||
|
|
||||||
|
const publicIdToClientID = new Map<string, ClientID>();
|
||||||
|
for (const c of this.activeClients) {
|
||||||
|
if (c.publicId) publicIdToClientID.set(c.publicId, c.clientID);
|
||||||
|
}
|
||||||
|
|
||||||
const result = GameStartInfoSchema.safeParse({
|
const result = GameStartInfoSchema.safeParse({
|
||||||
gameID: this.id,
|
gameID: this.id,
|
||||||
lobbyCreatedAt: this.createdAt,
|
lobbyCreatedAt: this.createdAt,
|
||||||
visibleAt: this.visibleAt,
|
visibleAt: this.visibleAt,
|
||||||
config: this.gameConfig,
|
config: this.gameConfig,
|
||||||
players: this.activeClients.map((c) => ({
|
players: this.activeClients.map((c) => {
|
||||||
username: c.username,
|
const friendClientIDs = c.friends
|
||||||
clanTag: c.clanTag ?? null,
|
.map((pid) => publicIdToClientID.get(pid))
|
||||||
clientID: c.clientID,
|
.filter((id): id is ClientID => id !== undefined);
|
||||||
cosmetics: c.cosmetics,
|
return {
|
||||||
isLobbyCreator: this.lobbyCreatorID === c.clientID,
|
username: c.username,
|
||||||
})),
|
clanTag: c.clanTag ?? null,
|
||||||
|
clientID: c.clientID,
|
||||||
|
cosmetics: c.cosmetics,
|
||||||
|
isLobbyCreator: this.lobbyCreatorID === c.clientID,
|
||||||
|
friends: friendClientIDs.length > 0 ? friendClientIDs : undefined,
|
||||||
|
};
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
const error = z.prettifyError(result.error);
|
const error = z.prettifyError(result.error);
|
||||||
|
|||||||
@@ -376,6 +376,8 @@ export async function startWorker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let flares: string[] | undefined;
|
let flares: string[] | undefined;
|
||||||
|
let publicId: string | undefined;
|
||||||
|
let friends: string[] = [];
|
||||||
|
|
||||||
const allowedFlares = ServerEnv.allowedFlares();
|
const allowedFlares = ServerEnv.allowedFlares();
|
||||||
if (claims === null) {
|
if (claims === null) {
|
||||||
@@ -396,6 +398,8 @@ export async function startWorker() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
flares = result.response.player.flares;
|
flares = result.response.player.flares;
|
||||||
|
publicId = result.response.player.publicId;
|
||||||
|
friends = result.response.player.friends;
|
||||||
|
|
||||||
if (allowedFlares !== undefined) {
|
if (allowedFlares !== undefined) {
|
||||||
const allowed =
|
const allowed =
|
||||||
@@ -462,6 +466,8 @@ export async function startWorker() {
|
|||||||
censoredClanTag,
|
censoredClanTag,
|
||||||
ws,
|
ws,
|
||||||
cosmeticResult.cosmetics,
|
cosmeticResult.cosmetics,
|
||||||
|
publicId,
|
||||||
|
friends,
|
||||||
);
|
);
|
||||||
|
|
||||||
const joinResult = gm.joinClient(client, clientMsg.gameID);
|
const joinResult = gm.joinClient(client, clientMsg.gameID);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ function makeUserMe(flares: string[] = []): UserMeResponse {
|
|||||||
adfree: false,
|
adfree: false,
|
||||||
flares,
|
flares,
|
||||||
achievements: { singleplayerMap: [] },
|
achievements: { singleplayerMap: [] },
|
||||||
|
friends: [],
|
||||||
subscription: null,
|
subscription: null,
|
||||||
},
|
},
|
||||||
} as UserMeResponse;
|
} as UserMeResponse;
|
||||||
|
|||||||
@@ -15,6 +15,26 @@ describe("assignTeams", () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Friend grouping is keyed on clientID. By default we pass clientID = id
|
||||||
|
// for brevity, but tests can override clientID to verify the lookup uses
|
||||||
|
// clientID rather than PlayerInfo.id.
|
||||||
|
const createPlayerWithFriends = (
|
||||||
|
id: string,
|
||||||
|
friends: string[],
|
||||||
|
clan?: string,
|
||||||
|
clientID: string = id,
|
||||||
|
): PlayerInfo => {
|
||||||
|
return new PlayerInfo(
|
||||||
|
`Player ${id}`,
|
||||||
|
PlayerType.Human,
|
||||||
|
clientID,
|
||||||
|
id, // PlayerInfo.id
|
||||||
|
false,
|
||||||
|
clan,
|
||||||
|
friends,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
it("should assign players to teams when no clans are present", () => {
|
it("should assign players to teams when no clans are present", () => {
|
||||||
const players = [
|
const players = [
|
||||||
createPlayer("1"),
|
createPlayer("1"),
|
||||||
@@ -164,4 +184,129 @@ describe("assignTeams", () => {
|
|||||||
expect(result.get(players[12])).toEqual(ColoredTeams.Purple);
|
expect(result.get(players[12])).toEqual(ColoredTeams.Purple);
|
||||||
expect(result.get(players[13])).toEqual(ColoredTeams.Orange);
|
expect(result.get(players[13])).toEqual(ColoredTeams.Orange);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should keep two friends on the same team", () => {
|
||||||
|
const players = [
|
||||||
|
createPlayerWithFriends("1", ["2"]),
|
||||||
|
createPlayerWithFriends("2", ["1"]),
|
||||||
|
createPlayerWithFriends("3", []),
|
||||||
|
createPlayerWithFriends("4", []),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = assignTeams(players, teams);
|
||||||
|
|
||||||
|
expect(result.get(players[0])).toEqual(result.get(players[1]));
|
||||||
|
expect(result.get(players[2])).not.toEqual(result.get(players[0]));
|
||||||
|
expect(result.get(players[3])).not.toEqual(result.get(players[0]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should group a chain of friends transitively", () => {
|
||||||
|
// 6 players, 2 teams → maxTeamSize = 3 (enough room for a 3-friend chain)
|
||||||
|
const players = [
|
||||||
|
createPlayerWithFriends("1", ["2"]),
|
||||||
|
createPlayerWithFriends("2", ["3"]),
|
||||||
|
createPlayerWithFriends("3", []),
|
||||||
|
createPlayerWithFriends("4", []),
|
||||||
|
createPlayerWithFriends("5", []),
|
||||||
|
createPlayerWithFriends("6", []),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = assignTeams(players, teams);
|
||||||
|
|
||||||
|
const teamOf1 = result.get(players[0]);
|
||||||
|
expect(result.get(players[1])).toEqual(teamOf1);
|
||||||
|
expect(result.get(players[2])).toEqual(teamOf1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should treat one-directional friendship as a group", () => {
|
||||||
|
const players = [
|
||||||
|
createPlayerWithFriends("1", ["2"]),
|
||||||
|
createPlayerWithFriends("2", []), // doesn't list 1 back
|
||||||
|
createPlayerWithFriends("3", []),
|
||||||
|
createPlayerWithFriends("4", []),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = assignTeams(players, teams);
|
||||||
|
|
||||||
|
expect(result.get(players[0])).toEqual(result.get(players[1]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should merge friend and clan groups when they overlap", () => {
|
||||||
|
// 1 and 2 share clan CLANA, 2 is friends with 3 (no clan)
|
||||||
|
// → all three end up on the same team. 6 players, maxTeamSize = 3.
|
||||||
|
const players = [
|
||||||
|
createPlayerWithFriends("1", [], "CLANA"),
|
||||||
|
createPlayerWithFriends("2", ["3"], "CLANA"),
|
||||||
|
createPlayerWithFriends("3", [], undefined),
|
||||||
|
createPlayerWithFriends("4", [], undefined),
|
||||||
|
createPlayerWithFriends("5", [], undefined),
|
||||||
|
createPlayerWithFriends("6", [], undefined),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = assignTeams(players, teams);
|
||||||
|
|
||||||
|
const teamOf1 = result.get(players[0]);
|
||||||
|
expect(result.get(players[1])).toEqual(teamOf1);
|
||||||
|
expect(result.get(players[2])).toEqual(teamOf1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should spill friend-group overflow to other teams (no kicks)", () => {
|
||||||
|
// 4-player friend group + 2 strangers, maxTeamSize = ceil(6/2) = 3.
|
||||||
|
// Friend overflow spills to the other team rather than getting kicked.
|
||||||
|
const players = [
|
||||||
|
createPlayerWithFriends("1", ["2", "3", "4"]),
|
||||||
|
createPlayerWithFriends("2", []),
|
||||||
|
createPlayerWithFriends("3", []),
|
||||||
|
createPlayerWithFriends("4", []),
|
||||||
|
createPlayerWithFriends("5", []),
|
||||||
|
createPlayerWithFriends("6", []),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = assignTeams(players, teams);
|
||||||
|
|
||||||
|
expect(result.get(players[0])).toEqual(ColoredTeams.Red);
|
||||||
|
expect(result.get(players[1])).toEqual(ColoredTeams.Red);
|
||||||
|
expect(result.get(players[2])).toEqual(ColoredTeams.Red);
|
||||||
|
expect(result.get(players[3])).toEqual(ColoredTeams.Blue);
|
||||||
|
expect(result.get(players[4])).toEqual(ColoredTeams.Blue);
|
||||||
|
expect(result.get(players[5])).toEqual(ColoredTeams.Blue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should key friend grouping on clientID, not PlayerInfo.id", () => {
|
||||||
|
// clientID and PlayerInfo.id are distinct. The friends list references
|
||||||
|
// clientIDs ("client-2", "client-1"). If grouping ever regressed to
|
||||||
|
// keying on PlayerInfo.id ("player-1"/"player-2"), no edges would form
|
||||||
|
// and these two would land on opposite teams.
|
||||||
|
const players = [
|
||||||
|
createPlayerWithFriends("player-1", ["client-2"], undefined, "client-1"),
|
||||||
|
createPlayerWithFriends("player-2", ["client-1"], undefined, "client-2"),
|
||||||
|
createPlayerWithFriends("player-3", [], undefined, "client-3"),
|
||||||
|
createPlayerWithFriends("player-4", [], undefined, "client-4"),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = assignTeams(players, teams);
|
||||||
|
|
||||||
|
expect(result.get(players[0])).toEqual(result.get(players[1]));
|
||||||
|
expect(result.get(players[2])).not.toEqual(result.get(players[0]));
|
||||||
|
expect(result.get(players[3])).not.toEqual(result.get(players[0]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should still kick when every team is at capacity", () => {
|
||||||
|
// 5 friends in a clique, 2 teams, maxTeamSize = ceil(5/2) = 3.
|
||||||
|
// Total capacity is 6, so we have slack — nobody should get kicked.
|
||||||
|
// But if we force capacity below player count, kicks resume.
|
||||||
|
const players = [
|
||||||
|
createPlayerWithFriends("1", ["2", "3", "4", "5"]),
|
||||||
|
createPlayerWithFriends("2", []),
|
||||||
|
createPlayerWithFriends("3", []),
|
||||||
|
createPlayerWithFriends("4", []),
|
||||||
|
createPlayerWithFriends("5", []),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = assignTeams(players, teams, 2);
|
||||||
|
|
||||||
|
// maxTeamSize=2, 2 teams → capacity 4, 5 players → 1 must be kicked.
|
||||||
|
const kicked = players.filter((p) => result.get(p) === "kicked");
|
||||||
|
expect(kicked.length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ function makeClient(
|
|||||||
null,
|
null,
|
||||||
ws as any,
|
ws as any,
|
||||||
undefined,
|
undefined,
|
||||||
|
undefined,
|
||||||
|
[],
|
||||||
);
|
);
|
||||||
return { client, ws };
|
return { client, ws };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user