Skip to content

campaign api: hello pawn - #33

Closed
keithharvey wants to merge 5 commits into
masterfrom
hello_pawns
Closed

campaign api: hello pawn#33
keithharvey wants to merge 5 commits into
masterfrom
hello_pawns

Conversation

@keithharvey

Copy link
Copy Markdown
Owner

The runnable Hello Pawns demo from the campaign-API plan: six lines of mission Lua win a skirmish, authored in the trigger DSL, loaded by chat command, verified end to end in the headless harness.

The whole mission (modules/missions/hello_pawns/triggers/win.lua):

T.When(Team.Player.Has(UnitDef("armpw"), 3))
	.Then(function()
		Objective("build_pawns").Complete()
		MatchFlow.Victory(Team.Player.allyTeam)
	end)
	.Register()

What's here

  • Loader bootstrap borrowed from the modules branchmodule_handler.lua, policy_builder.lua, modules/types/, plus minimal wiring (a 9-line module-gadget shim in gadgets.lua, 3 test-sandbox lines in dbg_test_runner). The borrow stayed small — this doubles as the master bootstrap step of the matchflow module plan.
  • Trigger engine (modules/missions/lib/trigger_engine.lua) — pure Lua, busted-spec'd. Trigger identity = filename + declaration order; fired flags live in the engine's plain state tables (GetState/SetState), never in closures — the savegame rule from commit one. UnregisterFile is the hot-reload path.
  • Dot-only authoring DSL (lib/dsl.lua, lib/verbs.lua) — every chain step is a plain call with parens, no colon calls, no metatables on the mission-facing surface. Chain → descriptor → sink, the policy_builder idiom.
  • matchflow, demo-minimal — the scripted-verdict path only (MatchFlow.Victory/Defeat → verdict gadget → Spring.GameOver). The call shape is the contract; the real module (game_end extraction) lands underneath it later. Coexists with game_end the way missions do today — demo-only, not multiplayer-safe.
  • mission_loader gadget/luarules mission hello_pawns loads triggers/*.lua with an injected env of exactly T, Team, UnitDef, Objective, MatchFlow; the sandbox is the API surface. Evaluates every 15 frames. Team.Player = first human team (demo rule). /luarules mission reload exists but is untested.
  • Three verbs, no more: Team.Player.Has(unitDef, n) (n completed units — nanoframes don't count, with an integration-test case proving a factory blueprint can't win the mission), Objective(name).Complete() (echo + rulesparam, no UI), MatchFlow.Victory/Defeat.

Testing

  • 18 busted specs for the engine/DSL/verbs (spec/modules/missions/), suite green.
  • luaui/Tests/hello_pawns/test_win.lua plays the mission for real under the headless harness (just bar::integrations): arms it via the chat command, spawns two Pawns plus a nanoframe, asserts no verdict across two cadences, completes the frame, expects GameOver with the player's ally team. Suite: 0 failures.
  • Not covered: the visual win screen in a real skirmish, and the reload command.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown

Integration Test Results

15 tests  +1   6 ✅ +1   4s ⏱️ +2s
 1 suites ±0   8 💤 ±0 
 1 files   ±0   1 ❌ ±0 

For more details on these failures, see this check.

Results for commit 9bff5ec. ± Comparison against base commit 4f8b1da.

♻️ This comment has been updated with latest results.

Comment thread .emmyrc.json
"UnitDef",
"Objective",
"MatchFlow",
"describe",

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these busted globals are unfortunately correct because transforms hasn't merged yet.

keithharvey and others added 5 commits July 21, 2026 13:21
module_handler + policy_builder + modules/types, the gadgets.lua
module-gadget shim, the dbg_test_runner sandbox exposures
(setmetatable/getmetatable/getfenv), busted helper wiring, and
emmyrc globals for the mission authoring surface.

Borrow footprint beyond the three planned files: gadgets.lua hunk
(9 lines), test-runner sandbox (3 lines), .busted helper, emmyrc
globals — small, as matchflow_module_plan.md hoped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pure-Lua core of the mission runtime: TriggerEngine (registration,
cadenced evaluation, once semantics, unregister-by-identity for hot
reload, progress in plain state tables per the savegame rule), the
chain builder (T.When(cond).Then(fn).Register(), dot-only — no colon
calls, no metatables on the mission-facing surface), and the pure
halves of the verbs (UnitDef refs, Team.Has reading counts from ctx).

Trigger identity = filename + declaration order, stamped at Register.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… mission

matchflow (demo-minimal, scripted-verdict only): api.lua forwards
Victory/Defeat to the verdict gadget through GG.MatchFlow (one owner
of the pending verdict; contract includes are per-consumer), and the
gadget applies it via Spring.GameOver on the next GameFrame. Coexists
with game_end the way missions do today — demo-only, called out.

mission_loader: /luarules mission <name> loads
modules/missions/<name>/triggers/*.lua into the trigger engine with
the injected environment — T, Team, UnitDef, Objective, MatchFlow and
nothing else; the sandbox is the API surface. Unregister-by-identity
before each file include makes loading idempotent and is the
hot-reload path (/luarules mission reload). Evaluation every 15
frames; Team.Player = first human team (demo rule).

hello_pawns: the six-line mission, dot-only chain.

emmyrc: busted globals (describe/it/...) — drops the repo-wide
spec error noise; new files check clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plays the mission for real in the headless harness: arm via
/luarules mission hello_pawns, cheat-spawn to 3 Pawns, expect the
GameOver callin with the player's ally team, and assert the objective
rulesparam flipped first. Passes under the headless integration
suite; the scripted GameOver does not disturb the rest of the suite
(deathmode=neverend startscript).

Gotchas encoded here: the runner wants bare global test()/setup()/
skip(), and SyncedRun ships the caller's stack locals, not upvalues.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The demo bug: the mission was won the moment the third Pawn's
blueprint appeared in the factory. GetTeamUnitDefCount includes
under-construction units, so the ctx now filters through
GetUnitIsBeingBuilt — Has(UnitDef, n) means n FINISHED units, as the
type annotation already promised.

The integration test now proves it: two finished Pawns plus one
nanoframe must not win across two evaluation cadences; completing the
frame (SetUnitHealth build=1) must. Test archaeology encoded in
comments: the nanoframe needs a live builder passed to CreateUnit or
unit_prevent_lab_hax2 destroys it a frame later, and it needs real
build progress or abandoned-frame decay would kill it anyway.

Headless suite: 0 failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@keithharvey

Copy link
Copy Markdown
Owner Author

Moving to a cross-repo draft against upstream.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant