mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 05:01:21 +00:00
@@ -48,7 +48,7 @@ import { RailNetwork } from "./RailNetwork";
|
||||
import { createRailNetwork } from "./RailNetworkImpl";
|
||||
import { Stats } from "./Stats";
|
||||
import { StatsImpl } from "./StatsImpl";
|
||||
import { assignTeams } from "./TeamAssignment";
|
||||
import { assignTeams, computeClanTeamName } from "./TeamAssignment";
|
||||
import { TerraNulliusImpl } from "./TerraNulliusImpl";
|
||||
import { UnitGrid, UnitPredicate } from "./UnitGrid";
|
||||
|
||||
@@ -211,6 +211,48 @@ export class GameImpl implements Game {
|
||||
...this._nations.map((n) => n.playerInfo),
|
||||
];
|
||||
const playerToTeam = assignTeams(allPlayers, this.playerTeams);
|
||||
|
||||
// Only rename numbered teams (8+ team mode), not colored teams
|
||||
const isNumberedTeams = !this.playerTeams.some((t) =>
|
||||
Object.values(ColoredTeams).includes(t as any),
|
||||
);
|
||||
if (isNumberedTeams) {
|
||||
// Build reverse map: team → assigned players
|
||||
const teamToPlayers = new Map<Team, PlayerInfo[]>();
|
||||
for (const [pi, team] of playerToTeam.entries()) {
|
||||
if (team === "kicked") continue;
|
||||
if (!teamToPlayers.has(team)) teamToPlayers.set(team, []);
|
||||
teamToPlayers.get(team)!.push(pi);
|
||||
}
|
||||
|
||||
// Compute candidate names
|
||||
const renameMap = new Map<Team, Team>();
|
||||
for (const [oldTeam, teamPlayers] of teamToPlayers.entries()) {
|
||||
const newName = computeClanTeamName(teamPlayers);
|
||||
if (newName !== null && newName !== oldTeam) {
|
||||
renameMap.set(oldTeam, newName);
|
||||
}
|
||||
}
|
||||
|
||||
// Collision check: remove any renames that produce duplicate names
|
||||
const newNames = Array.from(renameMap.values());
|
||||
for (const [oldTeam, newName] of renameMap.entries()) {
|
||||
if (newNames.filter((n) => n === newName).length > 1) {
|
||||
renameMap.delete(oldTeam);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply renames to playerTeams array (preserves index order for teamSpawnArea)
|
||||
this.playerTeams = this.playerTeams.map((t) => renameMap.get(t) ?? t);
|
||||
|
||||
// Apply renames to playerToTeam
|
||||
for (const [pi, team] of playerToTeam.entries()) {
|
||||
if (team !== "kicked" && renameMap.has(team)) {
|
||||
playerToTeam.set(pi, renameMap.get(team)!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [playerInfo, team] of playerToTeam.entries()) {
|
||||
if (team === "kicked") {
|
||||
console.warn(`Player ${playerInfo.name} was kicked from team`);
|
||||
|
||||
@@ -102,3 +102,35 @@ export function assignTeamsLobbyPreview(
|
||||
export function getMaxTeamSize(numPlayers: number, numTeams: number): number {
|
||||
return Math.ceil(numPlayers / numTeams);
|
||||
}
|
||||
|
||||
export function computeClanTeamName(players: PlayerInfo[]): string | null {
|
||||
const humans = players.filter((p) => p.playerType === PlayerType.Human);
|
||||
if (humans.length === 0) return null;
|
||||
|
||||
const clanCounts = new Map<string, number>();
|
||||
for (const player of humans) {
|
||||
if (player.clan !== null) {
|
||||
clanCounts.set(player.clan, (clanCounts.get(player.clan) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
if (clanCounts.size === 0) return null;
|
||||
|
||||
const sorted = Array.from(clanCounts.entries()).sort(
|
||||
(a, b) => b[1] - a[1] || a[0].localeCompare(b[0]),
|
||||
);
|
||||
const [topTag, topCount] = sorted[0];
|
||||
const total = humans.length;
|
||||
|
||||
// Unanimous or majority
|
||||
if (topCount / total > 0.5) return topTag;
|
||||
|
||||
// Coalition: top two clans cover the majority of humans
|
||||
if (sorted.length >= 2) {
|
||||
const [secondTag, secondCount] = sorted[1];
|
||||
if ((topCount + secondCount) / total > 0.5) {
|
||||
return `${topTag} / ${secondTag}`;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ColoredTeams, PlayerInfo, PlayerType } from "../src/core/game/Game";
|
||||
import { assignTeams } from "../src/core/game/TeamAssignment";
|
||||
import {
|
||||
assignTeams,
|
||||
computeClanTeamName,
|
||||
} from "../src/core/game/TeamAssignment";
|
||||
|
||||
const teams = [ColoredTeams.Red, ColoredTeams.Blue];
|
||||
|
||||
@@ -164,3 +167,69 @@ describe("assignTeams", () => {
|
||||
expect(result.get(players[13])).toEqual(ColoredTeams.Orange);
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeClanTeamName", () => {
|
||||
const human = (id: string, clan?: string): PlayerInfo => {
|
||||
const name = clan ? `[${clan}]Player${id}` : `Player${id}`;
|
||||
return new PlayerInfo(name, PlayerType.Human, null, id);
|
||||
};
|
||||
|
||||
const bot = (id: string): PlayerInfo =>
|
||||
new PlayerInfo(`Bot${id}`, PlayerType.Bot, null, id);
|
||||
|
||||
it("returns clan tag when all humans share the same clan", () => {
|
||||
const players = [human("1", "ALPHA"), human("2", "ALPHA")];
|
||||
expect(computeClanTeamName(players)).toBe("ALPHA");
|
||||
});
|
||||
|
||||
it("returns majority clan tag when one clan has more than 50% of humans", () => {
|
||||
const players = [
|
||||
human("1", "ALPHA"),
|
||||
human("2", "ALPHA"),
|
||||
human("3", "ALPHA"),
|
||||
human("4", "BETA"),
|
||||
];
|
||||
expect(computeClanTeamName(players)).toBe("ALPHA");
|
||||
});
|
||||
|
||||
it("returns coalition name when top two clans together cover all humans", () => {
|
||||
const players = [human("1", "ALPHA"), human("2", "BETA")];
|
||||
expect(computeClanTeamName(players)).toBe("ALPHA / BETA");
|
||||
});
|
||||
|
||||
it("returns majority tag when majority clan exists despite untagged players", () => {
|
||||
const players = [
|
||||
human("1", "ALPHA"),
|
||||
human("2", "ALPHA"),
|
||||
human("3", "ALPHA"),
|
||||
human("4"),
|
||||
];
|
||||
expect(computeClanTeamName(players)).toBe("ALPHA");
|
||||
});
|
||||
|
||||
it("returns coalition name when two clans together cover the majority of humans", () => {
|
||||
const players = [
|
||||
human("1", "ALPHA"),
|
||||
human("2", "ALPHA"),
|
||||
human("3", "BETA"),
|
||||
human("4", "BETA"),
|
||||
human("5"),
|
||||
];
|
||||
expect(computeClanTeamName(players)).toBe("ALPHA / BETA");
|
||||
});
|
||||
|
||||
it("returns null when no players have clan tags", () => {
|
||||
const players = [human("1"), human("2"), human("3")];
|
||||
expect(computeClanTeamName(players)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when all players are bots", () => {
|
||||
const players = [bot("1"), bot("2")];
|
||||
expect(computeClanTeamName(players)).toBeNull();
|
||||
});
|
||||
|
||||
it("ignores bots when computing clan name", () => {
|
||||
const players = [human("1", "ALPHA"), bot("2")];
|
||||
expect(computeClanTeamName(players)).toBe("ALPHA");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user