Skip to content

feat(sim): raise player events from scenario events and modeled state changes - #129

Closed
e54-bot wants to merge 3 commits into
mainfrom
feat/player-events
Closed

e54-bot wants to merge 3 commits into
mainfrom
feat/player-events

Conversation

@e54-bot

@e54-bot e54-bot commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Player events now reach a rule two ways: the scenario events entries the loader already injects, and modeled causes — Damage and Heal raise the player events their effect implies, delivered in occurrence order between rule-instance visits on the same tick. Implements #47 (part of #45's Bastion run-to-completion goal).

  • Damage on a living target raises Player Took Damage then, for a player dealer, Player Dealt Damage; a hit that brings health to zero writes the isDead cell and raises Player Died then Player Dealt Final Blow — occurrence order, so Player Died runs before Player Dealt Final Blow regardless of rule file order.
  • Heal raises Player Received Healing then Player Dealt Healing for a player healer. A Null dealer binds Attacker to Null and raises no dealer-side event; a Damage/Heal on an already-dead target raises nothing.
  • Raised events bind Event Player, Attacker, Victim, and Event Damage/Event Healing (the applied amount). attacker/victim join the event-value table; scenario values supply them as declared player ids (a new loader check) or null. eventAbility/eventDirection remain not_modeled.
  • Event team/player filters apply to raised and injected events alike. A busy or Wait-suspended instance queues the event like an injected one; raised events bound at 1024 per tick (a rule whose own actions re-raise its kind would otherwise fan out without bound), past which the run stops not_modeled.
  • Event dispatch/eligibility moved to a new events.rs module (size-guard); docs updated (simulator-interface.md, scenario-format.md, architecture.md, findings.md). Cargo.lock syncs the 0.11.1 release version the lock was missing.

