diff --git a/src/core/configuration/ColorAllocator.ts b/src/core/configuration/ColorAllocator.ts index f25f8a2fa..6284e3ced 100644 --- a/src/core/configuration/ColorAllocator.ts +++ b/src/core/configuration/ColorAllocator.ts @@ -6,14 +6,14 @@ import { ColoredTeams, Team } from "../game/Game"; import { PseudoRandom } from "../PseudoRandom"; import { simpleHash } from "../Util"; import { - blue, - botColor, - green, - orange, - purple, - red, - teal, - yellow, + blueTeamColors, + botTeamColors, + greenTeamColors, + orangeTeamColors, + purpleTeamColors, + redTeamColors, + tealTeamColors, + yellowTeamColors, } from "./Colors"; extend([lchPlugin]); extend([labPlugin]); @@ -22,12 +22,36 @@ export class ColorAllocator { private availableColors: Colord[]; private fallbackColors: Colord[]; private assigned = new Map(); + private teamPlayerColors = new Map(); constructor(colors: Colord[], fallback: Colord[]) { this.availableColors = [...colors]; this.fallbackColors = [...colors, ...fallback]; } + private getTeamColorVariations(team: Team): Colord[] { + switch (team) { + case ColoredTeams.Blue: + return blueTeamColors; + case ColoredTeams.Red: + return redTeamColors; + case ColoredTeams.Teal: + return tealTeamColors; + case ColoredTeams.Purple: + return purpleTeamColors; + case ColoredTeams.Yellow: + return yellowTeamColors; + case ColoredTeams.Orange: + return orangeTeamColors; + case ColoredTeams.Green: + return greenTeamColors; + case ColoredTeams.Bot: + return botTeamColors; + default: + throw new Error(`Unknown team color: ${team}`); + } + } + assignColor(id: string): Colord { if (this.assigned.has(id)) { return this.assigned.get(id)!; @@ -58,26 +82,23 @@ export class ColorAllocator { } assignTeamColor(team: Team): Colord { - switch (team) { - case ColoredTeams.Blue: - return blue; - case ColoredTeams.Red: - return red; - case ColoredTeams.Teal: - return teal; - case ColoredTeams.Purple: - return purple; - case ColoredTeams.Yellow: - return yellow; - case ColoredTeams.Orange: - return orange; - case ColoredTeams.Green: - return green; - case ColoredTeams.Bot: - return botColor; - default: - return this.assignColor(team); + const teamColors = this.getTeamColorVariations(team); + return teamColors[0]; + } + + assignTeamPlayerColor(team: Team, playerId: string): Colord { + if (this.teamPlayerColors.has(playerId)) { + return this.teamPlayerColors.get(playerId)!; } + + const teamColors = this.getTeamColorVariations(team); + const hashValue = simpleHash(playerId); + const colorIndex = hashValue % teamColors.length; + const color = teamColors[colorIndex]; + + this.teamPlayerColors.set(playerId, color); + + return color; } } diff --git a/src/core/configuration/Colors.ts b/src/core/configuration/Colors.ts index ec10305ab..1a0ccabd9 100644 --- a/src/core/configuration/Colors.ts +++ b/src/core/configuration/Colors.ts @@ -1,17 +1,45 @@ import { colord, Colord, extend } from "colord"; import labPlugin from "colord/plugins/lab"; import lchPlugin from "colord/plugins/lch"; + extend([lchPlugin]); extend([labPlugin]); -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 red = colord({ h: 0, s: 82, l: 56 }); +export const blue = colord({ h: 224, s: 100, l: 58 }); 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 botColor = colord({ h: 36, s: 10, l: 80 }); + +export const redTeamColors: Colord[] = generateTeamColors(red); +export const blueTeamColors: Colord[] = generateTeamColors(blue); +export const tealTeamColors: Colord[] = generateTeamColors(teal); +export const purpleTeamColors: Colord[] = generateTeamColors(purple); +export const yellowTeamColors: Colord[] = generateTeamColors(yellow); +export const orangeTeamColors: Colord[] = generateTeamColors(orange); +export const greenTeamColors: Colord[] = generateTeamColors(green); +export const botTeamColors: Colord[] = [colord(botColor)]; + +function generateTeamColors(baseColor: Colord): Colord[] { + const { h: baseHue, s: baseSaturation, l: baseLightness } = baseColor.toHsl(); + const colorCount = 64; + + return Array.from({ length: colorCount }, (_, index) => { + const progression = index / (colorCount - 1); + + const saturation = baseSaturation * (1.0 - 0.3 * progression); + const lightness = Math.min(100, baseLightness + progression * 30); + + return colord({ + h: baseHue, + s: saturation, + l: lightness, + }); + }); +} export const nationColors: Colord[] = [ colord({ r: 230, g: 100, b: 100 }), // Bright Red diff --git a/src/core/configuration/PastelTheme.ts b/src/core/configuration/PastelTheme.ts index 00ecc67db..2318d6ac3 100644 --- a/src/core/configuration/PastelTheme.ts +++ b/src/core/configuration/PastelTheme.ts @@ -42,7 +42,7 @@ export class PastelTheme implements Theme { territoryColor(player: PlayerView): Colord { const team = player.team(); if (team !== null) { - return this.teamColor(team); + return this.teamColorAllocator.assignTeamPlayerColor(team, player.id()); } if (player.type() === PlayerType.Human) { return this.humanColorAllocator.assignColor(player.id()); diff --git a/src/core/configuration/PastelThemeDark.ts b/src/core/configuration/PastelThemeDark.ts index e02a1862b..73e1e88e7 100644 --- a/src/core/configuration/PastelThemeDark.ts +++ b/src/core/configuration/PastelThemeDark.ts @@ -42,7 +42,7 @@ export class PastelThemeDark implements Theme { territoryColor(player: PlayerView): Colord { const team = player.team(); if (team !== null) { - return this.teamColor(team); + return this.teamColorAllocator.assignTeamPlayerColor(team, player.id()); } if (player.type() === PlayerType.Human) { return this.humanColorAllocator.assignColor(player.id()); diff --git a/tests/Colors.test.ts b/tests/Colors.test.ts index a4167419a..bbeccde57 100644 --- a/tests/Colors.test.ts +++ b/tests/Colors.test.ts @@ -3,7 +3,16 @@ import { ColorAllocator, selectDistinctColorIndex, } from "../src/core/configuration/ColorAllocator"; -import { blue, botColor, red, teal } from "../src/core/configuration/Colors"; +import { + blue, + botColor, + green, + orange, + purple, + red, + teal, + yellow, +} from "../src/core/configuration/Colors"; import { ColoredTeams } from "../src/core/game/Game"; const mockColors: Colord[] = [ @@ -72,12 +81,69 @@ describe("ColorAllocator", () => { expect(c2.isEqual(c2Again)).toBe(true); }); - test("assignTeamColor returns the expected static color for known teams", () => { + test("assignTeamColor returns the base color from the team", () => { expect(allocator.assignTeamColor(ColoredTeams.Blue)).toEqual(blue); expect(allocator.assignTeamColor(ColoredTeams.Red)).toEqual(red); expect(allocator.assignTeamColor(ColoredTeams.Teal)).toEqual(teal); + expect(allocator.assignTeamColor(ColoredTeams.Purple)).toEqual(purple); + expect(allocator.assignTeamColor(ColoredTeams.Yellow)).toEqual(yellow); + expect(allocator.assignTeamColor(ColoredTeams.Orange)).toEqual(orange); + expect(allocator.assignTeamColor(ColoredTeams.Green)).toEqual(green); expect(allocator.assignTeamColor(ColoredTeams.Bot)).toEqual(botColor); }); + + test("assignTeamPlayerColor always returns the same color for the same playerID", () => { + const playerId = "player123"; + + const blueColor1 = allocator.assignTeamPlayerColor( + ColoredTeams.Blue, + playerId, + ); + const blueColor2 = allocator.assignTeamPlayerColor( + ColoredTeams.Blue, + playerId, + ); + + expect(blueColor1.isEqual(blueColor2)).toBe(true); + + const redColor1 = allocator.assignTeamPlayerColor( + ColoredTeams.Red, + playerId, + ); + const redColor2 = allocator.assignTeamPlayerColor( + ColoredTeams.Red, + playerId, + ); + + expect(redColor1.isEqual(redColor2)).toBe(true); + }); + + test("assignTeamPlayerColor returns a different color when the playerID is different", () => { + const playerIdOne = "player1"; + const playerIdTwo = "player2"; + + const blueColorPlayerOne = allocator.assignTeamPlayerColor( + ColoredTeams.Blue, + playerIdOne, + ); + const blueColorPlayerTwo = allocator.assignTeamPlayerColor( + ColoredTeams.Blue, + playerIdTwo, + ); + + expect(blueColorPlayerOne.isEqual(blueColorPlayerTwo)).toBe(false); + + const redColorPlayerOne = allocator.assignTeamPlayerColor( + ColoredTeams.Red, + playerIdOne, + ); + const redColorPlayerTwo = allocator.assignTeamPlayerColor( + ColoredTeams.Red, + playerIdTwo, + ); + + expect(redColorPlayerOne.isEqual(redColorPlayerTwo)).toBe(false); + }); }); describe("selectDistinctColor", () => {