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; pub 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 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 fn distribute_skill_points(digest: &[u8; Digest::SIZE]) -> [u8; SKILLS] {
214 let mut skills = [1u8; SKILLS];
216 skills[0] = MIN_HEALTH_POINTS;
217
218 let min_sum: u16 = MIN_HEALTH_POINTS as u16 + ALLOWED_MOVES as u16; let remaining_points = TOTAL_SKILL_POINTS - min_sum;
221
222 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 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 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 fn calculate_action(traits: &[u8], index: u8, multiplier: u8) -> (bool, u8) {
256 if index == 0 || index > ALLOWED_MOVES as u8 {
259 return (false, 0);
260 }
261
262 let max_effectiveness = traits[index as usize];
265 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 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 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 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 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 Self::calculate_action(&self.traits, index, effectiveness[0])
307 }
308
309 pub fn get_move_strengths(&self) -> [u8; TOTAL_MOVES] {
313 [
314 0, self.traits[1], self.traits[2], self.traits[3], self.traits[4], ]
320 }
321
322 pub fn get_move_usage_limits(&self) -> [u8; TOTAL_MOVES] {
326 let strengths = [
328 self.traits[1], self.traits[2], self.traits[3], self.traits[4], ];
333
334 let weakest_strength = *strengths.iter().min().unwrap() as u16;
336
337 let mut limits = [0u8; TOTAL_MOVES];
339 limits[0] = u8::MAX; for (i, &strength) in strengths.iter().enumerate() {
342 let limit = (BASE_MOVE_LIMIT * weakest_strength / strength as u16).clamp(1, 20) as u8;
345 limits[i + 1] = limit; }
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 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 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 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
575pub 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 if let Some(index) = self.players.iter().position(|(p, _)| p == &player) {
686 self.players[index] = (player, stats);
690 } else {
691 self.players.push((player, stats));
693 }
694
695 self.players.sort_by(|a, b| b.1.elo.cmp(&a.1.elo));
697
698 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 let traits = {
1502 let mut t = [0u8; Digest::SIZE];
1503 t[0] = 100; t[1] = 50; t[2] = 60; t[3] = 70; t[4] = 80; t
1509 };
1510
1511 assert_eq!(Creature::calculate_action(&traits, 0, 128), (false, 0));
1513 assert_eq!(Creature::calculate_action(&traits, 5, 128), (false, 0));
1514
1515 let (is_defensive, effectiveness) = Creature::calculate_action(&traits, 1, 128);
1517 assert!(is_defensive);
1518 assert!(effectiveness >= 25);
1519 assert!(effectiveness <= 50);
1520
1521 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); assert_eq!(max_eff, traits[2]); }
1535
1536 #[test]
1537 fn test_get_move_usage_limits() {
1538 let mut creature = Creature {
1540 traits: [0u8; Digest::SIZE],
1541 };
1542 creature.traits[0] = 100; creature.traits[1] = 20; creature.traits[2] = 40; creature.traits[3] = 60; creature.traits[4] = 80; let limits = creature.get_move_usage_limits();
1549
1550 assert_eq!(limits[0], u8::MAX);
1552
1553 for limit in limits.iter().skip(1) {
1555 assert!(*limit >= 1 && *limit <= 20);
1556 }
1557
1558 assert_eq!(limits[1], 15);
1560
1561 assert!(limits[1] > limits[2]); assert!(limits[2] > limits[3]); assert!(limits[3] > limits[4]); }
1566
1567 #[test]
1568 fn test_get_move_usage_limits_equal_strengths() {
1569 let mut creature = Creature {
1571 traits: [0u8; Digest::SIZE],
1572 };
1573 creature.traits[0] = 100; creature.traits[1] = 50; 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 assert_eq!(limits[0], u8::MAX);
1583
1584 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 let mut creature = Creature {
1594 traits: [0u8; Digest::SIZE],
1595 };
1596 creature.traits[0] = 100; creature.traits[1] = 10; creature.traits[2] = 255; creature.traits[3] = 128; creature.traits[4] = 200; let limits = creature.get_move_usage_limits();
1603
1604 assert_eq!(limits[0], u8::MAX);
1606
1607 for limit in limits.iter().skip(1) {
1609 assert!(*limit >= 1);
1610 assert!(*limit <= 20);
1611 }
1612
1613 assert_eq!(limits[1], 15);
1615
1616 assert_eq!(limits[2], 1); assert_eq!(limits[2], 1); assert_eq!(limits[3], 1); assert_eq!(limits[4], 1); assert_eq!(limits[2], 1); assert_eq!(limits[3], 1); }
1630}