Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
8891711
feat(engine): pin per-iteration decision slots for bounded-loop shortcut
lgray Jul 28, 2026
a2a64ff
feat(engine): bounded CR 732.2a cycle fast-forward for a multiplayer …
lgray Jul 28, 2026
3b70724
fix(engine): a mandatory draw is not a resolution-time choice
lgray Jul 28, 2026
28121cf
fix(engine): delimit a bounded CR 732.2a cycle by its published per-p…
lgray Jul 28, 2026
cca940c
test(engine): cover the partial-crossing arm and the basis-A span at …
lgray Jul 28, 2026
ea6a90b
docs(engine): the aborted cycle rolls back alone, not the whole drive
lgray Jul 28, 2026
b37dd6e
docs(engine): correct four doc-accuracy claims and re-cite CR 101.2 i…
lgray Jul 28, 2026
8ef5fc8
docs(engine): fix the CLAIM, not the site -- four sibling doc correct…
lgray Jul 28, 2026
ebccdd7
docs(engine): scope two universals in the basis-B control's doc to th…
lgray Jul 28, 2026
c2029bb
docs(engine): point our repro commands at the renamed phase-engine pa…
lgray Jul 30, 2026
132d9b9
fix(engine): route player choice through one legality authority (5c)
lgray Jul 31, 2026
e057a12
feat(engine): split LoopDetectSample into normalized and live ring ha…
lgray Aug 1, 2026
7871f31
feat(engine): event-derived resolution obligation and prompt-cause pa…
lgray Aug 1, 2026
a2bf20b
feat(engine): shape-B mint conjuncts and declare-time owner firewall …
lgray Aug 1, 2026
4f4c651
fix(engine): re-derive the loop-shortcut probe budget from the offeri…
lgray Aug 2, 2026
30cc3ce
test(engine): pin the frozen exemption to its certifying disjunct (5d…
lgray Aug 2, 2026
d520764
test(engine): budget-refusal and warmup-meter rows for the bounded of…
lgray Aug 2, 2026
a97d0d3
test(engine): offer-writer census and carrying-frame rows (5d U3, R8/…
lgray Aug 2, 2026
e950373
test(engine): verdict-door totality, keying and proposer rows (5d U3,…
lgray Aug 2, 2026
0b793e8
test(engine): close the U3 remainder β€” R16, R17, R21(b)-family, R27(a…
lgray Aug 2, 2026
355652f
test(engine): retained-sample .live-reader rows close U3 (5d, R27 a3/…
lgray Aug 2, 2026
799b87a
feat(engine): OptionalEffectChoice pin-injection arm with seat and be…
lgray Aug 2, 2026
8ce23d7
test(engine): track the F4 4p dump and pin the bounded-offer behaviou…
lgray Aug 2, 2026
1f07655
test(engine): pin what the AI can actually do at the real F4 bounded …
lgray Aug 2, 2026
652d8c4
docs(engine): review-impl LOW fixes β€” board-not-prompt contract, real…
lgray Aug 2, 2026
c3debe4
test(engine): derive CR 603.7 firing carriers for the 4p dump corpus
lgray Aug 2, 2026
1168afb
test(engine): re-anchor the CR 603.5 prompt census after the rebase o…
lgray Aug 2, 2026
b2f9f47
test(engine): re-anchor the CR 603.5 prompt census after the rebase o…
lgray Aug 2, 2026
02b382a
fix(engine): close two fail-open seams in the loop-shortcut analysis …
lgray Aug 2, 2026
85eb87a
fix(engine): refuse the resolution probe when a prompt already stands
lgray Aug 2, 2026
2b8b1d4
fix(engine,ai): offer only legal quantity choices on a bounded shortcut
lgray Aug 2, 2026
71b1ca0
fix(scripts): read both trigger-definition shapes and stop overwritin…
lgray Aug 2, 2026
f806d49
fix(scripts): make fixture migration atomic and envelope-preserving
lgray Aug 2, 2026
181276f
fix(engine): route the choose-from-zone fallback chooser through the …
lgray Aug 2, 2026
fe09bca
style(engine): satisfy doc_lazy_continuation and drop a self-comparin…
lgray Aug 2, 2026
9e7d81f
test(engine): re-pin the F4 bounded-offer rows to the post-gate candi…
lgray Aug 2, 2026
fa6b13f
docs(engine): reattach the R10' doc block to the test it documents
lgray Aug 2, 2026
f2f76fa
fix(scripts): prune stale carriers, stop overwriting canonical ones, …
lgray Aug 2, 2026
03ac1cd
fix(engine): treat an `UpTo` repeat count as a resolution-time choice…
lgray Aug 2, 2026
431f051
perf(engine): probe a chain root once and gate before the board clone
lgray Aug 2, 2026
eb6a29b
style(engine): collapse the effect classifier's bool arms
lgray Aug 2, 2026
78d328f
fix(engine,scripts): gate the sibling clone site, make the atomicity …
lgray Aug 2, 2026
afca4ee
fix(engine): correct the ChoosePermanent CR citation and complete the…
lgray Aug 2, 2026
b7b2b82
fix(scripts): accept gameState-less envelopes and make the stage-2b a…
lgray Aug 2, 2026
2f1cd86
chore(i18n): mark the combo detector as experimental at game start
lgray Aug 2, 2026
0277009
fix(scripts): key the stage-2b signature on stamped fields and compos…
lgray Aug 2, 2026
ea1b0ac
docs(scripts): state the stage-2b arm's residual instead of overclaim…
lgray Aug 2, 2026
f598dab
chore(PR-6886): merge delayed trigger lifecycle
matthewevans Aug 3, 2026
d1a5270
fix(PR-6886): restore loop shortcut port
matthewevans Aug 3, 2026
01ed26c
fix(scripts,ai): stage the stamper beside its destination and cover t…
lgray Aug 3, 2026
1a3b472
fix(scripts): assert stage locality at the call site, not only in the…
lgray Aug 3, 2026
5c11069
fix(engine): guard the second decode ingress and drop the orphaned he…
lgray Aug 3, 2026
79d4f0a
test(engine): pin the bare-GameState ingress and rebuild r28_c's trus…
lgray Aug 3, 2026
596bdfa
fix(scripts): pin the live TriggerFiring wire vocabulary in the stamp…
lgray Aug 3, 2026
6face1f
fix(engine): re-pin the CR 603.5 prompt census after the #6933 fold
lgray Aug 3, 2026
a627828
Merge branch 'main' into feat/combo-detector-player-feedback
lgray Aug 3, 2026
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
2 changes: 1 addition & 1 deletion client/src/i18n/locales/de/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"updated": "aktualisiert"
},
"comboDetector": {
"label": "Combo-Detektor",
"label": "Combo-Detektor (experimentell)",
"title": "Unendliche Schleifen erkennen: Pflichtschleifen automatisch beenden und ∞-Ressourcen anzeigen (standardmÀßig aus)",
"on": "An",
"off": "Aus",
Expand Down
2 changes: 1 addition & 1 deletion client/src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"updated": "updated"
},
"comboDetector": {
"label": "Combo Detector",
"label": "Combo Detector (experimental)",
"title": "Detect infinite loops: end mandatory loops automatically and show ∞ resources (off by default)",
"on": "On",
"off": "Off",
Expand Down
2 changes: 1 addition & 1 deletion client/src/i18n/locales/es/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"updated": "actualizado"
},
"comboDetector": {
"label": "Detector de combos",
"label": "Detector de combos (experimental)",
"title": "Detectar bucles infinitos: terminar bucles obligatorios automÑticamente y mostrar recursos ∞ (desactivado por defecto)",
"on": "Activado",
"off": "Desactivado",
Expand Down
2 changes: 1 addition & 1 deletion client/src/i18n/locales/fr/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"updated": "Γ  jour"
},
"comboDetector": {
"label": "DΓ©tecteur de combos",
"label": "DΓ©tecteur de combos (expΓ©rimental)",
"title": "Détecter les boucles infinies : terminer automatiquement les boucles obligatoires et afficher les ressources ∞ (désactivé par défaut)",
"on": "ActivΓ©",
"off": "DΓ©sactivΓ©",
Expand Down
2 changes: 1 addition & 1 deletion client/src/i18n/locales/it/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"updated": "aggiornato"
},
"comboDetector": {
"label": "Rilevatore di combo",
"label": "Rilevatore di combo (sperimentale)",
"title": "Rileva i loop infiniti: termina automaticamente i loop obbligatori e mostra le risorse ∞ (disattivato per impostazione predefinita)",
"on": "Attivo",
"off": "Disattivato",
Expand Down
2 changes: 1 addition & 1 deletion client/src/i18n/locales/pl/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"updated": "zaktualizowano"
},
"comboDetector": {
"label": "Wykrywacz combosΓ³w",
"label": "Wykrywacz combosΓ³w (eksperymentalne)",
"title": "Wykrywaj nieskoΕ„czone pΔ™tle: automatycznie koΕ„cz obowiΔ…zkowe pΔ™tle i pokazuj zasoby ∞ (domyΕ›lnie wyΕ‚Δ…czone)",
"on": "WΕ‚.",
"off": "WyΕ‚.",
Expand Down
2 changes: 1 addition & 1 deletion client/src/i18n/locales/pt/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"updated": "atualizado"
},
"comboDetector": {
"label": "Detector de combos",
"label": "Detector de combos (experimental)",
"title": "Detectar loops infinitos: encerrar loops obrigatórios automaticamente e mostrar recursos ∞ (desativado por padrão)",
"on": "Ligado",
"off": "Desligado",
Expand Down
78 changes: 60 additions & 18 deletions crates/engine/src/ai_support/candidates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3037,12 +3037,14 @@ pub fn candidate_actions_broad_with_probe(
// accepted at `Fixed(0)`). Clamp, or the generator's sole candidate is rejected by
// the reducer's `amount > max` guard and the AI has no legal action at this prompt.
//
// Unreachable for the AI *today* and deliberately kept correct anyway: the AI's own
// `WaitingFor::LoopShortcut` arm below only ever proposes `IterationCount::
// UntilLethal`, which routes to `apply_until_lethal_shortcut` and never reaches
// `materialize_fixed_shortcut` β€” the only path that registers a stash. So no
// AI-declared shortcut currently produces this prompt; a human-declared one in a
// mixed game, or a future bounded AI offer, does.
// AI-reachable since the bounded fast-forward landed, which is what stales the older
// "the arm below only ever proposes `UntilLethal`" note this replaces: the
// `WaitingFor::LoopShortcut` arm below also proposes `Fixed(max_iterations)` against a
// bounded offer that publishes no pins, and only a `Fixed` count routes through
// `materialize_fixed_shortcut` β€” the single path that registers the stash `turns.rs`
// turns into this prompt. `UntilLethal` still routes to `apply_until_lethal_shortcut`
// and never gets here; it is now also not offered against a bounded offer at all. A
// human-declared shortcut in a mixed game reaches this prompt too.
WaitingFor::PayAmountChoice {
player,
resource: PayableResource::LoopCollapse { .. },
Expand Down Expand Up @@ -3232,21 +3234,60 @@ pub fn candidate_actions_broad_with_probe(
// policy/search layer, rather than the candidate generator, decides whether an AI
// proposer declares or returns to ordinary priority.
// (Scored by `phase_ai::policies::loop_shortcut::LoopShortcutPolicy`.)
WaitingFor::LoopShortcut { proposer, .. } => vec![
candidate(
GameAction::DeclareShortcut {
count: crate::analysis::decision_template::IterationCount::UntilLethal,
template: None,
},
TacticalClass::Utility,
Some(*proposer),
),
candidate(
WaitingFor::LoopShortcut {
proposer, schema, ..
} => {
// CR 732.2a: `UntilLethal` names no count, so it is legal ONLY against an offer
// that narrowed no bound. `handle_declare_shortcut` rejects it outright against
// a bounded one (`IterationCount::UntilLethal if offer.schema.is_bounded()` =>
// `reject_shortcut_declaration`), and that reject is a SUCCESSFUL, fail-closed
// handback to priority β€” `Ok(result)`, not an `Err`. So an unconditional
// `UntilLethal` candidate did not merely waste a search node: it handed the
// simulation layer an action the engine ACCEPTS and then silently discards,
// i.e. an illegal quantity choice wearing the shape of a legal one, which the
// policy layer then has to know to score away.
//
// Emit only the quantity choices the offer can actually take. A bounded offer
// gets `Fixed(max_iterations)` below when its pin set permits a `template: None`
// declaration; where neither applies, `DeclineShortcut` really is the only legal
// answer at the node, and representing that honestly is the point.
let mut v = Vec::new();
if !schema.is_bounded() {
v.push(candidate(
GameAction::DeclareShortcut {
count: crate::analysis::decision_template::IterationCount::UntilLethal,
template: None,
},
TacticalClass::Utility,
Some(*proposer),
));
}
// CR 732.2a: a BOUNDED offer states a legal repetition count, and the declare
// handler rejects `UntilLethal` against one outright β€” so without this candidate
// the AI's only non-declining option at such a node is an answer the engine
// refuses. `ShortcutDecisionSchema::is_bounded()` is the engine's single
// authority for "this producer narrowed the bound"; do NOT re-spell it as a
// comparison against `MAX_SHORTCUT_CYCLES`. Gated on empty `points` because this
// candidate carries `template: None`, which a published pin set fail-closes on.
if schema.points.is_empty() && schema.is_bounded() {
v.push(candidate(
GameAction::DeclareShortcut {
count: crate::analysis::decision_template::IterationCount::Fixed(
schema.max_iterations,
),
template: None,
},
TacticalClass::Utility,
Some(*proposer),
));
}
v.push(candidate(
GameAction::DeclineShortcut,
TacticalClass::Pass,
Some(*proposer),
),
],
));
v
}
// CR 732.2b/c: an opponent answers a loop-shortcut offer. PR-7 Phase 4c (LOW-2):
// self-preservation via the single-authority `smart_shortcut_response` β€” Shorten
// when the polled player has a meaningful way to break the loop, else Accept.
Expand Down Expand Up @@ -5234,6 +5275,7 @@ mod tests {
win_kind: crate::analysis::loop_check::WinKind::LethalDamage,
mandatory: false,
residual_board_delta: crate::analysis::resource::BoardDelta::default(),
per_cycle: None,
},
schema: crate::analysis::decision_template::ShortcutDecisionSchema::default(),
};
Expand Down
3 changes: 3 additions & 0 deletions crates/engine/src/analysis/corpus_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ fn classify_status_compares_against_spec_not_rubber_stamp() {
win_kind: WinKind::LethalDamage,
mandatory: false,
residual_board_delta: BoardDelta::default(),
per_cycle: None,
};
assert!(
matches!(
Expand All @@ -320,6 +321,7 @@ fn classify_status_compares_against_spec_not_rubber_stamp() {
win_kind: WinKind::Advantage,
mandatory: false,
residual_board_delta: BoardDelta::default(),
per_cycle: None,
};
assert!(
matches!(
Expand All @@ -345,6 +347,7 @@ fn classify_status_compares_against_spec_not_rubber_stamp() {
win_kind: WinKind::Advantage,
mandatory: false,
residual_board_delta: BoardDelta::default(),
per_cycle: None,
};
assert!(
matches!(
Expand Down
Loading
Loading