From 8add982daee996ca086847f46f9c9a27d0299e05 Mon Sep 17 00:00:00 2001 From: Christopher Ehrlich Date: Sun, 15 Feb 2026 20:03:23 +0700 Subject: [PATCH 01/10] chore(scenario-stage-4): complete 4.1 2026-02-15 --- SCENARIO_EDITOR_PLAN.md | 2 +- apps/scenario-editor/package.json | 1 + apps/scenario-editor/src/routes/__root.tsx | 5 +- apps/scenario-editor/src/routes/index.tsx | 299 +++++++++++++++++- .../src/state/editor-objective.test.ts | 164 ++++++++++ .../src/state/editor-objective.ts | 223 +++++++++++++ .../src/state/editor-state.test.ts | 36 ++- .../src/state/editor-state.tsx | 41 ++- apps/scenario-editor/src/styles.css | 52 ++- pnpm-lock.yaml | 3 + 10 files changed, 809 insertions(+), 17 deletions(-) create mode 100644 apps/scenario-editor/src/state/editor-objective.test.ts create mode 100644 apps/scenario-editor/src/state/editor-objective.ts diff --git a/SCENARIO_EDITOR_PLAN.md b/SCENARIO_EDITOR_PLAN.md index f50281b7..5146c899 100644 --- a/SCENARIO_EDITOR_PLAN.md +++ b/SCENARIO_EDITOR_PLAN.md @@ -132,7 +132,7 @@ - `apps/scenario-editor/` (from Stage 3) ### Requirements (Task Checklist) -- [ ] **4.1** Add objective editor for predicate DSL (`metric`, `all`, `any`, `not`). +- [x] **4.1** Add objective editor for predicate DSL (`metric`, `all`, `any`, `not`). - [ ] **4.2** Add event/action editor for declarative scripts (`atTick`, `everyTicks`, actions union). - [ ] **4.3** Add behavior profile assignment UI with closed profile validation. - [ ] **4.4** Add authoring-time semantic validation (unknown metric, invalid op/type combos, empty `all/any`, etc.). diff --git a/apps/scenario-editor/package.json b/apps/scenario-editor/package.json index fca6be19..bb4924be 100644 --- a/apps/scenario-editor/package.json +++ b/apps/scenario-editor/package.json @@ -14,6 +14,7 @@ "dependencies": { "@city/classicyui": "workspace:*", "@city/scenario-core": "workspace:*", + "@city/scenario-runtime": "workspace:*", "@tanstack/react-router": "1.157.16", "react": "19.2.4", "react-dom": "19.2.4" diff --git a/apps/scenario-editor/src/routes/__root.tsx b/apps/scenario-editor/src/routes/__root.tsx index f68508bc..c14c74a7 100644 --- a/apps/scenario-editor/src/routes/__root.tsx +++ b/apps/scenario-editor/src/routes/__root.tsx @@ -16,7 +16,7 @@ function RootRouteLayout() { return (
-
Scenario Editor MVP
+
Scenario Editor
@@ -76,5 +76,8 @@ function getScenarioEditorWorkbenchViewLabel(view: ScenarioEditorWorkbenchView): if (view === 'map') { return 'Map'; } + if (view === 'objective') { + return 'Objective'; + } return 'Export'; } diff --git a/apps/scenario-editor/src/routes/index.tsx b/apps/scenario-editor/src/routes/index.tsx index c4cb9864..e6a86f52 100644 --- a/apps/scenario-editor/src/routes/index.tsx +++ b/apps/scenario-editor/src/routes/index.tsx @@ -26,6 +26,17 @@ import { readScenarioEditorMapTileWord, type ScenarioEditorMapPoint, } from '../state/editor-map.ts'; +import { + appendScenarioObjectiveChildPredicate, + coerceScenarioObjectivePredicateKind, + removeScenarioObjectiveChildPredicate, + replaceScenarioObjectiveChildPredicate, + replaceScenarioObjectiveNotChildPredicate, + SCENARIO_EDITOR_OBJECTIVE_COMPARISONS, + SCENARIO_EDITOR_OBJECTIVE_METRIC_KEYS, + SCENARIO_EDITOR_OBJECTIVE_PREDICATE_KINDS, + type ScenarioEditorObjectivePredicate, +} from '../state/editor-objective.ts'; import { getScenarioEditorMetadataValidationIssues, parseScenarioEditorTagsInput, @@ -57,10 +68,10 @@ export const Route = createFileRoute('/')({ }); /** - * Stage 3 workbench route with metadata editing and manual map editing MVP sections. - * Parity note: metadata `startYear`/`startFunds` map to `LoadScenario` fields in - * `ref/micropolis/src/sim/s_fileio.c`; map edits mirror `Map[x][y]` writes from - * `SimCmdTile`/`SimCmdFill` in `ref/micropolis/src/sim/w_sim.c`. + * Stage 4 workbench route with metadata/map editing plus objective predicate authoring. + * Parity note: objective metric leaves map to `DoScenarioScore` checks in + * `ref/micropolis/src/sim/s_msg.c`, while logical composition forms are declarative + * runtime extensions from `packages/scenario-runtime`. */ function ScenarioEditorHomeRoute() { const { activeView } = useScenarioEditorState(); @@ -71,6 +82,9 @@ function ScenarioEditorHomeRoute() { if (activeView === 'map') { return ; } + if (activeView === 'objective') { + return ; + } return ; } @@ -382,6 +396,215 @@ function ScenarioMapEditorCard() { ); } +/** + * Objective authoring card for Stage 4.1 predicate DSL editing. + * Parity note: metric leaves mirror `DoScenarioScore` checks in + * `ref/micropolis/src/sim/s_msg.c`; logical nodes (`all`/`any`/`not`) are + * declarative extensions supported by `packages/scenario-runtime`. + */ +function ScenarioObjectiveEditorCard() { + const { objective, isDirty } = useScenarioEditorState(); + const dispatch = useScenarioEditorDispatch(); + const objectiveJson = useMemo( + () => JSON.stringify(objective.enabled ? objective.predicate : null, null, 2), + [objective.enabled, objective.predicate], + ); + + return ( +
+

Scenario Objective

+

+ Author objective predicates using the Stage 4 DSL. Metric comparisons track classic + `DoScenarioScore` fields, while `all`/`any`/`not` allow composed checks. +

+ + + + {objective.enabled ? ( + { + dispatch({ type: 'replace-objective-predicate', predicate }); + }} + predicate={objective.predicate} + /> + ) : ( +

Objective checks are disabled for this draft.

+ )} + +
+
Dirty State
+
{isDirty ? 'dirty' : 'clean'}
+
Objective Enabled
+
{objective.enabled ? 'yes' : 'no'}
+
Root Predicate
+
{objective.enabled ? objective.predicate.kind : 'none'}
+
+ +
+

Objective Predicate JSON

+