Skip to main content

tengu_api/state/
battle.rs

1//! PvP battle account (Fren Pet–style). One per Dojo.
2//! Win odds use champion shogun spirit power: AP / (AP + DP).
3//! If **attacker** BP is below [`crate::consts::BATTLE_POINTS_LOW_THRESHOLD`], Fren’s simplified rule: lose up to 0.5% of attacker; win up to [`crate::consts::BATTLE_LOW_WIN_CAP`], capped by defender-side bps. **`defender_cap_bps`**: [`crate::consts::BATTLE_TRANSFER_BPS`] normally, or [`crate::consts::BATTLE_TRANSFER_DEFENDER_DEPLETED_BPS`] when the defender’s champion has **no chakra** (dine to avoid being a juicier target — Fren hibernation-style).
4
5use super::{shogun::BARRACKS_SLOT_EMPTY, Barracks, DojosAccount};
6use steel::*;
7
8pub use crate::consts::{
9    BATTLE_CHAMPION_SLOT_UNSET as CHAMPION_SLOT_UNSET, BATTLE_DEFAULT_AP as DEFAULT_AP,
10    BATTLE_DEFAULT_BATTLE_POINTS as DEFAULT_BATTLE_POINTS, BATTLE_DEFAULT_DP as DEFAULT_DP,
11};
12
13#[repr(C)]
14#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
15pub struct Battle {
16    pub dojo: Pubkey,
17    /// Cached champion attack rating (= champion spirit power for odds).
18    pub attack_points: u64,
19    /// Cached champion defense rating (= defender’s champion spirit power in their role).
20    pub defense_points: u64,
21    /// PvP score; win/loss transfers capped per battle.
22    pub battle_points: u64,
23    /// Last slot this dojo initiated an attack (attacker cooldown).
24    pub last_battle_slot: u64,
25    /// Last slot this dojo was targeted in a duel (defender immunity window).
26    pub last_targeted_slot: u64,
27    /// Start of current 24h window for duel count.
28    pub duel_window_start_slot: u64,
29    /// Duels initiated in current 24h window (max [`crate::consts::BATTLE_MAX_DUELS_PER_24H`]).
30    pub duel_count_24h: u64,
31    /// Barracks slot index (0–11) of the PvP champion shogun.
32    pub champion_slot: u64,
33    /// Last slot when champion was changed (0 = never changed; first change skips cooldown).
34    pub last_champion_change_slot: u64,
35    pub buffer: [u64; 3],
36}
37
38account!(DojosAccount, Battle);
39
40impl Battle {
41    /// Spirit power of the champion at `champion_slot` for win odds (AP or DP as role).
42    pub fn champion_sp(barracks: &Barracks, champion_slot: u64) -> Option<u64> {
43        if champion_slot == crate::consts::BATTLE_CHAMPION_SLOT_UNSET {
44            return None;
45        }
46        let i = champion_slot as usize;
47        if i >= crate::consts::MAX_BARRACKS_SLOTS {
48            return None;
49        }
50        if !barracks.is_slot_valid(champion_slot) {
51            return None;
52        }
53        if barracks.slots[i] == BARRACKS_SLOT_EMPTY {
54            return None;
55        }
56        Some(barracks.slot_cache[i].spirit_power.max(1))
57    }
58
59    /// Chakra remaining for the champion at `champion_slot` (ore / dine). `0` = depleted (PvP: higher loss cap on defender; attacker cannot initiate).
60    pub fn champion_chakra_remaining(barracks: &Barracks, champion_slot: u64) -> Option<u64> {
61        if champion_slot == crate::consts::BATTLE_CHAMPION_SLOT_UNSET {
62            return None;
63        }
64        let i = champion_slot as usize;
65        if i >= crate::consts::MAX_BARRACKS_SLOTS {
66            return None;
67        }
68        if !barracks.is_slot_valid(champion_slot) {
69            return None;
70        }
71        if barracks.slots[i] == BARRACKS_SLOT_EMPTY {
72            return None;
73        }
74        Some(barracks.slot_cache[i].chakra_remaining)
75    }
76
77    /// Win probability in basis points (0..=10000): AP / (AP + DP).
78    pub fn win_probability_bps(attacker_ap: u64, defender_dp: u64) -> u64 {
79        let sum = attacker_ap.saturating_add(defender_dp);
80        if sum == 0 {
81            return 5000;
82        }
83        attacker_ap.saturating_mul(10_000) / sum
84    }
85
86    /// Points moved from loser → winner (Fren Pet rules).
87    ///
88    /// `win_bps` is attacker win probability (0..=10_000). Used only when attacker BP ≥ low threshold
89    /// (and for “attack stronger” vs “weaker”, `win_bps` < 5000 vs ≥ 5000).
90    ///
91    /// `defender_cap_bps`: [`crate::consts::BATTLE_TRANSFER_BPS`] or [`crate::consts::BATTLE_TRANSFER_DEFENDER_DEPLETED_BPS`] for defender-side % of BP.
92    pub fn point_transfer_fren(
93        attacker_bp: u64,
94        defender_bp: u64,
95        win_bps: u64,
96        attacker_won: bool,
97        defender_cap_bps: u64,
98    ) -> u64 {
99        use crate::consts::{
100            BATTLE_LOW_WIN_CAP, BATTLE_POINTS_LOW_THRESHOLD, BATTLE_TRANSFER_BPS,
101        };
102        if attacker_bp == 0 || defender_bp == 0 {
103            return 0;
104        }
105
106        // Fren: “My pet has less than 10k points” — lose 0.5% of self; win min(100, % of target).
107        if attacker_bp < BATTLE_POINTS_LOW_THRESHOLD {
108            if attacker_won {
109                let max_def = defender_bp.saturating_mul(defender_cap_bps) / 10_000;
110                let t = BATTLE_LOW_WIN_CAP.min(max_def).min(defender_bp);
111                return t.max(1);
112            }
113            let max_att = attacker_bp.saturating_mul(BATTLE_TRANSFER_BPS) / 10_000;
114            let t = max_att.min(attacker_bp);
115            return t.max(1);
116        }
117
118        let max_att = attacker_bp.saturating_mul(BATTLE_TRANSFER_BPS) / 10_000;
119        let max_def = defender_bp.saturating_mul(defender_cap_bps) / 10_000;
120        if max_att == 0 || max_def == 0 {
121            return 1;
122        }
123
124        // Fren: 0.5% of each side; odds in formulas use percent = win_bps / 100 (we keep bps).
125        let raw = if win_bps < 5000 {
126            // Attack stronger (lower win chance than 50%).
127            if attacker_won {
128                // Win: min(0.5% defender, 0.5% attacker + (50% − odds) + 60% of attacker 0.5%).
129                let pct = max_att.saturating_mul(5000u64.saturating_sub(win_bps)) / 10_000;
130                let sixty = max_att.saturating_mul(6000) / 10_000;
131                max_att
132                    .saturating_add(pct)
133                    .saturating_add(sixty)
134                    .min(max_def)
135            } else {
136                // Lose: min(0.5% attacker, 0.5% defender + (50% − odds) + 60% of defender 0.5%).
137                let pct = max_def.saturating_mul(5000u64.saturating_sub(win_bps)) / 10_000;
138                let sixty = max_def.saturating_mul(6000) / 10_000;
139                max_def
140                    .saturating_add(pct)
141                    .saturating_add(sixty)
142                    .min(max_att)
143            }
144        } else {
145            // Attack weaker (odds ≥ 50%).
146            if attacker_won {
147                // Win: min(0.5% defender, 0.5% attacker + 60% of attacker 0.5%).
148                let t = max_att.saturating_add(max_att.saturating_mul(6000) / 10_000);
149                t.min(max_def)
150            } else {
151                // Lose: min(0.5% attacker, 0.5% defender + odds × defender 0.5%).
152                let t = max_def.saturating_add(max_def.saturating_mul(win_bps) / 10_000);
153                t.min(max_att)
154            }
155        };
156
157        let loser_bp = if attacker_won {
158            defender_bp
159        } else {
160            attacker_bp
161        };
162        raw.max(1).min(loser_bp)
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::Battle;
169
170    #[test]
171    fn fren_stronger_opponent_examples() {
172        // Fren doc: 100k vs 200k, 20% win → max_att 500, max_def 1000
173        let a = 100_000u64;
174        let d = 200_000u64;
175        let win_bps = 2000;
176        assert_eq!(Battle::point_transfer_fren(a, d, win_bps, false, 50), 500);
177        assert_eq!(Battle::point_transfer_fren(a, d, win_bps, true, 50), 950);
178    }
179
180    #[test]
181    fn fren_low_attacker_under_10k() {
182        // Fren: lose 0.5%; win min(100, 0.5% target). Odds ignored.
183        assert_eq!(Battle::point_transfer_fren(9_000, 100_000, 2000, true, 50), 100);
184        assert_eq!(Battle::point_transfer_fren(9_000, 100_000, 2000, false, 50), 45);
185        assert_eq!(Battle::point_transfer_fren(9_000, 5_000, 7000, true, 50), 25);
186        // Defender depleted (1% cap): min(100, 50) = 50
187        assert_eq!(Battle::point_transfer_fren(9_000, 5_000, 7000, true, 100), 50);
188    }
189
190    #[test]
191    fn fren_weaker_opponent_examples() {
192        // 200k vs 100k, 70% win
193        let a = 200_000u64;
194        let d = 100_000u64;
195        let win_bps = 7000;
196        assert_eq!(Battle::point_transfer_fren(a, d, win_bps, false, 50), 850);
197        assert_eq!(Battle::point_transfer_fren(a, d, win_bps, true, 50), 500);
198
199        // 100k vs 200k, 70% win
200        let a = 100_000u64;
201        let d = 200_000u64;
202        let win_bps = 7000;
203        assert_eq!(Battle::point_transfer_fren(a, d, win_bps, false, 50), 500);
204        assert_eq!(Battle::point_transfer_fren(a, d, win_bps, true, 50), 800);
205    }
206
207    #[test]
208    fn fren_defender_depleted_doubles_cap_when_binding() {
209        // Stronger opponent, attacker wins: 500+250+300=1050; min(max_def): 1000 vs 2000
210        let a = 100_000u64;
211        let d = 200_000u64;
212        let win_bps = 0u64;
213        assert_eq!(Battle::point_transfer_fren(a, d, win_bps, true, 50), 1000);
214        assert_eq!(Battle::point_transfer_fren(a, d, win_bps, true, 100), 1050);
215    }
216}