mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-10 16:14:36 +00:00
a7f992e9b0
Restructure the single src/ tree into an npm-workspaces monorepo under
packages/, rename core -> engine, extract a types-only core-public layer,
and break the pre-existing engine -> client dependency cycle.
Structure (packages/):
core-public public API/wire schemas + shared enums (clean leaf)
shared framework-agnostic helpers (clean leaf)
engine deterministic simulation (was src/core)
client rendering/UI (was src/client)
server coordination (was src/server)
Dependency DAG: engine -> {core-public, shared}; client -> {core-public,
shared, engine}; server -> {core-public, engine}.
- npm workspaces: root package.json workspaces + per-package package.json;
tsconfig.base.json holds shared options + path aliases
(core-public/* shared/* engine/* client/* server/*) resolved uniformly by
tsc, Vite (resolve.tsconfigPaths), Vitest, and tsx. Lockfile regenerated.
- core-public: moved Schemas/ApiSchemas/CosmeticSchemas/StatsSchemas/
ClanApiSchemas/WorkerSchemas/Base64/PatternDecoder; extracted the enums
(GameTypes), GameEvent type, emoji table, and GraphicsOverrides schema.
Engine re-exports the moved enums/types so existing imports keep working.
- Broke engine -> client cycle:
- renderNumber/renderTroops -> shared/format
- NameBoxCalculator moved into engine
- username validation returns translation key + params; client translates
- applyStateUpdate moved to client (operates on the render-only PlayerState)
- Config/UnitGrid/execution-Util/GameImpl now use structural read
interfaces (engine/game/ReadViews: PlayerLike/UnitLike/GameLike) instead
of importing client view classes; client imports view classes from a new
client/view barrel; deleted the engine/game/GameView re-export shim.
- Build/deploy updated: vite.config, index.html, eslint, Dockerfile
(copies packages/ + tsconfig.base.json before npm ci), .vscode, tests.
Verified: tsc --noEmit clean; 1364 + 65 tests pass; production vite build
succeeds; engine has zero client/server imports; core-public and shared are
dependency leaves.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
169 lines
5.0 KiB
TypeScript
169 lines
5.0 KiB
TypeScript
import { colord, Colord } from "colord";
|
|
import {
|
|
ColorAllocator,
|
|
selectDistinctColorIndex,
|
|
} from "client/theme/ColorAllocator";
|
|
import {
|
|
blue,
|
|
botColor,
|
|
green,
|
|
orange,
|
|
purple,
|
|
red,
|
|
teal,
|
|
yellow,
|
|
} from "client/theme/Colors";
|
|
import { ColoredTeams } from "engine/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 base color from the team", () => {
|
|
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.Purple)).toEqual(purple);
|
|
expect(allocator.assignTeamColor(ColoredTeams.Yellow)).toEqual(yellow);
|
|
expect(allocator.assignTeamColor(ColoredTeams.Orange)).toEqual(orange);
|
|
expect(allocator.assignTeamColor(ColoredTeams.Green)).toEqual(green);
|
|
expect(allocator.assignTeamColor(ColoredTeams.Bot)).toEqual(botColor);
|
|
expect(allocator.assignTeamColor(ColoredTeams.Humans)).toEqual(blue);
|
|
expect(allocator.assignTeamColor(ColoredTeams.Nations)).toEqual(red);
|
|
});
|
|
|
|
test("assignTeamPlayerColor always returns the same color for the same playerID", () => {
|
|
const playerId = "player123";
|
|
|
|
const blueColor1 = allocator.assignTeamPlayerColor(
|
|
ColoredTeams.Blue,
|
|
playerId,
|
|
);
|
|
const blueColor2 = allocator.assignTeamPlayerColor(
|
|
ColoredTeams.Blue,
|
|
playerId,
|
|
);
|
|
|
|
expect(blueColor1.isEqual(blueColor2)).toBe(true);
|
|
|
|
const redColor1 = allocator.assignTeamPlayerColor(
|
|
ColoredTeams.Red,
|
|
playerId,
|
|
);
|
|
const redColor2 = allocator.assignTeamPlayerColor(
|
|
ColoredTeams.Red,
|
|
playerId,
|
|
);
|
|
|
|
expect(redColor1.isEqual(redColor2)).toBe(true);
|
|
});
|
|
|
|
test("assignTeamPlayerColor returns a different color when the playerID is different", () => {
|
|
const playerIdOne = "player1";
|
|
const playerIdTwo = "player2";
|
|
|
|
const blueColorPlayerOne = allocator.assignTeamPlayerColor(
|
|
ColoredTeams.Blue,
|
|
playerIdOne,
|
|
);
|
|
const blueColorPlayerTwo = allocator.assignTeamPlayerColor(
|
|
ColoredTeams.Blue,
|
|
playerIdTwo,
|
|
);
|
|
|
|
expect(blueColorPlayerOne.isEqual(blueColorPlayerTwo)).toBe(false);
|
|
|
|
const redColorPlayerOne = allocator.assignTeamPlayerColor(
|
|
ColoredTeams.Red,
|
|
playerIdOne,
|
|
);
|
|
const redColorPlayerTwo = allocator.assignTeamPlayerColor(
|
|
ColoredTeams.Red,
|
|
playerIdTwo,
|
|
);
|
|
|
|
expect(redColorPlayerOne.isEqual(redColorPlayerTwo)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("selectDistinctColor", () => {
|
|
test("returns the most distant color", () => {
|
|
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 = selectDistinctColorIndex(availableColors, assignedColors);
|
|
expect(result).not.toBeNull();
|
|
const rgb = availableColors[result!].toRgb();
|
|
expect([
|
|
{ r: 0, g: 255, b: 0, a: 1 },
|
|
{ r: 0, g: 0, b: 255, a: 1 },
|
|
]).toContainEqual(rgb);
|
|
});
|
|
});
|