Skip to main content

fforge_core/match_engine/
zone.rs

1//! The five-zone possession state space (`MATCH_MODEL.md` §2) and the
2//! role→zone presence table (§6): who is on the ball / defending where.
3//!
4//! Distinct from `fforge_domain::ROLE_WEIGHTS` (attribute *importance* for
5//! CA) — this rates spatial *presence*, and drives actor/defender sampling
6//! in the resolution model (§4). Verbatim from the calibrated
7//! `match_model_prototype.ipynb` (`PRES_ATT` / `PRES_DEF`).
8
9use fforge_domain::{NUM_ROLES, Role};
10
11pub const NUM_ZONES: usize = 5;
12
13/// `Def`/`Mid`/`AttC`/`AttW` are dwelling zones; `Box` is not dwelt in — an
14/// edge that reaches it resolves a shot immediately (arrival = chance).
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Zone {
17    Def,
18    Mid,
19    AttC,
20    AttW,
21    Box,
22}
23
24impl Zone {
25    #[inline]
26    pub const fn index(self) -> usize {
27        self as usize
28    }
29
30    /// A short phrase for commentary rendering (the humble text match view).
31    pub fn label(self) -> &'static str {
32        match self {
33            Zone::Def => "deep in their own third",
34            Zone::Mid => "in midfield",
35            Zone::AttC => "in the final third",
36            Zone::AttW => "out wide",
37            Zone::Box => "in the box",
38        }
39    }
40}
41
42/// Attacking presence: how often a role is the on-ball actor in a zone.
43/// Row = `Role::ALL` order, column = zone (declared order).
44#[rustfmt::skip]
45const PRES_ATT: [[u8; NUM_ZONES]; NUM_ROLES] = [
46    // Def Mid AttC AttW Box
47    [5,  0,  0,   0,   0], // Gk — starts build-up
48    [4,  1,  0,   0,   0], // Cb
49    [3,  3,  1,   3,   0], // Fb — overlaps wide
50    [3,  4,  1,   0,   0], // Dm
51    [1,  4,  3,   1,   1], // Cm
52    [0,  3,  4,   2,   2], // Am — central creation
53    [0,  2,  2,   5,   2], // W  — owns the wide zone
54    [0,  1,  3,   1,   5], // St — owns the box
55];
56
57/// Defensive presence: the primary challenger when the opponent attacks a
58/// zone. Row = `Role::ALL` order, column = zone (declared order).
59#[rustfmt::skip]
60const PRES_DEF: [[u8; NUM_ZONES]; NUM_ROLES] = [
61    // Def Mid AttC AttW Box
62    [0,  0,  0,   0,   3], // Gk — contests crosses/shots in the box
63    [1,  1,  4,   2,   5], // Cb — anchors central + box
64    [1,  2,  2,   5,   3], // Fb — primary wide defender
65    [2,  4,  3,   1,   1], // Dm
66    [2,  4,  2,   1,   0], // Cm
67    [2,  2,  1,   1,   0], // Am
68    [3,  2,  1,   2,   0], // W  — tracks back a little
69    [4,  1,  0,   0,   0], // St — presses deep build-up
70];
71
72pub fn attacking_presence(role: Role, zone: Zone) -> u32 {
73    PRES_ATT[role.index()][zone.index()] as u32
74}
75
76pub fn defending_presence(role: Role, zone: Zone) -> u32 {
77    PRES_DEF[role.index()][zone.index()] as u32
78}