sort teams based on clans (#381)

## Description:
Teams are not sorted based on clans, clan members always stay on the
same team.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

## Please put your Discord username so you can be contacted if a bug or
regression is found:

<DISCORD USERNAME>
This commit is contained in:
evanpelle
2025-03-30 19:37:48 -07:00
committed by GitHub
parent ab3f4fbac1
commit 6d5f218a44
5 changed files with 232 additions and 4 deletions
+14
View File
@@ -0,0 +1,14 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Jest Tests",
"runtimeExecutable": "npm",
"runtimeArgs": ["run-script", "test"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}
+24 -3
View File
@@ -37,6 +37,7 @@ import { UnitGrid } from "./UnitGrid";
import { StatsImpl } from "./StatsImpl";
import { Stats } from "./Stats";
import { simpleHash } from "../Util";
import { assignTeams } from "./TeamAssignment";
export function createGame(
humans: PlayerInfo[],
@@ -89,7 +90,7 @@ export class GameImpl implements Game {
nationMap: NationMap,
private _config: Config,
) {
this._humans.forEach((p) => this.addPlayer(p, 100));
this.addHumans();
this._terraNullius = new TerraNulliusImpl();
this._width = _map.width();
this._height = _map.height();
@@ -104,6 +105,22 @@ export class GameImpl implements Game {
);
this.unitGrid = new UnitGrid(this._map);
}
private addHumans() {
if (this.config().gameConfig().gameMode != GameMode.Team) {
this._humans.forEach((p) => this.addPlayer(p, 0));
return;
}
const playerToTeam = assignTeams(this._humans);
for (const [playerInfo, team] of playerToTeam.entries()) {
if (team == "kicked") {
console.warn(`Player ${playerInfo.name} was kicked from team`);
continue;
}
this.addPlayer(playerInfo, 0, team);
}
}
isOnEdgeOfMap(ref: TileRef): boolean {
return this._map.isOnEdgeOfMap(ref);
}
@@ -330,13 +347,17 @@ export class GameImpl implements Game {
return this.player(id);
}
addPlayer(playerInfo: PlayerInfo, manpower: number): Player {
addPlayer(
playerInfo: PlayerInfo,
manpower: number,
team: Team = null,
): Player {
const player = new PlayerImpl(
this,
this.nextPlayerID,
playerInfo,
manpower,
this.maybeAssignTeam(playerInfo),
team ?? this.maybeAssignTeam(playerInfo),
);
this._playersBySmallID.push(player);
this.nextPlayerID++;
+1 -1
View File
@@ -573,7 +573,7 @@ export class PlayerImpl implements Player {
if (this.team() == null || other.team() == null) {
return false;
}
return this._team == other.team();
return this._team.name == other.team().name;
}
isFriendly(other: Player): boolean {
+71
View File
@@ -0,0 +1,71 @@
import { Player, PlayerInfo, Team, TeamName } from "./Game";
export function assignTeams(
players: PlayerInfo[],
): Map<PlayerInfo, Team | "kicked"> {
const result = new Map<PlayerInfo, Team | "kicked">();
let redTeamCount = 0;
let blueTeamCount = 0;
// Group players by clan
const clanGroups = new Map<string, PlayerInfo[]>();
const noClanPlayers: PlayerInfo[] = [];
// Sort players into clan groups or no-clan list
for (const player of players) {
if (player.clan) {
if (!clanGroups.has(player.clan)) {
clanGroups.set(player.clan, []);
}
clanGroups.get(player.clan)!.push(player);
} else {
noClanPlayers.push(player);
}
}
const maxTeamSize = Math.ceil(players.length / 2);
// Sort clans by size (largest first)
const sortedClans = Array.from(clanGroups.entries()).sort(
(a, b) => b[1].length - a[1].length,
);
// First, assign clan players
for (const [_, clanPlayers] of sortedClans) {
// Try to keep the clan together on the team with fewer players
if (redTeamCount <= blueTeamCount) {
// Assign to red team
for (const player of clanPlayers) {
if (redTeamCount < maxTeamSize) {
redTeamCount++;
result.set(player, { name: TeamName.Red });
} else {
result.set(player, "kicked");
}
}
} else {
// Assign to blue team
for (const player of clanPlayers) {
if (blueTeamCount < maxTeamSize) {
blueTeamCount++;
result.set(player, { name: TeamName.Blue });
} else {
result.set(player, "kicked");
}
}
}
}
// Then, assign non-clan players to balance teams
for (const player of noClanPlayers) {
if (redTeamCount <= blueTeamCount) {
redTeamCount++;
result.set(player, { name: TeamName.Red });
} else {
blueTeamCount++;
result.set(player, { name: TeamName.Blue });
}
}
return result;
}
+122
View File
@@ -0,0 +1,122 @@
import { TeamName, PlayerType, PlayerInfo } from "../src/core/game/Game";
import { assignTeams } from "../src/core/game/TeamAssignment";
describe("assignTeams", () => {
const createPlayer = (id: string, clan?: string): PlayerInfo => {
const name = clan ? `[${clan}]Player ${id}` : `Player ${id}`;
return new PlayerInfo(
"🏳️", // flag
name,
PlayerType.Human,
null, // clientID (null for testing)
id,
null, // nation (null for testing)
);
};
it("should assign players to teams when no clans are present", () => {
const players = [
createPlayer("1"),
createPlayer("2"),
createPlayer("3"),
createPlayer("4"),
];
const result = assignTeams(players);
// Check that players are assigned alternately
expect(result.get(players[0])).toEqual({ name: TeamName.Red });
expect(result.get(players[1])).toEqual({ name: TeamName.Blue });
expect(result.get(players[2])).toEqual({ name: TeamName.Red });
expect(result.get(players[3])).toEqual({ name: TeamName.Blue });
});
it("should keep clan members together on the same team", () => {
const players = [
createPlayer("1", "CLANA"),
createPlayer("2", "CLANA"),
createPlayer("3", "CLANB"),
createPlayer("4", "CLANB"),
];
const result = assignTeams(players);
// Check that clan members are on the same team
expect(result.get(players[0])).toEqual({ name: TeamName.Red });
expect(result.get(players[1])).toEqual({ name: TeamName.Red });
expect(result.get(players[2])).toEqual({ name: TeamName.Blue });
expect(result.get(players[3])).toEqual({ name: TeamName.Blue });
});
it("should handle mixed clan and non-clan players", () => {
const players = [
createPlayer("1", "CLANA"),
createPlayer("2", "CLANA"),
createPlayer("3"),
createPlayer("4"),
];
const result = assignTeams(players);
// Check that clan members are together and non-clan players balance teams
expect(result.get(players[0])).toEqual({ name: TeamName.Red });
expect(result.get(players[1])).toEqual({ name: TeamName.Red });
expect(result.get(players[2])).toEqual({ name: TeamName.Blue });
expect(result.get(players[3])).toEqual({ name: TeamName.Blue });
});
it("should kick players when teams are full", () => {
const players = [
createPlayer("1", "CLANA"),
createPlayer("2", "CLANA"),
createPlayer("3", "CLANA"),
createPlayer("4", "CLANA"),
createPlayer("5", "CLANB"),
createPlayer("6", "CLANB"),
];
const result = assignTeams(players);
// Check that players are kicked when teams are full
expect(result.get(players[0])).toEqual({ name: TeamName.Red });
expect(result.get(players[1])).toEqual({ name: TeamName.Red });
expect(result.get(players[2])).toEqual({ name: TeamName.Red });
expect(result.get(players[3])).toEqual("kicked");
expect(result.get(players[4])).toEqual({ name: TeamName.Blue });
expect(result.get(players[5])).toEqual({ name: TeamName.Blue });
});
it("should handle empty player list", () => {
const result = assignTeams([]);
expect(result.size).toBe(0);
});
it("should handle single player", () => {
const players = [createPlayer("1")];
const result = assignTeams(players);
expect(result.get(players[0])).toEqual({ name: TeamName.Red });
});
it("should handle multiple clans with different sizes", () => {
const players = [
createPlayer("1", "CLANA"),
createPlayer("2", "CLANA"),
createPlayer("3", "CLANA"),
createPlayer("4", "CLANB"),
createPlayer("5", "CLANB"),
createPlayer("6", "CLANC"),
];
const result = assignTeams(players);
// Check that larger clans are assigned first
expect(result.get(players[0])).toEqual({ name: TeamName.Red });
expect(result.get(players[1])).toEqual({ name: TeamName.Red });
expect(result.get(players[2])).toEqual({ name: TeamName.Red });
expect(result.get(players[3])).toEqual({ name: TeamName.Blue });
expect(result.get(players[4])).toEqual({ name: TeamName.Blue });
expect(result.get(players[5])).toEqual({ name: TeamName.Blue });
});
});