Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions packages/factory/src/client/game/actors/StationActor.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Actor, Color, Font, GraphicsGroup, Rectangle, Text, vec, type Vector } from 'excalibur';
import { Actor, Color, Font, GraphicsGroup, Rectangle, Text, TextAlign, vec, type Vector } from 'excalibur';

import { PALETTE } from '../../../shared/constants/palette.js';

const STATION_WIDTH = 100;
const STATION_HEIGHT = 40;
const PLATFORM_HEIGHT = 20;

export class StationActor extends Actor {
constructor(phase: string, active: boolean, position: Vector) {
constructor(role: string, active: boolean, position: Vector) {
const color = active ? PALETTE.lightGray : PALETTE.darkGray;
super({
pos: position,
Expand All @@ -25,15 +26,15 @@ export class StationActor extends Actor {
const labelColor = Color.fromHex(PALETTE.white);
labelColor.a = 0.6;
const label = new Text({
text: phase,
text: role,
color: labelColor,
font: new Font({ size: 10 }),
font: new Font({ size: 10, textAlign: TextAlign.Center }),
});

const group = new GraphicsGroup({
members: [
{ graphic: rect, offset: vec(0, 0) },
{ graphic: label, offset: vec(10, STATION_HEIGHT / 2 - 5), useBounds: false },
{ graphic: label, offset: vec(STATION_WIDTH / 2, STATION_HEIGHT + PLATFORM_HEIGHT / 2 - 5), useBounds: false },
],
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,14 @@ vi.mock('excalibur', () => {
}
}

const MockTextAlign = { Center: 'center' };

class MockFont {
size: number;
constructor(opts: { size: number }) {
textAlign: string | undefined;
constructor(opts: { size: number; textAlign?: string }) {
this.size = opts.size;
this.textAlign = opts.textAlign;
}
}

Expand Down Expand Up @@ -72,6 +76,7 @@ vi.mock('excalibur', () => {
Color: MockColor,
Font: MockFont,
Text: MockText,
TextAlign: MockTextAlign,
Rectangle: MockRectangle,
GraphicsGroup: MockGraphicsGroup,
vec: (x: number, y: number) => ({ x, y }),
Expand All @@ -84,7 +89,7 @@ const { PALETTE } = await import('../../../../shared/constants/palette.js');

describe('StationActor', () => {
it('creates a Rectangle with semi-transparent lightGray when active', () => {
new StationActor('architecture', true, vec(100, 200));
new StationActor('architect', true, vec(100, 200));

const expectedColor = Color.fromHex(PALETTE.lightGray);
expectedColor.a = 0.15;
Expand All @@ -98,7 +103,7 @@ describe('StationActor', () => {
});

it('creates a Rectangle with semi-transparent darkGray when inactive', () => {
new StationActor('architecture', false, vec(100, 200));
new StationActor('architect', false, vec(100, 200));

const expectedColor = Color.fromHex(PALETTE.darkGray);
expectedColor.a = 0.15;
Expand All @@ -109,21 +114,21 @@ describe('StationActor', () => {
);
});

it('creates a Text label with the phase name at reduced opacity', () => {
new StationActor('planning', true, vec(0, 0));
it('creates a Text label with the role name at reduced opacity', () => {
new StationActor('planner', true, vec(0, 0));

const expectedColor = Color.fromHex(PALETTE.white);
expectedColor.a = 0.6;
expect(mockTextConstructor).toHaveBeenCalledWith(
expect.objectContaining({
text: 'planning',
text: 'planner',
color: expectedColor,
}),
);
});

it('creates a GraphicsGroup composing rectangle and text', () => {
new StationActor('implementation', true, vec(0, 0));
new StationActor('coder', true, vec(0, 0));

expect(mockGraphicsGroupConstructor).toHaveBeenCalledWith(
expect.objectContaining({
Expand All @@ -136,13 +141,13 @@ describe('StationActor', () => {
});

it('uses the GraphicsGroup via graphics.use()', () => {
new StationActor('review', true, vec(0, 0));
new StationActor('reviewer', true, vec(0, 0));

expect(mockGraphicsUse).toHaveBeenCalled();
});

it('sets correct dimensions on the Actor', () => {
new StationActor('architecture', true, vec(50, 75));
new StationActor('architect', true, vec(50, 75));

expect(mockActorConstructor).toHaveBeenCalledWith(
expect.objectContaining({
Expand All @@ -154,12 +159,32 @@ describe('StationActor', () => {

it('passes position to Actor constructor', () => {
const pos = vec(300, 400);
new StationActor('holistic', false, pos);
new StationActor('holistic reviewer', false, pos);

expect(mockActorConstructor).toHaveBeenCalledWith(
expect.objectContaining({
pos,
}),
);
});

it('positions label centered on platform below station', () => {
new StationActor('architect', true, vec(0, 0));

expect(mockGraphicsGroupConstructor).toHaveBeenCalledWith(
expect.objectContaining({
members: expect.arrayContaining([expect.objectContaining({ offset: { x: 50, y: 45 }, useBounds: false })]),
}),
);
});

it('uses center-aligned text on the Font', () => {
new StationActor('coder', true, vec(0, 0));

expect(mockTextConstructor).toHaveBeenCalledWith(
expect.objectContaining({
font: expect.objectContaining({ textAlign: 'center' }),
}),
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
createMockRunStatus,
emptyPhases,
} from '../../../../__test-helpers__/fixtures.js';
import { PHASE_ROLE_TYPE } from '../../../../shared/constants/role-types.js';
import { PHASE_ROLE, PHASE_ROLE_TYPE } from '../../../../shared/constants/role-types.js';
import { REVIEW_STATION_INDEX as LAYOUT_REVIEW_STATION_INDEX } from '../../layout/platform-layout.js';
import { createSceneConfig, PHASE_NAMES, REVIEW_STATION_INDEX } from '../run-to-scene.js';

Expand Down Expand Up @@ -892,6 +892,19 @@ describe('createSceneConfig', () => {
expect(station.phase).toBe(PHASE_NAMES[i]);
});
});

it('assigns station role from PHASE_ROLE for each phase', () => {
const status = createMockRunStatus();

const config = createSceneConfig(status);

config.stations.forEach((station, i) => {
const phase = PHASE_NAMES[i];
if (phase !== undefined) {
expect(station.role).toBe(PHASE_ROLE[phase]);
}
});
});
});

describe('constant synchronization', () => {
Expand Down
4 changes: 3 additions & 1 deletion packages/factory/src/client/game/mappers/run-to-scene.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { PhaseName, RoleType } from '../../../shared/constants/role-types.js';
import { PHASE_NAMES, PHASE_ROLE_TYPE } from '../../../shared/constants/role-types.js';
import { PHASE_NAMES, PHASE_ROLE, PHASE_ROLE_TYPE } from '../../../shared/constants/role-types.js';
import { findCurrentPhase, isPhasePresentInData } from '../../../shared/phase-inference.js';
import type { CanonicalRunStatus, Phases } from '../../../shared/types/canonical.js';

export interface StationConfig {
phase: string;
role: string;
active: boolean;
}

Expand Down Expand Up @@ -52,6 +53,7 @@ function isPhaseActive(phase: PhaseName, phases: Phases, runStatus: string, curr
function buildStations(status: CanonicalRunStatus, currentPhase?: PhaseName): StationConfig[] {
return PHASE_NAMES.map((phase) => ({
phase,
role: PHASE_ROLE[phase],
active: isPhaseActive(phase, status.phases, status.status, currentPhase),
}));
}
Expand Down
2 changes: 1 addition & 1 deletion packages/factory/src/client/game/scenes/FactoryScene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export class FactoryScene extends Scene {
const station = config.stations[i];
const pos = layout.stationPositions[i];
if (station === undefined || pos === undefined) continue;
this.add(new StationActor(station.phase, station.active, vec(pos.x, pos.y)));
this.add(new StationActor(station.role, station.active, vec(pos.x, pos.y)));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ vi.mock('../../../game/actors/StationActor.js', () => ({
StationActor: class StationActor {
kind = 'station';
constructor(
public phase: string,
public role: string,
public active: boolean,
public position: unknown,
) {}
Expand Down Expand Up @@ -155,7 +155,7 @@ const { FactoryScene } = await import('../FactoryScene.js');
interface MockActorWithKind {
kind?: string;
agentKey?: string;
phase?: string;
role?: string;
roleType?: string;
position?: { x: number; y: number };
}
Expand Down Expand Up @@ -314,7 +314,7 @@ describe('FactoryScene', () => {
expect(mockSceneAdd).toHaveBeenCalledTimes(27);
});

it('adds station actors with correct phase names', () => {
it('adds station actors with correct role names', () => {
const status = createMockRunStatus();
const scene = new FactoryScene(status);
scene.onInitialize();
Expand All @@ -324,15 +324,15 @@ describe('FactoryScene', () => {
);
expect(stationCalls).toHaveLength(7);

const phaseNames = stationCalls.map((call: unknown[]) => getActorFromCall(call).phase);
expect(phaseNames).toEqual([
'architecture',
'planning',
'implementation',
'review',
const roleNames = stationCalls.map((call: unknown[]) => getActorFromCall(call).role);
expect(roleNames).toEqual([
'architect',
'planner',
'coder',
'reviewer',
'simplifier',
'holistic',
'summary',
'holistic reviewer',
'orchestrator',
]);
});

Expand Down
10 changes: 10 additions & 0 deletions packages/factory/src/shared/constants/role-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ export const PHASE_NAMES = [
] as const;
export type PhaseName = (typeof PHASE_NAMES)[number];

export const PHASE_ROLE: Record<PhaseName, string> = {
architecture: 'architect',
planning: 'planner',
implementation: 'coder',
review: 'reviewer',
simplifier: 'simplifier',
holistic: 'holistic reviewer',
summary: 'orchestrator',
};

export const PHASE_ROLE_TYPE: Record<PhaseName, RoleType> = {
architecture: 'analyst',
planning: 'planner',
Expand Down