mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:30:45 +00:00
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:  With this PR: With 10 players:  With 150 players:  With 400 players:  ## 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>
This commit is contained in:
committed by
GitHub
parent
d2355b2796
commit
1ef05bfaca
@@ -1,6 +1,11 @@
|
||||
import { colord, Colord } from "colord";
|
||||
import { colord, Colord, extend, LabColor } from "colord";
|
||||
import labPlugin from "colord/plugins/lab";
|
||||
import lchPlugin from "colord/plugins/lch";
|
||||
import { ColoredTeams, Team } from "../game/Game";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { simpleHash } from "../Util";
|
||||
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
|
||||
@@ -341,18 +346,36 @@ export class ColorAllocator {
|
||||
|
||||
constructor(colors: Colord[], fallback: Colord[]) {
|
||||
this.availableColors = [...colors];
|
||||
this.fallbackColors = [...fallback];
|
||||
this.fallbackColors = [...colors, ...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];
|
||||
|
||||
const MIN_DELTA_E = 25; // Minimum Delta E for distinct colors
|
||||
const assignedColors = Array.from(this.assigned.values());
|
||||
const rand = new PseudoRandom(simpleHash(id));
|
||||
const shuffledColors = rand.shuffleArray(this.availableColors);
|
||||
|
||||
const selection = selectDistinctColor(
|
||||
shuffledColors,
|
||||
assignedColors,
|
||||
MIN_DELTA_E,
|
||||
);
|
||||
|
||||
let selectedIndex = 0;
|
||||
|
||||
if (selection) {
|
||||
selectedIndex = selection.selectedIndex;
|
||||
}
|
||||
|
||||
const color = this.availableColors.splice(selectedIndex, 1)[0];
|
||||
this.assigned.set(id, color);
|
||||
return color;
|
||||
}
|
||||
@@ -382,3 +405,32 @@ export class ColorAllocator {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Select a distinct color from the available colors that is sufficiently different from the assigned colors
|
||||
export function selectDistinctColor(
|
||||
shuffledColors: Colord[],
|
||||
assignedColors: Colord[],
|
||||
minDeltaE: number,
|
||||
): { selectedIndex: number; selectedColor: Colord } | null {
|
||||
const shuffled = shuffledColors.map((color, index) => ({ color, index }));
|
||||
|
||||
for (const { color, index } of shuffled) {
|
||||
const isDistinctEnough = assignedColors.every(
|
||||
(assigned) => deltaE76(color.toLab(), assigned.toLab()) >= minDeltaE,
|
||||
);
|
||||
if (isDistinctEnough) {
|
||||
return { selectedIndex: index, selectedColor: color };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate Delta E using the CIE76 formula
|
||||
export function deltaE76(c1: LabColor, c2: LabColor) {
|
||||
return Math.sqrt(
|
||||
Math.pow(c1.l - c2.l, 2) +
|
||||
Math.pow(c1.a - c2.a, 2) +
|
||||
Math.pow(c1.b - c2.b, 2),
|
||||
);
|
||||
}
|
||||
|
||||
+34
-1
@@ -4,6 +4,7 @@ import {
|
||||
botColor,
|
||||
ColorAllocator,
|
||||
red,
|
||||
selectDistinctColor,
|
||||
teal,
|
||||
} from "../src/core/configuration/Colors";
|
||||
import { ColoredTeams } from "../src/core/game/Game";
|
||||
@@ -19,6 +20,8 @@ const fallbackMockColors: Colord[] = [
|
||||
colord({ r: 255, g: 255, b: 255 }),
|
||||
];
|
||||
|
||||
const fallbackColors = [...fallbackMockColors, ...mockColors];
|
||||
|
||||
describe("ColorAllocator", () => {
|
||||
let allocator: ColorAllocator;
|
||||
|
||||
@@ -50,7 +53,7 @@ describe("ColorAllocator", () => {
|
||||
const fallback = allocator.assignColor("4");
|
||||
const fallback2 = allocator.assignColor("5");
|
||||
|
||||
const match = fallbackMockColors.some((color) => color.isEqual(fallback));
|
||||
const match = fallbackColors.some((color) => color.isEqual(fallback));
|
||||
expect(match).toBe(true);
|
||||
|
||||
const match2 = fallback.isEqual(fallback2);
|
||||
@@ -79,3 +82,33 @@ describe("ColorAllocator", () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user