From cd799b514c201584d7d64611bbd169ed7040a48d Mon Sep 17 00:00:00 2001 From: Christopher Mesona <45428623+Ble4Ch@users.noreply.github.com> Date: Tue, 10 Jun 2025 22:55:53 +0200 Subject: [PATCH] feat: assign unique colors for players (#1063) ## Description: * adds 100+ colors for players * assigns a unique color for each player * bot and team colors assignment unchanged ![image](https://github.com/user-attachments/assets/75061a50-7166-4c0b-8f53-b35074a85706) ## 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 - [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: George --------- Co-authored-by: cmesona --- src/core/configuration/Colors.ts | 314 +++++++++++++++------- src/core/configuration/PastelTheme.ts | 47 +--- src/core/configuration/PastelThemeDark.ts | 47 +--- tests/Colors.test.ts | 81 ++++++ 4 files changed, 320 insertions(+), 169 deletions(-) create mode 100644 tests/Colors.test.ts diff --git a/src/core/configuration/Colors.ts b/src/core/configuration/Colors.ts index f2bbfd0fe..747862cff 100644 --- a/src/core/configuration/Colors.ts +++ b/src/core/configuration/Colors.ts @@ -1,4 +1,6 @@ import { colord, Colord } from "colord"; +import { ColoredTeams, Team } from "../game/Game"; +import { simpleHash } from "../Util"; 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 @@ -9,7 +11,7 @@ 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[] = [ +export const nationColors: Colord[] = [ colord({ r: 230, g: 100, b: 100 }), // Bright Red colord({ r: 100, g: 180, b: 230 }), // Sky Blue colord({ r: 230, g: 180, b: 80 }), // Golden Yellow @@ -109,110 +111,71 @@ export const territoryColors: Colord[] = [ colord({ r: 170, g: 150, b: 170 }), // Dusty Rose ]; +// Bright pastel theme with 64 colors export const humanColors: Colord[] = [ - // Original set - colord({ r: 235, g: 75, b: 75 }), // Bright Red - colord({ r: 67, g: 190, b: 84 }), // Fresh Green - colord({ r: 59, g: 130, b: 246 }), // Royal Blue - colord({ r: 245, g: 158, b: 11 }), // Amber - colord({ r: 236, g: 72, b: 153 }), // Deep Pink - colord({ r: 48, g: 178, b: 180 }), // Teal - colord({ r: 168, g: 85, b: 247 }), // Vibrant Purple - colord({ r: 251, g: 191, b: 36 }), // Marigold - colord({ r: 74, g: 222, b: 128 }), // Mint - colord({ r: 239, g: 68, b: 68 }), // Crimson - colord({ r: 34, g: 197, b: 94 }), // Emerald - colord({ r: 96, g: 165, b: 250 }), // Sky Blue - colord({ r: 249, g: 115, b: 22 }), // Tangerine - colord({ r: 192, g: 132, b: 252 }), // Lavender - colord({ r: 45, g: 212, b: 191 }), // Turquoise - colord({ r: 244, g: 114, b: 182 }), // Rose - colord({ r: 132, g: 204, b: 22 }), // Lime - colord({ r: 56, g: 189, b: 248 }), // Light Blue - colord({ r: 234, g: 179, b: 8 }), // Sunflower - colord({ r: 217, g: 70, b: 239 }), // Fuchsia colord({ r: 16, g: 185, b: 129 }), // Sea Green - colord({ r: 251, g: 146, b: 60 }), // Light Orange - colord({ r: 147, g: 51, b: 234 }), // Bright Purple - colord({ r: 79, g: 70, b: 229 }), // Indigo - colord({ r: 245, g: 101, b: 101 }), // Coral - colord({ r: 134, g: 239, b: 172 }), // Light Green - colord({ r: 59, g: 130, b: 246 }), // Cerulean - colord({ r: 253, g: 164, b: 175 }), // Salmon Pink - colord({ r: 147, g: 197, b: 253 }), // Powder Blue - colord({ r: 252, g: 211, b: 77 }), // Golden - colord({ r: 190, g: 92, b: 251 }), // Amethyst - colord({ r: 82, g: 183, b: 136 }), // Jade - colord({ r: 248, g: 113, b: 113 }), // Warm Red - colord({ r: 99, g: 202, b: 253 }), // Azure - colord({ r: 240, g: 171, b: 252 }), // Orchid - colord({ r: 163, g: 230, b: 53 }), // Yellow Green - colord({ r: 234, g: 88, b: 12 }), // Burnt Orange - colord({ r: 125, g: 211, b: 252 }), // Crystal Blue - colord({ r: 251, g: 113, b: 133 }), // Watermelon + colord({ r: 34, g: 197, b: 94 }), // Emerald + colord({ r: 45, g: 212, b: 191 }), // Turquoise + colord({ r: 48, g: 178, b: 180 }), // Teal colord({ r: 52, g: 211, b: 153 }), // Spearmint - colord({ r: 167, g: 139, b: 250 }), // Periwinkle - colord({ r: 245, g: 158, b: 11 }), // Honey + colord({ r: 56, g: 189, b: 248 }), // Light Blue + colord({ r: 59, g: 130, b: 246 }), // Royal Blue + colord({ r: 67, g: 190, b: 84 }), // Fresh Green + colord({ r: 74, g: 222, b: 128 }), // Mint + colord({ r: 79, g: 70, b: 229 }), // Indigo + colord({ r: 82, g: 183, b: 136 }), // Jade + colord({ r: 96, g: 165, b: 250 }), // Sky Blue + colord({ r: 99, g: 202, b: 253 }), // Azure colord({ r: 110, g: 231, b: 183 }), // Seafoam - colord({ r: 233, g: 213, b: 255 }), // Light Lilac - colord({ r: 202, g: 138, b: 4 }), // Rich Gold - colord({ r: 151, g: 255, b: 187 }), // Fresh Mint - colord({ r: 220, g: 38, b: 38 }), // Ruby colord({ r: 124, g: 58, b: 237 }), // Royal Purple - colord({ r: 45, g: 212, b: 191 }), // Ocean - colord({ r: 252, g: 165, b: 165 }), // Peach - - // Additional 50 colors - colord({ r: 179, g: 136, b: 255 }), // Light Purple + colord({ r: 125, g: 211, b: 252 }), // Crystal Blue + colord({ r: 132, g: 204, b: 22 }), // Lime colord({ r: 133, g: 77, b: 14 }), // Chocolate - colord({ r: 52, g: 211, b: 153 }), // Aquamarine - colord({ r: 234, g: 179, b: 8 }), // Mustard - colord({ r: 236, g: 72, b: 153 }), // Hot Pink - colord({ r: 147, g: 197, b: 253 }), // Sky - colord({ r: 249, g: 115, b: 22 }), // Pumpkin - colord({ r: 167, g: 139, b: 250 }), // Iris - colord({ r: 16, g: 185, b: 129 }), // Pine - colord({ r: 251, g: 146, b: 60 }), // Mango - colord({ r: 192, g: 132, b: 252 }), // Wisteria - colord({ r: 79, g: 70, b: 229 }), // Sapphire - colord({ r: 245, g: 101, b: 101 }), // Salmon - colord({ r: 134, g: 239, b: 172 }), // Spring Green - colord({ r: 59, g: 130, b: 246 }), // Ocean Blue - colord({ r: 253, g: 164, b: 175 }), // Rose Gold - colord({ r: 16, g: 185, b: 129 }), // Forest - colord({ r: 252, g: 211, b: 77 }), // Sunshine - colord({ r: 190, g: 92, b: 251 }), // Grape - colord({ r: 82, g: 183, b: 136 }), // Eucalyptus - colord({ r: 248, g: 113, b: 113 }), // Cherry - colord({ r: 99, g: 202, b: 253 }), // Arctic - colord({ r: 240, g: 171, b: 252 }), // Lilac - colord({ r: 163, g: 230, b: 53 }), // Chartreuse - colord({ r: 234, g: 88, b: 12 }), // Rust - colord({ r: 125, g: 211, b: 252 }), // Ice Blue - colord({ r: 251, g: 113, b: 133 }), // Strawberry - colord({ r: 52, g: 211, b: 153 }), // Sage - colord({ r: 167, g: 139, b: 250 }), // Violet - colord({ r: 245, g: 158, b: 11 }), // Apricot - colord({ r: 110, g: 231, b: 183 }), // Mint Green - colord({ r: 233, g: 213, b: 255 }), // Thistle - colord({ r: 202, g: 138, b: 4 }), // Bronze - colord({ r: 151, g: 255, b: 187 }), // Pistachio - colord({ r: 220, g: 38, b: 38 }), // Fire Engine - colord({ r: 124, g: 58, b: 237 }), // Electric Purple - colord({ r: 45, g: 212, b: 191 }), // Caribbean - colord({ r: 252, g: 165, b: 165 }), // Melon - colord({ r: 168, g: 85, b: 247 }), // Byzantium - colord({ r: 74, g: 222, b: 128 }), // Kelly Green - colord({ r: 239, g: 68, b: 68 }), // Cardinal - colord({ r: 34, g: 197, b: 94 }), // Shamrock - colord({ r: 96, g: 165, b: 250 }), // Marina - colord({ r: 249, g: 115, b: 22 }), // Carrot - colord({ r: 192, g: 132, b: 252 }), // Heliotrope - colord({ r: 45, g: 212, b: 191 }), // Lagoon - colord({ r: 244, g: 114, b: 182 }), // Bubble Gum - colord({ r: 132, g: 204, b: 22 }), // Apple - colord({ r: 56, g: 189, b: 248 }), // Electric Blue - colord({ r: 234, g: 179, b: 8 }), // Daffodil + colord({ r: 134, g: 239, b: 172 }), // Light Green + colord({ r: 147, g: 51, b: 234 }), // Bright Purple + colord({ r: 147, g: 197, b: 253 }), // Powder Blue + colord({ r: 151, g: 255, b: 187 }), // Fresh Mint + colord({ r: 163, g: 230, b: 53 }), // Yellow Green + colord({ r: 167, g: 139, b: 250 }), // Periwinkle + colord({ r: 168, g: 85, b: 247 }), // Vibrant Purple + colord({ r: 179, g: 136, b: 255 }), // Light Purple + colord({ r: 186, g: 255, b: 201 }), // Pale Emerald + colord({ r: 190, g: 92, b: 251 }), // Amethyst + colord({ r: 192, g: 132, b: 252 }), // Lavender + colord({ r: 202, g: 138, b: 4 }), // Rich Gold + colord({ r: 202, g: 225, b: 255 }), // Baby Blue + colord({ r: 204, g: 204, b: 255 }), // Soft Lavender Blue + colord({ r: 217, g: 70, b: 239 }), // Fuchsia + colord({ r: 220, g: 38, b: 38 }), // Ruby + colord({ r: 220, g: 220, b: 255 }), // Meringue Blue + colord({ r: 220, g: 240, b: 250 }), // Ice Blue + colord({ r: 230, g: 250, b: 210 }), // Pastel Lime + colord({ r: 230, g: 255, b: 250 }), // Mint Whisper + colord({ r: 233, g: 213, b: 255 }), // Light Lilac + colord({ r: 234, g: 88, b: 12 }), // Burnt Orange + colord({ r: 234, g: 179, b: 8 }), // Sunflower + colord({ r: 235, g: 75, b: 75 }), // Bright Red + colord({ r: 236, g: 72, b: 153 }), // Deep Pink + colord({ r: 239, g: 68, b: 68 }), // Crimson + colord({ r: 240, g: 171, b: 252 }), // Orchid + colord({ r: 240, g: 240, b: 200 }), // Light Khaki + colord({ r: 244, g: 114, b: 182 }), // Rose + colord({ r: 245, g: 101, b: 101 }), // Coral + colord({ r: 245, g: 158, b: 11 }), // Amber + colord({ r: 248, g: 113, b: 113 }), // Warm Red + colord({ r: 249, g: 115, b: 22 }), // Tangerine + colord({ r: 250, g: 215, b: 225 }), // Cotton Candy + colord({ r: 250, g: 250, b: 210 }), // Pastel Lemon + colord({ r: 251, g: 113, b: 133 }), // Watermelon + colord({ r: 251, g: 146, b: 60 }), // Light Orange + colord({ r: 251, g: 191, b: 36 }), // Marigold + colord({ r: 251, g: 235, b: 245 }), // Rose Powder + colord({ r: 252, g: 165, b: 165 }), // Peach + colord({ r: 252, g: 211, b: 77 }), // Golden + colord({ r: 253, g: 164, b: 175 }), // Salmon Pink + colord({ r: 255, g: 204, b: 229 }), // Blush Pink + colord({ r: 255, g: 223, b: 186 }), // Apricot Cream + colord({ r: 255, g: 240, b: 200 }), // Vanilla ]; export const botColors: Colord[] = [ @@ -266,3 +229,156 @@ export const botColors: Colord[] = [ colord({ r: 150, g: 160, b: 140 }), // Muted Dark Olive Green colord({ r: 150, g: 140, b: 150 }), // Muted Dusty Rose ]; + +// Fallback colors for when the color palette is exhausted. Currently 100 colors. +export const fallbackColors: Colord[] = [ + colord({ r: 0, g: 5, b: 0 }), // Black Mint + colord({ r: 0, g: 15, b: 0 }), // Deep Forest + colord({ r: 0, g: 25, b: 0 }), // Jungle + colord({ r: 0, g: 35, b: 0 }), // Dark Emerald + colord({ r: 0, g: 45, b: 0 }), // Green Moss + colord({ r: 0, g: 55, b: 0 }), // Moss Shadow + colord({ r: 0, g: 65, b: 0 }), // Dark Meadow + colord({ r: 0, g: 75, b: 0 }), // Forest Fern + colord({ r: 0, g: 85, b: 0 }), // Pine Leaf + colord({ r: 0, g: 95, b: 0 }), // Shadow Grass + colord({ r: 0, g: 105, b: 0 }), // Classic Green + colord({ r: 0, g: 115, b: 0 }), // Deep Lime + colord({ r: 0, g: 125, b: 0 }), // Dense Leaf + colord({ r: 0, g: 135, b: 0 }), // Basil Green + colord({ r: 0, g: 145, b: 0 }), // Organic Green + colord({ r: 0, g: 155, b: 0 }), // Bitter Herb + colord({ r: 0, g: 165, b: 0 }), // Raw Spinach + colord({ r: 0, g: 175, b: 0 }), // Woodland + colord({ r: 0, g: 185, b: 0 }), // Spring Weed + colord({ r: 0, g: 195, b: 5 }), // Apple Stem + colord({ r: 0, g: 205, b: 10 }), // Crisp Lettuce + colord({ r: 0, g: 215, b: 15 }), // Vibrant Green + colord({ r: 0, g: 225, b: 20 }), // Bright Herb + colord({ r: 0, g: 235, b: 25 }), // Green Splash + colord({ r: 0, g: 245, b: 30 }), // Mint Leaf + colord({ r: 0, g: 255, b: 35 }), // Fresh Mint + colord({ r: 10, g: 255, b: 45 }), // Neon Grass + colord({ r: 20, g: 255, b: 55 }), // Lemon Balm + colord({ r: 30, g: 255, b: 65 }), // Juicy Green + colord({ r: 40, g: 255, b: 75 }), // Pear Tint + colord({ r: 50, g: 255, b: 85 }), // Avocado Pastel + colord({ r: 60, g: 255, b: 95 }), // Lime Glow + colord({ r: 70, g: 255, b: 105 }), // Light Leaf + colord({ r: 80, g: 255, b: 115 }), // Soft Fern + colord({ r: 90, g: 255, b: 125 }), // Pastel Green + colord({ r: 100, g: 255, b: 135 }), // Green Melon + colord({ r: 110, g: 255, b: 145 }), // Herbal Mist + colord({ r: 120, g: 255, b: 155 }), // Kiwi Foam + colord({ r: 130, g: 255, b: 165 }), // Aloe Fresh + colord({ r: 140, g: 255, b: 175 }), // Light Mint + colord({ r: 150, g: 200, b: 255 }), // Cornflower Mist + colord({ r: 150, g: 255, b: 185 }), // Green Sorbet + colord({ r: 160, g: 215, b: 255 }), // Powder Blue + colord({ r: 160, g: 255, b: 195 }), // Pastel Apple + colord({ r: 170, g: 190, b: 255 }), // Periwinkle Ice + colord({ r: 170, g: 225, b: 255 }), // Baby Sky + colord({ r: 170, g: 255, b: 205 }), // Aloe Breeze + colord({ r: 180, g: 180, b: 255 }), // Pale Indigo + colord({ r: 180, g: 235, b: 250 }), // Aqua Pastel + colord({ r: 180, g: 255, b: 215 }), // Pale Mint + colord({ r: 190, g: 140, b: 195 }), // Fuchsia Tint + colord({ r: 190, g: 245, b: 240 }), // Ice Mint + colord({ r: 190, g: 255, b: 225 }), // Mint Water + colord({ r: 195, g: 145, b: 200 }), // Dusky Rose + colord({ r: 200, g: 150, b: 205 }), // Plum Frost + colord({ r: 200, g: 170, b: 255 }), // Lilac Bloom + colord({ r: 200, g: 255, b: 215 }), // Cool Aloe + colord({ r: 200, g: 255, b: 235 }), // Cool Mist + colord({ r: 205, g: 155, b: 210 }), // Berry Foam + colord({ r: 210, g: 160, b: 215 }), // Grape Cloud + colord({ r: 210, g: 255, b: 245 }), // Sea Mist + colord({ r: 215, g: 165, b: 220 }), // Light Bloom + colord({ r: 215, g: 255, b: 200 }), // Fresh Mint + colord({ r: 220, g: 160, b: 255 }), // Violet Mist + colord({ r: 220, g: 170, b: 225 }), // Cherry Blossom + colord({ r: 220, g: 255, b: 255 }), // Pale Aqua + colord({ r: 225, g: 175, b: 230 }), // Faded Rose + colord({ r: 225, g: 255, b: 175 }), // Soft Lime + colord({ r: 230, g: 180, b: 235 }), // Dreamy Mauve + colord({ r: 230, g: 250, b: 255 }), // Sky Haze + colord({ r: 235, g: 150, b: 255 }), // Orchid Glow + colord({ r: 235, g: 185, b: 240 }), // Powder Violet + colord({ r: 240, g: 190, b: 245 }), // Pastel Violet + colord({ r: 240, g: 240, b: 255 }), // Frosted Lilac + colord({ r: 240, g: 250, b: 160 }), // Citrus Wash + colord({ r: 245, g: 160, b: 240 }), // Rose Lilac + colord({ r: 245, g: 195, b: 250 }), // Soft Magenta + colord({ r: 245, g: 245, b: 175 }), // Lemon Mist + colord({ r: 250, g: 200, b: 255 }), // Lilac Cream + colord({ r: 250, g: 230, b: 255 }), // Misty Mauve + colord({ r: 255, g: 170, b: 225 }), // Bubblegum Pink + colord({ r: 255, g: 185, b: 215 }), // Blush Mist + colord({ r: 255, g: 195, b: 235 }), // Faded Fuchsia + colord({ r: 255, g: 200, b: 220 }), // Cotton Rose + colord({ r: 255, g: 205, b: 245 }), // Pastel Orchid + colord({ r: 255, g: 205, b: 255 }), // Violet Bloom + colord({ r: 255, g: 210, b: 230 }), // Pastel Blush + colord({ r: 255, g: 210, b: 250 }), // Lavender Mist + colord({ r: 255, g: 210, b: 255 }), // Orchid Mist + colord({ r: 255, g: 215, b: 195 }), // Apricot Glow + colord({ r: 255, g: 215, b: 245 }), // Rose Whisper + colord({ r: 255, g: 220, b: 235 }), // Pink Mist + colord({ r: 255, g: 220, b: 250 }), // Powder Petal + colord({ r: 255, g: 225, b: 180 }), // Butter Peach + colord({ r: 255, g: 225, b: 255 }), // Petal Mist + colord({ r: 255, g: 230, b: 245 }), // Light Rose + colord({ r: 255, g: 235, b: 200 }), // Cream Peach + colord({ r: 255, g: 235, b: 235 }), // Blushed Petal + colord({ r: 255, g: 240, b: 220 }), // Pastel Sand + colord({ r: 255, g: 245, b: 210 }), // Soft Banana +]; + +export class ColorAllocator { + private availableColors: Colord[]; + private fallbackColors: Colord[]; + private assigned = new Map(); + + constructor(colors: Colord[], fallback: Colord[]) { + this.availableColors = [...colors]; + this.fallbackColors = [...fallback]; + } + + assignColor(id: string): Colord { + if (this.assigned.has(id)) { + return this.assigned.get(id)!; + } + if (this.availableColors.length === 0) { + this.availableColors = this.fallbackColors; + } + const index = 0; + const color = this.availableColors.splice(index, 1)[0]; + this.assigned.set(id, color); + return color; + } + + 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.availableColors[ + simpleHash(team) % this.availableColors.length + ]; + } + } +} diff --git a/src/core/configuration/PastelTheme.ts b/src/core/configuration/PastelTheme.ts index 8846348d0..c723d9bd3 100644 --- a/src/core/configuration/PastelTheme.ts +++ b/src/core/configuration/PastelTheme.ts @@ -1,21 +1,14 @@ import { Colord, colord } from "colord"; import { PseudoRandom } from "../PseudoRandom"; -import { simpleHash } from "../Util"; -import { ColoredTeams, PlayerType, Team, TerrainType } from "../game/Game"; +import { PlayerType, Team, TerrainType } from "../game/Game"; import { GameMap, TileRef } from "../game/GameMap"; import { PlayerView } from "../game/GameView"; import { - blue, - botColor, botColors, - green, + ColorAllocator, + fallbackColors, humanColors, - orange, - purple, - red, - teal, - territoryColors, - yellow, + nationColors, } from "./Colors"; import { Theme } from "./Config"; @@ -24,9 +17,12 @@ type ColorCache = Map; export class PastelTheme implements Theme { private borderColorCache: ColorCache = new Map(); private rand = new PseudoRandom(123); + private humanColorAllocator = new ColorAllocator(humanColors, fallbackColors); + private botColorAllocator = new ColorAllocator(botColors, botColors); + private teamColorAllocator = new ColorAllocator(humanColors, fallbackColors); + private nationColorAllocator = new ColorAllocator(nationColors, nationColors); private background = colord({ r: 60, g: 60, b: 60 }); - private land = colord({ r: 194, g: 193, b: 148 }); private shore = colord({ r: 204, g: 203, b: 158 }); private falloutColors = [ colord({ r: 120, g: 255, b: 71 }), // Original color @@ -45,26 +41,7 @@ export class PastelTheme implements Theme { private _spawnHighlightColor = colord({ r: 255, g: 213, b: 79 }); teamColor(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 humanColors[simpleHash(team) % humanColors.length]; - } + return this.teamColorAllocator.assignTeamColor(team); } territoryColor(player: PlayerView): Colord { @@ -73,12 +50,12 @@ export class PastelTheme implements Theme { return this.teamColor(team); } if (player.type() === PlayerType.Human) { - return humanColors[simpleHash(player.id()) % humanColors.length]; + return this.humanColorAllocator.assignColor(player.id()); } if (player.type() === PlayerType.Bot) { - return botColors[simpleHash(player.id()) % botColors.length]; + return this.botColorAllocator.assignColor(player.id()); } - return territoryColors[simpleHash(player.id()) % territoryColors.length]; + return this.nationColorAllocator.assignColor(player.id()); } textColor(player: PlayerView): string { diff --git a/src/core/configuration/PastelThemeDark.ts b/src/core/configuration/PastelThemeDark.ts index 3d428c447..467205cea 100644 --- a/src/core/configuration/PastelThemeDark.ts +++ b/src/core/configuration/PastelThemeDark.ts @@ -1,21 +1,14 @@ import { Colord, colord } from "colord"; import { PseudoRandom } from "../PseudoRandom"; -import { simpleHash } from "../Util"; -import { ColoredTeams, PlayerType, Team, TerrainType } from "../game/Game"; +import { PlayerType, Team, TerrainType } from "../game/Game"; import { GameMap, TileRef } from "../game/GameMap"; import { PlayerView } from "../game/GameView"; import { - blue, - botColor, botColors, - green, + ColorAllocator, + fallbackColors, humanColors, - orange, - purple, - red, - teal, - territoryColors, - yellow, + nationColors, } from "./Colors"; import { Theme } from "./Config"; @@ -24,9 +17,12 @@ type ColorCache = Map; export class PastelThemeDark implements Theme { private borderColorCache: ColorCache = new Map(); private rand = new PseudoRandom(123); + private humanColorAllocator = new ColorAllocator(humanColors, fallbackColors); + private botColorAllocator = new ColorAllocator(botColors, botColors); + private teamColorAllocator = new ColorAllocator(humanColors, fallbackColors); + private nationColorAllocator = new ColorAllocator(nationColors, nationColors); private background = colord({ r: 0, g: 0, b: 0 }); - private land = colord({ r: 194, g: 193, b: 148 }); private shore = colord({ r: 134, g: 133, b: 88 }); private falloutColors = [ colord({ r: 120, g: 255, b: 71 }), // Original color @@ -45,26 +41,7 @@ export class PastelThemeDark implements Theme { private _spawnHighlightColor = colord({ r: 255, g: 213, b: 79 }); teamColor(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 humanColors[simpleHash(team) % humanColors.length]; - } + return this.teamColorAllocator.assignTeamColor(team); } territoryColor(player: PlayerView): Colord { @@ -73,12 +50,12 @@ export class PastelThemeDark implements Theme { return this.teamColor(team); } if (player.type() === PlayerType.Human) { - return humanColors[simpleHash(player.id()) % humanColors.length]; + return this.humanColorAllocator.assignColor(player.id()); } if (player.type() === PlayerType.Bot) { - return botColors[simpleHash(player.id()) % botColors.length]; + return this.botColorAllocator.assignColor(player.id()); } - return territoryColors[simpleHash(player.id()) % territoryColors.length]; + return this.nationColorAllocator.assignColor(player.id()); } textColor(player: PlayerView): string { diff --git a/tests/Colors.test.ts b/tests/Colors.test.ts new file mode 100644 index 000000000..eb04cea0c --- /dev/null +++ b/tests/Colors.test.ts @@ -0,0 +1,81 @@ +import { colord, Colord } from "colord"; +import { + blue, + botColor, + ColorAllocator, + red, + teal, +} from "../src/core/configuration/Colors"; +import { ColoredTeams } from "../src/core/game/Game"; + +const mockColors: Colord[] = [ + colord({ r: 255, g: 0, b: 0 }), + colord({ r: 0, g: 255, b: 0 }), + colord({ r: 0, g: 0, b: 255 }), +]; + +const fallbackMockColors: Colord[] = [ + colord({ r: 0, g: 0, b: 0 }), + colord({ r: 255, g: 255, b: 255 }), +]; + +describe("ColorAllocator", () => { + let allocator: ColorAllocator; + + beforeEach(() => { + allocator = new ColorAllocator(mockColors, fallbackMockColors); + }); + + test("returns a unique color for each new ID", () => { + const c1 = allocator.assignColor("a"); + const c2 = allocator.assignColor("b"); + const c3 = allocator.assignColor("c"); + + expect(c1.isEqual(c2)).toBe(false); + expect(c1.isEqual(c3)).toBe(false); + expect(c2.isEqual(c3)).toBe(false); + }); + + test("returns the same color for the same ID", () => { + const c1 = allocator.assignColor("a"); + const c2 = allocator.assignColor("a"); + + expect(c1.isEqual(c2)).toBe(true); + }); + + test("falls back when colors are exhausted", () => { + allocator.assignColor("1"); + allocator.assignColor("2"); + allocator.assignColor("3"); + const fallback = allocator.assignColor("4"); + const fallback2 = allocator.assignColor("5"); + + const match = fallbackMockColors.some((color) => color.isEqual(fallback)); + expect(match).toBe(true); + + const match2 = fallback.isEqual(fallback2); + expect(match2).toBe(false); + }); + + test("assignBotColor returns deterministic color from botColors", () => { + const allocator = new ColorAllocator(mockColors, mockColors); + + const id1 = "bot123"; + const id2 = "bot456"; + + const c1 = allocator.assignColor(id1); + const c2 = allocator.assignColor(id2); + const c1Again = allocator.assignColor(id1); + const c2Again = allocator.assignColor(id2); + + expect(c1.isEqual(c1Again)).toBe(true); + expect(c2.isEqual(c2Again)).toBe(true); + }); + + test("assignTeamColor returns the expected static color for known teams", () => { + 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.Bot)).toEqual(botColor); + }); +});