battleware_types/
execution.rs

1use bytes::{Buf, BufMut};
2use commonware_codec::{
3    varint::UInt, Encode, EncodeSize, Error, FixedSize, RangeCfg, Read, ReadExt, ReadRangeExt,
4    Write,
5};
6use commonware_consensus::threshold_simplex::types::{
7    Activity as CActivity, Finalization as CFinalization, Notarization as CNotarization,
8    Seed as CSeed, View,
9};
10use commonware_cryptography::{
11    bls12381::{
12        primitives::variant::{MinSig, Variant},
13        tle::Ciphertext,
14    },
15    ed25519::{self, Batch, PublicKey},
16    sha256::{Digest, Sha256},
17    BatchVerifier, Committable, Digestible, Hasher, Signer, Verifier,
18};
19use commonware_utils::{modulo, union};
20use std::{collections::BTreeSet, fmt::Debug, hash::Hash};
21
22pub const MAX_LOBBY_SIZE: usize = 128;
23pub const ALLOWED_MOVES: usize = 4;
24pub const TOTAL_MOVES: usize = 1 + ALLOWED_MOVES; // Includes move 0 (no-op) + 4 actual moves
25pub const MIN_HEALTH_POINTS: u8 = 75;
26pub const TOTAL_SKILL_POINTS: u16 = 300;
27pub const SKILLS: usize = 5;
28pub const BASE_MOVE_LIMIT: u16 = 15;
29
30pub const NAMESPACE: &[u8] = b"_BATTLEWARE";
31pub const TRANSACTION_SUFFIX: &[u8] = b"_TX";
32pub const MAX_BLOCK_TRANSACTIONS: usize = 100;
33pub const MAX_BATTLE_ROUNDS: u8 = 15;
34pub const LOBBY_EXPIRY: u64 = 25;
35pub const MOVE_EXPIRY: u64 = 50;
36
37pub type Seed = CSeed<MinSig>;
38pub type Notarization = CNotarization<MinSig, Digest>;
39pub type Finalization = CFinalization<MinSig, Digest>;
40pub type Activity = CActivity<MinSig, Digest>;
41
42pub type Identity = <MinSig as Variant>::Public;
43pub type Evaluation = Identity;
44pub type Signature = <MinSig as Variant>::Signature;
45
46#[inline]
47pub fn transaction_namespace(namespace: &[u8]) -> Vec<u8> {
48    union(namespace, TRANSACTION_SUFFIX)
49}
50
51#[derive(Clone, Debug, PartialEq, Eq)]
52pub struct Transaction {
53    pub nonce: u64,
54    pub instruction: Instruction,
55
56    pub public: ed25519::PublicKey,
57    pub signature: ed25519::Signature,
58}
59
60impl Transaction {
61    fn payload(nonce: &u64, instruction: &Instruction) -> Vec<u8> {
62        let mut payload = Vec::new();
63        nonce.write(&mut payload);
64        instruction.write(&mut payload);
65
66        payload
67    }
68
69    pub fn sign(private: &ed25519::PrivateKey, nonce: u64, instruction: Instruction) -> Self {
70        let signature = private.sign(
71            Some(&transaction_namespace(NAMESPACE)),
72            &Self::payload(&nonce, &instruction),
73        );
74
75        Self {
76            nonce,
77            instruction,
78            public: private.public_key(),
79            signature,
80        }
81    }
82
83    pub fn verify(&self) -> bool {
84        self.public.verify(
85            Some(&transaction_namespace(NAMESPACE)),
86            &Self::payload(&self.nonce, &self.instruction),
87            &self.signature,
88        )
89    }
90
91    pub fn verify_batch(&self, batch: &mut Batch) {
92        batch.add(
93            Some(&transaction_namespace(NAMESPACE)),
94            &Self::payload(&self.nonce, &self.instruction),
95            &self.public,
96            &self.signature,
97        );
98    }
99}
100
101impl Write for Transaction {
102    fn write(&self, writer: &mut impl BufMut) {
103        self.nonce.write(writer);
104        self.instruction.write(writer);
105        self.public.write(writer);
106        self.signature.write(writer);
107    }
108}
109
110impl Read for Transaction {
111    type Cfg = ();
112
113    fn read_cfg(reader: &mut impl Buf, _: &Self::Cfg) -> Result<Self, Error> {
114        let nonce = u64::read(reader)?;
115        let instruction = Instruction::read(reader)?;
116        let public = ed25519::PublicKey::read(reader)?;
117        let signature = ed25519::Signature::read(reader)?;
118
119        Ok(Self {
120            nonce,
121            instruction,
122            public,
123            signature,
124        })
125    }
126}
127
128impl EncodeSize for Transaction {
129    fn encode_size(&self) -> usize {
130        self.nonce.encode_size()
131            + self.instruction.encode_size()
132            + self.public.encode_size()
133            + self.signature.encode_size()
134    }
135}
136
137impl Digestible for Transaction {
138    type Digest = Digest;
139
140    fn digest(&self) -> Digest {
141        let mut hasher = Sha256::new();
142        hasher.update(self.nonce.to_be_bytes().as_ref());
143        hasher.update(self.instruction.encode().as_ref());
144        hasher.update(self.public.as_ref());
145        // We don't include the signature as part of the digest (any valid
146        // signature will be valid for the transaction)
147        hasher.finalize()
148    }
149}
150
151#[derive(Clone, Debug, PartialEq, Eq)]
152#[allow(clippy::large_enum_variant)]
153pub enum Instruction {
154    Generate,
155    Match,
156    Move(Ciphertext<MinSig>),
157    Settle(Signature),
158}
159
160impl Write for Instruction {
161    fn write(&self, writer: &mut impl BufMut) {
162        match self {
163            Self::Generate => 0u8.write(writer),
164            Self::Match => 1u8.write(writer),
165            Self::Move(ciphertext) => {
166                2u8.write(writer);
167                ciphertext.write(writer);
168            }
169            Self::Settle(signature) => {
170                3u8.write(writer);
171                signature.write(writer);
172            }
173        }
174    }
175}
176
177impl Read for Instruction {
178    type Cfg = ();
179
180    fn read_cfg(reader: &mut impl Buf, _: &Self::Cfg) -> Result<Self, Error> {
181        let instruction = match reader.get_u8() {
182            0 => Self::Generate,
183            1 => Self::Match,
184            2 => Self::Move(Ciphertext::read(reader)?),
185            3 => Self::Settle(Signature::read(reader)?),
186            i => return Err(Error::InvalidEnum(i)),
187        };
188
189        Ok(instruction)
190    }
191}
192
193impl EncodeSize for Instruction {
194    fn encode_size(&self) -> usize {
195        u8::SIZE
196            + match self {
197                Self::Generate | Self::Match => 0,
198                Self::Move(ciphertext) => ciphertext.encode_size(),
199                Self::Settle(signature) => signature.encode_size(),
200            }
201    }
202}
203
204#[derive(Clone, Debug, PartialEq, Eq)]
205pub struct Creature {
206    pub traits: [u8; Digest::SIZE],
207}
208
209impl Creature {
210    /// Distributes skill points deterministically based on input hash
211    /// Ensures health >= MIN_HEALTH_POINTS and other skills >= 1
212    /// Total points sum to TOTAL_SKILL_POINTS
213    fn distribute_skill_points(digest: &[u8; Digest::SIZE]) -> [u8; SKILLS] {
214        // Start with minimum values
215        let mut skills = [1u8; SKILLS];
216        skills[0] = MIN_HEALTH_POINTS;
217
218        // Calculate remaining points to distribute
219        let min_sum: u16 = MIN_HEALTH_POINTS as u16 + ALLOWED_MOVES as u16; // health + 4 other skills at 1 each
220        let remaining_points = TOTAL_SKILL_POINTS - min_sum;
221
222        // Use hash bytes to deterministically distribute remaining points
223        // First, calculate weights from hash
224        let weights: Vec<u16> = digest[0..SKILLS].iter().map(|&b| b as u16 + 1).collect();
225        let weight_sum: u16 = weights.iter().sum();
226
227        // Distribute remaining points proportionally
228        let mut distributed: u16 = 0;
229        for i in 0..SKILLS {
230            let additional = (remaining_points * weights[i] / weight_sum) as u8;
231            let before = skills[i];
232            skills[i] = skills[i].saturating_add(additional);
233            let actual_added = (skills[i] - before) as u16;
234            distributed += actual_added;
235        }
236
237        // Handle any rounding remainder by adding to skills in order
238        let mut remainder = remaining_points - distributed;
239        let mut iter = 0;
240        while remainder > 0 {
241            let idx = iter % SKILLS;
242            let skill = &mut skills[idx];
243            if let Some(new_skill) = skill.checked_add(1) {
244                *skill = new_skill;
245                remainder = remainder.saturating_sub(1);
246            }
247            iter += 1;
248        }
249
250        skills
251    }
252
253    /// Calculate action effectiveness for a given move
254    /// Returns (is_defensive, effectiveness)
255    fn calculate_action(traits: &[u8], index: u8, multiplier: u8) -> (bool, u8) {
256        // If index is out of bounds or 0 (no move), return no action
257        // Valid moves are 1-4 (inclusive)
258        if index == 0 || index > ALLOWED_MOVES as u8 {
259            return (false, 0);
260        }
261
262        // Scale effectiveness from 1/2 to full strength
263        // Note: traits array starts at index 0, but move indices now start at 1
264        let max_effectiveness = traits[index as usize];
265        // multiplier ranges from 0 to u8::MAX, we want to map this to 0.5-1.0 of max_effectiveness
266        // Formula: min + (multiplier/u8::MAX) * (max - min) where min = max/2
267        let min_effectiveness = max_effectiveness / 2;
268        let range = max_effectiveness - min_effectiveness;
269        let scaled_effectiveness =
270            min_effectiveness + ((range as u16 * multiplier as u16) / u8::MAX as u16) as u8;
271
272        // Return scaled effectiveness (move 1 is defensive)
273        if index == 1 {
274            (true, scaled_effectiveness)
275        } else {
276            (false, scaled_effectiveness)
277        }
278    }
279
280    pub fn new(actor: PublicKey, nonce: u64, seed: Signature) -> Self {
281        // Compute raw traits from seed
282        let mut hasher = Sha256::new();
283        hasher.update(actor.as_ref());
284        hasher.update(nonce.to_be_bytes().as_ref());
285        hasher.update(seed.encode().as_ref());
286        let mut traits = hasher.finalize().0;
287
288        // Distribute skill points
289        let skills = Self::distribute_skill_points(&traits);
290        traits[..SKILLS].copy_from_slice(&skills);
291        Self { traits }
292    }
293
294    pub fn health(&self) -> u8 {
295        self.traits[0]
296    }
297
298    pub fn action(&self, index: u8, seed: Signature) -> (bool, u8) {
299        // Compute effectiveness
300        let mut hasher = Sha256::new();
301        hasher.update(self.traits.as_ref());
302        hasher.update(seed.encode().as_ref());
303        let effectiveness = hasher.finalize().0;
304
305        // Scale effectiveness
306        Self::calculate_action(&self.traits, index, effectiveness[0])
307    }
308
309    // Get the max effectiveness values for all moves
310    // Returns array indexed by move (0 = no-op, 1-4 = actual moves)
311    // Each element is the max strength for that move
312    pub fn get_move_strengths(&self) -> [u8; TOTAL_MOVES] {
313        [
314            0,              // Move 0: no-op
315            self.traits[1], // Move 1: defense
316            self.traits[2], // Move 2: attack 1
317            self.traits[3], // Move 3: attack 2
318            self.traits[4], // Move 4: attack 3
319        ]
320    }
321
322    // Get move usage limits based on strength ranking
323    // Returns array indexed by move (0 = no-op/unlimited, 1-4 = actual moves)
324    // All moves get limited uses inversely proportional to their strength
325    pub fn get_move_usage_limits(&self) -> [u8; TOTAL_MOVES] {
326        // Extract move strengths
327        let strengths = [
328            self.traits[1], // Defense
329            self.traits[2], // Attack 1
330            self.traits[3], // Attack 2
331            self.traits[4], // Attack 3
332        ];
333
334        // Find the weakest move's strength to use as reference
335        let weakest_strength = *strengths.iter().min().unwrap() as u16;
336
337        // Calculate limits for each move
338        let mut limits = [0u8; TOTAL_MOVES];
339        limits[0] = u8::MAX; // Move 0 (no-op) has unlimited uses
340
341        for (i, &strength) in strengths.iter().enumerate() {
342            // All moves get limited uses inversely proportional to their strength
343            // Weakest moves get BASE_MOVE_LIMIT uses, stronger moves get fewer
344            let limit = (BASE_MOVE_LIMIT * weakest_strength / strength as u16).clamp(1, 20) as u8;
345            limits[i + 1] = limit; // +1 because index 0 is no-op
346        }
347
348        limits
349    }
350}
351
352impl Write for Creature {
353    fn write(&self, writer: &mut impl BufMut) {
354        self.traits.write(writer);
355    }
356}
357
358impl Read for Creature {
359    type Cfg = ();
360
361    fn read_cfg(reader: &mut impl Buf, _: &Self::Cfg) -> Result<Self, Error> {
362        let traits = <[u8; Digest::SIZE]>::read(reader)?;
363        Ok(Self { traits })
364    }
365}
366
367impl FixedSize for Creature {
368    const SIZE: usize = Digest::SIZE;
369}
370
371#[derive(Clone, Debug, PartialEq, Eq)]
372pub struct Block {
373    pub parent: Digest,
374
375    pub view: View,
376    pub height: u64,
377
378    pub transactions: Vec<Transaction>,
379
380    digest: Digest,
381}
382
383impl Block {
384    fn compute_digest(
385        parent: &Digest,
386        view: View,
387        height: u64,
388        transactions: &[Transaction],
389    ) -> Digest {
390        let mut hasher = Sha256::new();
391        hasher.update(parent);
392        hasher.update(&view.to_be_bytes());
393        hasher.update(&height.to_be_bytes());
394        for transaction in transactions {
395            hasher.update(&transaction.digest());
396        }
397        hasher.finalize()
398    }
399
400    pub fn new(parent: Digest, view: View, height: u64, transactions: Vec<Transaction>) -> Self {
401        assert!(transactions.len() <= MAX_BLOCK_TRANSACTIONS);
402        let digest = Self::compute_digest(&parent, view, height, &transactions);
403        Self {
404            parent,
405            view,
406            height,
407            transactions,
408            digest,
409        }
410    }
411}
412
413impl Write for Block {
414    fn write(&self, writer: &mut impl BufMut) {
415        self.parent.write(writer);
416        UInt(self.view).write(writer);
417        UInt(self.height).write(writer);
418        self.transactions.write(writer);
419    }
420}
421
422impl Read for Block {
423    type Cfg = ();
424
425    fn read_cfg(reader: &mut impl Buf, _: &Self::Cfg) -> Result<Self, Error> {
426        let parent = Digest::read(reader)?;
427        let view = UInt::read(reader)?.into();
428        let height = UInt::read(reader)?.into();
429        let transactions = Vec::<Transaction>::read_cfg(
430            reader,
431            &(RangeCfg::from(0..=MAX_BLOCK_TRANSACTIONS), ()),
432        )?;
433
434        // Pre-compute the digest
435        let digest = Self::compute_digest(&parent, view, height, &transactions);
436        Ok(Self {
437            parent,
438            view,
439            height,
440            transactions,
441            digest,
442        })
443    }
444}
445
446impl EncodeSize for Block {
447    fn encode_size(&self) -> usize {
448        self.parent.encode_size()
449            + UInt(self.view).encode_size()
450            + UInt(self.height).encode_size()
451            + self.transactions.encode_size()
452    }
453}
454
455impl Digestible for Block {
456    type Digest = Digest;
457
458    fn digest(&self) -> Digest {
459        self.digest
460    }
461}
462
463impl Committable for Block {
464    type Commitment = Digest;
465
466    fn commitment(&self) -> Digest {
467        self.digest
468    }
469}
470
471impl commonware_consensus::Block for Block {
472    fn parent(&self) -> Digest {
473        self.parent
474    }
475
476    fn height(&self) -> u64 {
477        self.height
478    }
479}
480
481#[derive(Clone, Debug, PartialEq, Eq)]
482pub struct Notarized {
483    pub proof: CNotarization<MinSig, Digest>,
484    pub block: Block,
485}
486
487impl Notarized {
488    pub fn new(proof: CNotarization<MinSig, Digest>, block: Block) -> Self {
489        Self { proof, block }
490    }
491
492    pub fn verify(&self, namespace: &[u8], identity: &<MinSig as Variant>::Public) -> bool {
493        self.proof.verify(namespace, identity)
494    }
495}
496
497impl Write for Notarized {
498    fn write(&self, buf: &mut impl BufMut) {
499        self.proof.write(buf);
500        self.block.write(buf);
501    }
502}
503
504impl Read for Notarized {
505    type Cfg = ();
506
507    fn read_cfg(buf: &mut impl Buf, _: &Self::Cfg) -> Result<Self, Error> {
508        let proof = CNotarization::<MinSig, Digest>::read(buf)?;
509        let block = Block::read(buf)?;
510
511        // Ensure the proof is for the block
512        if proof.proposal.payload != block.digest() {
513            return Err(Error::Invalid(
514                "types::Notarized",
515                "Proof payload does not match block digest",
516            ));
517        }
518        Ok(Self { proof, block })
519    }
520}
521
522impl EncodeSize for Notarized {
523    fn encode_size(&self) -> usize {
524        self.proof.encode_size() + self.block.encode_size()
525    }
526}
527
528#[derive(Clone, Debug, PartialEq, Eq)]
529pub struct Finalized {
530    pub proof: CFinalization<MinSig, Digest>,
531    pub block: Block,
532}
533
534impl Finalized {
535    pub fn new(proof: CFinalization<MinSig, Digest>, block: Block) -> Self {
536        Self { proof, block }
537    }
538
539    pub fn verify(&self, namespace: &[u8], identity: &<MinSig as Variant>::Public) -> bool {
540        self.proof.verify(namespace, identity)
541    }
542}
543
544impl Write for Finalized {
545    fn write(&self, buf: &mut impl BufMut) {
546        self.proof.write(buf);
547        self.block.write(buf);
548    }
549}
550
551impl Read for Finalized {
552    type Cfg = ();
553
554    fn read_cfg(buf: &mut impl Buf, _: &Self::Cfg) -> Result<Self, Error> {
555        let proof = Finalization::read(buf)?;
556        let block = Block::read(buf)?;
557
558        // Ensure the proof is for the block
559        if proof.proposal.payload != block.digest() {
560            return Err(Error::Invalid(
561                "types::Finalized",
562                "Proof payload does not match block digest",
563            ));
564        }
565        Ok(Self { proof, block })
566    }
567}
568
569impl EncodeSize for Finalized {
570    fn encode_size(&self) -> usize {
571        self.proof.encode_size() + self.block.encode_size()
572    }
573}
574
575/// The leader for a given seed is determined by the modulo of the seed with the number of participants.
576pub fn leader_index(seed: &[u8], participants: usize) -> usize {
577    modulo(seed, participants as u64) as usize
578}
579
580#[derive(Clone, Debug, PartialEq, Eq)]
581pub struct Stats {
582    pub elo: u16,
583    pub wins: u32,
584    pub losses: u32,
585    pub draws: u32,
586}
587
588impl Stats {
589    pub fn plays(&self) -> u64 {
590        self.wins as u64 + self.losses as u64 + self.draws as u64
591    }
592}
593
594impl Default for Stats {
595    fn default() -> Self {
596        Self {
597            elo: 1000,
598            wins: 0,
599            losses: 0,
600            draws: 0,
601        }
602    }
603}
604
605impl Write for Stats {
606    fn write(&self, writer: &mut impl BufMut) {
607        self.elo.write(writer);
608        self.wins.write(writer);
609        self.losses.write(writer);
610        self.draws.write(writer);
611    }
612}
613
614impl Read for Stats {
615    type Cfg = ();
616
617    fn read_cfg(reader: &mut impl Buf, _: &Self::Cfg) -> Result<Self, Error> {
618        Ok(Self {
619            elo: u16::read(reader)?,
620            wins: u32::read(reader)?,
621            losses: u32::read(reader)?,
622            draws: u32::read(reader)?,
623        })
624    }
625}
626
627impl FixedSize for Stats {
628    const SIZE: usize = u16::SIZE + u32::SIZE * 3;
629}
630
631#[derive(Clone, Default, Eq, PartialEq, Debug)]
632pub struct Account {
633    pub nonce: u64,
634
635    pub creature: Option<Creature>,
636    pub battle: Option<Digest>,
637
638    pub stats: Stats,
639}
640
641impl Write for Account {
642    fn write(&self, writer: &mut impl BufMut) {
643        self.nonce.write(writer);
644        self.creature.write(writer);
645        self.battle.write(writer);
646        self.stats.write(writer);
647    }
648}
649
650impl Read for Account {
651    type Cfg = ();
652
653    fn read_cfg(reader: &mut impl Buf, _: &Self::Cfg) -> Result<Self, Error> {
654        let nonce = u64::read(reader)?;
655        let creature = Option::<Creature>::read(reader)?;
656        let battle = Option::<Digest>::read(reader)?;
657        let stats = Stats::read(reader)?;
658
659        Ok(Self {
660            nonce,
661            creature,
662            battle,
663            stats,
664        })
665    }
666}
667
668impl EncodeSize for Account {
669    fn encode_size(&self) -> usize {
670        self.nonce.encode_size()
671            + self.creature.encode_size()
672            + self.battle.encode_size()
673            + self.stats.encode_size()
674    }
675}
676
677#[derive(Clone, Debug, PartialEq, Eq, Default)]
678pub struct Leaderboard {
679    pub players: Vec<(PublicKey, Stats)>,
680}
681
682impl Leaderboard {
683    pub fn update(&mut self, player: PublicKey, stats: Stats) {
684        // Update player (if they already exist)
685        if let Some(index) = self.players.iter().position(|(p, _)| p == &player) {
686            // If an update drops a players score considerably, they may no longer actually be in the top 10
687            // but we don't have a reference to the other scores, so we can't replace them. The next player
688            // that settles with a score higher will replace them.
689            self.players[index] = (player, stats);
690        } else {
691            // Add the player to the leaderboard
692            self.players.push((player, stats));
693        }
694
695        // Sort the leaderboard
696        self.players.sort_by(|a, b| b.1.elo.cmp(&a.1.elo));
697
698        // Keep only the top 10 players
699        self.players.truncate(10);
700    }
701}
702
703impl Write for Leaderboard {
704    fn write(&self, writer: &mut impl BufMut) {
705        self.players.write(writer);
706    }
707}
708
709impl Read for Leaderboard {
710    type Cfg = ();
711
712    fn read_cfg(reader: &mut impl Buf, _: &Self::Cfg) -> Result<Self, Error> {
713        Ok(Self {
714            players: Vec::<_>::read_range(reader, 0..=10)?,
715        })
716    }
717}
718
719impl EncodeSize for Leaderboard {
720    fn encode_size(&self) -> usize {
721        self.players.encode_size()
722    }
723}
724
725#[derive(Hash, Eq, PartialEq, Ord, PartialOrd, Clone)]
726pub enum Key {
727    Account(PublicKey),
728    Lobby,
729    Battle(Digest),
730    Leaderboard,
731}
732
733impl Write for Key {
734    fn write(&self, writer: &mut impl BufMut) {
735        match self {
736            Self::Account(account) => {
737                0u8.write(writer);
738                account.write(writer);
739            }
740            Self::Lobby => 1u8.write(writer),
741            Self::Battle(battle) => {
742                2u8.write(writer);
743                battle.write(writer);
744            }
745            Self::Leaderboard => 3u8.write(writer),
746        }
747    }
748}
749
750impl Read for Key {
751    type Cfg = ();
752
753    fn read_cfg(reader: &mut impl Buf, _: &Self::Cfg) -> Result<Self, Error> {
754        let key = match reader.get_u8() {
755            0 => Self::Account(PublicKey::read(reader)?),
756            1 => Self::Lobby,
757            2 => Self::Battle(Digest::read(reader)?),
758            3 => Self::Leaderboard,
759            i => return Err(Error::InvalidEnum(i)),
760        };
761
762        Ok(key)
763    }
764}
765
766impl EncodeSize for Key {
767    fn encode_size(&self) -> usize {
768        u8::SIZE
769            + match self {
770                Self::Account(_) => PublicKey::SIZE,
771                Self::Lobby => 0,
772                Self::Battle(_) => Digest::SIZE,
773                Self::Leaderboard => 0,
774            }
775    }
776}
777
778#[derive(Clone, Eq, PartialEq, Debug)]
779#[allow(clippy::large_enum_variant)]
780pub enum Value {
781    Account(Account),
782    Lobby {
783        expiry: u64,
784
785        players: BTreeSet<PublicKey>,
786    },
787    Battle {
788        expiry: u64,
789        round: u8,
790
791        player_a: PublicKey,
792        player_a_max_health: u8,
793        player_a_health: u8,
794        player_a_pending: Option<Ciphertext<MinSig>>,
795        player_a_move_counts: [u8; TOTAL_MOVES],
796
797        player_b: PublicKey,
798        player_b_max_health: u8,
799        player_b_health: u8,
800        player_b_pending: Option<Ciphertext<MinSig>>,
801        player_b_move_counts: [u8; TOTAL_MOVES],
802    },
803    Commit {
804        height: u64,
805        start: u64,
806    },
807    Leaderboard(Leaderboard),
808}
809
810impl Write for Value {
811    fn write(&self, writer: &mut impl BufMut) {
812        match self {
813            Self::Account(account) => {
814                0u8.write(writer);
815                account.write(writer);
816            }
817            Self::Lobby { expiry, players } => {
818                1u8.write(writer);
819                expiry.write(writer);
820                players.write(writer);
821            }
822            Self::Battle {
823                expiry,
824                round,
825                player_a,
826                player_a_max_health,
827                player_a_health,
828                player_a_pending,
829                player_a_move_counts,
830                player_b,
831                player_b_max_health,
832                player_b_health,
833                player_b_pending,
834                player_b_move_counts,
835            } => {
836                2u8.write(writer);
837                expiry.write(writer);
838                round.write(writer);
839                player_a.write(writer);
840                player_a_max_health.write(writer);
841                player_a_health.write(writer);
842                player_a_pending.write(writer);
843                player_a_move_counts.write(writer);
844                player_b.write(writer);
845                player_b_max_health.write(writer);
846                player_b_health.write(writer);
847                player_b_pending.write(writer);
848                player_b_move_counts.write(writer);
849            }
850            Self::Commit { height, start } => {
851                3u8.write(writer);
852                height.write(writer);
853                start.write(writer);
854            }
855            Self::Leaderboard(leaderboard) => {
856                4u8.write(writer);
857                leaderboard.write(writer);
858            }
859        }
860    }
861}
862
863impl Read for Value {
864    type Cfg = ();
865
866    fn read_cfg(reader: &mut impl Buf, _: &Self::Cfg) -> Result<Self, Error> {
867        let value = match reader.get_u8() {
868            0 => Self::Account(Account::read(reader)?),
869            1 => Self::Lobby {
870                expiry: u64::read(reader)?,
871                players: BTreeSet::<PublicKey>::read_cfg(
872                    reader,
873                    &(RangeCfg::from(0..=MAX_LOBBY_SIZE), ()),
874                )?,
875            },
876            2 => Self::Battle {
877                expiry: u64::read(reader)?,
878                round: u8::read(reader)?,
879                player_a: PublicKey::read(reader)?,
880                player_a_max_health: u8::read(reader)?,
881                player_a_health: u8::read(reader)?,
882                player_a_pending: Option::<Ciphertext<MinSig>>::read(reader)?,
883                player_a_move_counts: <[u8; TOTAL_MOVES]>::read(reader)?,
884                player_b: PublicKey::read(reader)?,
885                player_b_max_health: u8::read(reader)?,
886                player_b_health: u8::read(reader)?,
887                player_b_pending: Option::<Ciphertext<MinSig>>::read(reader)?,
888                player_b_move_counts: <[u8; TOTAL_MOVES]>::read(reader)?,
889            },
890            3 => Self::Commit {
891                height: u64::read(reader)?,
892                start: u64::read(reader)?,
893            },
894            4 => Self::Leaderboard(Leaderboard::read(reader)?),
895            i => return Err(Error::InvalidEnum(i)),
896        };
897
898        Ok(value)
899    }
900}
901
902impl EncodeSize for Value {
903    fn encode_size(&self) -> usize {
904        u8::SIZE
905            + match self {
906                Self::Account(account) => account.encode_size(),
907                Self::Lobby { expiry, players } => expiry.encode_size() + players.encode_size(),
908                Self::Battle {
909                    expiry,
910                    round,
911                    player_a,
912                    player_a_max_health,
913                    player_a_health,
914                    player_a_pending,
915                    player_a_move_counts,
916                    player_b,
917                    player_b_max_health,
918                    player_b_health,
919                    player_b_pending,
920                    player_b_move_counts,
921                } => {
922                    expiry.encode_size()
923                        + round.encode_size()
924                        + player_a.encode_size()
925                        + player_a_max_health.encode_size()
926                        + player_a_health.encode_size()
927                        + player_a_pending.encode_size()
928                        + player_a_move_counts.encode_size()
929                        + player_b.encode_size()
930                        + player_b_max_health.encode_size()
931                        + player_b_health.encode_size()
932                        + player_b_pending.encode_size()
933                        + player_b_move_counts.encode_size()
934                }
935                Self::Commit { height, start } => height.encode_size() + start.encode_size(),
936                Self::Leaderboard(leaderboard) => leaderboard.encode_size(),
937            }
938    }
939}
940
941#[derive(Debug, Clone, PartialEq, Eq)]
942pub enum Outcome {
943    PlayerA,
944    PlayerB,
945    Draw,
946}
947
948impl Write for Outcome {
949    fn write(&self, writer: &mut impl BufMut) {
950        match self {
951            Self::PlayerA => 0u8.write(writer),
952            Self::PlayerB => 1u8.write(writer),
953            Self::Draw => 2u8.write(writer),
954        }
955    }
956}
957
958impl Read for Outcome {
959    type Cfg = ();
960
961    fn read_cfg(reader: &mut impl Buf, _: &Self::Cfg) -> Result<Self, Error> {
962        let outcome = match reader.get_u8() {
963            0 => Self::PlayerA,
964            1 => Self::PlayerB,
965            2 => Self::Draw,
966            i => return Err(Error::InvalidEnum(i)),
967        };
968
969        Ok(outcome)
970    }
971}
972
973impl FixedSize for Outcome {
974    const SIZE: usize = u8::SIZE;
975}
976
977#[derive(Debug, Clone, PartialEq, Eq)]
978#[allow(clippy::large_enum_variant)]
979pub enum Event {
980    Generated {
981        account: PublicKey,
982        creature: Creature,
983    },
984    Matched {
985        battle: Digest,
986        expiry: u64,
987        player_a: PublicKey,
988        player_a_creature: Creature,
989        player_a_stats: Stats,
990        player_b: PublicKey,
991        player_b_creature: Creature,
992        player_b_stats: Stats,
993    },
994    Locked {
995        battle: Digest,
996        round: u8,
997        locker: PublicKey,
998        observer: PublicKey,
999        ciphertext: Ciphertext<MinSig>,
1000    },
1001    Moved {
1002        battle: Digest,
1003        round: u8,
1004        expiry: u64,
1005        player_a: PublicKey,
1006        player_a_health: u8,
1007        player_a_move: u8,
1008        player_a_move_counts: [u8; TOTAL_MOVES],
1009        player_a_power: u8,
1010        player_b: PublicKey,
1011        player_b_health: u8,
1012        player_b_move: u8,
1013        player_b_move_counts: [u8; TOTAL_MOVES],
1014        player_b_power: u8,
1015    },
1016    Settled {
1017        battle: Digest,
1018        round: u8,
1019        player_a: PublicKey,
1020        player_a_old: Stats,
1021        player_a_new: Stats,
1022        player_b: PublicKey,
1023        player_b_old: Stats,
1024        player_b_new: Stats,
1025        outcome: Outcome,
1026        leaderboard: Leaderboard,
1027    },
1028}
1029
1030impl Write for Event {
1031    fn write(&self, writer: &mut impl BufMut) {
1032        match self {
1033            Self::Generated { account, creature } => {
1034                0u8.write(writer);
1035                account.write(writer);
1036                creature.write(writer);
1037            }
1038            Self::Matched {
1039                battle,
1040                expiry,
1041                player_a,
1042                player_a_creature,
1043                player_a_stats,
1044                player_b,
1045                player_b_creature,
1046                player_b_stats,
1047            } => {
1048                1u8.write(writer);
1049                battle.write(writer);
1050                expiry.write(writer);
1051                player_a.write(writer);
1052                player_a_creature.write(writer);
1053                player_a_stats.write(writer);
1054                player_b.write(writer);
1055                player_b_creature.write(writer);
1056                player_b_stats.write(writer);
1057            }
1058            Self::Locked {
1059                battle,
1060                round,
1061                locker,
1062                observer,
1063                ciphertext,
1064            } => {
1065                2u8.write(writer);
1066                battle.write(writer);
1067                round.write(writer);
1068                locker.write(writer);
1069                observer.write(writer);
1070                ciphertext.write(writer);
1071            }
1072            Self::Moved {
1073                battle,
1074                round,
1075                expiry,
1076                player_a,
1077                player_a_health,
1078                player_a_move,
1079                player_a_move_counts,
1080                player_a_power,
1081                player_b,
1082                player_b_health,
1083                player_b_move,
1084                player_b_move_counts,
1085                player_b_power,
1086            } => {
1087                3u8.write(writer);
1088                battle.write(writer);
1089                round.write(writer);
1090                expiry.write(writer);
1091                player_a.write(writer);
1092                player_a_health.write(writer);
1093                player_a_move.write(writer);
1094                player_a_move_counts.write(writer);
1095                player_a_power.write(writer);
1096                player_b.write(writer);
1097                player_b_health.write(writer);
1098                player_b_move.write(writer);
1099                player_b_move_counts.write(writer);
1100                player_b_power.write(writer);
1101            }
1102            Self::Settled {
1103                battle,
1104                round,
1105                player_a,
1106                player_a_old,
1107                player_a_new,
1108                player_b,
1109                player_b_old,
1110                player_b_new,
1111                outcome,
1112                leaderboard,
1113            } => {
1114                4u8.write(writer);
1115                battle.write(writer);
1116                round.write(writer);
1117                player_a.write(writer);
1118                player_a_old.write(writer);
1119                player_a_new.write(writer);
1120                player_b.write(writer);
1121                player_b_old.write(writer);
1122                player_b_new.write(writer);
1123                outcome.write(writer);
1124                leaderboard.write(writer);
1125            }
1126        }
1127    }
1128}
1129
1130impl Read for Event {
1131    type Cfg = ();
1132
1133    fn read_cfg(reader: &mut impl Buf, _: &Self::Cfg) -> Result<Self, Error> {
1134        let event = match reader.get_u8() {
1135            0 => Self::Generated {
1136                account: PublicKey::read(reader)?,
1137                creature: Creature::read(reader)?,
1138            },
1139            1 => Self::Matched {
1140                battle: Digest::read(reader)?,
1141                expiry: u64::read(reader)?,
1142                player_a: PublicKey::read(reader)?,
1143                player_a_creature: Creature::read(reader)?,
1144                player_a_stats: Stats::read(reader)?,
1145                player_b: PublicKey::read(reader)?,
1146                player_b_creature: Creature::read(reader)?,
1147                player_b_stats: Stats::read(reader)?,
1148            },
1149            2 => Self::Locked {
1150                battle: Digest::read(reader)?,
1151                round: u8::read(reader)?,
1152                locker: PublicKey::read(reader)?,
1153                observer: PublicKey::read(reader)?,
1154                ciphertext: Ciphertext::<MinSig>::read(reader)?,
1155            },
1156            3 => Self::Moved {
1157                battle: Digest::read(reader)?,
1158                round: u8::read(reader)?,
1159                expiry: u64::read(reader)?,
1160                player_a: PublicKey::read(reader)?,
1161                player_a_health: u8::read(reader)?,
1162                player_a_move: u8::read(reader)?,
1163                player_a_move_counts: <[u8; TOTAL_MOVES]>::read(reader)?,
1164                player_a_power: u8::read(reader)?,
1165                player_b: PublicKey::read(reader)?,
1166                player_b_health: u8::read(reader)?,
1167                player_b_move: u8::read(reader)?,
1168                player_b_move_counts: <[u8; TOTAL_MOVES]>::read(reader)?,
1169                player_b_power: u8::read(reader)?,
1170            },
1171            4 => Self::Settled {
1172                battle: Digest::read(reader)?,
1173                round: u8::read(reader)?,
1174                player_a: PublicKey::read(reader)?,
1175                player_a_old: Stats::read(reader)?,
1176                player_a_new: Stats::read(reader)?,
1177                player_b: PublicKey::read(reader)?,
1178                player_b_old: Stats::read(reader)?,
1179                player_b_new: Stats::read(reader)?,
1180                outcome: Outcome::read(reader)?,
1181                leaderboard: Leaderboard::read(reader)?,
1182            },
1183            i => return Err(Error::InvalidEnum(i)),
1184        };
1185
1186        Ok(event)
1187    }
1188}
1189
1190impl EncodeSize for Event {
1191    fn encode_size(&self) -> usize {
1192        u8::SIZE
1193            + match self {
1194                Self::Generated { account, creature } => {
1195                    account.encode_size() + creature.encode_size()
1196                }
1197                Self::Matched {
1198                    battle,
1199                    expiry,
1200                    player_a,
1201                    player_a_creature,
1202                    player_a_stats,
1203                    player_b,
1204                    player_b_creature,
1205                    player_b_stats,
1206                } => {
1207                    battle.encode_size()
1208                        + expiry.encode_size()
1209                        + player_a.encode_size()
1210                        + player_a_creature.encode_size()
1211                        + player_a_stats.encode_size()
1212                        + player_b.encode_size()
1213                        + player_b_creature.encode_size()
1214                        + player_b_stats.encode_size()
1215                }
1216                Self::Locked {
1217                    battle,
1218                    round,
1219                    locker,
1220                    observer,
1221                    ciphertext,
1222                } => {
1223                    battle.encode_size()
1224                        + round.encode_size()
1225                        + locker.encode_size()
1226                        + observer.encode_size()
1227                        + ciphertext.encode_size()
1228                }
1229                Self::Moved {
1230                    battle,
1231                    round,
1232                    expiry,
1233                    player_a,
1234                    player_a_health,
1235                    player_a_move,
1236                    player_a_move_counts,
1237                    player_a_power,
1238                    player_b,
1239                    player_b_health,
1240                    player_b_move,
1241                    player_b_move_counts,
1242                    player_b_power,
1243                } => {
1244                    battle.encode_size()
1245                        + round.encode_size()
1246                        + expiry.encode_size()
1247                        + player_a.encode_size()
1248                        + player_a_health.encode_size()
1249                        + player_a_move.encode_size()
1250                        + player_a_move_counts.encode_size()
1251                        + player_a_power.encode_size()
1252                        + player_b.encode_size()
1253                        + player_b_health.encode_size()
1254                        + player_b_move.encode_size()
1255                        + player_b_move_counts.encode_size()
1256                        + player_b_power.encode_size()
1257                }
1258                Self::Settled {
1259                    battle,
1260                    round,
1261                    player_a,
1262                    player_a_old,
1263                    player_a_new,
1264                    player_b,
1265                    player_b_old,
1266                    player_b_new,
1267                    outcome,
1268                    leaderboard,
1269                } => {
1270                    battle.encode_size()
1271                        + round.encode_size()
1272                        + player_a.encode_size()
1273                        + player_a_old.encode_size()
1274                        + player_a_new.encode_size()
1275                        + player_b.encode_size()
1276                        + player_b_old.encode_size()
1277                        + player_b_new.encode_size()
1278                        + outcome.encode_size()
1279                        + leaderboard.encode_size()
1280                }
1281            }
1282    }
1283}
1284
1285#[derive(Debug, Clone, PartialEq, Eq)]
1286pub enum Output {
1287    Event(Event),
1288    Transaction(Transaction),
1289    Commit { height: u64, start: u64 },
1290}
1291
1292impl Write for Output {
1293    fn write(&self, writer: &mut impl BufMut) {
1294        match self {
1295            Self::Event(event) => {
1296                0u8.write(writer);
1297                event.write(writer);
1298            }
1299            Self::Transaction(transaction) => {
1300                1u8.write(writer);
1301                transaction.write(writer);
1302            }
1303            Self::Commit { height, start } => {
1304                2u8.write(writer);
1305                height.write(writer);
1306                start.write(writer);
1307            }
1308        }
1309    }
1310}
1311
1312impl Read for Output {
1313    type Cfg = ();
1314
1315    fn read_cfg(reader: &mut impl Buf, _: &Self::Cfg) -> Result<Self, Error> {
1316        let kind = u8::read(reader)?;
1317        match kind {
1318            0 => Ok(Self::Event(Event::read(reader)?)),
1319            1 => Ok(Self::Transaction(Transaction::read(reader)?)),
1320            2 => Ok(Self::Commit {
1321                height: u64::read(reader)?,
1322                start: u64::read(reader)?,
1323            }),
1324            _ => Err(Error::InvalidEnum(kind)),
1325        }
1326    }
1327}
1328
1329impl EncodeSize for Output {
1330    fn encode_size(&self) -> usize {
1331        1 + match self {
1332            Self::Event(event) => event.encode_size(),
1333            Self::Transaction(transaction) => transaction.encode_size(),
1334            Self::Commit { height, start } => height.encode_size() + start.encode_size(),
1335        }
1336    }
1337}
1338
1339#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1340pub struct Progress {
1341    pub view: View,
1342    pub height: u64,
1343    pub block_digest: Digest,
1344    pub state_root: Digest,
1345    pub state_start_op: u64,
1346    pub state_end_op: u64,
1347    pub events_root: Digest,
1348    pub events_start_op: u64,
1349    pub events_end_op: u64,
1350}
1351
1352impl Progress {
1353    #[allow(clippy::too_many_arguments)]
1354    pub fn new(
1355        view: View,
1356        height: u64,
1357        block_digest: Digest,
1358        state_root: Digest,
1359        state_start_op: u64,
1360        state_end_op: u64,
1361        events_root: Digest,
1362        events_start_op: u64,
1363        events_end_op: u64,
1364    ) -> Self {
1365        Self {
1366            view,
1367            height,
1368            block_digest,
1369            state_root,
1370            state_start_op,
1371            state_end_op,
1372            events_root,
1373            events_start_op,
1374            events_end_op,
1375        }
1376    }
1377}
1378
1379impl Write for Progress {
1380    fn write(&self, writer: &mut impl BufMut) {
1381        self.view.write(writer);
1382        self.height.write(writer);
1383        self.block_digest.write(writer);
1384        self.state_root.write(writer);
1385        self.state_start_op.write(writer);
1386        self.state_end_op.write(writer);
1387        self.events_root.write(writer);
1388        self.events_start_op.write(writer);
1389        self.events_end_op.write(writer);
1390    }
1391}
1392
1393impl Read for Progress {
1394    type Cfg = ();
1395
1396    fn read_cfg(reader: &mut impl Buf, _: &Self::Cfg) -> Result<Self, Error> {
1397        Ok(Self {
1398            view: View::read(reader)?,
1399            height: u64::read(reader)?,
1400            block_digest: Digest::read(reader)?,
1401            state_root: Digest::read(reader)?,
1402            state_start_op: u64::read(reader)?,
1403            state_end_op: u64::read(reader)?,
1404            events_root: Digest::read(reader)?,
1405            events_start_op: u64::read(reader)?,
1406            events_end_op: u64::read(reader)?,
1407        })
1408    }
1409}
1410
1411impl FixedSize for Progress {
1412    const SIZE: usize = View::SIZE
1413        + u64::SIZE
1414        + Digest::SIZE
1415        + Digest::SIZE
1416        + u64::SIZE
1417        + u64::SIZE
1418        + Digest::SIZE
1419        + u64::SIZE
1420        + u64::SIZE;
1421}
1422
1423impl Digestible for Progress {
1424    type Digest = Digest;
1425
1426    fn digest(&self) -> Digest {
1427        Sha256::hash(&self.encode())
1428    }
1429}
1430
1431#[cfg(test)]
1432mod tests {
1433    use super::*;
1434
1435    #[test]
1436    fn test_distribute_skill_points() {
1437        let test_digests = [
1438            [0u8; Digest::SIZE],
1439            [255u8; Digest::SIZE],
1440            [128u8; Digest::SIZE],
1441        ];
1442
1443        for digest in test_digests {
1444            let skills = Creature::distribute_skill_points(&digest);
1445            assert!(skills[0] >= MIN_HEALTH_POINTS);
1446            for skill in skills.iter().skip(1) {
1447                assert!(*skill >= 1);
1448            }
1449            let sum: u16 = skills.iter().map(|&s| s as u16).sum();
1450            assert_eq!(sum, TOTAL_SKILL_POINTS);
1451        }
1452    }
1453
1454    #[test]
1455    fn test_distribute_skill_points_deterministic() {
1456        let digest = [42u8; Digest::SIZE];
1457        let skills1 = Creature::distribute_skill_points(&digest);
1458        let skills2 = Creature::distribute_skill_points(&digest);
1459        assert_eq!(skills1, skills2, "Not deterministic");
1460    }
1461
1462    #[test]
1463    fn test_distribute_skill_points_unbalanced_health() {
1464        let mut digest = [0u8; Digest::SIZE];
1465        digest[0] = 255;
1466        digest[1] = 0;
1467        digest[2] = 0;
1468        digest[3] = 0;
1469        digest[4] = 0;
1470        let skills = Creature::distribute_skill_points(&digest);
1471
1472        assert!(skills[0] >= MIN_HEALTH_POINTS);
1473        for skill in skills.iter().skip(1) {
1474            assert!(*skill >= 1);
1475        }
1476        let sum: u16 = skills.iter().map(|&s| s as u16).sum();
1477        assert_eq!(sum, TOTAL_SKILL_POINTS);
1478    }
1479
1480    #[test]
1481    fn test_distribute_skill_points_unbalanced_attack() {
1482        let mut digest = [0u8; Digest::SIZE];
1483        digest[0] = 0;
1484        digest[1] = 0;
1485        digest[2] = 0;
1486        digest[3] = 0;
1487        digest[4] = 255;
1488        let skills = Creature::distribute_skill_points(&digest);
1489
1490        assert!(skills[0] >= MIN_HEALTH_POINTS);
1491        for skill in skills.iter().skip(1) {
1492            assert!(*skill >= 1);
1493        }
1494        let sum: u16 = skills.iter().map(|&s| s as u16).sum();
1495        assert_eq!(sum, TOTAL_SKILL_POINTS);
1496    }
1497
1498    #[test]
1499    fn test_calculate_action() {
1500        // Test traits with different strengths
1501        let traits = {
1502            let mut t = [0u8; Digest::SIZE];
1503            t[0] = 100; // health
1504            t[1] = 50; // defense
1505            t[2] = 60; // attack 1
1506            t[3] = 70; // attack 2
1507            t[4] = 80; // attack 3
1508            t
1509        };
1510
1511        // Test invalid indices
1512        assert_eq!(Creature::calculate_action(&traits, 0, 128), (false, 0));
1513        assert_eq!(Creature::calculate_action(&traits, 5, 128), (false, 0));
1514
1515        // Test defensive move (index 1)
1516        let (is_defensive, effectiveness) = Creature::calculate_action(&traits, 1, 128);
1517        assert!(is_defensive);
1518        assert!(effectiveness >= 25);
1519        assert!(effectiveness <= 50);
1520
1521        // Test attack moves (indices 2-4)
1522        for i in 2..=4 {
1523            let (is_defensive, effectiveness) = Creature::calculate_action(&traits, i, 128);
1524            assert!(!is_defensive);
1525            let max_eff = traits[i as usize];
1526            assert!(effectiveness >= max_eff / 2 && effectiveness <= max_eff);
1527        }
1528
1529        let (_, min_eff) = Creature::calculate_action(&traits, 2, 0);
1530        let (_, max_eff) = Creature::calculate_action(&traits, 2, 255);
1531
1532        assert_eq!(min_eff, traits[2] / 2); // Minimum effectiveness
1533        assert_eq!(max_eff, traits[2]); // Maximum effectiveness
1534    }
1535
1536    #[test]
1537    fn test_get_move_usage_limits() {
1538        // Create a creature with known traits
1539        let mut creature = Creature {
1540            traits: [0u8; Digest::SIZE],
1541        };
1542        creature.traits[0] = 100; // health
1543        creature.traits[1] = 20; // defense (weakest)
1544        creature.traits[2] = 40; // attack 1
1545        creature.traits[3] = 60; // attack 2
1546        creature.traits[4] = 80; // attack 3 (strongest)
1547
1548        let limits = creature.get_move_usage_limits();
1549
1550        // Move 0 (no-op) should have unlimited uses
1551        assert_eq!(limits[0], u8::MAX);
1552
1553        // All actual moves (1-4) should have limited uses
1554        for limit in limits.iter().skip(1) {
1555            assert!(*limit >= 1 && *limit <= 20);
1556        }
1557
1558        // Defense (weakest, move 1) should have most uses (BASE_MOVE_LIMIT)
1559        assert_eq!(limits[1], 15);
1560
1561        // Stronger moves should have fewer uses
1562        assert!(limits[1] > limits[2]); // defense > attack 1
1563        assert!(limits[2] > limits[3]); // attack 1 > attack 2
1564        assert!(limits[3] > limits[4]); // attack 2 > attack 3
1565    }
1566
1567    #[test]
1568    fn test_get_move_usage_limits_equal_strengths() {
1569        // Test with equal strength moves
1570        let mut creature = Creature {
1571            traits: [0u8; Digest::SIZE],
1572        };
1573        creature.traits[0] = 100; // health
1574        creature.traits[1] = 50; // all moves equal strength
1575        creature.traits[2] = 50;
1576        creature.traits[3] = 50;
1577        creature.traits[4] = 50;
1578
1579        let limits = creature.get_move_usage_limits();
1580
1581        // Move 0 (no-op) should have unlimited uses
1582        assert_eq!(limits[0], u8::MAX);
1583
1584        // All actual moves (1-4) should have the same limit (BASE_MOVE_LIMIT)
1585        for limit in limits.iter().skip(1) {
1586            assert_eq!(*limit, 15);
1587        }
1588    }
1589
1590    #[test]
1591    fn test_get_move_usage_limits_edge_cases() {
1592        // Test with extreme differences in strength
1593        let mut creature = Creature {
1594            traits: [0u8; Digest::SIZE],
1595        };
1596        creature.traits[0] = 100; // health
1597        creature.traits[1] = 10; // defense (weakest)
1598        creature.traits[2] = 255; // attack 1 (very strong)
1599        creature.traits[3] = 128; // attack 2 (medium)
1600        creature.traits[4] = 200; // attack 3 (strong)
1601
1602        let limits = creature.get_move_usage_limits();
1603
1604        // Move 0 (no-op) should have unlimited uses
1605        assert_eq!(limits[0], u8::MAX);
1606
1607        // All actual moves (1-4) should be clamped between 1 and 20
1608        for limit in limits.iter().skip(1) {
1609            assert!(*limit >= 1);
1610            assert!(*limit <= 20);
1611        }
1612
1613        // Defense (weakest, move 1) should have BASE_MOVE_LIMIT
1614        assert_eq!(limits[1], 15);
1615
1616        // Very strong moves should be clamped at minimum
1617        assert_eq!(limits[2], 1); // attack 1 (move 2) is so strong it hits the minimum
1618
1619        // Check relative ordering based on strength
1620        // Calculation: 15 * 10 / 255 = 0.58, clamped to 1
1621        // Calculation: 15 * 10 / 128 = 1.17, clamped to 1
1622        // Calculation: 15 * 10 / 200 = 0.75, clamped to 1
1623        // All strong attacks get clamped to minimum
1624        assert_eq!(limits[2], 1); // attack 1 (move 2)
1625        assert_eq!(limits[3], 1); // attack 2 (move 3)
1626        assert_eq!(limits[4], 1); // attack 3 (move 4)
1627        assert_eq!(limits[2], 1); // attack 2
1628        assert_eq!(limits[3], 1); // attack 3
1629    }
1630}