Add more color variations
This commit is contained in:
parent
3655d4d456
commit
d6fa339755
2 changed files with 152 additions and 14 deletions
24
constants.ts
24
constants.ts
|
|
@ -101,17 +101,13 @@ export const getPlayerRank = (playerId: string, players: Player[]): number => {
|
||||||
return sorted.findIndex(p => p.id === playerId) + 1;
|
return sorted.findIndex(p => p.id === playerId) + 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PLAYER_COLORS = [
|
const GOLDEN_RATIO = 0.618033988749895;
|
||||||
'#2563eb',
|
|
||||||
'#e21b3c',
|
export const generatePlayerColor = (index: number): string => {
|
||||||
'#26890c',
|
const hue = ((index * GOLDEN_RATIO) % 1) * 360;
|
||||||
'#ffa602',
|
const saturation = 70 + (index % 3) * 10;
|
||||||
'#d89e00',
|
const lightness = 45 + (index % 2) * 10;
|
||||||
'#0aa3a3',
|
return `hsl(${Math.round(hue)}, ${saturation}%, ${lightness}%)`;
|
||||||
'#b8008a',
|
};
|
||||||
'#6a4c93',
|
|
||||||
'#ff6b6b',
|
export const PLAYER_COLORS = Array.from({ length: 50 }, (_, i) => generatePlayerColor(i));
|
||||||
'#4ecdc4',
|
|
||||||
'#45b7d1',
|
|
||||||
'#8b5cf6',
|
|
||||||
];
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import {
|
||||||
calculateBasePoints,
|
calculateBasePoints,
|
||||||
calculatePointsWithBreakdown,
|
calculatePointsWithBreakdown,
|
||||||
getPlayerRank,
|
getPlayerRank,
|
||||||
|
generatePlayerColor,
|
||||||
|
PLAYER_COLORS,
|
||||||
POINTS_PER_QUESTION,
|
POINTS_PER_QUESTION,
|
||||||
QUESTION_TIME_MS,
|
QUESTION_TIME_MS,
|
||||||
} from '../constants';
|
} 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue