mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:10:42 +00:00
Allow up to seven teams for players (#445)
## Description: - Allow up to seven teams for players, and one for bots. - Add team count selection to the single player dialog. - Select random number of teams in server rotation. Fixes #456 ## 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: fake.neo --------- Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com>
This commit is contained in:
@@ -147,6 +147,7 @@
|
||||
"host_modal": {
|
||||
"title": "Private Lobby",
|
||||
"mode": "Mode",
|
||||
"team_count": "Number of Teams",
|
||||
"options_title": "Options",
|
||||
"bots": "Bots: ",
|
||||
"bots_disabled": "Disabled",
|
||||
|
||||
@@ -23,6 +23,7 @@ export class HostLobbyModal extends LitElement {
|
||||
@state() private selectedDifficulty: Difficulty = Difficulty.Medium;
|
||||
@state() private disableNPCs = false;
|
||||
@state() private gameMode: GameMode = GameMode.FFA;
|
||||
@state() private teamCount: number = 2;
|
||||
@state() private disableNukes: boolean = false;
|
||||
@state() private bots: number = 400;
|
||||
@state() private infiniteGold: boolean = false;
|
||||
@@ -159,6 +160,33 @@ export class HostLobbyModal extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${
|
||||
this.gameMode === GameMode.FFA
|
||||
? ""
|
||||
: html`
|
||||
<!-- Team Count Selection -->
|
||||
<div class="options-section">
|
||||
<div class="option-title">
|
||||
${translateText("host_modal.team_count")}
|
||||
</div>
|
||||
<div class="option-cards">
|
||||
${[2, 3, 4, 5, 6, 7].map(
|
||||
(o) => html`
|
||||
<div
|
||||
class="option-card ${this.teamCount === o
|
||||
? "selected"
|
||||
: ""}"
|
||||
@click=${() => this.handleTeamCountSelection(o)}
|
||||
>
|
||||
<div class="option-card-title">${o}</div>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
<!-- Game Options -->
|
||||
<div class="options-section">
|
||||
<div class="option-title">
|
||||
@@ -413,6 +441,11 @@ export class HostLobbyModal extends LitElement {
|
||||
this.putGameConfig();
|
||||
}
|
||||
|
||||
private async handleTeamCountSelection(value: number) {
|
||||
this.teamCount = value;
|
||||
this.putGameConfig();
|
||||
}
|
||||
|
||||
private async putGameConfig() {
|
||||
const config = await getServerConfigFromClient();
|
||||
const response = await fetch(
|
||||
@@ -432,6 +465,7 @@ export class HostLobbyModal extends LitElement {
|
||||
infiniteTroops: this.infiniteTroops,
|
||||
instantBuild: this.instantBuild,
|
||||
gameMode: this.gameMode,
|
||||
numPlayerTeams: this.teamCount,
|
||||
} as GameConfig),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -30,6 +30,7 @@ export class SinglePlayerModal extends LitElement {
|
||||
@state() private instantBuild: boolean = false;
|
||||
@state() private useRandomMap: boolean = false;
|
||||
@state() private gameMode: GameMode = GameMode.FFA;
|
||||
@state() private teamCount: number = 2;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
@@ -136,6 +137,31 @@ export class SinglePlayerModal extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.gameMode === GameMode.FFA
|
||||
? ""
|
||||
: html`
|
||||
<!-- Team Count Selection -->
|
||||
<div class="options-section">
|
||||
<div class="option-title">
|
||||
${translateText("host_modal.team_count")}
|
||||
</div>
|
||||
<div class="option-cards">
|
||||
${[2, 3, 4, 5, 6, 7].map(
|
||||
(o) => html`
|
||||
<div
|
||||
class="option-card ${this.teamCount === o
|
||||
? "selected"
|
||||
: ""}"
|
||||
@click=${() => this.handleTeamCountSelection(o)}
|
||||
>
|
||||
<div class="option-card-title">${o}</div>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
|
||||
<!-- Game Options -->
|
||||
<div class="options-section">
|
||||
<div class="option-title">
|
||||
@@ -310,6 +336,10 @@ export class SinglePlayerModal extends LitElement {
|
||||
this.gameMode = value;
|
||||
}
|
||||
|
||||
private handleTeamCountSelection(value: number) {
|
||||
this.teamCount = value;
|
||||
}
|
||||
|
||||
private getRandomMap(): GameMapType {
|
||||
const maps = Object.values(GameMapType);
|
||||
const randIdx = Math.floor(Math.random() * maps.length);
|
||||
@@ -361,6 +391,7 @@ export class SinglePlayerModal extends LitElement {
|
||||
gameMap: this.selectedMap,
|
||||
gameType: GameType.Singleplayer,
|
||||
gameMode: this.gameMode,
|
||||
numPlayerTeams: this.teamCount,
|
||||
difficulty: this.selectedDifficulty,
|
||||
disableNPCs: this.disableNPCs,
|
||||
disableNukes: this.disableNukes,
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { blue, red } from "../../../core/configuration/Colors";
|
||||
import { GameMode, Team } from "../../../core/game/Game";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
export class SpawnTimer implements Layer {
|
||||
private ratio = 0;
|
||||
private leftColor = "rgba(0, 128, 255, 0.7)";
|
||||
private rightColor = "rgba(0, 0, 0, 0.5)";
|
||||
private ratios = [0];
|
||||
private colors = ["rgba(0, 128, 255, 0.7)", "rgba(0, 0, 0, 0.5)"];
|
||||
|
||||
constructor(
|
||||
private game: GameView,
|
||||
@@ -18,27 +16,35 @@ export class SpawnTimer implements Layer {
|
||||
|
||||
tick() {
|
||||
if (this.game.inSpawnPhase()) {
|
||||
this.ratio = this.game.ticks() / this.game.config().numSpawnPhaseTurns();
|
||||
this.ratios[0] =
|
||||
this.game.ticks() / this.game.config().numSpawnPhaseTurns();
|
||||
return;
|
||||
}
|
||||
|
||||
this.ratios = [];
|
||||
this.colors = [];
|
||||
|
||||
if (this.game.config().gameConfig().gameMode != GameMode.Team) {
|
||||
this.ratio = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const numBlueTiles = this.game
|
||||
.players()
|
||||
.filter((p) => p.team() == Team.Blue)
|
||||
.reduce((acc, p) => acc + p.numTilesOwned(), 0);
|
||||
const teamTiles: Map<Team, number> = new Map();
|
||||
for (const player of this.game.players()) {
|
||||
const team = player.team();
|
||||
const tiles = teamTiles.get(team) ?? 0;
|
||||
const sum = tiles + player.numTilesOwned();
|
||||
teamTiles.set(team, sum);
|
||||
}
|
||||
|
||||
const numRedTiles = this.game
|
||||
.players()
|
||||
.filter((p) => p.team() == Team.Red)
|
||||
.reduce((acc, p) => acc + p.numTilesOwned(), 0);
|
||||
|
||||
this.ratio = numBlueTiles / (numBlueTiles + numRedTiles);
|
||||
this.leftColor = blue.toRgbString();
|
||||
this.rightColor = red.toRgbString();
|
||||
const theme = this.game.config().theme();
|
||||
const total = sumIterator(teamTiles.values());
|
||||
if (total === 0) return;
|
||||
for (const [team, count] of teamTiles) {
|
||||
const ratio = count / total;
|
||||
const color = theme.teamColor(team).toRgbString();
|
||||
this.ratios.push(ratio);
|
||||
this.colors.push(color);
|
||||
}
|
||||
}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
@@ -46,18 +52,34 @@ export class SpawnTimer implements Layer {
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
if (this.ratio == 0) {
|
||||
return;
|
||||
}
|
||||
if (this.ratios === null) return;
|
||||
if (this.ratios.length === 0) return;
|
||||
if (this.colors.length === 0) return;
|
||||
|
||||
const barHeight = 10;
|
||||
const barBackgroundWidth = this.transformHandler.width();
|
||||
const barWidth = this.transformHandler.width();
|
||||
|
||||
// Draw bar background
|
||||
context.fillStyle = this.rightColor;
|
||||
context.fillRect(0, 0, barBackgroundWidth, barHeight);
|
||||
let x = 0;
|
||||
let filledRatio = 0;
|
||||
for (let i = 0; i < this.ratios.length && i < this.colors.length; i++) {
|
||||
const ratio = this.ratios[i];
|
||||
const segmentWidth = barWidth * ratio;
|
||||
|
||||
context.fillStyle = this.leftColor;
|
||||
context.fillRect(0, 0, barBackgroundWidth * this.ratio, barHeight);
|
||||
context.fillStyle = this.colors[i];
|
||||
context.fillRect(x, 0, segmentWidth, barHeight);
|
||||
|
||||
x += segmentWidth;
|
||||
filledRatio += ratio;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sumIterator(values: MapIterator<number>) {
|
||||
// To use reduce, we'd need to allocate an array:
|
||||
// return Array.from(values).reduce((sum, v) => sum + v, 0);
|
||||
let total = 0;
|
||||
for (const value of values) {
|
||||
total += value;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
@@ -121,6 +121,7 @@ const GameConfigSchema = z.object({
|
||||
infiniteTroops: z.boolean(),
|
||||
instantBuild: z.boolean(),
|
||||
maxPlayers: z.number().optional(),
|
||||
numPlayerTeams: z.number().optional(),
|
||||
});
|
||||
|
||||
const SafeString = z
|
||||
|
||||
@@ -2,6 +2,11 @@ import { colord, Colord } from "colord";
|
||||
|
||||
export const red: Colord = colord({ r: 235, g: 53, b: 53 }); // Bright Red
|
||||
export const blue: Colord = colord({ r: 41, g: 98, b: 255 }); // Royal Blue
|
||||
export const teal = colord({ h: 172, s: 66, l: 50 });
|
||||
export const purple = colord({ h: 271, s: 81, l: 56 });
|
||||
export const yellow = colord({ h: 45, s: 93, l: 47 });
|
||||
export const orange = colord({ h: 25, s: 95, l: 53 });
|
||||
export const green = colord({ h: 128, s: 49, l: 50 });
|
||||
export const botColor: Colord = colord({ r: 210, g: 206, b: 200 }); // Muted Beige Gray
|
||||
|
||||
export const territoryColors: Colord[] = [
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Gold,
|
||||
Player,
|
||||
PlayerInfo,
|
||||
Team,
|
||||
TerraNullius,
|
||||
Tick,
|
||||
UnitInfo,
|
||||
@@ -65,6 +66,7 @@ export interface Config {
|
||||
instantBuild(): boolean;
|
||||
numSpawnPhaseTurns(): number;
|
||||
userSettings(): UserSettings;
|
||||
numPlayerTeams(): number;
|
||||
|
||||
startManpower(playerInfo: PlayerInfo): number;
|
||||
populationIncreaseRate(player: Player | PlayerView): number;
|
||||
@@ -122,6 +124,7 @@ export interface Config {
|
||||
}
|
||||
|
||||
export interface Theme {
|
||||
teamColor(team: Team): Colord;
|
||||
territoryColor(playerInfo: PlayerView): Colord;
|
||||
specialBuildingColor(playerInfo: PlayerView): Colord;
|
||||
borderColor(playerInfo: PlayerView): Colord;
|
||||
|
||||
@@ -189,6 +189,9 @@ export class DefaultConfig implements Config {
|
||||
defensePostDefenseBonus(): number {
|
||||
return 5;
|
||||
}
|
||||
numPlayerTeams(): number {
|
||||
return this._gameConfig.numPlayerTeams ?? 0;
|
||||
}
|
||||
spawnNPCs(): boolean {
|
||||
return !this._gameConfig.disableNPCs;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,14 @@ import {
|
||||
blue,
|
||||
botColor,
|
||||
botColors,
|
||||
green,
|
||||
humanColors,
|
||||
orange,
|
||||
purple,
|
||||
red,
|
||||
teal,
|
||||
territoryColors,
|
||||
yellow,
|
||||
} from "./Colors";
|
||||
import { Theme } from "./Config";
|
||||
|
||||
@@ -36,15 +41,31 @@ export const pastelTheme = new (class implements Theme {
|
||||
|
||||
private _spawnHighlightColor = colord({ r: 255, g: 213, b: 79 });
|
||||
|
||||
teamColor(team: Team): Colord {
|
||||
switch (team) {
|
||||
case Team.Blue:
|
||||
return blue;
|
||||
case Team.Red:
|
||||
return red;
|
||||
case Team.Teal:
|
||||
return teal;
|
||||
case Team.Purple:
|
||||
return purple;
|
||||
case Team.Yellow:
|
||||
return yellow;
|
||||
case Team.Orange:
|
||||
return orange;
|
||||
case Team.Green:
|
||||
return green;
|
||||
case Team.Bot:
|
||||
return botColor;
|
||||
}
|
||||
throw new Error(`Missing color for ${team}`);
|
||||
}
|
||||
|
||||
territoryColor(player: PlayerView): Colord {
|
||||
if (player.team() == Team.Bot) {
|
||||
return botColor;
|
||||
}
|
||||
if (player.team() == Team.Red) {
|
||||
return red;
|
||||
}
|
||||
if (player.team() == Team.Blue) {
|
||||
return blue;
|
||||
if (player.team() !== null) {
|
||||
return this.teamColor(player.team());
|
||||
}
|
||||
if (player.info().playerType == PlayerType.Human) {
|
||||
return humanColors[simpleHash(player.id()) % humanColors.length];
|
||||
|
||||
@@ -8,9 +8,14 @@ import {
|
||||
blue,
|
||||
botColor,
|
||||
botColors,
|
||||
green,
|
||||
humanColors,
|
||||
orange,
|
||||
purple,
|
||||
red,
|
||||
teal,
|
||||
territoryColors,
|
||||
yellow,
|
||||
} from "./Colors";
|
||||
import { Theme } from "./Config";
|
||||
|
||||
@@ -36,15 +41,31 @@ export const pastelThemeDark = new (class implements Theme {
|
||||
|
||||
private _spawnHighlightColor = colord({ r: 255, g: 213, b: 79 });
|
||||
|
||||
teamColor(team: Team): Colord {
|
||||
switch (team) {
|
||||
case Team.Blue:
|
||||
return blue;
|
||||
case Team.Red:
|
||||
return red;
|
||||
case Team.Teal:
|
||||
return teal;
|
||||
case Team.Purple:
|
||||
return purple;
|
||||
case Team.Yellow:
|
||||
return yellow;
|
||||
case Team.Orange:
|
||||
return orange;
|
||||
case Team.Green:
|
||||
return green;
|
||||
case Team.Bot:
|
||||
return botColor;
|
||||
}
|
||||
throw new Error(`Missing color for ${team}`);
|
||||
}
|
||||
|
||||
territoryColor(player: PlayerView): Colord {
|
||||
if (player.team() == Team.Bot) {
|
||||
return botColor;
|
||||
}
|
||||
if (player.team() == Team.Red) {
|
||||
return red;
|
||||
}
|
||||
if (player.team() == Team.Blue) {
|
||||
return blue;
|
||||
if (player.team() !== null) {
|
||||
return this.teamColor(player.team());
|
||||
}
|
||||
if (player.info().playerType == PlayerType.Human) {
|
||||
return humanColors[simpleHash(player.id()) % humanColors.length];
|
||||
|
||||
@@ -40,6 +40,11 @@ export enum Difficulty {
|
||||
export enum Team {
|
||||
Red = "Red",
|
||||
Blue = "Blue",
|
||||
Teal = "Teal",
|
||||
Purple = "Purple",
|
||||
Yellow = "Yellow",
|
||||
Orange = "Orange",
|
||||
Green = "Green",
|
||||
Bot = "Bot",
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,17 @@ export class GameImpl implements Game {
|
||||
),
|
||||
);
|
||||
this.unitGrid = new UnitGrid(this._map);
|
||||
|
||||
if (_config.gameConfig().gameMode === GameMode.Team) {
|
||||
const numPlayerTeams = _config.numPlayerTeams();
|
||||
if (numPlayerTeams < 2) throw new Error("Too few teams!");
|
||||
if (numPlayerTeams >= 3) this.playerTeams.push(Team.Teal);
|
||||
if (numPlayerTeams >= 4) this.playerTeams.push(Team.Purple);
|
||||
if (numPlayerTeams >= 5) this.playerTeams.push(Team.Yellow);
|
||||
if (numPlayerTeams >= 6) this.playerTeams.push(Team.Orange);
|
||||
if (numPlayerTeams >= 7) this.playerTeams.push(Team.Green);
|
||||
if (numPlayerTeams >= 8) throw new Error("Too many teams!");
|
||||
}
|
||||
}
|
||||
|
||||
private addHumans() {
|
||||
@@ -106,7 +117,7 @@ export class GameImpl implements Game {
|
||||
this._humans.forEach((p) => this.addPlayer(p));
|
||||
return;
|
||||
}
|
||||
const playerToTeam = assignTeams(this._humans);
|
||||
const playerToTeam = assignTeams(this._humans, this.playerTeams);
|
||||
for (const [playerInfo, team] of playerToTeam.entries()) {
|
||||
if (team == "kicked") {
|
||||
console.warn(`Player ${playerInfo.name} was kicked from team`);
|
||||
|
||||
@@ -178,7 +178,7 @@ export class PlayerView {
|
||||
return this.data.id;
|
||||
}
|
||||
team(): Team | null {
|
||||
return this.data.team;
|
||||
return this.data.team ?? null;
|
||||
}
|
||||
type(): PlayerType {
|
||||
return this.data.playerType;
|
||||
|
||||
@@ -2,10 +2,10 @@ import { PlayerInfo, Team } from "./Game";
|
||||
|
||||
export function assignTeams(
|
||||
players: PlayerInfo[],
|
||||
teams: Team[],
|
||||
): Map<PlayerInfo, Team | "kicked"> {
|
||||
const result = new Map<PlayerInfo, Team | "kicked">();
|
||||
let redTeamCount = 0;
|
||||
let blueTeamCount = 0;
|
||||
const teamPlayerCount = new Map<Team, number>();
|
||||
|
||||
// Group players by clan
|
||||
const clanGroups = new Map<string, PlayerInfo[]>();
|
||||
@@ -23,7 +23,7 @@ export function assignTeams(
|
||||
}
|
||||
}
|
||||
|
||||
const maxTeamSize = Math.ceil(players.length / 2);
|
||||
const maxTeamSize = Math.ceil(players.length / teams.length);
|
||||
|
||||
// Sort clans by size (largest first)
|
||||
const sortedClans = Array.from(clanGroups.entries()).sort(
|
||||
@@ -33,38 +33,38 @@ export function assignTeams(
|
||||
// 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, Team.Red);
|
||||
} else {
|
||||
result.set(player, "kicked");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Assign to blue team
|
||||
for (const player of clanPlayers) {
|
||||
if (blueTeamCount < maxTeamSize) {
|
||||
blueTeamCount++;
|
||||
result.set(player, Team.Blue);
|
||||
} else {
|
||||
result.set(player, "kicked");
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
for (const player of clanPlayers) {
|
||||
if (teamSize < maxTeamSize) {
|
||||
teamSize++;
|
||||
result.set(player, team);
|
||||
} else {
|
||||
result.set(player, "kicked");
|
||||
}
|
||||
}
|
||||
teamPlayerCount.set(team, teamSize);
|
||||
}
|
||||
|
||||
// Then, assign non-clan players to balance teams
|
||||
for (const player of noClanPlayers) {
|
||||
if (redTeamCount <= blueTeamCount) {
|
||||
redTeamCount++;
|
||||
result.set(player, Team.Red);
|
||||
} else {
|
||||
blueTeamCount++;
|
||||
result.set(player, Team.Blue);
|
||||
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;
|
||||
}
|
||||
teamPlayerCount.set(team, teamSize + 1);
|
||||
result.set(player, team);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -237,9 +237,11 @@ async function schedulePublicGame(playlist: MapPlaylist) {
|
||||
}
|
||||
|
||||
const gameMode = playlist.getNextGameMode();
|
||||
const numPlayerTeams =
|
||||
gameMode === GameMode.Team ? 2 + Math.floor(Math.random() * 5) : undefined;
|
||||
|
||||
// Create the default public game config (from your GameManager)
|
||||
const defaultGameConfig = {
|
||||
const defaultGameConfig: GameConfig = {
|
||||
gameMap: map,
|
||||
maxPlayers: config.lobbyMaxPlayers(map),
|
||||
gameType: GameType.Public,
|
||||
@@ -249,9 +251,10 @@ async function schedulePublicGame(playlist: MapPlaylist) {
|
||||
instantBuild: false,
|
||||
disableNPCs: gameMode == GameMode.Team,
|
||||
disableNukes: false,
|
||||
gameMode: gameMode,
|
||||
gameMode,
|
||||
numPlayerTeams,
|
||||
bots: 400,
|
||||
} as GameConfig;
|
||||
};
|
||||
|
||||
const workerPath = config.workerPath(gameID);
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { PlayerInfo, PlayerType, Team } from "../src/core/game/Game";
|
||||
import { assignTeams } from "../src/core/game/TeamAssignment";
|
||||
|
||||
const teams = [Team.Red, Team.Blue];
|
||||
|
||||
describe("assignTeams", () => {
|
||||
const createPlayer = (id: string, clan?: string): PlayerInfo => {
|
||||
const name = clan ? `[${clan}]Player ${id}` : `Player ${id}`;
|
||||
@@ -22,7 +24,7 @@ describe("assignTeams", () => {
|
||||
createPlayer("4"),
|
||||
];
|
||||
|
||||
const result = assignTeams(players);
|
||||
const result = assignTeams(players, teams);
|
||||
|
||||
// Check that players are assigned alternately
|
||||
expect(result.get(players[0])).toEqual(Team.Red);
|
||||
@@ -39,7 +41,7 @@ describe("assignTeams", () => {
|
||||
createPlayer("4", "CLANB"),
|
||||
];
|
||||
|
||||
const result = assignTeams(players);
|
||||
const result = assignTeams(players, teams);
|
||||
|
||||
// Check that clan members are on the same team
|
||||
expect(result.get(players[0])).toEqual(Team.Red);
|
||||
@@ -56,7 +58,7 @@ describe("assignTeams", () => {
|
||||
createPlayer("4"),
|
||||
];
|
||||
|
||||
const result = assignTeams(players);
|
||||
const result = assignTeams(players, teams);
|
||||
|
||||
// Check that clan members are together and non-clan players balance teams
|
||||
expect(result.get(players[0])).toEqual(Team.Red);
|
||||
@@ -75,7 +77,7 @@ describe("assignTeams", () => {
|
||||
createPlayer("6", "CLANB"),
|
||||
];
|
||||
|
||||
const result = assignTeams(players);
|
||||
const result = assignTeams(players, teams);
|
||||
|
||||
// Check that players are kicked when teams are full
|
||||
expect(result.get(players[0])).toEqual(Team.Red);
|
||||
@@ -89,13 +91,13 @@ describe("assignTeams", () => {
|
||||
});
|
||||
|
||||
it("should handle empty player list", () => {
|
||||
const result = assignTeams([]);
|
||||
const result = assignTeams([], teams);
|
||||
expect(result.size).toBe(0);
|
||||
});
|
||||
|
||||
it("should handle single player", () => {
|
||||
const players = [createPlayer("1")];
|
||||
const result = assignTeams(players);
|
||||
const result = assignTeams(players, teams);
|
||||
expect(result.get(players[0])).toEqual(Team.Red);
|
||||
});
|
||||
|
||||
@@ -109,7 +111,7 @@ describe("assignTeams", () => {
|
||||
createPlayer("6", "CLANC"),
|
||||
];
|
||||
|
||||
const result = assignTeams(players);
|
||||
const result = assignTeams(players, teams);
|
||||
|
||||
// Check that larger clans are assigned first
|
||||
expect(result.get(players[0])).toEqual(Team.Red);
|
||||
@@ -119,4 +121,48 @@ describe("assignTeams", () => {
|
||||
expect(result.get(players[4])).toEqual(Team.Blue);
|
||||
expect(result.get(players[5])).toEqual(Team.Blue);
|
||||
});
|
||||
|
||||
it("should distribute players among a larger number of teams", () => {
|
||||
const players = [
|
||||
createPlayer("1", "CLANA"),
|
||||
createPlayer("2", "CLANA"),
|
||||
createPlayer("3", "CLANA"),
|
||||
createPlayer("4", "CLANB"),
|
||||
createPlayer("5", "CLANB"),
|
||||
createPlayer("6", "CLANC"),
|
||||
createPlayer("7"),
|
||||
createPlayer("8"),
|
||||
createPlayer("9"),
|
||||
createPlayer("10"),
|
||||
createPlayer("11"),
|
||||
createPlayer("12"),
|
||||
createPlayer("13"),
|
||||
createPlayer("14"),
|
||||
];
|
||||
|
||||
const result = assignTeams(players, [
|
||||
Team.Red,
|
||||
Team.Blue,
|
||||
Team.Teal,
|
||||
Team.Purple,
|
||||
Team.Yellow,
|
||||
Team.Orange,
|
||||
Team.Green,
|
||||
]);
|
||||
|
||||
expect(result.get(players[0])).toEqual(Team.Red);
|
||||
expect(result.get(players[1])).toEqual(Team.Red);
|
||||
expect(result.get(players[2])).toEqual("kicked");
|
||||
expect(result.get(players[3])).toEqual(Team.Blue);
|
||||
expect(result.get(players[4])).toEqual(Team.Blue);
|
||||
expect(result.get(players[5])).toEqual(Team.Teal);
|
||||
expect(result.get(players[6])).toEqual(Team.Purple);
|
||||
expect(result.get(players[7])).toEqual(Team.Yellow);
|
||||
expect(result.get(players[8])).toEqual(Team.Orange);
|
||||
expect(result.get(players[9])).toEqual(Team.Green);
|
||||
expect(result.get(players[10])).toEqual(Team.Teal);
|
||||
expect(result.get(players[11])).toEqual(Team.Purple);
|
||||
expect(result.get(players[12])).toEqual(Team.Yellow);
|
||||
expect(result.get(players[13])).toEqual(Team.Orange);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user