Files
OpenFrontIO/tests/Colors.test.ts
T
Christopher Mesona 1ef05bfaca feat: colors are better mixed up when players count is low (#1149)
## Description:

Two improvments in this PR:

* Shuffles availableColors list, to better use full palette, instead of
getting the next one in the list (as this list is now ordered for
readability)
* Uses DeltaE as best effort to try having diversity of colors,
especially useful for low count of players. Falls back to the full list
of availableColors if DeltaE requirement is not met (typically if the
list length is too low)

Without this PR:

With 10 players:

![image](https://github.com/user-attachments/assets/3de65a0e-5ff1-4752-b6d5-aef9db5716f3)


With this PR:

With 10 players:

![image](https://github.com/user-attachments/assets/d7cfcd87-ad83-45a4-a5ad-61d7efa2d5e8)

With 150 players:

![image](https://github.com/user-attachments/assets/247fe7fe-9a1d-499d-8afc-a7c0100e24d4)

With 400 players:

![image](https://github.com/user-attachments/assets/f3f0af17-000b-40d5-8571-0dbec0bdcb94)


## 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 <christopher.mesona@ubisoft.com>
2025-06-16 13:42:59 -07:00

115 lines
3.5 KiB
TypeScript

import { colord, Colord } from "colord";
import {
blue,
botColor,
ColorAllocator,
red,
selectDistinctColor,
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 }),
];
const fallbackColors = [...fallbackMockColors, ...mockColors];
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 = fallbackColors.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);
});
});
describe("selectDistinctColor", () => {
test("returns a distinct color when one exceeds the delta threshold", () => {
const assignedColors = [colord({ r: 255, g: 0, b: 0 })]; // bright red
const availableColors = [
colord({ r: 254, g: 1, b: 1 }), // too close
colord({ r: 0, g: 255, b: 0 }), // distinct green
colord({ r: 0, g: 0, b: 255 }), // distinct blue
];
const result = selectDistinctColor(availableColors, assignedColors, 25);
expect(result).not.toBeNull();
const rgb = result!.selectedColor.toRgb();
expect([
{ r: 0, g: 255, b: 0, a: 1 },
{ r: 0, g: 0, b: 255, a: 1 },
]).toContainEqual(rgb);
});
test("returns null if all available colors are too close", () => {
const assignedColors = [colord({ r: 255, g: 0, b: 0 })];
const availableColors = [
colord({ r: 250, g: 5, b: 5 }),
colord({ r: 245, g: 10, b: 10 }),
];
const result = selectDistinctColor(availableColors, assignedColors, 50);
expect(result).toBeNull();
});
});