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}