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}