fforge_core/match_engine/
stream.rs1use super::zone::Zone;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Side {
17 Home,
18 Away,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
27pub enum ShotKind {
28 Finish,
29 Header,
30 LongShot,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
42pub enum ShotSource {
43 Through,
44 Dribble,
45 Cutback,
46 Cross,
47 Long,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum ShotOutcome {
52 Goal,
53 Saved,
54 Off,
55 Blocked,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum MatchEventKind {
60 Pass {
61 success: bool,
62 },
63 TakeOn {
64 success: bool,
65 },
66 Cross {
72 success: bool,
73 },
74 Clearance,
76 Turnover,
78 Shot {
79 kind: ShotKind,
80 source: ShotSource,
81 outcome: ShotOutcome,
82 },
83 Save {
86 parried: bool,
87 },
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub struct MatchEvent {
94 pub minute: u8,
95 pub side: Side,
96 pub zone: Zone,
97 pub kind: MatchEventKind,
98}
99
100impl MatchEvent {
101 pub fn commentary(&self, side_name: &str) -> String {
105 let m = self.minute;
106 let z = self.zone.label();
107 match self.kind {
108 MatchEventKind::Pass { success: true } => format!("{m}' {side_name} pick a pass {z}."),
109 MatchEventKind::Pass { success: false } => {
110 format!("{m}' {side_name} pass cut out {z}.")
111 }
112 MatchEventKind::TakeOn { success: true } => {
113 format!("{m}' {side_name} beat their marker {z}.")
114 }
115 MatchEventKind::TakeOn { success: false } => {
116 format!("{m}' {side_name} dispossessed {z}.")
117 }
118 MatchEventKind::Cross { success: true } => format!("{m}' {side_name} whip in a cross."),
119 MatchEventKind::Cross { success: false } => {
120 format!("{m}' {side_name} cross doesn't find a man.")
121 }
122 MatchEventKind::Clearance => format!("{m}' Cleared."),
123 MatchEventKind::Turnover => format!("{m}' Turned over {z}."),
124 MatchEventKind::Shot { kind, outcome, .. } => {
125 let k = match kind {
126 ShotKind::Finish => "shot",
127 ShotKind::Header => "header",
128 ShotKind::LongShot => "effort from distance",
129 };
130 match outcome {
131 ShotOutcome::Goal => format!("{m}' GOAL! {side_name} score with a {k}!"),
132 ShotOutcome::Saved => format!("{m}' {side_name} {k} — saved!"),
133 ShotOutcome::Off => format!("{m}' {side_name} {k} — off target."),
134 ShotOutcome::Blocked => format!("{m}' {side_name} {k} — blocked."),
135 }
136 }
137 MatchEventKind::Save { parried: true } => {
138 format!("{m}' Parried! Loose ball in the box.")
139 }
140 MatchEventKind::Save { parried: false } => format!("{m}' Keeper collects."),
141 }
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn commentary_never_panics_across_the_whole_alphabet() {
151 let kinds = [
152 MatchEventKind::Pass { success: true },
153 MatchEventKind::Pass { success: false },
154 MatchEventKind::TakeOn { success: true },
155 MatchEventKind::TakeOn { success: false },
156 MatchEventKind::Cross { success: true },
157 MatchEventKind::Cross { success: false },
158 MatchEventKind::Clearance,
159 MatchEventKind::Turnover,
160 MatchEventKind::Shot {
161 kind: ShotKind::Finish,
162 source: ShotSource::Through,
163 outcome: ShotOutcome::Goal,
164 },
165 MatchEventKind::Shot {
166 kind: ShotKind::Header,
167 source: ShotSource::Cross,
168 outcome: ShotOutcome::Saved,
169 },
170 MatchEventKind::Shot {
171 kind: ShotKind::LongShot,
172 source: ShotSource::Long,
173 outcome: ShotOutcome::Off,
174 },
175 MatchEventKind::Shot {
176 kind: ShotKind::Finish,
177 source: ShotSource::Cutback,
178 outcome: ShotOutcome::Blocked,
179 },
180 MatchEventKind::Save { parried: true },
181 MatchEventKind::Save { parried: false },
182 ];
183 for kind in kinds {
184 let event = MatchEvent {
185 minute: 42,
186 side: Side::Home,
187 zone: Zone::AttW,
188 kind,
189 };
190 assert!(!event.commentary("Home").is_empty());
191 }
192 }
193}