Skip to main content

fforge_core/match_engine/
knobs.rs

1//! The calibration knob table (`MATCH_MODEL.md` §8). Field names and every
2//! value except `b_beat` are transcribed verbatim from the fitted `Knobs`
3//! dataclass in `match_model_prototype.ipynb` (the throwaway Python
4//! shape-finder — this Rust struct is the port target, not a re-guess), so
5//! the two stay diffable against each other.
6//!
7//! `b_beat` (beat-the-keeper bias) is the one Rust-side re-tune, landed by
8//! the calibration harness (`bin/calibrate.rs`, `MATCH_MODEL.md` §10):
9//! real `worldgen`'s attribute distribution differs enough from the
10//! notebook's own synthetic squad generator that the notebook's fitted
11//! -1.7 under-converted against real inputs (~7% vs the ~10% target) while
12//! every other aggregate — shots/game, on-target rate, wide-origin share —
13//! already landed on target. `b_beat` only gates the second of the two
14//! chained shot sigmoids (beat-the-keeper, given on-target), so raising it
15//! moves conversion without disturbing the already-correct on-target rate
16//! or shot volume — confirmed empirically (`bin/calibrate.rs -- --seeds
17//! 16`): shots/game stayed ~25.6, on-target stayed ~33.2%, across the whole
18//! sweep. Pooled over 16 seeds at -1.05: goals/game 2.58 (target ~2.6),
19//! home/draw/away 43/26/31% (target ~45/26/29%), conversion 10.1% (target
20//! ~10%), wide-origin goal share 26.7% (target 25–35%) — this is now a
21//! finished, real-`worldgen`-calibrated point, not just the notebook's.
22
23#[derive(Debug, Clone, Copy, PartialEq)]
24pub struct Knobs {
25    // --- clock ---
26    /// Match-minutes advanced per possession step.
27    pub delta: f64,
28
29    // --- logistic resolver: p = sigmoid(k*(atk-def)/s + bias) ---
30    /// Attribute-difference sensitivity (open-play contests).
31    pub k: f64,
32    /// Scale — normalizes the 0..100 attribute range.
33    pub s: f64,
34    /// Additive edge to the home side's attacking contests.
35    pub home_bias: f64,
36
37    // --- per-action base rates (bias term) ---
38    pub b_pass: f64,
39    pub b_takeon: f64,
40    pub b_cross_delivery: f64,
41
42    // --- shot resolver: two sigmoids (on-target, then beat-keeper) ---
43    pub k_ontarget: f64,
44    pub k_gk: f64,
45    pub b_ontarget: f64,
46    pub b_beat: f64,
47    /// Of shots that miss the on-target sigmoid, the share narrated as
48    /// "off" vs "blocked" — a cosmetic split only (both transition to the
49    /// same opponent-`Def` outcome; `MATCH_MODEL.md` §3's transition table
50    /// treats them as one branch, §9's stream schema wants them as two
51    /// distinct, narratable outcomes).
52    pub p_off_frac: f64,
53    /// Save → parried rebound (follow-up shot) vs collected.
54    pub p_rebound: f64,
55    /// Rebound chances are scrappy.
56    pub q_rebound: f64,
57
58    // --- chance quality by arrival (added into both shot sigmoids) ---
59    pub q_through: f64,
60    pub q_dribble: f64,
61    pub q_cutback: f64,
62    pub q_header: f64,
63    pub q_long: f64,
64
65    // --- transition splits ---
66    /// Completed build-up pass advances `Def` → `Mid` (else retain `Def`).
67    pub p_def_advance: f64,
68    /// Completed `Mid` action advances to the final third.
69    pub p_mid_advance: f64,
70    /// Of advances, share going wide (`AttW`) vs central (`AttC`).
71    pub p_wide: f64,
72    /// Through-ball reaches the box.
73    pub p_attc_penetrate: f64,
74    /// `AttC` take-on reaches the box.
75    pub p_attc_dribble_box: f64,
76    /// `AttW` take-on becomes a cutback chance.
77    pub p_attw_cutback: f64,
78    /// `AttW` take-on cuts inside to `AttC` instead.
79    pub p_attw_cut_inside: f64,
80
81    // --- action-selection base weights (modulated by the actor's attributes) ---
82    pub w_pass_mid: f64,
83    pub w_takeon_mid: f64,
84    pub w_pass_attc: f64,
85    pub w_takeon_attc: f64,
86    pub w_longshot_attc: f64,
87    pub w_cross_attw: f64,
88    pub w_takeon_attw: f64,
89    pub w_pass_attw: f64,
90
91    // --- fatigue: effective attr *= 1 - drop, drop grows over 90' ---
92    /// Max drop at 90' for a 0-stamina, low-work-rate player.
93    pub fatigue_base: f64,
94    /// How much Work Rate accelerates fatigue.
95    pub fatigue_wr: f64,
96
97    // --- resolution support term: blend actor with team quality ---
98    /// 0 = pure actor, 1 = pure team mean.
99    pub support: f64,
100}
101
102impl Default for Knobs {
103    fn default() -> Self {
104        Knobs {
105            delta: 0.11,
106            k: 1.0,
107            s: 12.0,
108            home_bias: 0.52,
109            b_pass: 1.35,
110            b_takeon: -0.15,
111            b_cross_delivery: -1.3,
112            k_ontarget: 0.9,
113            k_gk: 0.9,
114            b_ontarget: -0.9,
115            b_beat: -1.05,
116            p_off_frac: 0.5,
117            p_rebound: 0.08,
118            q_rebound: -0.6,
119            q_through: 0.56,
120            q_dribble: 0.02,
121            q_cutback: 0.6,
122            q_header: -0.45,
123            q_long: -1.8,
124            p_def_advance: 0.55,
125            p_mid_advance: 0.2,
126            p_wide: 0.34,
127            p_attc_penetrate: 0.08,
128            p_attc_dribble_box: 0.06,
129            p_attw_cutback: 0.08,
130            p_attw_cut_inside: 0.30,
131            w_pass_mid: 0.85,
132            w_takeon_mid: 0.15,
133            w_pass_attc: 0.58,
134            w_takeon_attc: 0.27,
135            w_longshot_attc: 0.05,
136            w_cross_attw: 0.35,
137            w_takeon_attw: 0.35,
138            w_pass_attw: 0.20,
139            fatigue_base: 0.12,
140            fatigue_wr: 0.5,
141            support: 0.25,
142        }
143    }
144}