essences/
plinko.rs

1use crate::items::AttributeId;
2use crate::prelude::*;
3use std::collections::BTreeMap;
4
5/// Шаг шарика в мини-игре Plinko: на каждом ряду колышков шарик уходит влево
6/// или вправо. Путь целиком решает сервер, клиент только проигрывает анимацию.
7#[derive(Copy, PartialEq, Eq, Hash, Debug, Clone, Serialize, Deserialize, Tsify, JsonSchema)]
8pub enum PlinkoDirection {
9    Left,
10    Right,
11}
12
13/// Путь шарика: ровно `rows` шагов, по одному на ряд колышков.
14pub type PlinkoPath = Vec<PlinkoDirection>;
15
16/// Колышек, которого коснулся шарик: ряд и колонка внутри ряда.
17/// В ряду `row` колонки нумеруются `0..=row`.
18#[derive(Copy, PartialEq, Eq, Hash, Debug, Clone, Serialize, Deserialize, Tsify, JsonSchema)]
19pub struct PlinkoPin {
20    pub row: i64,
21    pub column: i64,
22}
23
24/// Колышки, которых коснулся шарик, выводятся из пути детерминированно:
25/// в ряду `r` колонка равна числу шагов `Right` среди первых `r` шагов
26/// (шаг с индексом `r` — это решение, принятое уже НА этом колышке).
27/// Итоговый слот — общее число шагов `Right`.
28pub fn pins_touched_by_path(path: &[PlinkoDirection]) -> Vec<PlinkoPin> {
29    let mut column = 0i64;
30    let mut pins = Vec::with_capacity(path.len());
31    for (row, step) in path.iter().enumerate() {
32        pins.push(PlinkoPin {
33            row: row as i64,
34            column,
35        });
36        if *step == PlinkoDirection::Right {
37            column += 1;
38        }
39    }
40    pins
41}
42
43/// Индекс слота, в который приземлится шарик: число шагов `Right`.
44pub fn slot_index_for_path(path: &[PlinkoDirection]) -> i64 {
45    path.iter()
46        .filter(|step| **step == PlinkoDirection::Right)
47        .count() as i64
48}
49
50/// Накопленные за всё время бонусы от колышков Plinko: атрибут → суммарная
51/// прибавка. Постоянный прогресс, как таланты и статуя; ограничен потолком
52/// активного транша (`plinko_settings.tranches`, BAL-021).
53#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
54pub struct PlinkoPinBonusesMap(pub BTreeMap<AttributeId, i64>);
55
56impl PlinkoPinBonusesMap {
57    pub fn get(&self, attribute_id: &AttributeId) -> i64 {
58        self.0.get(attribute_id).copied().unwrap_or(0)
59    }
60
61    pub fn set(&mut self, attribute_id: AttributeId, value: i64) {
62        self.0.insert(attribute_id, value);
63    }
64
65    pub fn is_empty(&self) -> bool {
66        self.0.is_empty()
67    }
68
69    pub fn iter(&self) -> impl Iterator<Item = (&AttributeId, &i64)> {
70        self.0.iter()
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn pins_are_one_per_row_and_follow_the_path() {
80        use PlinkoDirection::{Left as L, Right as R};
81        let path = vec![R, L, R, R, L, L, R];
82
83        let pins = pins_touched_by_path(&path);
84
85        assert_eq!(pins.len(), path.len(), "one pin per row");
86        assert_eq!(
87            pins,
88            vec![
89                PlinkoPin { row: 0, column: 0 },
90                PlinkoPin { row: 1, column: 1 },
91                PlinkoPin { row: 2, column: 1 },
92                PlinkoPin { row: 3, column: 2 },
93                PlinkoPin { row: 4, column: 3 },
94                PlinkoPin { row: 5, column: 3 },
95                PlinkoPin { row: 6, column: 3 },
96            ]
97        );
98        assert_eq!(slot_index_for_path(&path), 4);
99        for pin in &pins {
100            assert!(pin.column >= 0 && pin.column <= pin.row, "{pin:?}");
101        }
102    }
103
104    #[test]
105    fn same_path_always_yields_the_same_pins() {
106        use PlinkoDirection::{Left as L, Right as R};
107        let path = vec![L, L, R, L, R, R, L];
108
109        assert_eq!(pins_touched_by_path(&path), pins_touched_by_path(&path));
110    }
111}