1pub const BUFFER_LEN: usize = 16;
12pub const MOTION_WINDOW: usize = 12;
13pub const HCB_WINDOW: usize = 16;
14pub const CHARGE_FRAMES: u16 = 45;
15pub const CHARGE_RELEASE_WINDOW: u8 = 5;
16pub const CHORD_WINDOW: u8 = 3;
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
20pub enum Btn {
21 P,
22 K,
23 S,
24 HS,
25 FL,
26 ST,
27}
28
29impl Btn {
30 pub const ALL: [Btn; 6] = [Btn::P, Btn::K, Btn::S, Btn::HS, Btn::FL, Btn::ST];
31
32 pub fn label(self) -> &'static str {
33 match self {
34 Btn::P => "P",
35 Btn::K => "K",
36 Btn::S => "S",
37 Btn::HS => "HS",
38 Btn::FL => "FL",
39 Btn::ST => "ST",
40 }
41 }
42
43 pub fn is_slash(self) -> bool {
45 matches!(self, Btn::S | Btn::HS)
46 }
47
48 pub fn is_flash(self) -> bool {
50 matches!(self, Btn::FL | Btn::ST)
51 }
52}
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
55pub enum Chord {
56 Throw,
58 RomanCancel,
60 Ex,
62 Feint,
64 Overhead,
66}
67
68impl Chord {
69 pub fn pair(self) -> (Btn, Btn) {
70 match self {
71 Chord::Throw => (Btn::P, Btn::K),
72 Chord::RomanCancel => (Btn::S, Btn::FL),
73 Chord::Ex => (Btn::S, Btn::HS),
74 Chord::Feint => (Btn::FL, Btn::ST),
75 Chord::Overhead => (Btn::HS, Btn::ST),
76 }
77 }
78
79 pub const ALL: [Chord; 5] = [
80 Chord::Throw,
81 Chord::RomanCancel,
82 Chord::Ex,
83 Chord::Feint,
84 Chord::Overhead,
85 ];
86}
87
88#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
89pub struct Buttons {
90 pub p: bool,
91 pub k: bool,
92 pub s: bool,
93 pub hs: bool,
94 pub fl: bool,
95 pub st: bool,
96}
97
98impl Buttons {
99 pub const NONE: Buttons = Buttons {
100 p: false,
101 k: false,
102 s: false,
103 hs: false,
104 fl: false,
105 st: false,
106 };
107
108 pub fn one(b: Btn) -> Self {
109 let mut out = Self::default();
110 out.set(b, true);
111 out
112 }
113
114 pub fn two(a: Btn, b: Btn) -> Self {
115 let mut out = Self::one(a);
116 out.set(b, true);
117 out
118 }
119
120 pub fn chord(c: Chord) -> Self {
121 let (a, b) = c.pair();
122 Self::two(a, b)
123 }
124
125 pub fn get(self, b: Btn) -> bool {
126 match b {
127 Btn::P => self.p,
128 Btn::K => self.k,
129 Btn::S => self.s,
130 Btn::HS => self.hs,
131 Btn::FL => self.fl,
132 Btn::ST => self.st,
133 }
134 }
135
136 pub fn set(&mut self, b: Btn, v: bool) {
137 match b {
138 Btn::P => self.p = v,
139 Btn::K => self.k = v,
140 Btn::S => self.s = v,
141 Btn::HS => self.hs = v,
142 Btn::FL => self.fl = v,
143 Btn::ST => self.st = v,
144 }
145 }
146
147 pub fn any(self) -> bool {
148 self.p || self.k || self.s || self.hs || self.fl || self.st
149 }
150
151 pub fn count(self) -> u8 {
152 Btn::ALL.iter().filter(|b| self.get(**b)).count() as u8
153 }
154
155 pub fn iter(self) -> impl Iterator<Item = Btn> {
156 Btn::ALL.into_iter().filter(move |b| self.get(*b))
157 }
158
159 pub fn single(self) -> Option<Btn> {
161 let mut it = self.iter();
162 let first = it.next()?;
163 if it.next().is_some() {
164 None
165 } else {
166 Some(first)
167 }
168 }
169
170 pub fn has_chord(self, c: Chord) -> bool {
171 let (a, b) = c.pair();
172 self.get(a) && self.get(b)
173 }
174
175 pub fn just_pressed(self, prev: Self) -> Self {
176 Self {
177 p: self.p && !prev.p,
178 k: self.k && !prev.k,
179 s: self.s && !prev.s,
180 hs: self.hs && !prev.hs,
181 fl: self.fl && !prev.fl,
182 st: self.st && !prev.st,
183 }
184 }
185
186 pub fn or(self, other: Self) -> Self {
187 Self {
188 p: self.p || other.p,
189 k: self.k || other.k,
190 s: self.s || other.s,
191 hs: self.hs || other.hs,
192 fl: self.fl || other.fl,
193 st: self.st || other.st,
194 }
195 }
196}
197
198#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
201pub struct InputFrame {
202 pub dir: u8,
203 pub buttons: Buttons,
204}
205
206impl Default for InputFrame {
207 fn default() -> Self {
208 Self {
209 dir: 5,
210 buttons: Buttons::default(),
211 }
212 }
213}
214
215impl InputFrame {
216 pub fn dir(d: u8) -> Self {
217 Self {
218 dir: d,
219 buttons: Buttons::default(),
220 }
221 }
222
223 pub fn press(b: Btn) -> Self {
224 Self {
225 dir: 5,
226 buttons: Buttons::one(b),
227 }
228 }
229
230 pub fn dir_press(d: u8, b: Btn) -> Self {
231 Self {
232 dir: d,
233 buttons: Buttons::one(b),
234 }
235 }
236
237 pub fn chord(c: Chord) -> Self {
238 Self {
239 dir: 5,
240 buttons: Buttons::chord(c),
241 }
242 }
243
244 pub fn down(self) -> bool {
245 matches!(self.dir, 1..=3)
246 }
247 pub fn up(self) -> bool {
248 matches!(self.dir, 7..=9)
249 }
250 pub fn forward(self) -> bool {
251 matches!(self.dir, 3 | 6 | 9)
252 }
253 pub fn back(self) -> bool {
254 matches!(self.dir, 1 | 4 | 7)
255 }
256 pub fn down_back(self) -> bool {
257 self.dir == 1
258 }
259}
260
261pub fn stick_to_dir(sx: i32, sy: i32, facing_right: bool) -> u8 {
264 let fx = if facing_right { sx } else { -sx };
265 match (fx, sy) {
266 (-1, -1) => 1,
267 (0, -1) => 2,
268 (1, -1) => 3,
269 (-1, 0) => 4,
270 (0, 0) => 5,
271 (1, 0) => 6,
272 (-1, 1) => 7,
273 (0, 1) => 8,
274 (1, 1) => 9,
275 _ => 5,
276 }
277}
278
279#[derive(Clone, Debug, PartialEq, Eq, Hash)]
280pub struct InputBuffer {
281 frames: [InputFrame; BUFFER_LEN],
282 head: usize,
283 filled: usize,
284 back_charge: u16,
285 down_charge: u16,
286 back_release: u8,
287 down_release: u8,
288 press_age: [u8; 6],
291}
292
293impl Default for InputBuffer {
294 fn default() -> Self {
295 Self {
296 frames: [InputFrame::default(); BUFFER_LEN],
297 head: 0,
298 filled: 0,
299 back_charge: 0,
300 down_charge: 0,
301 back_release: 0,
302 down_release: 0,
303 press_age: [u8::MAX; 6],
304 }
305 }
306}
307
308impl InputBuffer {
309 pub fn push(&mut self, frame: InputFrame) {
310 let prev = self.latest();
311 let was_back = prev.back();
312 let was_down = prev.down();
313
314 if frame.back() {
315 self.back_charge = self.back_charge.saturating_add(1);
316 self.back_release = 0;
317 } else {
318 if was_back && self.back_charge >= CHARGE_FRAMES {
319 self.back_release = CHARGE_RELEASE_WINDOW;
320 } else {
321 self.back_release = self.back_release.saturating_sub(1);
322 }
323 self.back_charge = 0;
324 }
325
326 if frame.down() {
327 self.down_charge = self.down_charge.saturating_add(1);
328 self.down_release = 0;
329 } else {
330 if was_down && self.down_charge >= CHARGE_FRAMES {
331 self.down_release = CHARGE_RELEASE_WINDOW;
332 } else {
333 self.down_release = self.down_release.saturating_sub(1);
334 }
335 self.down_charge = 0;
336 }
337
338 let edges = frame.buttons.just_pressed(prev.buttons);
339 for (i, b) in Btn::ALL.iter().enumerate() {
340 if edges.get(*b) {
341 self.press_age[i] = 0;
342 } else {
343 self.press_age[i] = self.press_age[i].saturating_add(1);
344 }
345 }
346
347 self.frames[self.head] = frame;
348 self.head = (self.head + 1) % BUFFER_LEN;
349 self.filled = (self.filled + 1).min(BUFFER_LEN);
350 }
351
352 pub fn latest(&self) -> InputFrame {
353 if self.filled == 0 {
354 return InputFrame::default();
355 }
356 let i = (self.head + BUFFER_LEN - 1) % BUFFER_LEN;
357 self.frames[i]
358 }
359
360 pub fn prev(&self) -> InputFrame {
361 if self.filled < 2 {
362 return InputFrame::default();
363 }
364 let i = (self.head + BUFFER_LEN - 2) % BUFFER_LEN;
365 self.frames[i]
366 }
367
368 pub fn pressed(&self) -> Buttons {
370 self.latest().buttons.just_pressed(self.prev().buttons)
371 }
372
373 pub fn pressed_within(&self, n: u8) -> Buttons {
376 let held = self.latest().buttons;
377 let mut out = Buttons::default();
378 for (i, b) in Btn::ALL.iter().enumerate() {
379 if held.get(*b) && self.press_age[i] < n {
380 out.set(*b, true);
381 }
382 }
383 out
384 }
385
386 pub fn pressed_recently(&self, n: u8) -> Buttons {
389 let mut out = Buttons::default();
390 for (i, b) in Btn::ALL.iter().enumerate() {
391 if self.press_age[i] < n {
392 out.set(*b, true);
393 }
394 }
395 out
396 }
397
398 pub fn chord(&self) -> Option<Chord> {
403 let edge = self.pressed();
404 if !edge.any() {
405 return None;
406 }
407 let recent = self.pressed_within(CHORD_WINDOW);
408 for c in [
409 Chord::RomanCancel,
410 Chord::Ex,
411 Chord::Feint,
412 Chord::Overhead,
413 Chord::Throw,
414 ] {
415 let (a, b) = c.pair();
416 let completes_now = (edge.get(a) && recent.get(b)) || (edge.get(b) && recent.get(a));
417 if completes_now {
418 return Some(c);
419 }
420 }
421 None
422 }
423
424 pub fn window(&self, n: usize) -> impl Iterator<Item = InputFrame> + '_ {
426 let n = n.min(self.filled);
427 (0..n).map(move |k| {
428 let i = (self.head + BUFFER_LEN - n + k) % BUFFER_LEN;
429 self.frames[i]
430 })
431 }
432
433 pub fn motion(&self, motion: Motion) -> bool {
434 match motion {
435 Motion::None => true,
436 Motion::Qcf => self.sequence(&[2, 3, 6], MOTION_WINDOW),
437 Motion::Qcb => self.sequence(&[2, 1, 4], MOTION_WINDOW),
438 Motion::Dp => self.sequence(&[6, 2, 3], MOTION_WINDOW),
439 Motion::Hcb => self.sequence(&[6, 2, 4], HCB_WINDOW) && self.hcb_passes_down(),
440 Motion::SuperQcf => self.sequence(&[2, 3, 6, 2, 3, 6], BUFFER_LEN),
441 Motion::ForwardDash => self.dash(6),
442 Motion::BackDash => self.dash(4),
443 Motion::ChargeBackForward => self.back_release > 0 && self.latest().forward(),
444 Motion::ChargeDownUp => self.down_release > 0 && self.latest().up(),
445 }
446 }
447
448 pub fn back_charge(&self) -> u16 {
449 self.back_charge
450 }
451
452 pub fn down_charge(&self) -> u16 {
453 self.down_charge
454 }
455
456 fn hcb_passes_down(&self) -> bool {
459 self.window(HCB_WINDOW).any(|f| matches!(f.dir, 1..=3))
460 }
461
462 fn sequence(&self, parts: &[u8], window: usize) -> bool {
465 let frames: Vec<u8> = self.window(window).map(|f| f.dir).collect();
466 let mut at = 0;
467 for &need in parts {
468 let mut found = false;
469 while at < frames.len() {
470 if dir_matches(frames[at], need) {
471 found = true;
472 at += 1;
473 break;
474 }
475 at += 1;
476 }
477 if !found {
478 return false;
479 }
480 }
481 true
482 }
483
484 fn dash(&self, dir: u8) -> bool {
488 let opposite = if dir == 6 { 4 } else { 6 };
489 let frames: Vec<InputFrame> = self.window(10).collect();
490 let mut taps = 0;
491 let mut released = true;
492 for f in frames {
493 let match_dir = dir_matches(f.dir, dir);
494 if match_dir && released {
495 taps += 1;
496 released = false;
497 } else if f.dir == 5 || f.dir == 8 || dir_matches(f.dir, opposite) {
498 released = true;
499 }
500 }
501 taps >= 2
502 }
503}
504
505fn dir_matches(got: u8, need: u8) -> bool {
506 if got == need {
507 return true;
508 }
509 match need {
510 2 => matches!(got, 1..=3),
511 4 => matches!(got, 1 | 4 | 7),
512 6 => matches!(got, 3 | 6 | 9),
513 8 => matches!(got, 7..=9),
514 _ => false,
515 }
516}
517
518#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
519pub enum Motion {
520 None,
522 Qcf, Qcb, Dp, Hcb, SuperQcf, ForwardDash,
528 BackDash,
529 ChargeBackForward,
530 ChargeDownUp,
531}
532
533impl Motion {
534 pub fn rank(self) -> u8 {
537 match self {
538 Motion::SuperQcf => 4,
539 Motion::Hcb => 3,
540 Motion::Dp | Motion::ChargeBackForward | Motion::ChargeDownUp => 2,
541 Motion::Qcf | Motion::Qcb | Motion::ForwardDash | Motion::BackDash => 1,
542 Motion::None => 0,
543 }
544 }
545
546 pub fn notation(self) -> &'static str {
547 match self {
548 Motion::None => "",
549 Motion::Qcf => "236",
550 Motion::Qcb => "214",
551 Motion::Dp => "623",
552 Motion::Hcb => "63214",
553 Motion::SuperQcf => "236236",
554 Motion::ForwardDash => "66",
555 Motion::BackDash => "44",
556 Motion::ChargeBackForward => "[4]6",
557 Motion::ChargeDownUp => "[2]8",
558 }
559 }
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565
566 fn dir_only(d: u8) -> InputFrame {
567 InputFrame::dir(d)
568 }
569
570 #[test]
571 fn qcf_reads_236() {
572 let mut b = InputBuffer::default();
573 for d in [5, 2, 3, 6, 5] {
574 b.push(dir_only(d));
575 }
576 assert!(b.motion(Motion::Qcf));
577 assert!(!b.motion(Motion::Dp));
578 }
579
580 #[test]
581 fn qcf_accepts_diagonals() {
582 let mut b = InputBuffer::default();
583 for d in [2, 3, 6] {
584 b.push(dir_only(d));
585 }
586 assert!(b.motion(Motion::Qcf));
587 }
588
589 #[test]
590 fn hcb_reads_63214_and_not_64() {
591 let mut b = InputBuffer::default();
592 for d in [6, 3, 2, 1, 4] {
593 b.push(dir_only(d));
594 }
595 assert!(b.motion(Motion::Hcb));
596 let mut b = InputBuffer::default();
597 for d in [6, 5, 4] {
598 b.push(dir_only(d));
599 }
600 assert!(!b.motion(Motion::Hcb));
601 }
602
603 #[test]
604 fn charge_release_is_first_class() {
605 let mut b = InputBuffer::default();
606 for _ in 0..CHARGE_FRAMES {
607 b.push(dir_only(4));
608 }
609 assert_eq!(b.back_charge(), CHARGE_FRAMES);
610 b.push(dir_only(6));
611 assert!(b.motion(Motion::ChargeBackForward));
612 }
613
614 #[test]
615 fn short_charge_does_not_release() {
616 let mut b = InputBuffer::default();
617 for _ in 0..(CHARGE_FRAMES - 1) {
618 b.push(dir_only(4));
619 }
620 b.push(dir_only(6));
621 assert!(!b.motion(Motion::ChargeBackForward));
622 }
623
624 #[test]
625 fn chord_same_frame_and_within_window() {
626 let mut b = InputBuffer::default();
627 b.push(InputFrame::chord(Chord::RomanCancel));
628 assert_eq!(b.chord(), Some(Chord::RomanCancel));
629 b.push(InputFrame::chord(Chord::RomanCancel));
630 assert_eq!(b.chord(), None, "held chord does not re-fire");
631
632 let mut b = InputBuffer::default();
633 b.push(InputFrame::press(Btn::FL));
634 b.push(InputFrame::press(Btn::FL));
635 b.push(InputFrame {
636 dir: 5,
637 buttons: Buttons::two(Btn::FL, Btn::ST),
638 });
639 assert_eq!(b.chord(), Some(Chord::Feint));
640
641 let mut b = InputBuffer::default();
642 for _ in 0..CHORD_WINDOW + 1 {
643 b.push(InputFrame::press(Btn::HS));
644 }
645 b.push(InputFrame {
646 dir: 5,
647 buttons: Buttons::two(Btn::HS, Btn::ST),
648 });
649 assert_eq!(b.chord(), None, "too late to be a chord");
650 }
651}