Add more color variations

This commit is contained in:
Joey Yakimowich-Payne 2026-01-19 15:33:42 -07:00
commit d6fa339755
No known key found for this signature in database
GPG key ID: DDF6AF5B21B407D4
2 changed files with 152 additions and 14 deletions

View file

@ -3,6 +3,8 @@ import {
calculateBasePoints,
calculatePointsWithBreakdown,
getPlayerRank,
generatePlayerColor,
PLAYER_COLORS,
POINTS_PER_QUESTION,
QUESTION_TIME_MS,
} from '../constants';
@ -563,3 +565,143 @@ describe('getPlayerRank', () => {
});
});
});
describe('generatePlayerColor', () => {
describe('output format', () => {
it('returns valid HSL format', () => {
const color = generatePlayerColor(0);
expect(color).toMatch(/^hsl\(\d+, \d+%, \d+%\)$/);
});
it('returns valid HSL values within correct ranges', () => {
for (let i = 0; i < 20; i++) {
const color = generatePlayerColor(i);
const match = color.match(/^hsl\((\d+), (\d+)%, (\d+)%\)$/);
expect(match).not.toBeNull();
const [, hue, saturation, lightness] = match!.map(Number);
expect(hue).toBeGreaterThanOrEqual(0);
expect(hue).toBeLessThan(360);
expect(saturation).toBeGreaterThanOrEqual(70);
expect(saturation).toBeLessThanOrEqual(90);
expect(lightness).toBeGreaterThanOrEqual(45);
expect(lightness).toBeLessThanOrEqual(55);
}
});
});
describe('determinism', () => {
it('returns same color for same index', () => {
const color1 = generatePlayerColor(5);
const color2 = generatePlayerColor(5);
expect(color1).toBe(color2);
});
it('returns consistent colors across multiple calls', () => {
const colors = Array.from({ length: 10 }, (_, i) => generatePlayerColor(i));
const colorsAgain = Array.from({ length: 10 }, (_, i) => generatePlayerColor(i));
expect(colors).toEqual(colorsAgain);
});
});
describe('color distribution (golden ratio)', () => {
it('generates distinct hues for consecutive indices (golden ratio creates ~137° separation)', () => {
const hues: number[] = [];
for (let i = 0; i < 10; i++) {
const color = generatePlayerColor(i);
const match = color.match(/^hsl\((\d+),/);
hues.push(Number(match![1]));
}
for (let i = 1; i < hues.length; i++) {
const diff = Math.abs(hues[i] - hues[i - 1]);
const wrappedDiff = Math.min(diff, 360 - diff);
expect(wrappedDiff).toBeGreaterThan(30);
}
});
it('generates well-distributed colors for large player counts', () => {
const hues = new Set<number>();
for (let i = 0; i < 50; i++) {
const color = generatePlayerColor(i);
const match = color.match(/^hsl\((\d+),/);
hues.add(Number(match![1]));
}
expect(hues.size).toBeGreaterThan(30);
});
it('produces 3 saturation levels (70, 80, 90)', () => {
const saturations = new Set<number>();
for (let i = 0; i < 10; i++) {
const color = generatePlayerColor(i);
const match = color.match(/, (\d+)%,/);
saturations.add(Number(match![1]));
}
expect(saturations.size).toBe(3);
});
it('produces 2 lightness levels (45, 55)', () => {
const lightnesses = new Set<number>();
for (let i = 0; i < 10; i++) {
const color = generatePlayerColor(i);
const match = color.match(/, (\d+)%\)$/);
lightnesses.add(Number(match![1]));
}
expect(lightnesses.size).toBe(2);
});
});
describe('edge cases', () => {
it('handles index 0', () => {
const color = generatePlayerColor(0);
expect(color).toMatch(/^hsl\(\d+, \d+%, \d+%\)$/);
});
it('handles large indices', () => {
const color = generatePlayerColor(1000);
expect(color).toMatch(/^hsl\(\d+, \d+%, \d+%\)$/);
const match = color.match(/^hsl\((\d+), (\d+)%, (\d+)%\)$/);
const [, hue, sat, light] = match!.map(Number);
expect(hue).toBeLessThan(360);
expect(sat).toBeLessThanOrEqual(90);
expect(light).toBeLessThanOrEqual(55);
});
it('handles negative indices gracefully', () => {
const color = generatePlayerColor(-1);
expect(color).toMatch(/^hsl\(-?\d+, \d+%, \d+%\)$/);
});
});
});
describe('PLAYER_COLORS', () => {
it('contains exactly 50 colors', () => {
expect(PLAYER_COLORS).toHaveLength(50);
});
it('all colors are valid HSL strings', () => {
for (const color of PLAYER_COLORS) {
expect(color).toMatch(/^hsl\(\d+, \d+%, \d+%\)$/);
}
});
it('first color matches generatePlayerColor(0)', () => {
expect(PLAYER_COLORS[0]).toBe(generatePlayerColor(0));
});
it('colors are generated in order', () => {
for (let i = 0; i < PLAYER_COLORS.length; i++) {
expect(PLAYER_COLORS[i]).toBe(generatePlayerColor(i));
}
});
it('has 20 unique colors for first 20 indices (typical game size)', () => {
const uniqueColors = new Set(PLAYER_COLORS.slice(0, 20));
expect(uniqueColors.size).toBe(20);
});
});