Verification

  • cargo fmt --check — clean.
  • cargo clippy --all-targets -- -D warnings — clean.
  • cargo test — all suites pass (new tests/player_events.rs: injected playerLeft runs its rule once with Event Player; killing Damage runs Player Died then Player Dealt Final Blow in occurrence order with rules declared out of order; bindings for Event Player/Attacker/Victim/Event Damage; team-filter rejection; Heal raises received-then-dealt).
  • cargo check --locked --target wasm32-unknown-unknown -p athena-sim — clean.
  • Bastion acceptance (291 rules): runs to tick 91 and stops at not_modeled: "action wait under a 90% simulation rate" (the documented athena#120 match-clock gap — the next stop, past every player-event construct). No Damage/Heal executes before that stop, so no player-event rules fire in the reference run; with an injected playerLeft on p2 the run additionally executes Bastion's Update targetPlayer when player left rule and reaches the same stop.

Unverified

  • No client capture backs the client's event ordering, per-event value sets, or queue bound — all marked modeled, not measured in architecture.md/findings.md.
  • The client binding set per event kind is not cataloged, so each event of one cause carries the same Attacker/Victim/amount bindings (e.g. Event Damage reads the killing blow inside Player Died); a probe would be needed to measure the real per-event sets.
  • Whether a Wait-suspended Bastion rule ever queues a derived event end-to-end is unexercised before the tick-91 stop.

… changes

Player events now reach a rule two ways: the scenario `events` entries
the loader already injects, and modeled causes — `Damage` and `Heal`
raise the player events their effect implies, delivered in occurrence
order between rule-instance visits on the same tick.

- `Damage` on a living target raises `Player Took Damage` then, for a
  player dealer, `Player Dealt Damage`; a hit that brings health to
  zero writes the `isDead` cell and raises `Player Died` then `Player
  Dealt Final Blow`. `Heal` raises `Player Received Healing` then
  `Player Dealt Healing`. A `Null` dealer raises no dealer-side event;
  a hit on an already-dead target raises nothing.
- Each raised event binds `Event Player`, `Attacker` (the cause's
  player, `Null` for none), `Victim` and the applied amount as
  `Event Damage`/`Event Healing`; `attacker`/`victim` join the
  event-value table and scenario `values` supply them as player ids.
- Raised events bound at 1024 per tick: a rule whose own actions
  re-raise its kind would fan the queue out without bound. Event team
  and player filters apply to raised and injected events alike;
  `eventAbility`/`eventDirection` stay not_modeled.

Verified: the 291-rule Bastion workload runs to tick 91 and stops at
`action wait under a 90% simulation rate` (the athena#120 match-clock
gap), past every player-event construct; an injected `playerLeft`
fires Bastion's matching rule end to end.

Fixes #47
Address the review findings on the player-events change:

- `Damage`/`Heal` gated liveness on the health pool, so a `Kill`ed
  target still took damage events and died a second time while a
  `Resurrect`ed one could take none. The gate now reads `isDead` —
  the model's death marker, as `Is Alive` and `Set Health` read it —
  and falls back to the health pool only where `isDead` cannot answer.
- An event value a raised event does not bind stopped `missing_input`,
  but only injected events take scenario `values`. `EventCtx` now
  marks raised events, and the read stops `not_modeled` naming the
  construct, as `eventAbility`/`eventDirection` already do.
- `attacker`/`victim` event values accepted any JSON; the loader now
  requires a player `id` or `null`, per `docs/scenario-format.md`.
- Comments that still said "injected event" for context that now also
  holds raised events were reworded, and `scenario-format.md`,
  `trace-format.md` and `architecture.md` record the contract
  clarifications.
@e54-bot

e54-bot commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

All four review findings are addressed in 0ce5992 (fix(sim): gate Damage/Heal on isDead and correct raised-event reads), pushed to feat/player-events.

  1. Damage/Heal liveness gate — fixed. Both actions now gate on the isDead read (the model's death marker, as Is Alive/Set Health read it), falling back to the health pool only where isDead cannot answer. A Killed target takes no pool change and raises no events — no second Player Died, no isDead rewrite; a Resurrected target takes damage and healing events again.
  2. Unbound event values on raised events — fixed. EventCtx carries a raised flag; an EVENT_VALUES read a raised event does not bind now stops not_modeled naming the construct (value eventWasCriticalHit), matching eventAbility/eventDirection. Injected events keep missing_input.
  3. attacker/victim value types — fixed. The loader accepts a scenario player id string or null and refuses any other JSON kind with a load error, per docs/scenario-format.md.
  4. Stale "injected event" comments — fixed in load.rs, scenario.rs, eval.rs, sched.rs, timeline.rs and lib.rs; scenario-format.md, trace-format.md and architecture.md record the contract clarifications (raised-event values are not_modeled, the eventPlayer trace field, the isDead gate).

Verification: cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test (all suites pass, incl. new regression tests a_dead_target_takes_no_pool_change_or_events_and_a_revived_one_does, an_event_value_a_raised_event_does_not_bind_is_not_modeled, and the loader-check cases), and cargo check --locked --target wasm32-unknown-unknown -p athena-sim — all clean.

The PR is ready for follow-up review.

Address the follow-up review findings on the player-events change:

- A `Damage`-killed player leaves the health pool at zero, and the
  revive actions wrote only the `isDead` cell, so a revived player
  could never be brought to zero — or raise `Player Died`/`Player
  Dealt Final Blow` — again. `Respawn`/`Resurrect` now refill the
  resolved world's `health` pool to `health.max` — `Respawn`'s
  "with full health", `Resurrect` the same under
  modeled-not-measured — recording the move as `Damage`/`Heal` do.
  The regression test now covers the `Damage`-kill→revive path, not
  only `Kill`→`Resurrect`.
- The loader-check bullet in `scenario-format.md` now states
  `attacker`/`victim` event values are a player `id` or `null`, as
  the loader accepts.
- The `dead()` docstring no longer claims `construct` names the stop
  a fallback `world_state` touch raises — `world_state` carries the
  stored resolution error and the read's `missing_input` is
  swallowed, so `construct` names nothing observable.
@e54-bot

e54-bot commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up review findings addressed in 3199498:

  • Finding 1 (fixed): Respawn/Resurrect now refill the resolved world's health pool to health.max — Respawn's "with full health", Resurrect the same under modeled-not-measured — so a Damage-killed and revived player takes damage and raises Player Died/Player Dealt Final Blow again. The pool move records a world change as Damage/Heal do. New regression test a_damage_killed_then_revived_player_dies_again covers the Damage-kill→Resurrect/Respawn→kill path (seq = [1,3,1,3,1,3]), alongside the existing Kill→Resurrect test.
  • Finding 2 (fixed): docs/scenario-format.md loader-check bullet now states attacker/victim event values are a player id or null, matching the loader.
  • Finding 3 (fixed): dropped the dead() docstring's last sentence — construct names nothing observable, since world_state carries the stored resolution error and the read's missing_input is swallowed.

Verified: cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test (all suites), cargo check --locked --target wasm32-unknown-unknown -p athena-sim — all clean. docs/findings.md records the revive refill in the state-cells bullet.

Ready for follow-up review.

@e54-bot

e54-bot commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Independent review took three rounds. Round 3 found a regression introduced by the round-2 fix: Resurrect now fully heals living targets, which suppresses Player Died and Player Dealt Final Blow on a hit that must kill. Per the orchestration rule, a third round of patches on a design that failed review repeatedly does not converge, so this PR is closed. Issue #47 will be redone from a fresh branch with the findings from all three review rounds as input.

@e54-bot e54-bot closed this Oct 28, 2026
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.

2 participants