From 813320c07887d88fa45a146614cea95e904c1b3b Mon Sep 17 00:00:00 2001 From: gethi Date: Thu, 14 May 2026 16:46:04 +0100 Subject: [PATCH 1/7] docs: P4c slice 1 (bomber reuse + Carnage bias) design spec Two tightly-coupled changes: - Bombers reusable: applyLaunches restores bomber on impact, lost on intercept. One mission per bomber per round (validation already enforces via consumeStockFor decrement). - Carnage build-bias + launch-bias toward bombers. Netanyahoo unchanged. Narrows the original P4c scope (AI scoring-weight tuning + Approach B/C lookahead) to a tight first slice. Slice 2+ does full AI rebalance once the new bomber rule's duel-distribution baseline is visible. --- ...-14-phase-4c-slice1-bomber-reuse-design.md | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-14-phase-4c-slice1-bomber-reuse-design.md diff --git a/docs/superpowers/specs/2026-05-14-phase-4c-slice1-bomber-reuse-design.md b/docs/superpowers/specs/2026-05-14-phase-4c-slice1-bomber-reuse-design.md new file mode 100644 index 0000000..a03da1c --- /dev/null +++ b/docs/superpowers/specs/2026-05-14-phase-4c-slice1-bomber-reuse-design.md @@ -0,0 +1,171 @@ +# Phase 4c slice 1 — Bomber reuse + Carnage bomber bias design spec + +**Date:** 2026-05-14 +**Status:** drafted from playtesting brainstorming session; pending user review +**Source of feedback:** playtesting after P4b merge (commit `174139a`). User notes captured in conversation: bombers should be reusable (the whole point of bombers); Carnage should value bombers, Netanyahoo should not. + +--- + +## 1. Overview + +Phase 4c was originally scoped as "AI scoring-weight balance pass + Approach B/C lookahead upgrades". This slice 1 narrows the first PR to two tightly-coupled changes: + +1. **Bombers are reusable** — engine rule. A bomber consumed during a launch returns to the attacker's stockpile if the launch impacts; stays gone if intercepted. Warheads always consumed. One bomber = one mission per round. +2. **Carnage favours bombers** — AI personality tuning. Carnage builds bombers (not missiles) and launches with bombers when available. Netanyahoo unchanged (stays missile-biased). + +The wider P4c scope (full AI scoring-weight tuning, Approach B/C lookahead upgrades, threat-aware defence deployment per personality) lands in subsequent slices once we see the duel distribution under the new bomber rule. + +Phase order: **P3 ✓ → P4a ✓ → P4b ✓ → P4c.1 (this slice) → P4c.2+ → P5**. + +--- + +## 2. Scope + +### 2.1 Bomber reuse engine rule + +**Lifecycle:** + +- **Launch (consumeStockFor)** — bomber decremented from `attacker.stockpile.bombers` exactly as today. Warhead also decremented. This enforces "one mission per bomber per round" naturally — a second `launch` order with the same `delivery: 'bomber'` validates against `bombers === 0` and fails. +- **Intercept (applyLaunches intercept branch)** — bomber stays gone. The existing intercept-decrement of `receiver.deployedShields/deployedAA` is unaffected. +- **Impact (applyLaunches impact branches)** — for `ImpactPeople` AND `ImpactInfrastructure`, if `l.delivery === 'bomber'`, restore the bomber: `attacker.stockpile.bombers += 1`. Restoration is unconditional on attacker liveness (a dead attacker's stockpile is irrelevant; restoration is harmless). +- **Final Retaliation** — FR cascade reuses `applyLaunches`, so dying-leader bomber-impacts technically restore the bomber to a dead leader's stockpile. Harmless — nothing reads dead-leader stockpile after death. + +### 2.2 Carnage AI bomber bias + +Two narrow changes in `src/engine/ai/carnage.ts`: + +- **Build bias** — where Carnage currently emits `{ kind: 'build-missile' }`, emit `{ kind: 'build-bomber' }` instead. Warhead pairing unchanged. +- **Launch bias** — when constructing a launch order: if `me.stockpile.bombers >= 1`, set `delivery: 'bomber'`; else fall back to `'missile'`. + +Existing threat scoring, opportunism scoring, Chump-exception, target-eligibility logic — all untouched. + +### 2.3 Netanyahoo unchanged + +No code change. Netanyahoo continues to build missiles + launch with missiles. The regression test (§5) confirms. + +### 2.4 UI projection unchanged + +`src/ui/util/projection.ts`'s `projectInventory` already subtracts bomber for a queued launch — matches consume-during-round semantics. The post-impact restoration happens AFTER `resolveRound`, so it's already next round's inventory and outside the projection's scope. + +### 2.5 AI duel test comment + +`tests/engine/ai-duel.test.ts` — append a one-line note that the bomber rule + Carnage tuning shift the distribution again; P4c slice 2+ uses the new distribution. + +--- + +## 3. Round flow + +**Unchanged** structurally. The launch phase already runs: + +``` +collectLaunches → consumeStockFor → applyLaunches +``` + +The only delta is inside `applyLaunches`: after damage application, a single line per impact branch restores the bomber. + +--- + +## 4. Engine schema changes + +**None.** No new event kinds, no new fields on `Leader` or `GameState`, no `Order` shape changes. `ResolutionEvent` variants stay as P4b left them. This is a behaviour change only. + +--- + +## 5. Testing + +P4b baseline: 246 tests. Target end-state: **~252 tests**. + +### 5.1 Engine tests + +``` +tests/engine/launches.test.ts (extend, ~3 tests) + ↳ bomber impact (people) restores bomber to attacker stockpile + ↳ bomber impact (infra) restores bomber + ↳ bomber intercept does NOT restore (intercepted bomber stays gone) + ↳ same-round double-launch with 1 bomber rejected (validation guard works) + +tests/engine/launches.test.ts (extend, regression) + ↳ missile launch behaviour unchanged: missile gone on impact AND on intercept + +tests/engine/ai/carnage.test.ts (extend, ~2 tests) + ↳ Carnage builds bomber when AP allows (verify queued orders contain build-bomber, not build-missile) + ↳ Carnage launches with delivery='bomber' when bombers >= 1 in stockpile + +tests/engine/ai/netanyahoo.test.ts (extend, ~1 test) + ↳ Netanyahoo still builds missiles (regression — bomber bias does NOT bleed across personalities) +``` + +### 5.2 What is NOT tested + +- Exact AI-duel win distribution under the new rule (deferred to P4c slice 2 tuning pass). +- UI behavior — `projectInventory` requires no change so existing tests cover it. +- Cross-leader bomber semantics — no shared bomber pool exists. + +--- + +## 6. Assumptions (3 buckets) + +### 6.1 Real concerns + +1. **AI duel under the new bomber rule may shift in surprising ways.** Carnage with bombers + reusability becomes much more durable per-AP. Expect his win rate to increase noticeably. Acceptable — slice 2 tunes against the new baseline. AI-duel test continues to assert only "no crash". + +### 6.2 Verified safe + +1. **Engine purity preserved.** No React imports under `src/engine/**`. +2. **Determinism preserved.** No new RNG consumption. The bomber-restore is deterministic (always restores on impact, never restores on intercept). +3. **P4b backward compatibility.** `Order` shape unchanged; `Leader` shape unchanged; `ResolutionEvent` shape unchanged. Existing UI consumers see no difference. +4. **CSS Modules pattern continues** — no UI changes. +5. **TDD posture continues** — engine TDD strict; the four new test cases drive the implementation. + +### 6.3 Minor / accepted + +1. **Dead-leader bomber restoration on FR impact** is a harmless no-op. Not worth special-casing. +2. **Carnage threat scoring of OPPONENT bombers** stays unchanged for now. If it turns out he undervalues opponents with bomber-heavy arsenals, that's a slice 2 tuning question. +3. **Slice 1 doesn't touch other personalities.** Khameneverhere, Starmless, Mileigh-hem may also "logically" benefit from bombers under the new rule. Their tuning lives in slice 2. + +--- + +## 7. Out of scope + +### 7.1 Deferred to P4c slice 2+ + +- Full AI scoring-weight balance pass against the new rules (Khameneverhere/Starmless/Mileigh-hem revisited; mutual-shield-saturation imbalance from P2 known-issues) +- Approach B / C lookahead upgrades (sliding-window history; personality-fit modelling) +- Threat-aware defence deployment per personality +- Opponent-bomber threat weighting reconsideration + +### 7.2 Deferred to P5 (polish) + +- Persistence, replay scrubber UI, animations, audio, SVG art, PWA — unchanged from P4b spec. + +--- + +## 8. File list + +### 8.1 Modified + +``` +src/engine/launches.ts — applyLaunches restores bomber in 2 impact branches +src/engine/ai/carnage.ts — build-bomber instead of build-missile; launch delivery='bomber' when available +tests/engine/launches.test.ts — extend with bomber lifecycle tests + missile regression +tests/engine/ai/carnage.test.ts — extend with bomber-bias tests +tests/engine/ai/netanyahoo.test.ts — extend with regression test +tests/engine/ai-duel.test.ts — one-line comment update +README.md — Phase 4c slice 1 status section +``` + +### 8.2 New files + +None. + +### 8.3 Deleted files + +None. + +--- + +## 9. References + +- P4b spec: `docs/superpowers/specs/2026-05-13-phase-4b-balance-planning-rework-design.md` +- P4b plan: `docs/superpowers/plans/2026-05-13-phase-4b-balance-planning-rework.md` +- Engine entry points: `src/engine/launches.ts` (consumeStockFor + applyLaunches), `src/engine/ai/carnage.ts` From 87d15d2e12b02e030c4631ea8582ece46f65daa5 Mon Sep 17 00:00:00 2001 From: gethi Date: Thu, 14 May 2026 16:54:00 +0100 Subject: [PATCH 2/7] docs: P4c slice 1 implementation plan (bomber reuse + Carnage bias) --- ...2026-05-14-phase-4c-slice1-bomber-reuse.md | 552 ++++++++++++++++++ 1 file changed, 552 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-14-phase-4c-slice1-bomber-reuse.md diff --git a/docs/superpowers/plans/2026-05-14-phase-4c-slice1-bomber-reuse.md b/docs/superpowers/plans/2026-05-14-phase-4c-slice1-bomber-reuse.md new file mode 100644 index 0000000..69e7c7b --- /dev/null +++ b/docs/superpowers/plans/2026-05-14-phase-4c-slice1-bomber-reuse.md @@ -0,0 +1,552 @@ +# Phase 4c slice 1 — Bomber reuse + Carnage bomber bias + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Bombers return to stockpile on impact (lost only on intercept). Carnage builds + launches with bombers; Netanyahoo stays missile-biased. + +**Architecture:** One-line restoration in `applyLaunches` impact branches. Carnage's planner gains a "build a bomber if I don't own one" first-priority + flips launch delivery to bomber when available. Netanyahoo untouched. + +**Tech Stack:** TypeScript 5.4, Vitest 1.5. No new runtime deps. Engine-only — no UI changes. + +**Source of truth:** `docs/superpowers/specs/2026-05-14-phase-4c-slice1-bomber-reuse-design.md` (committed `813320c`). If anything below conflicts with the spec, the spec wins. + +**Per-step confidence:** all 4 tasks rated. Min confidence post-lift: **92 %** (Task 2 — Carnage build/launch bias). Mitigations inline. + +--- + +## File structure + +**Modified:** + +| Path | Change | +|---|---| +| `src/engine/launches.ts` | `applyLaunches` impact branches restore bomber: `+attacker.stockpile.bombers += 1` | +| `src/engine/ai/carnage.ts` | New "build 1 bomber if owned=0" priority before warhead builds; `canLaunch` accepts bomber-or-missile; launch order delivery flips to 'bomber' when available | +| `tests/engine/launches.test.ts` | Extend with bomber lifecycle tests (impact restores; intercept doesn't; double-queue rejected; missile regression) | +| `tests/engine/ai/carnage.test.ts` | Extend with bomber-bias tests | +| `tests/engine/ai/netanyahoo.test.ts` | Extend with regression test (missile-bias preserved) | +| `tests/engine/ai-duel.test.ts` | Comment update flagging slice 2 rebaseline | +| `README.md` | Phase 4c slice 1 status section | + +**New / Deleted:** None. + +--- + +## Task confidence summary + +| Task | Confidence | Notes | +|---|---|---| +| 1. Engine bomber-reuse rule | 95 % | Mechanical 2-line addition + 4 tests | +| 2. Carnage build/launch bias | 92 % | Need to add delivery-building (he had none); first-priority bomber build pattern; lift via single-shot conditional rather than loop | +| 3. Netanyahoo regression test | 98 % | Assertion-only | +| 4. Docs (ai-duel comment + README) | 98 % | Text changes | + +--- + +## Task 1: Bomber-reuse rule in `applyLaunches` + +**Files:** +- Modify: `src/engine/launches.ts` +- Test: `tests/engine/launches.test.ts` (extend) + +**Confidence: 95 %** + +### Step 1: Write failing tests + +Append to `tests/engine/launches.test.ts` (inside the existing top-level describe, or a new describe block): + +```ts +describe('bombers are reusable (P4c slice 1)', () => { + it('bomber impact (people) restores bomber to attacker stockpile', () => { + let s = initialState({ + cast: ['chump', 'carnage'], + difficulty: 'normal', + seed: 'bomber-people-restore', + }); + s.leaders.chump.stockpile.bombers = 1; + s.leaders.chump.stockpile.warheadsSmall = 1; + s.leaders.chump.ap = 5; + s.leaders.carnage.deployedShields = 0; // ensure no intercept + + s = reduce(s, { + type: 'SUBMIT_ORDERS', + leaderId: 'chump', + orders: [{ kind: 'launch', target: 'carnage', delivery: 'bomber', warhead: 'small', targetType: 'people' }], + }); + s = reduce(s, { type: 'SUBMIT_ORDERS', leaderId: 'carnage', orders: [] }); + const r = resolveRound(s); + + const impact = r.events.find((e) => e.kind === 'ImpactPeople'); + expect(impact).toBeDefined(); + expect(r.state.leaders.chump.stockpile.bombers).toBe(1); // restored + expect(r.state.leaders.chump.stockpile.warheadsSmall).toBe(0); // warhead still consumed + }); + + it('bomber impact (infra) restores bomber to attacker stockpile', () => { + let s = initialState({ + cast: ['chump', 'carnage'], + difficulty: 'normal', + seed: 'bomber-infra-restore', + }); + s.leaders.chump.stockpile.bombers = 1; + s.leaders.chump.stockpile.warheadsSmall = 1; + s.leaders.chump.ap = 5; + s.leaders.carnage.deployedShields = 0; + + s = reduce(s, { + type: 'SUBMIT_ORDERS', + leaderId: 'chump', + orders: [{ kind: 'launch', target: 'carnage', delivery: 'bomber', warhead: 'small', targetType: 'infra' }], + }); + s = reduce(s, { type: 'SUBMIT_ORDERS', leaderId: 'carnage', orders: [] }); + const r = resolveRound(s); + + const impact = r.events.find((e) => e.kind === 'ImpactInfrastructure'); + expect(impact).toBeDefined(); + expect(r.state.leaders.chump.stockpile.bombers).toBe(1); // restored + }); + + it('bomber intercept does NOT restore bomber', () => { + let s = initialState({ + cast: ['chump', 'carnage'], + difficulty: 'normal', + seed: 'bomber-intercept-gone', + }); + s.leaders.chump.stockpile.bombers = 1; + s.leaders.chump.stockpile.warheadsSmall = 1; + s.leaders.chump.ap = 5; + s.leaders.carnage.deployedAA = 5; // ensure 100% intercept (first incoming, defenders >= 1) + + s = reduce(s, { + type: 'SUBMIT_ORDERS', + leaderId: 'chump', + orders: [{ kind: 'launch', target: 'carnage', delivery: 'bomber', warhead: 'small', targetType: 'people' }], + }); + s = reduce(s, { type: 'SUBMIT_ORDERS', leaderId: 'carnage', orders: [] }); + const r = resolveRound(s); + + const intercepted = r.events.find((e) => e.kind === 'MissileIntercepted'); + expect(intercepted).toBeDefined(); + expect(r.state.leaders.chump.stockpile.bombers).toBe(0); // gone after intercept + }); + + it('missile launch unchanged: missile consumed on impact AND on intercept', () => { + let s = initialState({ + cast: ['chump', 'carnage'], + difficulty: 'normal', + seed: 'missile-regression', + }); + s.leaders.chump.stockpile.missiles = 2; + s.leaders.chump.stockpile.warheadsSmall = 2; + s.leaders.chump.ap = 10; + + s = reduce(s, { + type: 'SUBMIT_ORDERS', + leaderId: 'chump', + orders: [ + { kind: 'launch', target: 'carnage', delivery: 'missile', warhead: 'small', targetType: 'people' }, + { kind: 'launch', target: 'carnage', delivery: 'missile', warhead: 'small', targetType: 'people' }, + ], + }); + s = reduce(s, { type: 'SUBMIT_ORDERS', leaderId: 'carnage', orders: [] }); + const r = resolveRound(s); + + // Both missiles consumed regardless of outcome + expect(r.state.leaders.chump.stockpile.missiles).toBe(0); + expect(r.state.leaders.chump.stockpile.warheadsSmall).toBe(0); + }); +}); +``` + +(Make sure `initialState`, `reduce`, `resolveRound` are imported at the top. They likely already are; if not, add them.) + +### Step 2: Run tests to verify they fail + +```bash +npm run test:run -- tests/engine/launches.test.ts -t "bombers are reusable" +``` + +Expected: FAIL — the first two tests fail because `applyLaunches` doesn't restore bombers yet (stockpile.bombers stays 0). The third and fourth tests likely PASS already (no restoration is the current behavior). + +### Step 3: Add bomber restoration in `applyLaunches` + +Edit `src/engine/launches.ts`. Find the `case 'people'` and `case 'infra'` impact branches inside `applyLaunches` (around lines 133-153 — they currently push `ImpactPeople` / `ImpactInfrastructure` events after applying damage). + +For BOTH branches, after the damage is applied and the event is pushed, add a bomber-restoration block. The attacker reference is `next.leaders[l.from]`; restoration is unconditional on attacker liveness (a dead attacker's stockpile is irrelevant, restoration is harmless). + +After the existing impact-event push, add: + +```ts + // P4c.1: bomber is reusable — restore on impact (lost only on intercept). + if (l.delivery === 'bomber') { + const attacker = next.leaders[l.from]; + if (attacker) attacker.stockpile.bombers += 1; + } +``` + +Add this block in both the people-impact branch and the infra-impact branch. The block is identical in both — extract a local helper if you prefer DRY, but two inlined copies of the 4-line block is fine given the surrounding pattern. + +### Step 4: Run tests to verify they pass + +```bash +npm run test:run -- tests/engine/launches.test.ts -t "bombers are reusable" +``` + +Expected: PASS (4 tests). + +### Step 5: Run full suite + +```bash +npm run test:run +npm run typecheck +``` + +Expected: PASS — 246 + 4 = 250 tests, typecheck clean. + +### Step 6: Commit + +```bash +git add src/engine/launches.ts tests/engine/launches.test.ts +git commit -m "engine: bombers are reusable — restored on impact, lost on intercept" +``` + +--- + +## Task 2: Carnage build/launch bomber bias + +**Files:** +- Modify: `src/engine/ai/carnage.ts` +- Test: `tests/engine/ai/carnage.test.ts` (extend) + +**Confidence: 92 %** + +**Mitigation:** Carnage currently doesn't build any deliveries (only warheads), so `canLaunch` is always false in practice. The spec implies a build-bias swap (missile → bomber) but the literal code has no missile-build. The mitigation: keep the new bomber-build a **single-shot** ("if I own zero bombers AND have budget, build one") rather than a loop, to avoid the projection-vs-validator gotcha from the P4b BugBot finding. Single order = no projection needed. + +### Step 1: Write failing tests + +Append to `tests/engine/ai/carnage.test.ts` (or create if missing — check first by reading the directory). New describe block: + +```ts +describe('Carnage AI bomber bias (P4c.1)', () => { + it('builds a bomber when none owned and budget allows', () => { + let s = initialState({ + cast: ['carnage', 'chump'], + difficulty: 'normal', + seed: 'carnage-build-bomber', + }); + s.leaders.carnage.stockpile.bombers = 0; + s.leaders.carnage.stockpile.missiles = 0; + s.leaders.carnage.ap = 6; // P4b default + + const orders = planCarnage(s, 'carnage'); + expect(orders.some((o) => o.kind === 'build-bomber')).toBe(true); + expect(orders.some((o) => o.kind === 'build-missile')).toBe(false); // bias is bombers only + }); + + it('does NOT build a second bomber when one is already owned', () => { + let s = initialState({ + cast: ['carnage', 'chump'], + difficulty: 'normal', + seed: 'carnage-already-has-bomber', + }); + s.leaders.carnage.stockpile.bombers = 1; // already owns one + s.leaders.carnage.stockpile.missiles = 0; + s.leaders.carnage.ap = 6; + + const orders = planCarnage(s, 'carnage'); + expect(orders.filter((o) => o.kind === 'build-bomber')).toHaveLength(0); + }); + + it('launches with delivery=bomber when a bomber is in stockpile', () => { + let s = initialState({ + cast: ['carnage', 'chump'], + difficulty: 'normal', + seed: 'carnage-launch-bomber', + }); + s.leaders.carnage.stockpile.bombers = 1; + s.leaders.carnage.stockpile.missiles = 1; + s.leaders.carnage.stockpile.warheadsSmall = 1; + s.leaders.carnage.ap = 6; + // Give Chump arsenal so Carnage's threat score is non-zero (opportunism only is fine too) + s.leaders.chump.stockpile.missiles = 2; + + const orders = planCarnage(s, 'carnage'); + const launch = orders.find((o) => o.kind === 'launch'); + expect(launch).toBeDefined(); + if (launch && launch.kind === 'launch') { + expect(launch.delivery).toBe('bomber'); + } + }); + + it('falls back to delivery=missile when no bomber but missile available', () => { + let s = initialState({ + cast: ['carnage', 'chump'], + difficulty: 'normal', + seed: 'carnage-launch-missile-fallback', + }); + s.leaders.carnage.stockpile.bombers = 0; + s.leaders.carnage.stockpile.missiles = 1; + s.leaders.carnage.stockpile.warheadsSmall = 1; + s.leaders.carnage.ap = 6; + s.leaders.chump.stockpile.missiles = 2; + + const orders = planCarnage(s, 'carnage'); + const launch = orders.find((o) => o.kind === 'launch'); + if (launch && launch.kind === 'launch') { + expect(launch.delivery).toBe('missile'); + } + // It's OK if no launch order is emitted at all in this scenario; the + // assertion is "IF Carnage launches, delivery is missile". The boolean + // existence check is asserted in the previous test (with bomber present). + }); +}); +``` + +If `tests/engine/ai/carnage.test.ts` doesn't exist, create it with appropriate imports: + +```ts +import { describe, expect, it } from 'vitest'; +import { planCarnage } from '../../../src/engine/ai/carnage'; +import { initialState } from '../../../src/engine/state'; +// (existing tests if file exists) +``` + +### Step 2: Verify failing + +```bash +npm run test:run -- tests/engine/ai/carnage.test.ts -t "P4c.1" +``` + +Expected: FAIL — Carnage doesn't build bombers (only warheads); launches default to `delivery: 'missile'`. + +### Step 3: Update Carnage's planner + +Edit `src/engine/ai/carnage.ts`. Two changes: + +**Change A: Add bomber-build as first build-budget priority.** Find the build section (around lines 70-83 — the `while (remaining >= 1)` loop that builds small warheads). Insert a **single-shot** bomber build BEFORE the warhead loop: + +```ts + // --- 1. Builds --- + let remaining = buildBudget; + + // P4c.1: Carnage values bombers (reusable assets). If he owns none, + // build one as first priority. Single-shot — projection-safe. + if (me.stockpile.bombers === 0 && remaining >= 1) { + const o: Order = { kind: 'build-bomber' }; + if (validateOrder(state, leaderId, o).ok) { + orders.push(o); + remaining -= 1; + } + } + + // Build small warheads with the rest. + while (remaining >= 1) { + const o: Order = { kind: 'build-warhead', yield: 'small' }; + if (validateOrder(state, leaderId, o).ok) { + orders.push(o); + remaining -= 1; + } else { + break; + } + } +``` + +**Change B: Launch with bomber when available; relax `canLaunch` to accept either delivery.** Find the existing `canLaunch` check (around line 54-58) and the launch construction (around line 87-93). + +Replace the `canLaunch` block: + +```ts + const hasDelivery = me.stockpile.bombers >= 1 || me.stockpile.missiles >= 1; + const canLaunch = + launchTarget !== undefined && + hasDelivery && + me.stockpile.warheadsSmall >= 1 && + budget >= LAUNCH_COST; +``` + +Replace the launch construction: + +```ts + if (canLaunch && launchTarget !== undefined && budget >= LAUNCH_COST) { + const delivery: 'bomber' | 'missile' = me.stockpile.bombers >= 1 ? 'bomber' : 'missile'; + const launch: Order = { + kind: 'launch', + target: launchTarget, + delivery, + warhead: 'small', + targetType: 'people', + }; + if (validateOrder(state, leaderId, launch).ok) { + orders.push(launch); + budget -= apCostOf(launch); + } + } +``` + +### Step 4: Run tests + +```bash +npm run test:run -- tests/engine/ai/carnage.test.ts +``` + +Expected: PASS — 4 new tests green (plus any pre-existing carnage tests). + +### Step 5: Run full suite + typecheck + +```bash +npm run test:run +npm run typecheck +``` + +Expected: typecheck clean, all tests green. The AI-duel test will print a different distribution now (Carnage's win rate likely climbs); no assertion change needed since the duel has no balance assertions. + +### Step 6: Commit + +```bash +git add src/engine/ai/carnage.ts tests/engine/ai/carnage.test.ts +git commit -m "engine: Carnage AI — bomber build + launch bias + +- Single-shot bomber build when stockpile.bombers === 0 (first build priority) +- canLaunch accepts bomber-or-missile delivery +- Launch order uses delivery='bomber' when available, falls back to 'missile'" +``` + +--- + +## Task 3: Netanyahoo regression test + +**Files:** +- Test: `tests/engine/ai/netanyahoo.test.ts` (extend or create) + +**Confidence: 98 %** + +### Step 1: Add regression test + +Append to `tests/engine/ai/netanyahoo.test.ts` (or create it if missing — check first): + +```ts +describe('Netanyahoo missile-bias regression (P4c.1)', () => { + it('still builds missiles, not bombers — bomber bias does NOT bleed across personalities', () => { + let s = initialState({ + cast: ['netanyahoo', 'chump'], + difficulty: 'normal', + seed: 'netanyahoo-still-missile', + }); + s.leaders.netanyahoo.stockpile.bombers = 0; + s.leaders.netanyahoo.stockpile.missiles = 0; + s.leaders.netanyahoo.ap = 6; + + const orders = planNetanyahoo(s, 'netanyahoo'); + // The personality should not emit any build-bomber orders. + expect(orders.filter((o) => o.kind === 'build-bomber')).toHaveLength(0); + // It should emit build-missile (warmonger profile builds missiles). + expect(orders.some((o) => o.kind === 'build-missile')).toBe(true); + }); +}); +``` + +If the test file doesn't exist, create it with imports: + +```ts +import { describe, expect, it } from 'vitest'; +import { planNetanyahoo } from '../../../src/engine/ai/netanyahoo'; +import { initialState } from '../../../src/engine/state'; +``` + +### Step 2: Verify the test passes (no code change needed) + +```bash +npm run test:run -- tests/engine/ai/netanyahoo.test.ts +``` + +Expected: PASS — Netanyahoo's existing code already builds missiles + small warheads as the warmonger pattern (see `src/engine/ai/netanyahoo.ts` line 62-70). The new test just locks the behavior in place against future drift. + +### Step 3: Commit + +```bash +git add tests/engine/ai/netanyahoo.test.ts +git commit -m "tests: Netanyahoo regression — still missile-biased (bomber rule doesn't bleed)" +``` + +--- + +## Task 4: Docs (ai-duel comment + README) + +**Files:** +- Modify: `tests/engine/ai-duel.test.ts` +- Modify: `README.md` + +**Confidence: 98 %** + +### Step 1: Update the ai-duel test comment + +Edit `tests/engine/ai-duel.test.ts`. Find the comment block flagging the P4b baseline as stale. After the existing P4b note, append: + +```ts + // P4c.1 note: bombers are now reusable (return on impact, lost on intercept) + // and Carnage gains a bomber build/launch bias. Expect Carnage's win rate + // to climb under the new rule. P4c.2+ uses this new distribution as the + // tuning baseline for wider AI weight rebalancing. +``` + +### Step 2: Append Phase 4c.1 status to README + +Edit `README.md`. After the existing `## Phase 4b status` section, append: + +```markdown +## Phase 4c slice 1 status + +Phase 4c is the AI tuning pass. Slice 1 ships two tightly coupled changes from playtesting: bombers are now reusable (return on impact, lost on intercept) and Carnage favours bombers in his build + launch picks. Netanyahoo stays missile-biased. Verification: `npm run test:run` (~252 tests). + +What's in this slice: + +- **Bombers reusable** — `applyLaunches` restores `attacker.stockpile.bombers` after a successful impact (both people and infra). Intercepted bombers stay gone. Warheads always consumed. One bomber = one mission per round (enforced by the existing `consumeStockFor` decrement). +- **Carnage AI** — single-shot build-bomber when stockpile.bombers === 0 (first build priority); `canLaunch` accepts bomber-or-missile delivery; launch order uses `delivery: 'bomber'` when available. +- **Netanyahoo unchanged** — warmonger profile preserved. Regression test locks it in. + +What's NOT in this slice (deferred to P4c slice 2+): + +- Wider AI scoring-weight rebalancing (Khameneverhere/Starmless/Mileigh-hem/Chump bomber-awareness) +- Approach B / C lookahead upgrades (sliding-window history; personality-fit modelling) +- Threat-aware defence deployment per personality +- AI-duel balance assertions (still asserts only "no crash"; baseline data informs slice 2) +``` + +### Step 3: Run full suite for regression + +```bash +npm run test:run +npm run typecheck +``` + +Expected: PASS — 252 tests, typecheck clean. + +### Step 4: Commit + +```bash +git add tests/engine/ai-duel.test.ts README.md +git commit -m "docs: Phase 4c slice 1 status — bomber reuse + Carnage bias" +``` + +--- + +## Self-review checklist (controller runs before handoff) + +1. **Spec coverage:** + - §2.1 Bomber-reuse engine rule → Task 1. + - §2.2 Carnage AI bomber bias → Task 2. + - §2.3 Netanyahoo unchanged → Task 3 regression test. + - §2.4 UI projection unchanged → no task (confirmed unchanged in spec). + - §2.5 AI duel test comment → Task 4 step 1. + - §5 Testing → tests embedded in each task; total +6 tests, ending at ~252. + +2. **Placeholder scan:** none found. + +3. **Type consistency:** + - `planCarnage` and `planNetanyahoo` function names match existing exports in `src/engine/ai/*.ts`. + - `delivery: 'bomber' | 'missile'` type is the existing `DeliveryType` from `src/engine/types.ts`. + - All test imports point at real engine modules; no fabricated symbols. + +--- + +## Execution handoff From 30e6f9b8d3fe9ab41e86163ef0f78326913da8d0 Mon Sep 17 00:00:00 2001 From: gethi Date: Thu, 14 May 2026 17:02:58 +0100 Subject: [PATCH 3/7] =?UTF-8?q?engine:=20bombers=20are=20reusable=20?= =?UTF-8?q?=E2=80=94=20restored=20on=20impact,=20lost=20on=20intercept?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/engine/launches.ts | 10 ++++ tests/engine/launches.test.ts | 102 ++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/src/engine/launches.ts b/src/engine/launches.ts index a965120..468a76b 100644 --- a/src/engine/launches.ts +++ b/src/engine/launches.ts @@ -142,6 +142,11 @@ export function applyLaunches( warhead: l.warhead, deaths, }); + // P4c.1: bomber is reusable — restore on impact (lost only on intercept). + if (l.delivery === 'bomber') { + const attacker = next.leaders[l.from]; + if (attacker) attacker.stockpile.bombers += 1; + } } else { const destroyed = factoriesDestroyed(l.warhead, receiver.factories); receiver.factories -= destroyed; @@ -152,6 +157,11 @@ export function applyLaunches( warhead: l.warhead, factoriesDestroyed: destroyed, }); + // P4c.1: bomber is reusable — restore on impact (lost only on intercept). + if (l.delivery === 'bomber') { + const attacker = next.leaders[l.from]; + if (attacker) attacker.stockpile.bombers += 1; + } } } diff --git a/tests/engine/launches.test.ts b/tests/engine/launches.test.ts index 128beb5..4238e73 100644 --- a/tests/engine/launches.test.ts +++ b/tests/engine/launches.test.ts @@ -1,6 +1,8 @@ import { describe, it, expect } from 'vitest'; import { applyLaunches, collectLaunches, consumeStockFor, makeIncomingCounter } from '../../src/engine/launches'; import { initialState } from '../../src/engine/state'; +import { reduce } from '../../src/engine/reducer'; +import { resolveRound } from '../../src/engine/resolution'; import type { Launch, Order } from '../../src/engine/types'; const smallLaunch: Launch = { @@ -180,3 +182,103 @@ describe('applyLaunches (assumes stock pre-consumed)', () => { expect(r4.incoming.carnage.missile).toBe(4); }); }); + +describe('bombers are reusable (P4c slice 1)', () => { + it('bomber impact (people) restores bomber to attacker stockpile', () => { + let s = initialState({ + cast: ['chump', 'carnage'], + difficulty: 'normal', + seed: 'bomber-people-restore', + }); + s.leaders.chump.stockpile.bombers = 1; + s.leaders.chump.stockpile.warheadsSmall = 1; + s.leaders.chump.ap = 5; + s.leaders.carnage.deployedShields = 0; + + s = reduce(s, { + type: 'SUBMIT_ORDERS', + leaderId: 'chump', + orders: [{ kind: 'launch', target: 'carnage', delivery: 'bomber', warhead: 'small', targetType: 'people' }], + }); + s = reduce(s, { type: 'SUBMIT_ORDERS', leaderId: 'carnage', orders: [] }); + const r = resolveRound(s); + + const impact = r.events.find((e) => e.kind === 'ImpactPeople'); + expect(impact).toBeDefined(); + expect(r.state.leaders.chump.stockpile.bombers).toBe(1); // restored + expect(r.state.leaders.chump.stockpile.warheadsSmall).toBe(0); // warhead consumed + }); + + it('bomber impact (infra) restores bomber to attacker stockpile', () => { + let s = initialState({ + cast: ['chump', 'carnage'], + difficulty: 'normal', + seed: 'bomber-infra-restore', + }); + s.leaders.chump.stockpile.bombers = 1; + s.leaders.chump.stockpile.warheadsSmall = 1; + s.leaders.chump.ap = 5; + s.leaders.carnage.deployedShields = 0; + + s = reduce(s, { + type: 'SUBMIT_ORDERS', + leaderId: 'chump', + orders: [{ kind: 'launch', target: 'carnage', delivery: 'bomber', warhead: 'small', targetType: 'infra' }], + }); + s = reduce(s, { type: 'SUBMIT_ORDERS', leaderId: 'carnage', orders: [] }); + const r = resolveRound(s); + + const impact = r.events.find((e) => e.kind === 'ImpactInfrastructure'); + expect(impact).toBeDefined(); + expect(r.state.leaders.chump.stockpile.bombers).toBe(1); // restored + }); + + it('bomber intercept does NOT restore bomber', () => { + let s = initialState({ + cast: ['chump', 'carnage'], + difficulty: 'normal', + seed: 'bomber-intercept-gone', + }); + s.leaders.chump.stockpile.bombers = 1; + s.leaders.chump.stockpile.warheadsSmall = 1; + s.leaders.chump.ap = 5; + s.leaders.carnage.deployedAA = 5; // ensure 100% intercept + + s = reduce(s, { + type: 'SUBMIT_ORDERS', + leaderId: 'chump', + orders: [{ kind: 'launch', target: 'carnage', delivery: 'bomber', warhead: 'small', targetType: 'people' }], + }); + s = reduce(s, { type: 'SUBMIT_ORDERS', leaderId: 'carnage', orders: [] }); + const r = resolveRound(s); + + const intercepted = r.events.find((e) => e.kind === 'MissileIntercepted'); + expect(intercepted).toBeDefined(); + expect(r.state.leaders.chump.stockpile.bombers).toBe(0); // gone after intercept + }); + + it('missile launch unchanged: missile consumed on impact AND on intercept', () => { + let s = initialState({ + cast: ['chump', 'carnage'], + difficulty: 'normal', + seed: 'missile-regression', + }); + s.leaders.chump.stockpile.missiles = 2; + s.leaders.chump.stockpile.warheadsSmall = 2; + s.leaders.chump.ap = 10; + + s = reduce(s, { + type: 'SUBMIT_ORDERS', + leaderId: 'chump', + orders: [ + { kind: 'launch', target: 'carnage', delivery: 'missile', warhead: 'small', targetType: 'people' }, + { kind: 'launch', target: 'carnage', delivery: 'missile', warhead: 'small', targetType: 'people' }, + ], + }); + s = reduce(s, { type: 'SUBMIT_ORDERS', leaderId: 'carnage', orders: [] }); + const r = resolveRound(s); + + expect(r.state.leaders.chump.stockpile.missiles).toBe(0); + expect(r.state.leaders.chump.stockpile.warheadsSmall).toBe(0); + }); +}); From abb7f5eb6c7d35c93af844891a676f7f57c8f615 Mon Sep 17 00:00:00 2001 From: gethi Date: Thu, 14 May 2026 17:09:34 +0100 Subject: [PATCH 4/7] =?UTF-8?q?engine:=20Carnage=20AI=20=E2=80=94=20bomber?= =?UTF-8?q?=20build=20+=20launch=20bias?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Single-shot bomber build when stockpile.bombers === 0 (first build priority) - canLaunch accepts bomber-or-missile delivery - Launch order uses delivery='bomber' when available, falls back to 'missile' --- src/engine/ai/carnage.ts | 19 +++++++-- tests/engine/ai/carnage.test.ts | 72 +++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/src/engine/ai/carnage.ts b/src/engine/ai/carnage.ts index a71ea09..d69c7b3 100644 --- a/src/engine/ai/carnage.ts +++ b/src/engine/ai/carnage.ts @@ -51,9 +51,10 @@ export function planCarnage(state: GameState, leaderId: LeaderId): Order[] { ); } + const hasDelivery = me.stockpile.bombers >= 1 || me.stockpile.missiles >= 1; const canLaunch = launchTarget !== undefined && - me.stockpile.missiles >= 1 && + hasDelivery && me.stockpile.warheadsSmall >= 1 && budget >= LAUNCH_COST; @@ -67,9 +68,20 @@ export function planCarnage(state: GameState, leaderId: LeaderId): Order[] { const totalReserve = launchReserve + propagandaSlots * PROPAGANDA_COST; const buildBudget = budget - totalReserve; - // --- 1. Builds: warheads with leftover budget --- + // --- 1. Builds --- let remaining = buildBudget; + // P4c.1: Carnage values bombers (reusable assets). If he owns none, + // build one as first priority. Single-shot — projection-safe. + if (me.stockpile.bombers === 0 && remaining >= 1) { + const o: Order = { kind: 'build-bomber' }; + if (validateOrder(state, leaderId, o).ok) { + orders.push(o); + remaining -= 1; + } + } + + // Build small warheads with the rest. while (remaining >= 1) { const o: Order = { kind: 'build-warhead', yield: 'small' }; if (validateOrder(state, leaderId, o).ok) { @@ -84,10 +96,11 @@ export function planCarnage(state: GameState, leaderId: LeaderId): Order[] { // --- 2. Launch at the best combined-score target --- if (canLaunch && launchTarget !== undefined && budget >= LAUNCH_COST) { + const delivery: 'bomber' | 'missile' = me.stockpile.bombers >= 1 ? 'bomber' : 'missile'; const launch: Order = { kind: 'launch', target: launchTarget, - delivery: 'missile', + delivery, warhead: 'small', targetType: 'people', }; diff --git a/tests/engine/ai/carnage.test.ts b/tests/engine/ai/carnage.test.ts index 1aa32c8..cbb7212 100644 --- a/tests/engine/ai/carnage.test.ts +++ b/tests/engine/ai/carnage.test.ts @@ -79,3 +79,75 @@ describe('Carnage (Rational + Opportunist)', () => { expect(props).toHaveLength(0); }); }); + +describe('Carnage AI bomber bias (P4c.1)', () => { + it('builds a bomber when none owned and budget allows', () => { + let s = initialState({ + cast: ['carnage', 'chump'], + difficulty: 'normal', + seed: 'carnage-build-bomber', + }); + s.leaders.carnage.stockpile.bombers = 0; + s.leaders.carnage.stockpile.missiles = 0; + s.leaders.carnage.ap = 6; + + const orders = planCarnage(s, 'carnage'); + expect(orders.some((o) => o.kind === 'build-bomber')).toBe(true); + expect(orders.some((o) => o.kind === 'build-missile')).toBe(false); + }); + + it('does NOT build a second bomber when one is already owned', () => { + let s = initialState({ + cast: ['carnage', 'chump'], + difficulty: 'normal', + seed: 'carnage-already-has-bomber', + }); + s.leaders.carnage.stockpile.bombers = 1; + s.leaders.carnage.stockpile.missiles = 0; + s.leaders.carnage.ap = 6; + + const orders = planCarnage(s, 'carnage'); + expect(orders.filter((o) => o.kind === 'build-bomber')).toHaveLength(0); + }); + + it('launches with delivery=bomber when a bomber is in stockpile', () => { + let s = initialState({ + cast: ['carnage', 'chump'], + difficulty: 'normal', + seed: 'carnage-launch-bomber', + }); + s.leaders.carnage.stockpile.bombers = 1; + s.leaders.carnage.stockpile.missiles = 1; + s.leaders.carnage.stockpile.warheadsSmall = 1; + s.leaders.carnage.ap = 6; + s.leaders.chump.stockpile.missiles = 2; + + const orders = planCarnage(s, 'carnage'); + const launch = orders.find((o) => o.kind === 'launch'); + expect(launch).toBeDefined(); + if (launch && launch.kind === 'launch') { + expect(launch.delivery).toBe('bomber'); + } + }); + + it('falls back to delivery=missile when no bomber but missile available', () => { + let s = initialState({ + cast: ['carnage', 'chump'], + difficulty: 'normal', + seed: 'carnage-launch-missile-fallback', + }); + s.leaders.carnage.stockpile.bombers = 0; + s.leaders.carnage.stockpile.missiles = 1; + s.leaders.carnage.stockpile.warheadsSmall = 1; + s.leaders.carnage.ap = 6; + s.leaders.chump.stockpile.missiles = 2; + + const orders = planCarnage(s, 'carnage'); + const launch = orders.find((o) => o.kind === 'launch'); + // It's OK if no launch is emitted at all (Carnage may build instead); + // the assertion is: IF Carnage launches, delivery is missile. + if (launch && launch.kind === 'launch') { + expect(launch.delivery).toBe('missile'); + } + }); +}); From 98c84d65afa6c3ea313698b60ad399d430561a9a Mon Sep 17 00:00:00 2001 From: gethi Date: Thu, 14 May 2026 17:22:29 +0100 Subject: [PATCH 5/7] tests: Netanyahoo missile-bias regression (P4c.1) Co-Authored-By: Claude Sonnet 4.6 --- tests/engine/ai/netanyahoo.test.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/engine/ai/netanyahoo.test.ts b/tests/engine/ai/netanyahoo.test.ts index 71253f5..d26925c 100644 --- a/tests/engine/ai/netanyahoo.test.ts +++ b/tests/engine/ai/netanyahoo.test.ts @@ -41,3 +41,23 @@ describe('Netanyahoo (Warmonger)', () => { expect(launch?.target).toBe('carnage'); }); }); + +describe('Netanyahoo missile bias regression (P4c.1)', () => { + it('still emits build-missile and never build-bomber', () => { + // No delivery owned → planner must build. Full AP budget ensures the build + // path is reached. Uses the same initialState factory as the existing tests. + const s = initialState({ + cast: ['netanyahoo', 'chump'], + difficulty: 'normal', + seed: 'netanyahoo-missile-bias', + }); + s.leaders.netanyahoo.stockpile.missiles = 0; + s.leaders.netanyahoo.stockpile.bombers = 0; + s.leaders.netanyahoo.ap = 6; + + const orders = planNetanyahoo(s, 'netanyahoo'); + + expect(orders.some((o) => o.kind === 'build-missile')).toBe(true); + expect(orders.some((o) => o.kind === 'build-bomber')).toBe(false); + }); +}); From b13cf91a2a7029fba614fe9d37aa42c52069c7ac Mon Sep 17 00:00:00 2001 From: gethi Date: Thu, 14 May 2026 17:32:03 +0100 Subject: [PATCH 6/7] docs: ai-duel comment + README Phase 4c.1 status Co-Authored-By: Claude Sonnet 4.6 --- README.md | 4 ++++ tests/engine/ai-duel.test.ts | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/README.md b/README.md index c4d6e8f..3b1d427 100644 --- a/README.md +++ b/README.md @@ -172,3 +172,7 @@ What's NOT in this phase (deferred to P4c / P5): - Approach B / C lookahead upgrades — P4c - Threat-aware defence deployment per personality — P4c - Persistence, animations, audio, SVG art, PWA — P5 + +## Phase 4c slice 1 status + +Phase 4c slice 1 (Bomber Reuse + Carnage Bias) ships two tightly-coupled changes: a bomber-reuse engine rule (a bomber that impacts returns to the attacker's stockpile; an intercepted bomber stays gone) and Carnage AI personality tuning (builds bombers instead of missiles, prefers bomber delivery when stock is available). Full AI scoring-weight tuning, Approach B/C lookahead upgrades, and threat-aware defence deployment per personality are deferred to slice 2+, which will use the new duel distribution as its balance baseline. Verification: `npm run test:run` (255 tests). diff --git a/tests/engine/ai-duel.test.ts b/tests/engine/ai-duel.test.ts index 9e6f191..d90975a 100644 --- a/tests/engine/ai-duel.test.ts +++ b/tests/engine/ai-duel.test.ts @@ -52,6 +52,11 @@ describe('AI-duel headless', () => { // and changed defences to consumable; the AI duel will produce a different // distribution. P4c uses the new distribution as the tuning baseline. This // test continues to assert only "no crash" — it has no balance assertions. + // + // P4c slice 1 shifts the distribution again: bomber reuse (engine rule) means + // successful bomber launches return the bomber to stock, and Carnage now builds + // and launches bombers preferentially. Slice 2+ treats the post-slice-1 + // distribution as its new baseline for balance assertions and further tuning. it('runs 100 all-AI games over full cast without crashing (balance assertions deferred to P4)', () => { // Player1..player5 are zero-initialised because the duel is AI-only and // never includes player slots — kept to satisfy Record. From a475331e59bf6ad3fb195de5319dbd995567c25f Mon Sep 17 00:00:00 2001 From: gethi Date: Thu, 14 May 2026 17:59:40 +0100 Subject: [PATCH 7/7] tests: bomber-impact tests zero deployedAA, not deployedShields (BugBot) --- tests/engine/launches.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/engine/launches.test.ts b/tests/engine/launches.test.ts index 4238e73..6499fab 100644 --- a/tests/engine/launches.test.ts +++ b/tests/engine/launches.test.ts @@ -193,7 +193,7 @@ describe('bombers are reusable (P4c slice 1)', () => { s.leaders.chump.stockpile.bombers = 1; s.leaders.chump.stockpile.warheadsSmall = 1; s.leaders.chump.ap = 5; - s.leaders.carnage.deployedShields = 0; + s.leaders.carnage.deployedAA = 0; // bombers are intercepted by AA, not shields s = reduce(s, { type: 'SUBMIT_ORDERS', @@ -218,7 +218,7 @@ describe('bombers are reusable (P4c slice 1)', () => { s.leaders.chump.stockpile.bombers = 1; s.leaders.chump.stockpile.warheadsSmall = 1; s.leaders.chump.ap = 5; - s.leaders.carnage.deployedShields = 0; + s.leaders.carnage.deployedAA = 0; // bombers are intercepted by AA, not shields s = reduce(s, { type: 'SUBMIT_ORDERS',