From 8a58c84d47675371fdd9d06792cf946c938275cb Mon Sep 17 00:00:00 2001 From: bal7hazar Date: Wed, 17 Apr 2024 18:41:00 +0200 Subject: [PATCH 1/7] feat(rating): elo rating change --- crates/src/lib.cairo | 5 ++ crates/src/rating/elo.cairo | 156 ++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 crates/src/rating/elo.cairo diff --git a/crates/src/lib.cairo b/crates/src/lib.cairo index 8c56b3c6..a0719f2c 100644 --- a/crates/src/lib.cairo +++ b/crates/src/lib.cairo @@ -12,6 +12,11 @@ mod defi { } } +mod rating { + mod elo; +} + + mod random { mod deck; mod dice; diff --git a/crates/src/rating/elo.cairo b/crates/src/rating/elo.cairo new file mode 100644 index 00000000..07863167 --- /dev/null +++ b/crates/src/rating/elo.cairo @@ -0,0 +1,156 @@ +//! Elo struct and rating methods. +//! Source: https://github.com/saucepoint/elo-lib/blob/main/src/Elo.sol + +// Core imports + +use core::integer::u256_sqrt; + +// Constants + +const MULTIPLIER: u256 = 10_000; +const SCALER: u256 = 800; + +// Errors + +mod errors { + const ELO_DIFFERENCE_TOO_LARGE: felt252 = 'Elo: difference too large'; +} + +/// Elo implementation.. +#[generate_trait] +impl EloImpl of Elo { + /// Calculates the change in ELO rating, after a given outcome. + /// # Arguments + /// * `rating_a` - The ELO rating of the player A. + /// * `rating_b` - The ELO rating of the player B. + /// * `score` - The score of the player A, scaled by 100. 100 = win, 50 = draw, 0 = loss. + /// * `k` - The k-factor or development multiplier used to calculate the change in ELO rating. 20 is the typical value. + /// # Returns + /// * `change` - The change in ELO rating of player A. + /// * `negative` - The directional change of player A's ELO. Opposite sign for player B. + #[inline(always)] + fn rating_change(rating_a: u256, rating_b: u256, score: u256, k: u256) -> (u256, bool) { + let negative = rating_a > rating_b; + let rating_diff: u256 = if negative { + rating_a - rating_b + } else { + rating_b - rating_a + }; + + // [Check] Checks against overflow/underflow + // Large rating diffs leads to 10 ** rating_diff being too large to fit in a u256 + // Large rating diffs when applying the scale factor leads to underflow (800 - rating_diff) + assert( + rating_diff < 800 || (!negative && rating_diff < 1126), errors::ELO_DIFFERENCE_TOO_LARGE + ); + + // [Compute] Expected score = 1 / (1 + 10 ^ (rating_diff / 400)) + // Apply offset of 800 to scale the result by 100 + // Divide by 25 to avoid reach u256 max + // (x / 400) is the same as ((x / 25) / 16) + // x ^ (1 / 16) is the same as 16th root of x + let order: u256 = if negative { + (SCALER - rating_diff) / 25 + } else { + (SCALER + rating_diff) / 25 + }; + let powered: u256 = PrivateTrait::pow(10, order); + let rooted: u256 = u256_sqrt(u256_sqrt(u256_sqrt(u256_sqrt(powered).into()).into()).into()) + .into(); + + // [Compute] Change = k * (score - expectedScore) + let k_expected_score = k * MULTIPLIER / (100 + rooted); + let k_score = k * score; + let negative = k_score < k_expected_score; + let change = if negative { + k_expected_score - k_score + } else { + k_score - k_expected_score + }; + + // [Return] Change rounded and its sign + (PrivateTrait::round_div(change, 100), negative) + } +} + +#[generate_trait] +impl Private of PrivateTrait { + fn pow, +Mul, +Div, +Rem, +PartialEq, +Into, +Drop, +Copy>( + base: T, exp: T + ) -> T { + if exp == 0_u8.into() { + 1_u8.into() + } else if exp == 1_u8.into() { + base + } else if exp % 2_u8.into() == 0_u8.into() { + PrivateTrait::pow(base * base, exp / 2_u8.into()) + } else { + base * PrivateTrait::pow(base * base, exp / 2_u8.into()) + } + } + + fn round_div< + T, +Add, +Sub, +Div, +Rem, +PartialOrd, +Into, +Drop, +Copy + >( + a: T, b: T + ) -> T { + let remained = a % b; + if b - remained <= remained { + return a / b + 1_u8.into(); + } + return a / b; + } +} + +#[cfg(test)] +mod tests { + // Core imports + + use debug::PrintTrait; + + // Local imports + + use super::Elo; + + #[test] + fn test_elo_change_positive_01() { + let (mag, sign) = Elo::rating_change(1200, 1400, 100, 20); + assert(mag == 15, 'Elo: wrong change mag'); + assert(!sign, 'Elo: wrong change sign'); + } + + #[test] + fn test_elo_change_positive_02() { + let (mag, sign) = Elo::rating_change(1300, 1200, 100, 20); + assert(mag == 7, 'Elo: wrong change mag'); + assert(!sign, 'Elo: wrong change sign'); + } + + #[test] + fn test_elo_change_positive_03() { + let (mag, sign) = Elo::rating_change(1900, 2100, 100, 20); + assert(mag == 15, 'Elo: wrong change mag'); + assert(!sign, 'Elo: wrong change sign'); + } + + #[test] + fn test_elo_change_negative_01() { + let (mag, sign) = Elo::rating_change(1200, 1400, 0, 20); + assert(mag == 5, 'Elo: wrong change mag'); + assert(sign, 'Elo: wrong change sign'); + } + + #[test] + fn test_elo_change_negative_02() { + let (mag, sign) = Elo::rating_change(1300, 1200, 0, 20); + assert(mag == 13, 'Elo: wrong change mag'); + assert(sign, 'Elo: wrong change sign'); + } + + #[test] + fn test_elo_change_draw() { + let (mag, sign) = Elo::rating_change(1200, 1400, 50, 20); + assert(mag == 5, 'Elo: wrong change mag'); + assert(!sign, 'Elo: wrong change sign'); + } +} From e614be69c4ee08e3b914625508a977ee868c454b Mon Sep 17 00:00:00 2001 From: bal7hazar Date: Wed, 17 Apr 2024 19:43:33 +0200 Subject: [PATCH 2/7] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Make=20it=20generic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/src/rating/elo.cairo | 52 +++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/crates/src/rating/elo.cairo b/crates/src/rating/elo.cairo index 07863167..4ed381c9 100644 --- a/crates/src/rating/elo.cairo +++ b/crates/src/rating/elo.cairo @@ -3,11 +3,11 @@ // Core imports -use core::integer::u256_sqrt; +use core::integer::{u32_sqrt, u64_sqrt, u128_sqrt, u256_sqrt}; // Constants -const MULTIPLIER: u256 = 10_000; +const MULTIPLIER: u32 = 10_000; const SCALER: u256 = 800; // Errors @@ -28,13 +28,31 @@ impl EloImpl of Elo { /// # Returns /// * `change` - The change in ELO rating of player A. /// * `negative` - The directional change of player A's ELO. Opposite sign for player B. - #[inline(always)] - fn rating_change(rating_a: u256, rating_b: u256, score: u256, k: u256) -> (u256, bool) { + fn rating_change< + T, + +Sub, + +PartialOrd, + +Into, + +Drop, + +Copy, + S, + +Into, + +Drop, + +Copy, + K, + +Into, + +Drop, + +Copy, + C, + +Into, + >( + rating_a: T, rating_b: T, score: S, k: K + ) -> (C, bool) { let negative = rating_a > rating_b; let rating_diff: u256 = if negative { - rating_a - rating_b + (rating_a - rating_b).into() } else { - rating_b - rating_a + (rating_b - rating_a).into() }; // [Check] Checks against overflow/underflow @@ -54,13 +72,13 @@ impl EloImpl of Elo { } else { (SCALER + rating_diff) / 25 }; + // [Info] Order should be less or equal to 77 to fit a u256 let powered: u256 = PrivateTrait::pow(10, order); - let rooted: u256 = u256_sqrt(u256_sqrt(u256_sqrt(u256_sqrt(powered).into()).into()).into()) - .into(); + let rooted: u16 = u32_sqrt(u64_sqrt(u128_sqrt(u256_sqrt(powered)))); // [Compute] Change = k * (score - expectedScore) - let k_expected_score = k * MULTIPLIER / (100 + rooted); - let k_score = k * score; + let k_expected_score = k.into() * MULTIPLIER / (100 + rooted.into()); + let k_score = k.into() * score.into(); let negative = k_score < k_expected_score; let change = if negative { k_expected_score - k_score @@ -69,7 +87,7 @@ impl EloImpl of Elo { }; // [Return] Change rounded and its sign - (PrivateTrait::round_div(change, 100), negative) + (PrivateTrait::round_div(change, 100).into(), negative) } } @@ -114,42 +132,42 @@ mod tests { #[test] fn test_elo_change_positive_01() { - let (mag, sign) = Elo::rating_change(1200, 1400, 100, 20); + let (mag, sign) = Elo::rating_change(1200_u128, 1400_u128, 100_u16, 20_u8); assert(mag == 15, 'Elo: wrong change mag'); assert(!sign, 'Elo: wrong change sign'); } #[test] fn test_elo_change_positive_02() { - let (mag, sign) = Elo::rating_change(1300, 1200, 100, 20); + let (mag, sign) = Elo::rating_change(1300_u128, 1200_u128, 100_u16, 20_u8); assert(mag == 7, 'Elo: wrong change mag'); assert(!sign, 'Elo: wrong change sign'); } #[test] fn test_elo_change_positive_03() { - let (mag, sign) = Elo::rating_change(1900, 2100, 100, 20); + let (mag, sign) = Elo::rating_change(1900_u256, 2100_u256, 100_u16, 20_u8); assert(mag == 15, 'Elo: wrong change mag'); assert(!sign, 'Elo: wrong change sign'); } #[test] fn test_elo_change_negative_01() { - let (mag, sign) = Elo::rating_change(1200, 1400, 0, 20); + let (mag, sign) = Elo::rating_change(1200_u128, 1400_u128, 0_u16, 20_u8); assert(mag == 5, 'Elo: wrong change mag'); assert(sign, 'Elo: wrong change sign'); } #[test] fn test_elo_change_negative_02() { - let (mag, sign) = Elo::rating_change(1300, 1200, 0, 20); + let (mag, sign) = Elo::rating_change(1300_u128, 1200_u128, 0_u16, 20_u8); assert(mag == 13, 'Elo: wrong change mag'); assert(sign, 'Elo: wrong change sign'); } #[test] fn test_elo_change_draw() { - let (mag, sign) = Elo::rating_change(1200, 1400, 50, 20); + let (mag, sign) = Elo::rating_change(1200_u128, 1400_u128, 50_u16, 20_u8); assert(mag == 5, 'Elo: wrong change mag'); assert(!sign, 'Elo: wrong change sign'); } From 8766483c007c495a840fa8fd28508cbad9e83629 Mon Sep 17 00:00:00 2001 From: bal7hazar Date: Thu, 18 Apr 2024 21:12:10 +0200 Subject: [PATCH 3/7] =?UTF-8?q?=E2=9C=A8=20Introduce=20matchmaker=20exampl?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 11 + Scarb.lock | 8 + Scarb.toml | 1 + crates/src/rating/elo.cairo | 16 +- examples/matchmaker/Scarb.toml | 9 + examples/matchmaker/src/constants.cairo | 271 ++++++++++ examples/matchmaker/src/helpers/bitmap.cairo | 490 ++++++++++++++++++ examples/matchmaker/src/lib.cairo | 18 + examples/matchmaker/src/models/league.cairo | 178 +++++++ examples/matchmaker/src/models/player.cairo | 166 ++++++ examples/matchmaker/src/models/registry.cairo | 222 ++++++++ examples/matchmaker/src/models/slot.cairo | 65 +++ examples/matchmaker/src/store.cairo | 107 ++++ examples/matchmaker/src/systems/maker.cairo | 171 ++++++ examples/matchmaker/src/tests/setup.cairo | 1 + .../matchmaker/src/tests/test_suscribe.cairo | 1 + 16 files changed, 1727 insertions(+), 8 deletions(-) create mode 100644 examples/matchmaker/Scarb.toml create mode 100644 examples/matchmaker/src/constants.cairo create mode 100644 examples/matchmaker/src/helpers/bitmap.cairo create mode 100644 examples/matchmaker/src/lib.cairo create mode 100644 examples/matchmaker/src/models/league.cairo create mode 100644 examples/matchmaker/src/models/player.cairo create mode 100644 examples/matchmaker/src/models/registry.cairo create mode 100644 examples/matchmaker/src/models/slot.cairo create mode 100644 examples/matchmaker/src/store.cairo create mode 100644 examples/matchmaker/src/systems/maker.cairo create mode 100644 examples/matchmaker/src/tests/setup.cairo create mode 100644 examples/matchmaker/src/tests/test_suscribe.cairo diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 711f5995..5a386f29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,6 +63,17 @@ jobs: run: sozo test -f market shell: bash + matchmaker: + needs: [check, build] + runs-on: ubuntu-latest + name: Test example matchmaker + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + - name: Test + run: sozo test -f matchmaker + shell: bash + projectile: needs: [check, build] runs-on: ubuntu-latest diff --git a/Scarb.lock b/Scarb.lock index ca8796db..4750cc6e 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -43,6 +43,14 @@ dependencies = [ "dojo", ] +[[package]] +name = "matchmaker" +version = "0.0.0" +dependencies = [ + "dojo", + "origami", +] + [[package]] name = "origami" version = "0.6.0" diff --git a/Scarb.toml b/Scarb.toml index a787a887..a1a3c04f 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -4,6 +4,7 @@ members = [ "examples/chess", "examples/hex_map", "examples/market", + "examples/matchmaker", "examples/projectile", "token", ] diff --git a/crates/src/rating/elo.cairo b/crates/src/rating/elo.cairo index 4ed381c9..85bf2957 100644 --- a/crates/src/rating/elo.cairo +++ b/crates/src/rating/elo.cairo @@ -18,7 +18,7 @@ mod errors { /// Elo implementation.. #[generate_trait] -impl EloImpl of Elo { +impl EloImpl of EloTrait { /// Calculates the change in ELO rating, after a given outcome. /// # Arguments /// * `rating_a` - The ELO rating of the player A. @@ -128,46 +128,46 @@ mod tests { // Local imports - use super::Elo; + use super::EloTrait; #[test] fn test_elo_change_positive_01() { - let (mag, sign) = Elo::rating_change(1200_u128, 1400_u128, 100_u16, 20_u8); + let (mag, sign) = EloTrait::rating_change(1200_u128, 1400_u128, 100_u16, 20_u8); assert(mag == 15, 'Elo: wrong change mag'); assert(!sign, 'Elo: wrong change sign'); } #[test] fn test_elo_change_positive_02() { - let (mag, sign) = Elo::rating_change(1300_u128, 1200_u128, 100_u16, 20_u8); + let (mag, sign) = EloTrait::rating_change(1300_u128, 1200_u128, 100_u16, 20_u8); assert(mag == 7, 'Elo: wrong change mag'); assert(!sign, 'Elo: wrong change sign'); } #[test] fn test_elo_change_positive_03() { - let (mag, sign) = Elo::rating_change(1900_u256, 2100_u256, 100_u16, 20_u8); + let (mag, sign) = EloTrait::rating_change(1900_u256, 2100_u256, 100_u16, 20_u8); assert(mag == 15, 'Elo: wrong change mag'); assert(!sign, 'Elo: wrong change sign'); } #[test] fn test_elo_change_negative_01() { - let (mag, sign) = Elo::rating_change(1200_u128, 1400_u128, 0_u16, 20_u8); + let (mag, sign) = EloTrait::rating_change(1200_u128, 1400_u128, 0_u16, 20_u8); assert(mag == 5, 'Elo: wrong change mag'); assert(sign, 'Elo: wrong change sign'); } #[test] fn test_elo_change_negative_02() { - let (mag, sign) = Elo::rating_change(1300_u128, 1200_u128, 0_u16, 20_u8); + let (mag, sign) = EloTrait::rating_change(1300_u128, 1200_u128, 0_u16, 20_u8); assert(mag == 13, 'Elo: wrong change mag'); assert(sign, 'Elo: wrong change sign'); } #[test] fn test_elo_change_draw() { - let (mag, sign) = Elo::rating_change(1200_u128, 1400_u128, 50_u16, 20_u8); + let (mag, sign) = EloTrait::rating_change(1200_u128, 1400_u128, 50_u16, 20_u8); assert(mag == 5, 'Elo: wrong change mag'); assert(!sign, 'Elo: wrong change sign'); } diff --git a/examples/matchmaker/Scarb.toml b/examples/matchmaker/Scarb.toml new file mode 100644 index 00000000..48e20b0e --- /dev/null +++ b/examples/matchmaker/Scarb.toml @@ -0,0 +1,9 @@ +[package] +name = "matchmaker" +version = "0.0.0" +description = "Example of elo rating crate usage." +homepage = "https://github.com/dojoengine/origami/tree/examples/matchmaker" + +[dependencies] +dojo.workspace = true +origami.workspace = true \ No newline at end of file diff --git a/examples/matchmaker/src/constants.cairo b/examples/matchmaker/src/constants.cairo new file mode 100644 index 00000000..5e1acbcd --- /dev/null +++ b/examples/matchmaker/src/constants.cairo @@ -0,0 +1,271 @@ +// ELO + +const DEFAULT_K_FACTOR: u8 = 20; +const DEFAULT_RATING: u32 = 1000; + +// Leagues + +const LEAGUE_SIZE: u8 = 20; + +// Constants + +fn ZERO() -> starknet::ContractAddress { + starknet::contract_address_const::<0>() +} + +// Bitmap + +const TWO_POW_0: u256 = 0x1; +const TWO_POW_1: u256 = 0x2; +const TWO_POW_2: u256 = 0x4; +const TWO_POW_3: u256 = 0x8; +const TWO_POW_4: u256 = 0x10; +const TWO_POW_5: u256 = 0x20; +const TWO_POW_6: u256 = 0x40; +const TWO_POW_7: u256 = 0x80; +const TWO_POW_8: u256 = 0x100; +const TWO_POW_9: u256 = 0x200; +const TWO_POW_10: u256 = 0x400; +const TWO_POW_11: u256 = 0x800; +const TWO_POW_12: u256 = 0x1000; +const TWO_POW_13: u256 = 0x2000; +const TWO_POW_14: u256 = 0x4000; +const TWO_POW_15: u256 = 0x8000; +const TWO_POW_16: u256 = 0x10000; +const TWO_POW_17: u256 = 0x20000; +const TWO_POW_18: u256 = 0x40000; +const TWO_POW_19: u256 = 0x80000; +const TWO_POW_20: u256 = 0x100000; +const TWO_POW_21: u256 = 0x200000; +const TWO_POW_22: u256 = 0x400000; +const TWO_POW_23: u256 = 0x800000; +const TWO_POW_24: u256 = 0x1000000; +const TWO_POW_25: u256 = 0x2000000; +const TWO_POW_26: u256 = 0x4000000; +const TWO_POW_27: u256 = 0x8000000; +const TWO_POW_28: u256 = 0x10000000; +const TWO_POW_29: u256 = 0x20000000; +const TWO_POW_30: u256 = 0x40000000; +const TWO_POW_31: u256 = 0x80000000; +const TWO_POW_32: u256 = 0x100000000; +const TWO_POW_33: u256 = 0x200000000; +const TWO_POW_34: u256 = 0x400000000; +const TWO_POW_35: u256 = 0x800000000; +const TWO_POW_36: u256 = 0x1000000000; +const TWO_POW_37: u256 = 0x2000000000; +const TWO_POW_38: u256 = 0x4000000000; +const TWO_POW_39: u256 = 0x8000000000; +const TWO_POW_40: u256 = 0x10000000000; +const TWO_POW_41: u256 = 0x20000000000; +const TWO_POW_42: u256 = 0x40000000000; +const TWO_POW_43: u256 = 0x80000000000; +const TWO_POW_44: u256 = 0x100000000000; +const TWO_POW_45: u256 = 0x200000000000; +const TWO_POW_46: u256 = 0x400000000000; +const TWO_POW_47: u256 = 0x800000000000; +const TWO_POW_48: u256 = 0x1000000000000; +const TWO_POW_49: u256 = 0x2000000000000; +const TWO_POW_50: u256 = 0x4000000000000; +const TWO_POW_51: u256 = 0x8000000000000; +const TWO_POW_52: u256 = 0x10000000000000; +const TWO_POW_53: u256 = 0x20000000000000; +const TWO_POW_54: u256 = 0x40000000000000; +const TWO_POW_55: u256 = 0x80000000000000; +const TWO_POW_56: u256 = 0x100000000000000; +const TWO_POW_57: u256 = 0x200000000000000; +const TWO_POW_58: u256 = 0x400000000000000; +const TWO_POW_59: u256 = 0x800000000000000; +const TWO_POW_60: u256 = 0x1000000000000000; +const TWO_POW_61: u256 = 0x2000000000000000; +const TWO_POW_62: u256 = 0x4000000000000000; +const TWO_POW_63: u256 = 0x8000000000000000; +const TWO_POW_64: u256 = 0x10000000000000000; +const TWO_POW_65: u256 = 0x20000000000000000; +const TWO_POW_66: u256 = 0x40000000000000000; +const TWO_POW_67: u256 = 0x80000000000000000; +const TWO_POW_68: u256 = 0x100000000000000000; +const TWO_POW_69: u256 = 0x200000000000000000; +const TWO_POW_70: u256 = 0x400000000000000000; +const TWO_POW_71: u256 = 0x800000000000000000; +const TWO_POW_72: u256 = 0x1000000000000000000; +const TWO_POW_73: u256 = 0x2000000000000000000; +const TWO_POW_74: u256 = 0x4000000000000000000; +const TWO_POW_75: u256 = 0x8000000000000000000; +const TWO_POW_76: u256 = 0x10000000000000000000; +const TWO_POW_77: u256 = 0x20000000000000000000; +const TWO_POW_78: u256 = 0x40000000000000000000; +const TWO_POW_79: u256 = 0x80000000000000000000; +const TWO_POW_80: u256 = 0x100000000000000000000; +const TWO_POW_81: u256 = 0x200000000000000000000; +const TWO_POW_82: u256 = 0x400000000000000000000; +const TWO_POW_83: u256 = 0x800000000000000000000; +const TWO_POW_84: u256 = 0x1000000000000000000000; +const TWO_POW_85: u256 = 0x2000000000000000000000; +const TWO_POW_86: u256 = 0x4000000000000000000000; +const TWO_POW_87: u256 = 0x8000000000000000000000; +const TWO_POW_88: u256 = 0x10000000000000000000000; +const TWO_POW_89: u256 = 0x20000000000000000000000; +const TWO_POW_90: u256 = 0x40000000000000000000000; +const TWO_POW_91: u256 = 0x80000000000000000000000; +const TWO_POW_92: u256 = 0x100000000000000000000000; +const TWO_POW_93: u256 = 0x200000000000000000000000; +const TWO_POW_94: u256 = 0x400000000000000000000000; +const TWO_POW_95: u256 = 0x800000000000000000000000; +const TWO_POW_96: u256 = 0x1000000000000000000000000; +const TWO_POW_97: u256 = 0x2000000000000000000000000; +const TWO_POW_98: u256 = 0x4000000000000000000000000; +const TWO_POW_99: u256 = 0x8000000000000000000000000; +const TWO_POW_100: u256 = 0x10000000000000000000000000; +const TWO_POW_101: u256 = 0x20000000000000000000000000; +const TWO_POW_102: u256 = 0x40000000000000000000000000; +const TWO_POW_103: u256 = 0x80000000000000000000000000; +const TWO_POW_104: u256 = 0x100000000000000000000000000; +const TWO_POW_105: u256 = 0x200000000000000000000000000; +const TWO_POW_106: u256 = 0x400000000000000000000000000; +const TWO_POW_107: u256 = 0x800000000000000000000000000; +const TWO_POW_108: u256 = 0x1000000000000000000000000000; +const TWO_POW_109: u256 = 0x2000000000000000000000000000; +const TWO_POW_110: u256 = 0x4000000000000000000000000000; +const TWO_POW_111: u256 = 0x8000000000000000000000000000; +const TWO_POW_112: u256 = 0x10000000000000000000000000000; +const TWO_POW_113: u256 = 0x20000000000000000000000000000; +const TWO_POW_114: u256 = 0x40000000000000000000000000000; +const TWO_POW_115: u256 = 0x80000000000000000000000000000; +const TWO_POW_116: u256 = 0x100000000000000000000000000000; +const TWO_POW_117: u256 = 0x200000000000000000000000000000; +const TWO_POW_118: u256 = 0x400000000000000000000000000000; +const TWO_POW_119: u256 = 0x800000000000000000000000000000; +const TWO_POW_120: u256 = 0x1000000000000000000000000000000; +const TWO_POW_121: u256 = 0x2000000000000000000000000000000; +const TWO_POW_122: u256 = 0x4000000000000000000000000000000; +const TWO_POW_123: u256 = 0x8000000000000000000000000000000; +const TWO_POW_124: u256 = 0x10000000000000000000000000000000; +const TWO_POW_125: u256 = 0x20000000000000000000000000000000; +const TWO_POW_126: u256 = 0x40000000000000000000000000000000; +const TWO_POW_127: u256 = 0x80000000000000000000000000000000; +const TWO_POW_128: u256 = 0x100000000000000000000000000000000; +const TWO_POW_129: u256 = 0x200000000000000000000000000000000; +const TWO_POW_130: u256 = 0x400000000000000000000000000000000; +const TWO_POW_131: u256 = 0x800000000000000000000000000000000; +const TWO_POW_132: u256 = 0x1000000000000000000000000000000000; +const TWO_POW_133: u256 = 0x2000000000000000000000000000000000; +const TWO_POW_134: u256 = 0x4000000000000000000000000000000000; +const TWO_POW_135: u256 = 0x8000000000000000000000000000000000; +const TWO_POW_136: u256 = 0x10000000000000000000000000000000000; +const TWO_POW_137: u256 = 0x20000000000000000000000000000000000; +const TWO_POW_138: u256 = 0x40000000000000000000000000000000000; +const TWO_POW_139: u256 = 0x80000000000000000000000000000000000; +const TWO_POW_140: u256 = 0x100000000000000000000000000000000000; +const TWO_POW_141: u256 = 0x200000000000000000000000000000000000; +const TWO_POW_142: u256 = 0x400000000000000000000000000000000000; +const TWO_POW_143: u256 = 0x800000000000000000000000000000000000; +const TWO_POW_144: u256 = 0x1000000000000000000000000000000000000; +const TWO_POW_145: u256 = 0x2000000000000000000000000000000000000; +const TWO_POW_146: u256 = 0x4000000000000000000000000000000000000; +const TWO_POW_147: u256 = 0x8000000000000000000000000000000000000; +const TWO_POW_148: u256 = 0x10000000000000000000000000000000000000; +const TWO_POW_149: u256 = 0x20000000000000000000000000000000000000; +const TWO_POW_150: u256 = 0x40000000000000000000000000000000000000; +const TWO_POW_151: u256 = 0x80000000000000000000000000000000000000; +const TWO_POW_152: u256 = 0x100000000000000000000000000000000000000; +const TWO_POW_153: u256 = 0x200000000000000000000000000000000000000; +const TWO_POW_154: u256 = 0x400000000000000000000000000000000000000; +const TWO_POW_155: u256 = 0x800000000000000000000000000000000000000; +const TWO_POW_156: u256 = 0x1000000000000000000000000000000000000000; +const TWO_POW_157: u256 = 0x2000000000000000000000000000000000000000; +const TWO_POW_158: u256 = 0x4000000000000000000000000000000000000000; +const TWO_POW_159: u256 = 0x8000000000000000000000000000000000000000; +const TWO_POW_160: u256 = 0x10000000000000000000000000000000000000000; +const TWO_POW_161: u256 = 0x20000000000000000000000000000000000000000; +const TWO_POW_162: u256 = 0x40000000000000000000000000000000000000000; +const TWO_POW_163: u256 = 0x80000000000000000000000000000000000000000; +const TWO_POW_164: u256 = 0x100000000000000000000000000000000000000000; +const TWO_POW_165: u256 = 0x200000000000000000000000000000000000000000; +const TWO_POW_166: u256 = 0x400000000000000000000000000000000000000000; +const TWO_POW_167: u256 = 0x800000000000000000000000000000000000000000; +const TWO_POW_168: u256 = 0x1000000000000000000000000000000000000000000; +const TWO_POW_169: u256 = 0x2000000000000000000000000000000000000000000; +const TWO_POW_170: u256 = 0x4000000000000000000000000000000000000000000; +const TWO_POW_171: u256 = 0x8000000000000000000000000000000000000000000; +const TWO_POW_172: u256 = 0x10000000000000000000000000000000000000000000; +const TWO_POW_173: u256 = 0x20000000000000000000000000000000000000000000; +const TWO_POW_174: u256 = 0x40000000000000000000000000000000000000000000; +const TWO_POW_175: u256 = 0x80000000000000000000000000000000000000000000; +const TWO_POW_176: u256 = 0x100000000000000000000000000000000000000000000; +const TWO_POW_177: u256 = 0x200000000000000000000000000000000000000000000; +const TWO_POW_178: u256 = 0x400000000000000000000000000000000000000000000; +const TWO_POW_179: u256 = 0x800000000000000000000000000000000000000000000; +const TWO_POW_180: u256 = 0x1000000000000000000000000000000000000000000000; +const TWO_POW_181: u256 = 0x2000000000000000000000000000000000000000000000; +const TWO_POW_182: u256 = 0x4000000000000000000000000000000000000000000000; +const TWO_POW_183: u256 = 0x8000000000000000000000000000000000000000000000; +const TWO_POW_184: u256 = 0x10000000000000000000000000000000000000000000000; +const TWO_POW_185: u256 = 0x20000000000000000000000000000000000000000000000; +const TWO_POW_186: u256 = 0x40000000000000000000000000000000000000000000000; +const TWO_POW_187: u256 = 0x80000000000000000000000000000000000000000000000; +const TWO_POW_188: u256 = 0x100000000000000000000000000000000000000000000000; +const TWO_POW_189: u256 = 0x200000000000000000000000000000000000000000000000; +const TWO_POW_190: u256 = 0x400000000000000000000000000000000000000000000000; +const TWO_POW_191: u256 = 0x800000000000000000000000000000000000000000000000; +const TWO_POW_192: u256 = 0x1000000000000000000000000000000000000000000000000; +const TWO_POW_193: u256 = 0x2000000000000000000000000000000000000000000000000; +const TWO_POW_194: u256 = 0x4000000000000000000000000000000000000000000000000; +const TWO_POW_195: u256 = 0x8000000000000000000000000000000000000000000000000; +const TWO_POW_196: u256 = 0x10000000000000000000000000000000000000000000000000; +const TWO_POW_197: u256 = 0x20000000000000000000000000000000000000000000000000; +const TWO_POW_198: u256 = 0x40000000000000000000000000000000000000000000000000; +const TWO_POW_199: u256 = 0x80000000000000000000000000000000000000000000000000; +const TWO_POW_200: u256 = 0x100000000000000000000000000000000000000000000000000; +const TWO_POW_201: u256 = 0x200000000000000000000000000000000000000000000000000; +const TWO_POW_202: u256 = 0x400000000000000000000000000000000000000000000000000; +const TWO_POW_203: u256 = 0x800000000000000000000000000000000000000000000000000; +const TWO_POW_204: u256 = 0x1000000000000000000000000000000000000000000000000000; +const TWO_POW_205: u256 = 0x2000000000000000000000000000000000000000000000000000; +const TWO_POW_206: u256 = 0x4000000000000000000000000000000000000000000000000000; +const TWO_POW_207: u256 = 0x8000000000000000000000000000000000000000000000000000; +const TWO_POW_208: u256 = 0x10000000000000000000000000000000000000000000000000000; +const TWO_POW_209: u256 = 0x20000000000000000000000000000000000000000000000000000; +const TWO_POW_210: u256 = 0x40000000000000000000000000000000000000000000000000000; +const TWO_POW_211: u256 = 0x80000000000000000000000000000000000000000000000000000; +const TWO_POW_212: u256 = 0x100000000000000000000000000000000000000000000000000000; +const TWO_POW_213: u256 = 0x200000000000000000000000000000000000000000000000000000; +const TWO_POW_214: u256 = 0x400000000000000000000000000000000000000000000000000000; +const TWO_POW_215: u256 = 0x800000000000000000000000000000000000000000000000000000; +const TWO_POW_216: u256 = 0x1000000000000000000000000000000000000000000000000000000; +const TWO_POW_217: u256 = 0x2000000000000000000000000000000000000000000000000000000; +const TWO_POW_218: u256 = 0x4000000000000000000000000000000000000000000000000000000; +const TWO_POW_219: u256 = 0x8000000000000000000000000000000000000000000000000000000; +const TWO_POW_220: u256 = 0x10000000000000000000000000000000000000000000000000000000; +const TWO_POW_221: u256 = 0x20000000000000000000000000000000000000000000000000000000; +const TWO_POW_222: u256 = 0x40000000000000000000000000000000000000000000000000000000; +const TWO_POW_223: u256 = 0x80000000000000000000000000000000000000000000000000000000; +const TWO_POW_224: u256 = 0x100000000000000000000000000000000000000000000000000000000; +const TWO_POW_225: u256 = 0x200000000000000000000000000000000000000000000000000000000; +const TWO_POW_226: u256 = 0x400000000000000000000000000000000000000000000000000000000; +const TWO_POW_227: u256 = 0x800000000000000000000000000000000000000000000000000000000; +const TWO_POW_228: u256 = 0x1000000000000000000000000000000000000000000000000000000000; +const TWO_POW_229: u256 = 0x2000000000000000000000000000000000000000000000000000000000; +const TWO_POW_230: u256 = 0x4000000000000000000000000000000000000000000000000000000000; +const TWO_POW_231: u256 = 0x8000000000000000000000000000000000000000000000000000000000; +const TWO_POW_232: u256 = 0x10000000000000000000000000000000000000000000000000000000000; +const TWO_POW_233: u256 = 0x20000000000000000000000000000000000000000000000000000000000; +const TWO_POW_234: u256 = 0x40000000000000000000000000000000000000000000000000000000000; +const TWO_POW_235: u256 = 0x80000000000000000000000000000000000000000000000000000000000; +const TWO_POW_236: u256 = 0x100000000000000000000000000000000000000000000000000000000000; +const TWO_POW_237: u256 = 0x200000000000000000000000000000000000000000000000000000000000; +const TWO_POW_238: u256 = 0x400000000000000000000000000000000000000000000000000000000000; +const TWO_POW_239: u256 = 0x800000000000000000000000000000000000000000000000000000000000; +const TWO_POW_240: u256 = 0x1000000000000000000000000000000000000000000000000000000000000; +const TWO_POW_241: u256 = 0x2000000000000000000000000000000000000000000000000000000000000; +const TWO_POW_242: u256 = 0x4000000000000000000000000000000000000000000000000000000000000; +const TWO_POW_243: u256 = 0x8000000000000000000000000000000000000000000000000000000000000; +const TWO_POW_244: u256 = 0x10000000000000000000000000000000000000000000000000000000000000; +const TWO_POW_245: u256 = 0x20000000000000000000000000000000000000000000000000000000000000; +const TWO_POW_246: u256 = 0x40000000000000000000000000000000000000000000000000000000000000; +const TWO_POW_247: u256 = 0x80000000000000000000000000000000000000000000000000000000000000; +const TWO_POW_248: u256 = 0x100000000000000000000000000000000000000000000000000000000000000; +const TWO_POW_249: u256 = 0x200000000000000000000000000000000000000000000000000000000000000; +const TWO_POW_250: u256 = 0x400000000000000000000000000000000000000000000000000000000000000; +const TWO_POW_251: u256 = 0x800000000000000000000000000000000000000000000000000000000000000; +const TWO_POW_252: u256 = 0x1000000000000000000000000000000000000000000000000000000000000000; + diff --git a/examples/matchmaker/src/helpers/bitmap.cairo b/examples/matchmaker/src/helpers/bitmap.cairo new file mode 100644 index 00000000..21d7abf9 --- /dev/null +++ b/examples/matchmaker/src/helpers/bitmap.cairo @@ -0,0 +1,490 @@ +// Core imports + +use core::integer::BoundedInt; +use core::debug::PrintTrait; + +// Internal imports + +use matchmaker::constants; + +// Errors + +mod errors { + const INVALID_INDEX: felt252 = 'Bitmap: invalid index'; +} + +#[generate_trait] +impl Bitmap of BitmapTrait { + #[inline(always)] + fn get_bit_at(bitmap: u256, index: felt252) -> bool { + let mask = Bitmap::two_pow(index); + bitmap & mask == mask + } + + #[inline(always)] + fn set_bit_at(bitmap: u256, index: felt252, value: bool) -> u256 { + let mask = Bitmap::two_pow(index); + if value { + bitmap | mask + } else { + bitmap & (BoundedInt::max() - mask) + } + } + + /// The index of the nearest significant bit to the index of the number, + /// where the least significant bit is at index 0 and the most significant bit is at index 255 + /// # Arguments + /// * `x` - The value for which to compute the most significant bit, must be greater than 0. + /// * `s` - The index for which to start the search. + /// # Returns + /// * The index of the nearest significant bit + #[inline(always)] + fn nearest_significant_bit(x: u256, s: u8) -> u8 { + let lower_mask = Bitmap::set_bit_at(0, (s + 1).into(), true) - 1; + let lower = Bitmap::most_significant_bit(x & lower_mask); + let upper_mask = ~(lower_mask / 2); + let upper = Bitmap::least_significant_bit(x & upper_mask); + match (lower, upper) { + (Option::Some(l), Option::Some(u)) => { if s - l < u - s { + l + } else { + u + } }, + (Option::Some(l), Option::None) => l, + (Option::None, Option::Some(u)) => u, + (Option::None, Option::None) => 0, + } + } + + /// The index of the most significant bit of the number, + /// where the least significant bit is at index 0 and the most significant bit is at index 255 + /// Source: https://github.com/lambdaclass/yet-another-swap/blob/main/crates/yas_core/src/libraries/bit_math.cairo + /// # Arguments + /// * `x` - The value for which to compute the most significant bit, must be greater than 0. + /// # Returns + /// * The index of the most significant bit + #[inline(always)] + fn most_significant_bit(mut x: u256) -> Option { + if x == 0 { + return Option::None; + } + let mut r: u8 = 0; + + if x >= 0x100000000000000000000000000000000 { + x /= 0x100000000000000000000000000000000; + r += 128; + } + if x >= 0x10000000000000000 { + x /= 0x10000000000000000; + r += 64; + } + if x >= 0x100000000 { + x /= 0x100000000; + r += 32; + } + if x >= 0x10000 { + x /= 0x10000; + r += 16; + } + if x >= 0x100 { + x /= 0x100; + r += 8; + } + if x >= 0x10 { + x /= 0x10; + r += 4; + } + if x >= 0x4 { + x /= 0x4; + r += 2; + } + if x >= 0x2 { + r += 1; + } + Option::Some(r) + } + + /// The index of the least significant bit of the number, + /// where the least significant bit is at index 0 and the most significant bit is at index 255 + /// Source: https://github.com/lambdaclass/yet-another-swap/blob/main/crates/yas_core/src/libraries/bit_math.cairo + /// # Arguments + /// * `x` - The value for which to compute the least significant bit, must be greater than 0. + /// # Returns + /// * The index of the least significant bit + #[inline(always)] + fn least_significant_bit(mut x: u256) -> Option { + if x == 0 { + return Option::None; + } + let mut r: u8 = 255; + + if (x & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) > 0 { + r -= 128; + } else { + x /= 0x100000000000000000000000000000000; + } + if (x & 0xFFFFFFFFFFFFFFFF) > 0 { + r -= 64; + } else { + x /= 0x10000000000000000; + } + if (x & 0xFFFFFFFF) > 0 { + r -= 32; + } else { + x /= 0x100000000; + } + if (x & 0xFFFF) > 0 { + r -= 16; + } else { + x /= 0x10000; + } + if (x & 0xFF) > 0 { + r -= 8; + } else { + x /= 0x100; + } + if (x & 0xF) > 0 { + r -= 4; + } else { + x /= 0x10; + } + if (x & 0x3) > 0 { + r -= 2; + } else { + x /= 0x4; + } + if (x & 0x1) > 0 { + r -= 1; + } + Option::Some(r) + } + + #[inline(always)] + fn two_pow(exponent: felt252) -> u256 { + match exponent { + 0 => constants::TWO_POW_0, + 1 => constants::TWO_POW_1, + 2 => constants::TWO_POW_2, + 3 => constants::TWO_POW_3, + 4 => constants::TWO_POW_4, + 5 => constants::TWO_POW_5, + 6 => constants::TWO_POW_6, + 7 => constants::TWO_POW_7, + 8 => constants::TWO_POW_8, + 9 => constants::TWO_POW_9, + 10 => constants::TWO_POW_10, + 11 => constants::TWO_POW_11, + 12 => constants::TWO_POW_12, + 13 => constants::TWO_POW_13, + 14 => constants::TWO_POW_14, + 15 => constants::TWO_POW_15, + 16 => constants::TWO_POW_16, + 17 => constants::TWO_POW_17, + 18 => constants::TWO_POW_18, + 19 => constants::TWO_POW_19, + 20 => constants::TWO_POW_20, + 21 => constants::TWO_POW_21, + 22 => constants::TWO_POW_22, + 23 => constants::TWO_POW_23, + 24 => constants::TWO_POW_24, + 25 => constants::TWO_POW_25, + 26 => constants::TWO_POW_26, + 27 => constants::TWO_POW_27, + 28 => constants::TWO_POW_28, + 29 => constants::TWO_POW_29, + 30 => constants::TWO_POW_30, + 31 => constants::TWO_POW_31, + 32 => constants::TWO_POW_32, + 33 => constants::TWO_POW_33, + 34 => constants::TWO_POW_34, + 35 => constants::TWO_POW_35, + 36 => constants::TWO_POW_36, + 37 => constants::TWO_POW_37, + 38 => constants::TWO_POW_38, + 39 => constants::TWO_POW_39, + 40 => constants::TWO_POW_40, + 41 => constants::TWO_POW_41, + 42 => constants::TWO_POW_42, + 43 => constants::TWO_POW_43, + 44 => constants::TWO_POW_44, + 45 => constants::TWO_POW_45, + 46 => constants::TWO_POW_46, + 47 => constants::TWO_POW_47, + 48 => constants::TWO_POW_48, + 49 => constants::TWO_POW_49, + 50 => constants::TWO_POW_50, + 51 => constants::TWO_POW_51, + 52 => constants::TWO_POW_52, + 53 => constants::TWO_POW_53, + 54 => constants::TWO_POW_54, + 55 => constants::TWO_POW_55, + 56 => constants::TWO_POW_56, + 57 => constants::TWO_POW_57, + 58 => constants::TWO_POW_58, + 59 => constants::TWO_POW_59, + 60 => constants::TWO_POW_60, + 61 => constants::TWO_POW_61, + 62 => constants::TWO_POW_62, + 63 => constants::TWO_POW_63, + 64 => constants::TWO_POW_64, + 65 => constants::TWO_POW_65, + 66 => constants::TWO_POW_66, + 67 => constants::TWO_POW_67, + 68 => constants::TWO_POW_68, + 69 => constants::TWO_POW_69, + 70 => constants::TWO_POW_70, + 71 => constants::TWO_POW_71, + 72 => constants::TWO_POW_72, + 73 => constants::TWO_POW_73, + 74 => constants::TWO_POW_74, + 75 => constants::TWO_POW_75, + 76 => constants::TWO_POW_76, + 77 => constants::TWO_POW_77, + 78 => constants::TWO_POW_78, + 79 => constants::TWO_POW_79, + 80 => constants::TWO_POW_80, + 81 => constants::TWO_POW_81, + 82 => constants::TWO_POW_82, + 83 => constants::TWO_POW_83, + 84 => constants::TWO_POW_84, + 85 => constants::TWO_POW_85, + 86 => constants::TWO_POW_86, + 87 => constants::TWO_POW_87, + 88 => constants::TWO_POW_88, + 89 => constants::TWO_POW_89, + 90 => constants::TWO_POW_90, + 91 => constants::TWO_POW_91, + 92 => constants::TWO_POW_92, + 93 => constants::TWO_POW_93, + 94 => constants::TWO_POW_94, + 95 => constants::TWO_POW_95, + 96 => constants::TWO_POW_96, + 97 => constants::TWO_POW_97, + 98 => constants::TWO_POW_98, + 99 => constants::TWO_POW_99, + 100 => constants::TWO_POW_100, + 101 => constants::TWO_POW_101, + 102 => constants::TWO_POW_102, + 103 => constants::TWO_POW_103, + 104 => constants::TWO_POW_104, + 105 => constants::TWO_POW_105, + 106 => constants::TWO_POW_106, + 107 => constants::TWO_POW_107, + 108 => constants::TWO_POW_108, + 109 => constants::TWO_POW_109, + 110 => constants::TWO_POW_110, + 111 => constants::TWO_POW_111, + 112 => constants::TWO_POW_112, + 113 => constants::TWO_POW_113, + 114 => constants::TWO_POW_114, + 115 => constants::TWO_POW_115, + 116 => constants::TWO_POW_116, + 117 => constants::TWO_POW_117, + 118 => constants::TWO_POW_118, + 119 => constants::TWO_POW_119, + 120 => constants::TWO_POW_120, + 121 => constants::TWO_POW_121, + 122 => constants::TWO_POW_122, + 123 => constants::TWO_POW_123, + 124 => constants::TWO_POW_124, + 125 => constants::TWO_POW_125, + 126 => constants::TWO_POW_126, + 127 => constants::TWO_POW_127, + 128 => constants::TWO_POW_128, + 129 => constants::TWO_POW_129, + 130 => constants::TWO_POW_130, + 131 => constants::TWO_POW_131, + 132 => constants::TWO_POW_132, + 133 => constants::TWO_POW_133, + 134 => constants::TWO_POW_134, + 135 => constants::TWO_POW_135, + 136 => constants::TWO_POW_136, + 137 => constants::TWO_POW_137, + 138 => constants::TWO_POW_138, + 139 => constants::TWO_POW_139, + 140 => constants::TWO_POW_140, + 141 => constants::TWO_POW_141, + 142 => constants::TWO_POW_142, + 143 => constants::TWO_POW_143, + 144 => constants::TWO_POW_144, + 145 => constants::TWO_POW_145, + 146 => constants::TWO_POW_146, + 147 => constants::TWO_POW_147, + 148 => constants::TWO_POW_148, + 149 => constants::TWO_POW_149, + 150 => constants::TWO_POW_150, + 151 => constants::TWO_POW_151, + 152 => constants::TWO_POW_152, + 153 => constants::TWO_POW_153, + 154 => constants::TWO_POW_154, + 155 => constants::TWO_POW_155, + 156 => constants::TWO_POW_156, + 157 => constants::TWO_POW_157, + 158 => constants::TWO_POW_158, + 159 => constants::TWO_POW_159, + 160 => constants::TWO_POW_160, + 161 => constants::TWO_POW_161, + 162 => constants::TWO_POW_162, + 163 => constants::TWO_POW_163, + 164 => constants::TWO_POW_164, + 165 => constants::TWO_POW_165, + 166 => constants::TWO_POW_166, + 167 => constants::TWO_POW_167, + 168 => constants::TWO_POW_168, + 169 => constants::TWO_POW_169, + 170 => constants::TWO_POW_170, + 171 => constants::TWO_POW_171, + 172 => constants::TWO_POW_172, + 173 => constants::TWO_POW_173, + 174 => constants::TWO_POW_174, + 175 => constants::TWO_POW_175, + 176 => constants::TWO_POW_176, + 177 => constants::TWO_POW_177, + 178 => constants::TWO_POW_178, + 179 => constants::TWO_POW_179, + 180 => constants::TWO_POW_180, + 181 => constants::TWO_POW_181, + 182 => constants::TWO_POW_182, + 183 => constants::TWO_POW_183, + 184 => constants::TWO_POW_184, + 185 => constants::TWO_POW_185, + 186 => constants::TWO_POW_186, + 187 => constants::TWO_POW_187, + 188 => constants::TWO_POW_188, + 189 => constants::TWO_POW_189, + 190 => constants::TWO_POW_190, + 191 => constants::TWO_POW_191, + 192 => constants::TWO_POW_192, + 193 => constants::TWO_POW_193, + 194 => constants::TWO_POW_194, + 195 => constants::TWO_POW_195, + 196 => constants::TWO_POW_196, + 197 => constants::TWO_POW_197, + 198 => constants::TWO_POW_198, + 199 => constants::TWO_POW_199, + 200 => constants::TWO_POW_200, + 201 => constants::TWO_POW_201, + 202 => constants::TWO_POW_202, + 203 => constants::TWO_POW_203, + 204 => constants::TWO_POW_204, + 205 => constants::TWO_POW_205, + 206 => constants::TWO_POW_206, + 207 => constants::TWO_POW_207, + 208 => constants::TWO_POW_208, + 209 => constants::TWO_POW_209, + 210 => constants::TWO_POW_210, + 211 => constants::TWO_POW_211, + 212 => constants::TWO_POW_212, + 213 => constants::TWO_POW_213, + 214 => constants::TWO_POW_214, + 215 => constants::TWO_POW_215, + 216 => constants::TWO_POW_216, + 217 => constants::TWO_POW_217, + 218 => constants::TWO_POW_218, + 219 => constants::TWO_POW_219, + 220 => constants::TWO_POW_220, + 221 => constants::TWO_POW_221, + 222 => constants::TWO_POW_222, + 223 => constants::TWO_POW_223, + 224 => constants::TWO_POW_224, + 225 => constants::TWO_POW_225, + 226 => constants::TWO_POW_226, + 227 => constants::TWO_POW_227, + 228 => constants::TWO_POW_228, + 229 => constants::TWO_POW_229, + 230 => constants::TWO_POW_230, + 231 => constants::TWO_POW_231, + 232 => constants::TWO_POW_232, + 233 => constants::TWO_POW_233, + 234 => constants::TWO_POW_234, + 235 => constants::TWO_POW_235, + 236 => constants::TWO_POW_236, + 237 => constants::TWO_POW_237, + 238 => constants::TWO_POW_238, + 239 => constants::TWO_POW_239, + 240 => constants::TWO_POW_240, + 241 => constants::TWO_POW_241, + 242 => constants::TWO_POW_242, + 243 => constants::TWO_POW_243, + 244 => constants::TWO_POW_244, + 245 => constants::TWO_POW_245, + 246 => constants::TWO_POW_246, + 247 => constants::TWO_POW_247, + 248 => constants::TWO_POW_248, + 249 => constants::TWO_POW_249, + 250 => constants::TWO_POW_250, + 251 => constants::TWO_POW_251, + 252 => constants::TWO_POW_252, + _ => { + panic(array![errors::INVALID_INDEX,]); + 0 + }, + } + } +} + +#[cfg(test)] +mod tests { + // Core imports + + use core::debug::PrintTrait; + + // Local imports + + use super::{Bitmap}; + + #[test] + fn test_helpers_get_bit_at_0() { + let bitmap = 0; + let result = Bitmap::get_bit_at(bitmap, 0); + assert(!result, 'Bitmap: Invalid bit'); + } + + #[test] + fn test_helpers_get_bit_at_1() { + let bitmap = 255; + let result = Bitmap::get_bit_at(bitmap, 1); + assert(result, 'Bitmap: Invalid bit'); + } + + #[test] + fn test_helpers_get_bit_at_10() { + let bitmap = 3071; + let result = Bitmap::get_bit_at(bitmap, 10); + assert(!result, 'Bitmap: Invalid bit'); + } + + #[test] + fn test_helpers_set_bit_at_0() { + let bitmap = 0; + let result = Bitmap::set_bit_at(bitmap, 0, true); + assert(result == 1, 'Bitmap: Invalid bitmap'); + let result = Bitmap::set_bit_at(bitmap, 0, false); + assert(result == bitmap, 'Bitmap: Invalid bitmap'); + } + + #[test] + fn test_helpers_set_bit_at_1() { + let bitmap = 1; + let result = Bitmap::set_bit_at(bitmap, 1, true); + assert(result == 3, 'Bitmap: Invalid bitmap'); + let result = Bitmap::set_bit_at(bitmap, 1, false); + assert(result == bitmap, 'Bitmap: Invalid bitmap'); + } + + #[test] + fn test_helpers_set_bit_at_10() { + let bitmap = 3; + let result = Bitmap::set_bit_at(bitmap, 10, true); + assert(result == 1027, 'Bitmap: Invalid bitmap'); + let result = Bitmap::set_bit_at(bitmap, 10, false); + assert(result == bitmap, 'Bitmap: Invalid bitmap'); + } + + #[test] + #[should_panic(expected: ('Bitmap: invalid index',))] + fn test_helpers_set_bit_at_253() { + let bitmap = 0; + Bitmap::set_bit_at(bitmap, 253, true); + } +} diff --git a/examples/matchmaker/src/lib.cairo b/examples/matchmaker/src/lib.cairo new file mode 100644 index 00000000..904ecdd5 --- /dev/null +++ b/examples/matchmaker/src/lib.cairo @@ -0,0 +1,18 @@ +mod constants; +mod store; + +mod models { + mod league; + mod player; + mod registry; + mod slot; +} + +mod systems { + mod maker; +} + +mod helpers { + mod bitmap; +} + diff --git a/examples/matchmaker/src/models/league.cairo b/examples/matchmaker/src/models/league.cairo new file mode 100644 index 00000000..1bb9baa6 --- /dev/null +++ b/examples/matchmaker/src/models/league.cairo @@ -0,0 +1,178 @@ +use core::option::OptionTrait; +// Starknet imports + +use starknet::ContractAddress; + +// Internal imports + +use matchmaker::constants::LEAGUE_SIZE; +use matchmaker::models::player::{Player, PlayerTrait, PlayerAssert}; +use matchmaker::models::slot::{Slot, SlotTrait}; + +// Errors + +mod errors { + const LEAGUE_NOT_SUBSCRIBED: felt252 = 'League: player not subscribed'; +} + +#[derive(Model, Copy, Drop, Serde)] +struct League { + #[key] + registry_id: u32, + #[key] + id: u8, + size: u32, +} + +#[generate_trait] +impl LeagueImpl of LeagueTrait { + #[inline(always)] + fn new(registry_id: u32, league_id: u8) -> League { + League { registry_id, id: league_id, size: 0, } + } + + #[inline(always)] + fn compute_id(rating: u32) -> u8 { + let id = rating / LEAGUE_SIZE.into(); + if id > 251 { + 251 + } else if id < 1 { + 1 + } else { + id.try_into().unwrap() + } + } + + #[inline(always)] + fn subscribe(ref self: League, ref player: Player) -> Slot { + // [Check] Player can subscribe + PlayerAssert::assert_subscribable(player); + // [Effect] Update + let index = self.size; + self.size += 1; + player.league_id = self.id; + player.index = index; + // [Return] Corresponding slot + SlotTrait::new(player) + } + + #[inline(always)] + fn unsubscribe(ref self: League, ref player: Player) { + // [Check] Player belongs to the league + LeagueAssert::assert_subscribed(self, player); + // [Effect] Update + self.size -= 1; + player.league_id = 0; + player.index = 0; + } + + #[inline(always)] + fn search_player(self: League, seed: felt252) -> u32 { + let seed: u256 = seed.into(); + let index = seed % self.size.into(); + index.try_into().unwrap() + } +} + +#[generate_trait] +impl LeagueAssert of AssertTrait { + #[inline(always)] + fn assert_subscribed(self: League, player: Player) { + assert(player.league_id == self.id, errors::LEAGUE_NOT_SUBSCRIBED); + } +} + +#[cfg(test)] +mod tests { + // Core imports + + use core::debug::PrintTrait; + + // Local imports + + use super::{League, LeagueTrait, Player, PlayerTrait, ContractAddress}; + + // Constants + + fn PLAYER() -> ContractAddress { + starknet::contract_address_const::<'PLAYER'>() + } + + const REGISTER_ID: u32 = 1; + const LEAGUE_ID: u8 = 1; + + #[test] + fn test_new() { + let league = LeagueTrait::new(REGISTER_ID, LEAGUE_ID); + assert_eq!(league.registry_id, REGISTER_ID); + assert_eq!(league.id, LEAGUE_ID); + assert_eq!(league.size, 0); + } + + #[test] + fn test_compute_id() { + let rating = 1000; + let league_id = LeagueTrait::compute_id(rating); + assert_eq!(league_id, 50); + } + + #[test] + fn test_compute_id_overflow() { + let rating = 10000; + let league_id = LeagueTrait::compute_id(rating); + assert_eq!(league_id, 251); + } + + #[test] + fn test_compute_id_underflow() { + let rating = 0; + let league_id = LeagueTrait::compute_id(rating); + assert_eq!(league_id, 1); + } + + #[test] + fn test_subscribe_once() { + let mut player = PlayerTrait::new(REGISTER_ID, PLAYER()); + let mut league = LeagueTrait::new(REGISTER_ID, LEAGUE_ID); + let slot = LeagueTrait::subscribe(ref league, ref player); + // [Assert] League + assert_eq!(league.size, 1); + // [Assert] Player + assert_eq!(player.league_id, LEAGUE_ID); + assert_eq!(player.index, 0); + // [Assert] Slot + assert_eq!(slot.player_id, player.id); + } + + #[test] + #[should_panic(expected: ('Player: not subscribable',))] + fn test_subscribe_twice() { + let mut player = PlayerTrait::new(REGISTER_ID, PLAYER()); + let mut league = LeagueTrait::new(REGISTER_ID, LEAGUE_ID); + LeagueTrait::subscribe(ref league, ref player); + LeagueTrait::subscribe(ref league, ref player); + } + + #[test] + fn test_unsubscribe_once() { + let mut player = PlayerTrait::new(REGISTER_ID, PLAYER()); + let mut league = LeagueTrait::new(REGISTER_ID, LEAGUE_ID); + LeagueTrait::subscribe(ref league, ref player); + LeagueTrait::unsubscribe(ref league, ref player); + // [Assert] League + assert_eq!(league.size, 0); + // [Assert] Player + assert_eq!(player.league_id, 0); + assert_eq!(player.index, 0); + } + + #[test] + #[should_panic(expected: ('League: player not subscribed',))] + fn test_unsubscribe_twice() { + let mut player = PlayerTrait::new(REGISTER_ID, PLAYER()); + let mut league = LeagueTrait::new(REGISTER_ID, LEAGUE_ID); + LeagueTrait::subscribe(ref league, ref player); + LeagueTrait::unsubscribe(ref league, ref player); + LeagueTrait::unsubscribe(ref league, ref player); + } +} diff --git a/examples/matchmaker/src/models/player.cairo b/examples/matchmaker/src/models/player.cairo new file mode 100644 index 00000000..e4481fc1 --- /dev/null +++ b/examples/matchmaker/src/models/player.cairo @@ -0,0 +1,166 @@ +// Core imports + +use core::zeroable::Zeroable; + +// Starknet imports + +use starknet::ContractAddress; + +// External imports + +use origami::rating::elo::EloTrait; + +// Internal imports + +use matchmaker::constants::{ZERO, DEFAULT_RATING, DEFAULT_K_FACTOR}; + +// Errors + +mod errors { + const PLAYER_DOES_NOT_EXIST: felt252 = 'Player: does not exist'; + const PLAYER_ALREADY_EXIST: felt252 = 'Player: already exist'; + const PLAYER_NOT_SUBSCRIBABLE: felt252 = 'Player: not subscribable'; + const PLAYER_NOT_SUBSCRIBED: felt252 = 'Player: not subscribed'; +} + +#[derive(Model, Copy, Drop, Serde)] +struct Player { + #[key] + registry_id: u32, + #[key] + id: ContractAddress, + league_id: u8, + index: u32, + rating: u32, +} + +#[generate_trait] +impl PlayerImpl of PlayerTrait { + #[inline(always)] + fn new(registry_id: u32, id: ContractAddress) -> Player { + Player { registry_id, id, league_id: 0, index: 0, rating: DEFAULT_RATING, } + } + + #[inline(always)] + fn fight(ref self: Player, ref foe: Player, seed: felt252) { + let win: u8 = (seed.into() % 3_u256).try_into().unwrap(); + let score: u16 = match win { + 0 => 0, // Lose + 1 => 50, // Draw + 2 => 100, // Win + _ => 0, + }; + let (change, negative) = EloTrait::rating_change( + self.rating, foe.rating, score, DEFAULT_K_FACTOR + ); + if negative { + self.rating -= change; + foe.rating += change; + } else { + self.rating += change; + foe.rating -= change; + }; + } +} + +#[generate_trait] +impl PlayerAssert of AssertTrait { + #[inline(always)] + fn assert_does_exist(player: Player) { + assert(player.is_non_zero(), errors::PLAYER_DOES_NOT_EXIST); + } + + #[inline(always)] + fn assert_not_exist(player: Player) { + assert(player.is_non_zero(), errors::PLAYER_ALREADY_EXIST); + } + + #[inline(always)] + fn assert_subscribable(player: Player) { + assert(player.league_id == 0, errors::PLAYER_NOT_SUBSCRIBABLE); + } + + #[inline(always)] + fn assert_subscribed(player: Player) { + assert(player.league_id != 0, errors::PLAYER_NOT_SUBSCRIBED); + } +} + +impl PlayerZeroable of Zeroable { + #[inline(always)] + fn zero() -> Player { + Player { registry_id: 0, id: ZERO(), league_id: 0, index: 0, rating: 0, } + } + + #[inline(always)] + fn is_zero(self: Player) -> bool { + self.league_id == 0 && self.index == 0 && self.rating == 0 + } + + #[inline(always)] + fn is_non_zero(self: Player) -> bool { + !self.is_zero() + } +} + +#[cfg(test)] +mod tests { + // Core imports + + use core::debug::PrintTrait; + + // Local imports + + use super::{Player, PlayerTrait, DEFAULT_RATING, ContractAddress, AssertTrait}; + + // Constants + + fn PLAYER() -> ContractAddress { + starknet::contract_address_const::<'PLAYER'>() + } + + const REGISTER_ID: u32 = 1; + + #[test] + fn test_new() { + let player_id = PLAYER(); + let player = PlayerTrait::new(REGISTER_ID, player_id); + assert_eq!(player.registry_id, REGISTER_ID); + assert_eq!(player.id, player_id); + assert_eq!(player.league_id, 0); + assert_eq!(player.index, 0); + assert_eq!(player.rating, DEFAULT_RATING); + } + + #[test] + fn test_subscribable() { + let player_id = PLAYER(); + let player = PlayerTrait::new(REGISTER_ID, player_id); + AssertTrait::assert_subscribable(player); + } + + #[test] + #[should_panic(expected: ('Player: not subscribable',))] + fn test_subscribable_revert_not_subscribable() { + let player_id = PLAYER(); + let mut player = PlayerTrait::new(REGISTER_ID, player_id); + player.league_id = 1; + AssertTrait::assert_subscribable(player); + } + + #[test] + fn test_subscribed() { + let player_id = PLAYER(); + let mut player = PlayerTrait::new(REGISTER_ID, player_id); + player.league_id = 1; + AssertTrait::assert_subscribed(player); + } + + #[test] + #[should_panic(expected: ('Player: not subscribed',))] + fn test_subscribed_revert_not_subscribed() { + let player_id = PLAYER(); + let player = PlayerTrait::new(REGISTER_ID, player_id); + AssertTrait::assert_subscribed(player); + } +} diff --git a/examples/matchmaker/src/models/registry.cairo b/examples/matchmaker/src/models/registry.cairo new file mode 100644 index 00000000..2392bbee --- /dev/null +++ b/examples/matchmaker/src/models/registry.cairo @@ -0,0 +1,222 @@ +// Starknet imports + +use starknet::ContractAddress; + +// Internal imports + +use matchmaker::constants::{LEAGUE_SIZE, DEFAULT_RATING}; +use matchmaker::store::{Store, StoreTrait}; +use matchmaker::models::league::{League, LeagueTrait}; +use matchmaker::models::player::{Player, PlayerTrait, PlayerAssert}; +use matchmaker::models::slot::{Slot, SlotTrait}; +use matchmaker::helpers::bitmap::Bitmap; + +// Errors + +mod errors { + const REGISTRY_INVALID_INDEX: felt252 = 'Registry: invalid bitmap index'; + const REGISTRY_IS_EMPTY: felt252 = 'Registry: is empty'; + const REGISTRY_LEAGUE_NOT_FOUND: felt252 = 'Registry: league not found'; +} + +#[derive(Model, Copy, Drop, Serde)] +struct Registry { + #[key] + id: u32, + leagues: felt252, +} + +#[generate_trait] +impl RegistryImpl of RegistryTrait { + #[inline(always)] + fn new(id: u32) -> Registry { + Registry { id, leagues: 0 } + } + + #[inline(always)] + fn subscribe(ref self: Registry, ref league: League, ref player: Player) -> Slot { + let slot = league.subscribe(ref player); + Private::update(ref self, league.id, league.size); + slot + } + + #[inline(always)] + fn unsubscribe(ref self: Registry, ref league: League, ref player: Player) { + league.unsubscribe(ref player); + Private::update(ref self, league.id, league.size); + } + + #[inline(always)] + fn search_league(mut self: Registry, mut league: League, mut player: Player) -> u8 { + // [Check] Player has subscribed + PlayerAssert::assert_subscribed(player); + // [Effect] Unsubcribe player from his league + self.unsubscribe(ref league, ref player); + // [Check] Registry is not empty + RegistryAssert::assert_not_empty(self); + // [Compute] Loop over the bitmap to find the nearest league with at least 1 player + Bitmap::nearest_significant_bit(self.leagues.into(), league.id) + } +} + +#[generate_trait] +impl Private of PrivateTrait { + #[inline(always)] + fn update(ref registry: Registry, index: u8, count: u32,) { + let bit = Bitmap::get_bit_at(registry.leagues.into(), index.into()); + let new_bit = count != 0; + if bit != new_bit { + let leagues = Bitmap::set_bit_at(registry.leagues.into(), index.into(), new_bit); + registry.leagues = leagues.try_into().expect(errors::REGISTRY_INVALID_INDEX); + } + } +} + +#[generate_trait] +impl RegistryAssert of AssertTrait { + #[inline(always)] + fn assert_not_empty(registry: Registry) { + // [Check] Registry is not empty + assert(registry.leagues.into() > 0_u256, errors::REGISTRY_IS_EMPTY); + } +} + +#[cfg(test)] +mod tests { + // Core imports + + use core::debug::PrintTrait; + + // Local imports + + use super::{ + Registry, RegistryTrait, PrivateTrait, League, LeagueTrait, Slot, SlotTrait, Player, + PlayerTrait, ContractAddress + }; + + // Constants + + fn PLAYER() -> ContractAddress { + starknet::contract_address_const::<'PLAYER'>() + } + + fn TARGET() -> ContractAddress { + starknet::contract_address_const::<'TARGET'>() + } + + const REGISTRY_ID: u32 = 1; + const LEAGUE_ID: u8 = 1; + const CLOSEST_LEAGUE_ID: u8 = 2; + const TARGET_LEAGUE_ID: u8 = 100; + const FAREST_LEAGUE_ID: u8 = 251; + const INDEX: u8 = 3; + + #[test] + fn test_new() { + let registry = RegistryTrait::new(REGISTRY_ID); + assert_eq!(registry.id, REGISTRY_ID); + assert_eq!(registry.leagues, 0); + } + + #[test] + fn test_subscribe() { + let mut registry = RegistryTrait::new(REGISTRY_ID); + let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER()); + let mut league = LeagueTrait::new(REGISTRY_ID, LEAGUE_ID); + registry.subscribe(ref league, ref player); + // [Assert] Registry + assert(registry.leagues.into() > 0_u256, 'Registry: wrong leagues value'); + } + + #[test] + fn test_unsubscribe() { + let mut registry = RegistryTrait::new(REGISTRY_ID); + let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER()); + let mut league = LeagueTrait::new(REGISTRY_ID, LEAGUE_ID); + registry.subscribe(ref league, ref player); + registry.unsubscribe(ref league, ref player); + // [Assert] Registry + assert_eq!(registry.leagues, 0); + } + + #[test] + fn test_search_league_same() { + let mut registry = RegistryTrait::new(REGISTRY_ID); + let mut league = LeagueTrait::new(REGISTRY_ID, LEAGUE_ID); + let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER()); + registry.subscribe(ref league, ref player); + let mut foe = PlayerTrait::new(REGISTRY_ID, TARGET()); + registry.subscribe(ref league, ref foe); + let league_id = registry.search_league(league, player); + // [Assert] Registry + assert(league_id == LEAGUE_ID, 'Registry: wrong search league'); + } + + #[test] + fn test_search_league_close() { + let mut registry = RegistryTrait::new(REGISTRY_ID); + let mut league = LeagueTrait::new(REGISTRY_ID, LEAGUE_ID); + let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER()); + registry.subscribe(ref league, ref player); + let mut foe_league = LeagueTrait::new(REGISTRY_ID, CLOSEST_LEAGUE_ID); + let mut foe = PlayerTrait::new(REGISTRY_ID, TARGET()); + registry.subscribe(ref foe_league, ref foe); + let league_id = registry.search_league(league, player); + // [Assert] Registry + assert(league_id == CLOSEST_LEAGUE_ID, 'Registry: wrong search league'); + } + + #[test] + fn test_search_league_target() { + let mut registry = RegistryTrait::new(REGISTRY_ID); + let mut league = LeagueTrait::new(REGISTRY_ID, LEAGUE_ID); + let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER()); + registry.subscribe(ref league, ref player); + let mut foe_league = LeagueTrait::new(REGISTRY_ID, TARGET_LEAGUE_ID); + let mut foe = PlayerTrait::new(REGISTRY_ID, TARGET()); + registry.subscribe(ref foe_league, ref foe); + let league_id = registry.search_league(league, player); + // [Assert] Registry + assert(league_id == TARGET_LEAGUE_ID, 'Registry: wrong search league'); + } + + #[test] + fn test_search_league_far_down_top() { + let mut registry = RegistryTrait::new(REGISTRY_ID); + let mut league = LeagueTrait::new(REGISTRY_ID, LEAGUE_ID); + let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER()); + registry.subscribe(ref league, ref player); + let mut foe_league = LeagueTrait::new(REGISTRY_ID, FAREST_LEAGUE_ID); + let mut foe = PlayerTrait::new(REGISTRY_ID, TARGET()); + registry.subscribe(ref foe_league, ref foe); + let league_id = registry.search_league(league, player); + // [Assert] Registry + league_id.print(); + assert(league_id == FAREST_LEAGUE_ID, 'Registry: wrong search league'); + } + + #[test] + fn test_search_league_far_top_down() { + let mut registry = RegistryTrait::new(REGISTRY_ID); + let mut league = LeagueTrait::new(REGISTRY_ID, FAREST_LEAGUE_ID); + let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER()); + registry.subscribe(ref league, ref player); + let mut foe_league = LeagueTrait::new(REGISTRY_ID, LEAGUE_ID); + let mut foe = PlayerTrait::new(REGISTRY_ID, TARGET()); + registry.subscribe(ref foe_league, ref foe); + let league_id = registry.search_league(league, player); + // [Assert] Registry + league_id.print(); + assert(league_id == LEAGUE_ID, 'Registry: wrong search league'); + } + + #[test] + #[should_panic(expected: ('Registry: is empty',))] + fn test_search_league_revert_empty() { + let mut registry = RegistryTrait::new(REGISTRY_ID); + let mut league = LeagueTrait::new(REGISTRY_ID, LEAGUE_ID); + let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER()); + registry.subscribe(ref league, ref player); + registry.search_league(league, player); + } +} diff --git a/examples/matchmaker/src/models/slot.cairo b/examples/matchmaker/src/models/slot.cairo new file mode 100644 index 00000000..ab1c5056 --- /dev/null +++ b/examples/matchmaker/src/models/slot.cairo @@ -0,0 +1,65 @@ +// Starknet imports + +use starknet::ContractAddress; + +// Internal imports + +use matchmaker::models::player::{Player, PlayerTrait}; + +#[derive(Model, Copy, Drop, Serde)] +struct Slot { + #[key] + registry_id: u32, + #[key] + league_id: u8, + #[key] + index: u32, + player_id: ContractAddress, +} + +#[generate_trait] +impl SlotImpl of SlotTrait { + #[inline(always)] + fn new(player: Player) -> Slot { + Slot { + registry_id: player.registry_id, + league_id: player.league_id, + index: player.index, + player_id: player.id, + } + } +} + +#[cfg(test)] +mod tests { + // Core imports + + use core::debug::PrintTrait; + + // Local imports + + use super::{Slot, SlotTrait, Player, PlayerTrait, ContractAddress}; + + // Constants + + fn PLAYER() -> ContractAddress { + starknet::contract_address_const::<'PLAYER'>() + } + + const REGISTER_ID: u32 = 1; + const LEAGUE_ID: u8 = 2; + const INDEX: u32 = 3; + + #[test] + fn test_new() { + let player_id = PLAYER(); + let mut player = PlayerTrait::new(REGISTER_ID, player_id); + player.league_id = LEAGUE_ID; + player.index = INDEX; + let slot = SlotTrait::new(player); + assert_eq!(slot.registry_id, REGISTER_ID); + assert_eq!(slot.league_id, LEAGUE_ID); + assert_eq!(slot.index, INDEX); + assert_eq!(slot.player_id, player_id); + } +} diff --git a/examples/matchmaker/src/store.cairo b/examples/matchmaker/src/store.cairo new file mode 100644 index 00000000..fa63aa30 --- /dev/null +++ b/examples/matchmaker/src/store.cairo @@ -0,0 +1,107 @@ +//! Store struct and component management methods. + +// Core imports + +use core::debug::PrintTrait; + +// Straknet imports + +use starknet::ContractAddress; + +// Dojo imports + +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + +// Models imports + +use matchmaker::models::league::{League, LeagueTrait}; +use matchmaker::models::player::Player; +use matchmaker::models::registry::Registry; +use matchmaker::models::slot::Slot; + + +/// Store struct. +#[derive(Copy, Drop)] +struct Store { + world: IWorldDispatcher, +} + +/// Implementation of the `StoreTrait` trait for the `Store` struct. +#[generate_trait] +impl StoreImpl of StoreTrait { + #[inline(always)] + fn new(world: IWorldDispatcher) -> Store { + Store { world: world } + } + + #[inline(always)] + fn registry(self: Store, registry_id: u32,) -> Registry { + get!(self.world, registry_id, (Registry)) + } + + #[inline(always)] + fn league(self: Store, registry_id: u32, league_id: u8) -> League { + get!(self.world, (registry_id, league_id), (League)) + } + + #[inline(always)] + fn slot(self: Store, registry_id: u32, league_id: u8, index: u32) -> Slot { + get!(self.world, (registry_id, league_id, index), (Slot)) + } + + #[inline(always)] + fn player(self: Store, registry_id: u32, player_id: ContractAddress) -> Player { + get!(self.world, (registry_id, player_id), (Player)) + } + + #[inline(always)] + fn set_registry(self: Store, registry: Registry) { + set!(self.world, (registry)) + } + + #[inline(always)] + fn set_league(self: Store, league: League) { + set!(self.world, (league)) + } + + #[inline(always)] + fn set_slot(self: Store, slot: Slot) { + set!(self.world, (slot)) + } + + #[inline(always)] + fn set_player(self: Store, player: Player) { + set!(self.world, (player)) + } + + #[inline(always)] + fn add_player_to_league(self: Store, player: Player) { + // [Effect] Add the player to the last slot + let mut league = self.league(player.registry_id, player.league_id); + let mut last_slot = self.slot(league.registry_id, player.league_id, league.size); + last_slot.index = player.index; + last_slot.player_id = player.id; + self.set_slot(last_slot); + // [Effect] Update the league size + league.size += 1; + self.set_league(league); + } + + #[inline(always)] + fn remove_player_from_league(self: Store, player: Player) { + // [Effect] Replace the slot with the last slot if needed + let mut league = self.league(player.registry_id, player.league_id); + let mut last_slot = self.slot(league.registry_id, player.league_id, league.size - 1); + if last_slot.player_id != player.id { + last_slot.index = player.index; + self.set_slot(last_slot); + } + // [Effect] Remove the last slot + let mut empty_slot = self.slot(league.registry_id, player.league_id, league.size); + empty_slot.index = league.size - 1; + self.set_slot(empty_slot); + // [Effect] Update the league size + league.size -= 1; + self.set_league(league); + } +} diff --git a/examples/matchmaker/src/systems/maker.cairo b/examples/matchmaker/src/systems/maker.cairo new file mode 100644 index 00000000..d44a8f8d --- /dev/null +++ b/examples/matchmaker/src/systems/maker.cairo @@ -0,0 +1,171 @@ +// Starknet imports + +use starknet::ContractAddress; + +// Dojo imports + +use dojo::world::IWorldDispatcher; + +// Interface + +#[dojo::interface] +trait IMaker { + fn create(); + fn subscribe(); + fn unsubscribe(); + fn fight(); +} + +// Contract + +#[dojo::contract] +mod maker { + // Core imports + + use core::array::ArrayTrait; + use core::debug::PrintTrait; + + // Starknet imports + + use starknet::ContractAddress; + use starknet::info::{get_caller_address, get_tx_info}; + + // Internal imports + + use matchmaker::store::{Store, StoreTrait}; + use matchmaker::models::player::{Player, PlayerTrait, PlayerAssert}; + use matchmaker::models::league::{League, LeagueTrait, LeagueAssert}; + use matchmaker::models::registry::{Registry, RegistryTrait, RegistryAssert}; + use matchmaker::models::slot::{Slot, SlotTrait}; + + // Local imports + + use super::IMaker; + + // Errors + + mod errors { + const CHARACTER_DUPLICATE: felt252 = 'Battle: character duplicate'; + } + + // Implementations + + #[abi(embed_v0)] + impl MakerImpl of IMaker { + fn create(world: IWorldDispatcher) { + // [Setup] Datastore + let mut store: Store = StoreTrait::new(world); + + // [Check] Player does not exist + let caller = get_caller_address(); + let player = store.player(0, caller); + PlayerAssert::assert_not_exist(player); + + // [Effect] Create one + let player = PlayerTrait::new(0, caller); + store.set_player(player); + } + + fn subscribe(world: IWorldDispatcher) { + // [Setup] Datastore + let mut store: Store = StoreTrait::new(world); + + // [Check] Player exists + let caller = get_caller_address(); + let mut player = store.player(0, caller); + PlayerAssert::assert_does_exist(player); + + // [Effect] Subscribe to Registry + let league_id = LeagueTrait::compute_id(player.rating); + let mut league = store.league(0, league_id); + let mut registry = store.registry(0); + let slot = registry.subscribe(ref league, ref player); + + // [Effect] Update Slot + store.set_slot(slot); + + // [Effect] Update Player + store.set_player(player); + + // [Effect] Update League + store.set_league(league); + + // [Effect] Update Registry + store.set_registry(registry); + } + + fn unsubscribe(world: IWorldDispatcher) { + // [Setup] Datastore + let mut store: Store = StoreTrait::new(world); + + // [Check] Player exists + let caller = get_caller_address(); + let mut player = store.player(0, caller); + PlayerAssert::assert_does_exist(player); + + // [Effect] Unsubscribe to Registry + let mut league = store.league(0, player.league_id); + let mut registry = store.registry(0); + registry.unsubscribe(ref league, ref player); + + // [Effect] Update Player + store.set_player(player); + + // [Effect] Update League + store.set_league(league); + + // [Effect] Update Registry + store.set_registry(registry); + } + + fn fight(world: IWorldDispatcher) { + // [Setup] Datastore + let mut store: Store = StoreTrait::new(world); + + // [Check] Player exists + let caller = get_caller_address(); + let mut player = store.player(0, caller); + PlayerAssert::assert_does_exist(player); + + // [Compute] Search opponent + let seed = get_tx_info().unbox().transaction_hash; + let mut registry = store.registry(0); + let mut player_league = store.league(0, player.league_id); + let foe_league_id = registry.search_league(player_league, player); + let mut foe_league = store.league(0, foe_league_id); + let foe_slot_id = foe_league.search_player(seed); + let foe_slot = store.slot(0, foe_league_id, foe_slot_id); + let mut foe = store.player(0, foe_slot.player_id); + + // [Effect] Fight + player.fight(ref foe, seed); + + // [Effect] Update Player league and slot + registry.unsubscribe(ref player_league, ref player); + let league_id = LeagueTrait::compute_id(player.rating); + let mut player_league = store.league(0, league_id); + let player_slot = registry.subscribe(ref player_league, ref player); + + // [Effect] Update Foe league and slot + registry.unsubscribe(ref foe_league, ref foe); + let foe_league_id = LeagueTrait::compute_id(foe.rating); + let mut foe_league = store.league(0, foe_league_id); + let foe_slot = registry.subscribe(ref foe_league, ref foe); + + // [Effect] Update Slots + store.set_slot(player_slot); + store.set_slot(foe_slot); + + // [Effect] Update Players + store.set_player(player); + store.set_player(foe); + + // [Effect] Update League + store.set_league(player_league); + store.set_league(foe_league); + + // [Effect] Update Registry + store.set_registry(registry); + } + } +} diff --git a/examples/matchmaker/src/tests/setup.cairo b/examples/matchmaker/src/tests/setup.cairo new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/matchmaker/src/tests/setup.cairo @@ -0,0 +1 @@ + diff --git a/examples/matchmaker/src/tests/test_suscribe.cairo b/examples/matchmaker/src/tests/test_suscribe.cairo new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/matchmaker/src/tests/test_suscribe.cairo @@ -0,0 +1 @@ + From 1810c8548aee833eace42431f735b495e81c29ea Mon Sep 17 00:00:00 2001 From: bal7hazar Date: Fri, 19 Apr 2024 11:12:33 +0200 Subject: [PATCH 4/7] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Use=20signed=20integer?= =?UTF-8?q?=20for=20internal=20computations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/src/rating/elo.cairo | 36 +++++++++++++----------------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/crates/src/rating/elo.cairo b/crates/src/rating/elo.cairo index 85bf2957..19607cdd 100644 --- a/crates/src/rating/elo.cairo +++ b/crates/src/rating/elo.cairo @@ -4,11 +4,12 @@ // Core imports use core::integer::{u32_sqrt, u64_sqrt, u128_sqrt, u256_sqrt}; +use core::integer::i128; // Constants const MULTIPLIER: u32 = 10_000; -const SCALER: u256 = 800; +const SCALER: i128 = 800; // Errors @@ -32,7 +33,7 @@ impl EloImpl of EloTrait { T, +Sub, +PartialOrd, - +Into, + +Into, +Drop, +Copy, S, @@ -48,30 +49,19 @@ impl EloImpl of EloTrait { >( rating_a: T, rating_b: T, score: S, k: K ) -> (C, bool) { - let negative = rating_a > rating_b; - let rating_diff: u256 = if negative { - (rating_a - rating_b).into() - } else { - (rating_b - rating_a).into() - }; - // [Check] Checks against overflow/underflow // Large rating diffs leads to 10 ** rating_diff being too large to fit in a u256 // Large rating diffs when applying the scale factor leads to underflow (800 - rating_diff) - assert( - rating_diff < 800 || (!negative && rating_diff < 1126), errors::ELO_DIFFERENCE_TOO_LARGE - ); + let rating_diff: i128 = rating_b.into() - rating_a.into(); + assert(-800 < rating_diff && rating_diff < 1126, errors::ELO_DIFFERENCE_TOO_LARGE); // [Compute] Expected score = 1 / (1 + 10 ^ (rating_diff / 400)) // Apply offset of 800 to scale the result by 100 // Divide by 25 to avoid reach u256 max // (x / 400) is the same as ((x / 25) / 16) // x ^ (1 / 16) is the same as 16th root of x - let order: u256 = if negative { - (SCALER - rating_diff) / 25 - } else { - (SCALER + rating_diff) / 25 - }; + let order_felt: felt252 = (SCALER + rating_diff).into(); + let order: u256 = order_felt.into() / 25; // [Info] Order should be less or equal to 77 to fit a u256 let powered: u256 = PrivateTrait::pow(10, order); let rooted: u16 = u32_sqrt(u64_sqrt(u128_sqrt(u256_sqrt(powered)))); @@ -132,42 +122,42 @@ mod tests { #[test] fn test_elo_change_positive_01() { - let (mag, sign) = EloTrait::rating_change(1200_u128, 1400_u128, 100_u16, 20_u8); + let (mag, sign) = EloTrait::rating_change(1200_u64, 1400_u64, 100_u16, 20_u8); assert(mag == 15, 'Elo: wrong change mag'); assert(!sign, 'Elo: wrong change sign'); } #[test] fn test_elo_change_positive_02() { - let (mag, sign) = EloTrait::rating_change(1300_u128, 1200_u128, 100_u16, 20_u8); + let (mag, sign) = EloTrait::rating_change(1300_u64, 1200_u64, 100_u16, 20_u8); assert(mag == 7, 'Elo: wrong change mag'); assert(!sign, 'Elo: wrong change sign'); } #[test] fn test_elo_change_positive_03() { - let (mag, sign) = EloTrait::rating_change(1900_u256, 2100_u256, 100_u16, 20_u8); + let (mag, sign) = EloTrait::rating_change(1900_u64, 2100_u64, 100_u16, 20_u8); assert(mag == 15, 'Elo: wrong change mag'); assert(!sign, 'Elo: wrong change sign'); } #[test] fn test_elo_change_negative_01() { - let (mag, sign) = EloTrait::rating_change(1200_u128, 1400_u128, 0_u16, 20_u8); + let (mag, sign) = EloTrait::rating_change(1200_u64, 1400_u64, 0_u16, 20_u8); assert(mag == 5, 'Elo: wrong change mag'); assert(sign, 'Elo: wrong change sign'); } #[test] fn test_elo_change_negative_02() { - let (mag, sign) = EloTrait::rating_change(1300_u128, 1200_u128, 0_u16, 20_u8); + let (mag, sign) = EloTrait::rating_change(1300_u64, 1200_u64, 0_u16, 20_u8); assert(mag == 13, 'Elo: wrong change mag'); assert(sign, 'Elo: wrong change sign'); } #[test] fn test_elo_change_draw() { - let (mag, sign) = EloTrait::rating_change(1200_u128, 1400_u128, 50_u16, 20_u8); + let (mag, sign) = EloTrait::rating_change(1200_u64, 1400_u64, 50_u16, 20_u8); assert(mag == 5, 'Elo: wrong change mag'); assert(!sign, 'Elo: wrong change sign'); } From ef0783354568716e8f7154c9d175203c231bc95f Mon Sep 17 00:00:00 2001 From: bal7hazar Date: Fri, 19 Apr 2024 11:45:15 +0200 Subject: [PATCH 5/7] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Improve=20bitmap=20nea?= =?UTF-8?q?rest=20bit=20output?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/matchmaker/src/helpers/bitmap.cairo | 16 +++++++++------- examples/matchmaker/src/models/registry.cairo | 8 +++++++- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/examples/matchmaker/src/helpers/bitmap.cairo b/examples/matchmaker/src/helpers/bitmap.cairo index 21d7abf9..f10809b0 100644 --- a/examples/matchmaker/src/helpers/bitmap.cairo +++ b/examples/matchmaker/src/helpers/bitmap.cairo @@ -39,20 +39,22 @@ impl Bitmap of BitmapTrait { /// # Returns /// * The index of the nearest significant bit #[inline(always)] - fn nearest_significant_bit(x: u256, s: u8) -> u8 { + fn nearest_significant_bit(x: u256, s: u8) -> Option:: { let lower_mask = Bitmap::set_bit_at(0, (s + 1).into(), true) - 1; let lower = Bitmap::most_significant_bit(x & lower_mask); let upper_mask = ~(lower_mask / 2); let upper = Bitmap::least_significant_bit(x & upper_mask); match (lower, upper) { - (Option::Some(l), Option::Some(u)) => { if s - l < u - s { - l + ( + Option::Some(l), Option::Some(u) + ) => { if s - l < u - s { + Option::Some(l) } else { - u + Option::Some(u) } }, - (Option::Some(l), Option::None) => l, - (Option::None, Option::Some(u)) => u, - (Option::None, Option::None) => 0, + (Option::Some(l), Option::None) => Option::Some(l), + (Option::None, Option::Some(u)) => Option::Some(u), + (Option::None, Option::None) => Option::None, } } diff --git a/examples/matchmaker/src/models/registry.cairo b/examples/matchmaker/src/models/registry.cairo index 2392bbee..80cf55a6 100644 --- a/examples/matchmaker/src/models/registry.cairo +++ b/examples/matchmaker/src/models/registry.cairo @@ -55,7 +55,13 @@ impl RegistryImpl of RegistryTrait { // [Check] Registry is not empty RegistryAssert::assert_not_empty(self); // [Compute] Loop over the bitmap to find the nearest league with at least 1 player - Bitmap::nearest_significant_bit(self.leagues.into(), league.id) + match Bitmap::nearest_significant_bit(self.leagues.into(), league.id) { + Option::Some(bit) => bit, + Option::None => { + panic(array![errors::REGISTRY_LEAGUE_NOT_FOUND]); + 0 + }, + } } } From 4e92356faf84fe520b876b41e8a79eeafe898aaf Mon Sep 17 00:00:00 2001 From: bal7hazar Date: Fri, 19 Apr 2024 14:03:46 +0200 Subject: [PATCH 6/7] =?UTF-8?q?=E2=9C=85=20Create=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/matchmaker/src/constants.cairo | 6 ++ examples/matchmaker/src/lib.cairo | 8 ++ examples/matchmaker/src/models/player.cairo | 2 +- examples/matchmaker/src/models/registry.cairo | 2 - examples/matchmaker/src/store.cairo | 18 +--- examples/matchmaker/src/systems/maker.cairo | 63 ++++++++++--- examples/matchmaker/src/tests/setup.cairo | 93 +++++++++++++++++++ .../matchmaker/src/tests/test_create.cairo | 36 +++++++ .../matchmaker/src/tests/test_fight.cairo | 43 +++++++++ .../matchmaker/src/tests/test_subscribe.cairo | 38 ++++++++ .../matchmaker/src/tests/test_suscribe.cairo | 1 - .../src/tests/test_unsubscribe.cairo | 41 ++++++++ 12 files changed, 317 insertions(+), 34 deletions(-) create mode 100644 examples/matchmaker/src/tests/test_create.cairo create mode 100644 examples/matchmaker/src/tests/test_fight.cairo create mode 100644 examples/matchmaker/src/tests/test_subscribe.cairo delete mode 100644 examples/matchmaker/src/tests/test_suscribe.cairo create mode 100644 examples/matchmaker/src/tests/test_unsubscribe.cairo diff --git a/examples/matchmaker/src/constants.cairo b/examples/matchmaker/src/constants.cairo index 5e1acbcd..58186d54 100644 --- a/examples/matchmaker/src/constants.cairo +++ b/examples/matchmaker/src/constants.cairo @@ -7,6 +7,12 @@ const DEFAULT_RATING: u32 = 1000; const LEAGUE_SIZE: u8 = 20; +// World + +fn WORLD() -> starknet::ContractAddress { + starknet::contract_address_const::<'WORLD'>() +} + // Constants fn ZERO() -> starknet::ContractAddress { diff --git a/examples/matchmaker/src/lib.cairo b/examples/matchmaker/src/lib.cairo index 904ecdd5..5cd4710c 100644 --- a/examples/matchmaker/src/lib.cairo +++ b/examples/matchmaker/src/lib.cairo @@ -16,3 +16,11 @@ mod helpers { mod bitmap; } +#[cfg(test)] +mod tests { + mod setup; + mod test_create; + mod test_subscribe; + mod test_unsubscribe; + mod test_fight; +} diff --git a/examples/matchmaker/src/models/player.cairo b/examples/matchmaker/src/models/player.cairo index e4481fc1..d6da4540 100644 --- a/examples/matchmaker/src/models/player.cairo +++ b/examples/matchmaker/src/models/player.cairo @@ -72,7 +72,7 @@ impl PlayerAssert of AssertTrait { #[inline(always)] fn assert_not_exist(player: Player) { - assert(player.is_non_zero(), errors::PLAYER_ALREADY_EXIST); + assert(player.is_zero(), errors::PLAYER_ALREADY_EXIST); } #[inline(always)] diff --git a/examples/matchmaker/src/models/registry.cairo b/examples/matchmaker/src/models/registry.cairo index 80cf55a6..1904d1a6 100644 --- a/examples/matchmaker/src/models/registry.cairo +++ b/examples/matchmaker/src/models/registry.cairo @@ -197,7 +197,6 @@ mod tests { registry.subscribe(ref foe_league, ref foe); let league_id = registry.search_league(league, player); // [Assert] Registry - league_id.print(); assert(league_id == FAREST_LEAGUE_ID, 'Registry: wrong search league'); } @@ -212,7 +211,6 @@ mod tests { registry.subscribe(ref foe_league, ref foe); let league_id = registry.search_league(league, player); // [Assert] Registry - league_id.print(); assert(league_id == LEAGUE_ID, 'Registry: wrong search league'); } diff --git a/examples/matchmaker/src/store.cairo b/examples/matchmaker/src/store.cairo index fa63aa30..e74c6425 100644 --- a/examples/matchmaker/src/store.cairo +++ b/examples/matchmaker/src/store.cairo @@ -75,20 +75,7 @@ impl StoreImpl of StoreTrait { } #[inline(always)] - fn add_player_to_league(self: Store, player: Player) { - // [Effect] Add the player to the last slot - let mut league = self.league(player.registry_id, player.league_id); - let mut last_slot = self.slot(league.registry_id, player.league_id, league.size); - last_slot.index = player.index; - last_slot.player_id = player.id; - self.set_slot(last_slot); - // [Effect] Update the league size - league.size += 1; - self.set_league(league); - } - - #[inline(always)] - fn remove_player_from_league(self: Store, player: Player) { + fn remove_player_slot(self: Store, player: Player) { // [Effect] Replace the slot with the last slot if needed let mut league = self.league(player.registry_id, player.league_id); let mut last_slot = self.slot(league.registry_id, player.league_id, league.size - 1); @@ -100,8 +87,5 @@ impl StoreImpl of StoreTrait { let mut empty_slot = self.slot(league.registry_id, player.league_id, league.size); empty_slot.index = league.size - 1; self.set_slot(empty_slot); - // [Effect] Update the league size - league.size -= 1; - self.set_league(league); } } diff --git a/examples/matchmaker/src/systems/maker.cairo b/examples/matchmaker/src/systems/maker.cairo index d44a8f8d..ae49071c 100644 --- a/examples/matchmaker/src/systems/maker.cairo +++ b/examples/matchmaker/src/systems/maker.cairo @@ -8,17 +8,17 @@ use dojo::world::IWorldDispatcher; // Interface -#[dojo::interface] -trait IMaker { - fn create(); - fn subscribe(); - fn unsubscribe(); - fn fight(); +#[starknet::interface] +trait IMaker { + fn create(self: @TContractState, world: IWorldDispatcher); + fn subscribe(self: @TContractState, world: IWorldDispatcher); + fn unsubscribe(self: @TContractState, world: IWorldDispatcher); + fn fight(self: @TContractState, world: IWorldDispatcher); } // Contract -#[dojo::contract] +#[starknet::contract] mod maker { // Core imports @@ -30,8 +30,17 @@ mod maker { use starknet::ContractAddress; use starknet::info::{get_caller_address, get_tx_info}; + // Dojo imports + + use dojo::world; + use dojo::world::IWorldDispatcher; + use dojo::world::IWorldDispatcherTrait; + use dojo::world::IWorldProvider; + use dojo::world::IDojoResourceProvider; + // Internal imports + use matchmaker::constants::WORLD; use matchmaker::store::{Store, StoreTrait}; use matchmaker::models::player::{Player, PlayerTrait, PlayerAssert}; use matchmaker::models::league::{League, LeagueTrait, LeagueAssert}; @@ -48,11 +57,30 @@ mod maker { const CHARACTER_DUPLICATE: felt252 = 'Battle: character duplicate'; } + // Storage + + #[storage] + struct Storage {} + // Implementations + #[abi(embed_v0)] + impl DojoResourceProviderImpl of IDojoResourceProvider { + fn dojo_resource(self: @ContractState) -> felt252 { + 'account' + } + } + + #[abi(embed_v0)] + impl WorldProviderImpl of IWorldProvider { + fn world(self: @ContractState) -> IWorldDispatcher { + IWorldDispatcher { contract_address: WORLD() } + } + } + #[abi(embed_v0)] impl MakerImpl of IMaker { - fn create(world: IWorldDispatcher) { + fn create(self: @ContractState, world: IWorldDispatcher) { // [Setup] Datastore let mut store: Store = StoreTrait::new(world); @@ -66,7 +94,7 @@ mod maker { store.set_player(player); } - fn subscribe(world: IWorldDispatcher) { + fn subscribe(self: @ContractState, world: IWorldDispatcher) { // [Setup] Datastore let mut store: Store = StoreTrait::new(world); @@ -94,7 +122,7 @@ mod maker { store.set_registry(registry); } - fn unsubscribe(world: IWorldDispatcher) { + fn unsubscribe(self: @ContractState, world: IWorldDispatcher) { // [Setup] Datastore let mut store: Store = StoreTrait::new(world); @@ -103,9 +131,12 @@ mod maker { let mut player = store.player(0, caller); PlayerAssert::assert_does_exist(player); + // [Effect] Remove slot + store.remove_player_slot(player); + // [Effect] Unsubscribe to Registry - let mut league = store.league(0, player.league_id); - let mut registry = store.registry(0); + let mut league = store.league(player.registry_id, player.league_id); + let mut registry = store.registry(player.registry_id); registry.unsubscribe(ref league, ref player); // [Effect] Update Player @@ -118,7 +149,7 @@ mod maker { store.set_registry(registry); } - fn fight(world: IWorldDispatcher) { + fn fight(self: @ContractState, world: IWorldDispatcher) { // [Setup] Datastore let mut store: Store = StoreTrait::new(world); @@ -140,12 +171,18 @@ mod maker { // [Effect] Fight player.fight(ref foe, seed); + // [Effect] Remove player slot + store.remove_player_slot(player); + // [Effect] Update Player league and slot registry.unsubscribe(ref player_league, ref player); let league_id = LeagueTrait::compute_id(player.rating); let mut player_league = store.league(0, league_id); let player_slot = registry.subscribe(ref player_league, ref player); + // [Effect] Remove foe slot + store.remove_player_slot(foe); + // [Effect] Update Foe league and slot registry.unsubscribe(ref foe_league, ref foe); let foe_league_id = LeagueTrait::compute_id(foe.rating); diff --git a/examples/matchmaker/src/tests/setup.cairo b/examples/matchmaker/src/tests/setup.cairo index 8b137891..cee0866a 100644 --- a/examples/matchmaker/src/tests/setup.cairo +++ b/examples/matchmaker/src/tests/setup.cairo @@ -1 +1,94 @@ +mod setup { + // Core imports + use core::debug::PrintTrait; + + // Starknet imports + + use starknet::ContractAddress; + use starknet::testing::{set_contract_address}; + + // Dojo imports + + use dojo::world::{IWorldDispatcherTrait, IWorldDispatcher}; + use dojo::test_utils::{spawn_test_world, deploy_contract}; + + // Internal imports + + use matchmaker::models::player::Player; + use matchmaker::models::league::League; + use matchmaker::models::registry::Registry; + use matchmaker::models::slot::Slot; + use matchmaker::systems::maker::{maker, IMakerDispatcher, IMakerDispatcherTrait}; + + // Constants + + fn PLAYER() -> ContractAddress { + starknet::contract_address_const::<'PLAYER'>() + } + + fn ANYONE() -> ContractAddress { + starknet::contract_address_const::<'ANYONE'>() + } + + fn SOMEONE() -> ContractAddress { + starknet::contract_address_const::<'SOMEONE'>() + } + + fn NOONE() -> ContractAddress { + starknet::contract_address_const::<'NOONE'>() + } + + const REGISTRY_ID: u32 = 0; + + #[derive(Drop)] + struct Systems { + maker: IMakerDispatcher, + } + + #[derive(Drop)] + struct Context { + registry_id: u32, + player_id: ContractAddress, + someone_id: ContractAddress, + anyone_id: ContractAddress, + noone_id: ContractAddress, + } + + #[inline(always)] + fn spawn() -> (IWorldDispatcher, Systems, Context) { + // [Setup] World + let mut models = core::array::ArrayTrait::new(); + models.append(matchmaker::models::player::player::TEST_CLASS_HASH); + models.append(matchmaker::models::league::league::TEST_CLASS_HASH); + models.append(matchmaker::models::registry::registry::TEST_CLASS_HASH); + models.append(matchmaker::models::slot::slot::TEST_CLASS_HASH); + let world = spawn_test_world(models); + + // [Setup] Systems + let maker_address = deploy_contract(maker::TEST_CLASS_HASH, array![].span()); + let systems = Systems { maker: IMakerDispatcher { contract_address: maker_address }, }; + + // [Setup] Context + set_contract_address(SOMEONE()); + systems.maker.create(world); + systems.maker.subscribe(world); + set_contract_address(ANYONE()); + systems.maker.create(world); + systems.maker.subscribe(world); + set_contract_address(NOONE()); + systems.maker.create(world); + systems.maker.subscribe(world); + set_contract_address(PLAYER()); + let context = Context { + registry_id: REGISTRY_ID, + player_id: PLAYER(), + someone_id: SOMEONE(), + anyone_id: ANYONE(), + noone_id: NOONE() + }; + + // [Return] + (world, systems, context) + } +} diff --git a/examples/matchmaker/src/tests/test_create.cairo b/examples/matchmaker/src/tests/test_create.cairo new file mode 100644 index 00000000..6a41fbae --- /dev/null +++ b/examples/matchmaker/src/tests/test_create.cairo @@ -0,0 +1,36 @@ +// Core imports + +use core::debug::PrintTrait; + +// Starknet imports + +use starknet::testing::set_contract_address; + +// Dojo imports + +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + +// Internal imports + +use matchmaker::store::{Store, StoreTrait}; +use matchmaker::models::player::{Player, PlayerTrait, PlayerAssert}; +use matchmaker::models::registry::{Registry, RegistryTrait, RegistryAssert}; +use matchmaker::models::league::{League, LeagueTrait, LeagueAssert}; +use matchmaker::models::slot::{Slot, SlotTrait}; +use matchmaker::systems::maker::IMakerDispatcherTrait; +use matchmaker::tests::setup::{setup, setup::Systems}; + +#[test] +fn test_maker_create() { + // [Setup] + let (world, systems, context) = setup::spawn(); + let store = StoreTrait::new(world); + + // [Create] + systems.maker.create(world); + + // [Assert] Player + let player = store.player(context.registry_id, context.player_id); + assert(player.id == context.player_id, 'Create: wrong player id'); + assert(player.league_id == 0, 'Create: wrong league id'); +} diff --git a/examples/matchmaker/src/tests/test_fight.cairo b/examples/matchmaker/src/tests/test_fight.cairo new file mode 100644 index 00000000..3c8d83f5 --- /dev/null +++ b/examples/matchmaker/src/tests/test_fight.cairo @@ -0,0 +1,43 @@ +// Core imports + +use core::debug::PrintTrait; + +// Starknet imports + +use starknet::testing::set_contract_address; + +// Dojo imports + +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + +// Internal imports + +use matchmaker::store::{Store, StoreTrait}; +use matchmaker::models::player::{Player, PlayerTrait, PlayerAssert}; +use matchmaker::models::registry::{Registry, RegistryTrait, RegistryAssert}; +use matchmaker::models::league::{League, LeagueTrait, LeagueAssert}; +use matchmaker::models::slot::{Slot, SlotTrait}; +use matchmaker::systems::maker::IMakerDispatcherTrait; +use matchmaker::tests::setup::{setup, setup::Systems}; + +#[test] +fn test_maker_fight() { + // [Setup] + let (world, systems, context) = setup::spawn(); + let store = StoreTrait::new(world); + + // [Create] + systems.maker.create(world); + + // [Subscribe] + systems.maker.subscribe(world); + + // [Fight] + let player = store.player(context.registry_id, context.player_id); + let rating = player.rating; + systems.maker.fight(world); + + // [Assert] Player + let player = store.player(context.registry_id, context.player_id); + assert(player.rating != rating, 'Fight: wrong player rating'); +} diff --git a/examples/matchmaker/src/tests/test_subscribe.cairo b/examples/matchmaker/src/tests/test_subscribe.cairo new file mode 100644 index 00000000..5c5555d4 --- /dev/null +++ b/examples/matchmaker/src/tests/test_subscribe.cairo @@ -0,0 +1,38 @@ +// Core imports + +use core::debug::PrintTrait; + +// Starknet imports + +use starknet::testing::set_contract_address; + +// Dojo imports + +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + +// Internal imports + +use matchmaker::store::{Store, StoreTrait}; +use matchmaker::models::player::{Player, PlayerTrait, PlayerAssert}; +use matchmaker::models::registry::{Registry, RegistryTrait, RegistryAssert}; +use matchmaker::models::league::{League, LeagueTrait, LeagueAssert}; +use matchmaker::models::slot::{Slot, SlotTrait}; +use matchmaker::systems::maker::IMakerDispatcherTrait; +use matchmaker::tests::setup::{setup, setup::Systems}; + +#[test] +fn test_maker_subscribe() { + // [Setup] + let (world, systems, context) = setup::spawn(); + let store = StoreTrait::new(world); + + // [Create] + systems.maker.create(world); + + // [Subscribe] + systems.maker.subscribe(world); + + // [Assert] Player + let player = store.player(context.registry_id, context.player_id); + assert(player.league_id != 0, 'Sub: wrong league id'); +} diff --git a/examples/matchmaker/src/tests/test_suscribe.cairo b/examples/matchmaker/src/tests/test_suscribe.cairo deleted file mode 100644 index 8b137891..00000000 --- a/examples/matchmaker/src/tests/test_suscribe.cairo +++ /dev/null @@ -1 +0,0 @@ - diff --git a/examples/matchmaker/src/tests/test_unsubscribe.cairo b/examples/matchmaker/src/tests/test_unsubscribe.cairo new file mode 100644 index 00000000..dab14f06 --- /dev/null +++ b/examples/matchmaker/src/tests/test_unsubscribe.cairo @@ -0,0 +1,41 @@ +// Core imports + +use core::debug::PrintTrait; + +// Starknet imports + +use starknet::testing::set_contract_address; + +// Dojo imports + +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + +// Internal imports + +use matchmaker::store::{Store, StoreTrait}; +use matchmaker::models::player::{Player, PlayerTrait, PlayerAssert}; +use matchmaker::models::registry::{Registry, RegistryTrait, RegistryAssert}; +use matchmaker::models::league::{League, LeagueTrait, LeagueAssert}; +use matchmaker::models::slot::{Slot, SlotTrait}; +use matchmaker::systems::maker::IMakerDispatcherTrait; +use matchmaker::tests::setup::{setup, setup::Systems}; + +#[test] +fn test_maker_unsubscribe() { + // [Setup] + let (world, systems, context) = setup::spawn(); + let store = StoreTrait::new(world); + + // [Create] + systems.maker.create(world); + + // [Subscribe] + systems.maker.subscribe(world); + + // [Subscribe] + systems.maker.unsubscribe(world); + + // [Assert] Player + let player = store.player(context.registry_id, context.player_id); + assert(player.league_id == 0, 'Unsub: wrong league id'); +} From 8e0c127e5defbf425dece9e86771d6045a038d7a Mon Sep 17 00:00:00 2001 From: bal7hazar Date: Sat, 20 Apr 2024 09:25:53 +0200 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=90=9B=20Fix=20slot=20manamgent=20iss?= =?UTF-8?q?ue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/matchmaker/src/constants.cairo | 2 + examples/matchmaker/src/models/league.cairo | 49 ++++---- examples/matchmaker/src/models/player.cairo | 22 ++-- examples/matchmaker/src/models/registry.cairo | 41 +++---- examples/matchmaker/src/models/slot.cairo | 13 ++- examples/matchmaker/src/store.cairo | 21 ++-- examples/matchmaker/src/systems/maker.cairo | 59 +++++----- examples/matchmaker/src/tests/setup.cairo | 26 ++--- .../matchmaker/src/tests/test_create.cairo | 2 +- .../matchmaker/src/tests/test_fight.cairo | 105 +++++++++++++++++- .../matchmaker/src/tests/test_subscribe.cairo | 2 +- .../src/tests/test_unsubscribe.cairo | 2 +- 12 files changed, 226 insertions(+), 118 deletions(-) diff --git a/examples/matchmaker/src/constants.cairo b/examples/matchmaker/src/constants.cairo index 58186d54..42b296f7 100644 --- a/examples/matchmaker/src/constants.cairo +++ b/examples/matchmaker/src/constants.cairo @@ -6,6 +6,8 @@ const DEFAULT_RATING: u32 = 1000; // Leagues const LEAGUE_SIZE: u8 = 20; +const LEAGUE_COUNT: u8 = 17; +const LEAGUE_MIN_THRESHOLD: u32 = 1000; // World diff --git a/examples/matchmaker/src/models/league.cairo b/examples/matchmaker/src/models/league.cairo index 1bb9baa6..28e6a2a0 100644 --- a/examples/matchmaker/src/models/league.cairo +++ b/examples/matchmaker/src/models/league.cairo @@ -5,7 +5,7 @@ use starknet::ContractAddress; // Internal imports -use matchmaker::constants::LEAGUE_SIZE; +use matchmaker::constants::{LEAGUE_SIZE, LEAGUE_COUNT, LEAGUE_MIN_THRESHOLD}; use matchmaker::models::player::{Player, PlayerTrait, PlayerAssert}; use matchmaker::models::slot::{Slot, SlotTrait}; @@ -33,7 +33,14 @@ impl LeagueImpl of LeagueTrait { #[inline(always)] fn compute_id(rating: u32) -> u8 { - let id = rating / LEAGUE_SIZE.into(); + if rating <= LEAGUE_MIN_THRESHOLD { + return 1; + } + let max_rating = LEAGUE_MIN_THRESHOLD + LEAGUE_SIZE.into() * LEAGUE_COUNT.into(); + if rating >= max_rating { + return LEAGUE_COUNT; + } + let id = 1 + (rating - LEAGUE_MIN_THRESHOLD) / LEAGUE_SIZE.into(); if id > 251 { 251 } else if id < 1 { @@ -90,7 +97,10 @@ mod tests { // Local imports - use super::{League, LeagueTrait, Player, PlayerTrait, ContractAddress}; + use super::{ + League, LeagueTrait, Player, PlayerTrait, ContractAddress, LEAGUE_SIZE, LEAGUE_COUNT, + LEAGUE_MIN_THRESHOLD + }; // Constants @@ -98,29 +108,30 @@ mod tests { starknet::contract_address_const::<'PLAYER'>() } - const REGISTER_ID: u32 = 1; + const PLAYER_NAME: felt252 = 'NAME'; + const REGISTRY_ID: u32 = 1; const LEAGUE_ID: u8 = 1; #[test] fn test_new() { - let league = LeagueTrait::new(REGISTER_ID, LEAGUE_ID); - assert_eq!(league.registry_id, REGISTER_ID); + let league = LeagueTrait::new(REGISTRY_ID, LEAGUE_ID); + assert_eq!(league.registry_id, REGISTRY_ID); assert_eq!(league.id, LEAGUE_ID); assert_eq!(league.size, 0); } #[test] fn test_compute_id() { - let rating = 1000; + let rating = LEAGUE_MIN_THRESHOLD - 1; let league_id = LeagueTrait::compute_id(rating); - assert_eq!(league_id, 50); + assert_eq!(league_id, 1); } #[test] fn test_compute_id_overflow() { - let rating = 10000; - let league_id = LeagueTrait::compute_id(rating); - assert_eq!(league_id, 251); + let max_rating = LEAGUE_MIN_THRESHOLD + LEAGUE_SIZE.into() * LEAGUE_COUNT.into() + 1; + let league_id = LeagueTrait::compute_id(max_rating); + assert_eq!(league_id, LEAGUE_COUNT); } #[test] @@ -132,8 +143,8 @@ mod tests { #[test] fn test_subscribe_once() { - let mut player = PlayerTrait::new(REGISTER_ID, PLAYER()); - let mut league = LeagueTrait::new(REGISTER_ID, LEAGUE_ID); + let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER(), PLAYER_NAME); + let mut league = LeagueTrait::new(REGISTRY_ID, LEAGUE_ID); let slot = LeagueTrait::subscribe(ref league, ref player); // [Assert] League assert_eq!(league.size, 1); @@ -147,16 +158,16 @@ mod tests { #[test] #[should_panic(expected: ('Player: not subscribable',))] fn test_subscribe_twice() { - let mut player = PlayerTrait::new(REGISTER_ID, PLAYER()); - let mut league = LeagueTrait::new(REGISTER_ID, LEAGUE_ID); + let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER(), PLAYER_NAME); + let mut league = LeagueTrait::new(REGISTRY_ID, LEAGUE_ID); LeagueTrait::subscribe(ref league, ref player); LeagueTrait::subscribe(ref league, ref player); } #[test] fn test_unsubscribe_once() { - let mut player = PlayerTrait::new(REGISTER_ID, PLAYER()); - let mut league = LeagueTrait::new(REGISTER_ID, LEAGUE_ID); + let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER(), PLAYER_NAME); + let mut league = LeagueTrait::new(REGISTRY_ID, LEAGUE_ID); LeagueTrait::subscribe(ref league, ref player); LeagueTrait::unsubscribe(ref league, ref player); // [Assert] League @@ -169,8 +180,8 @@ mod tests { #[test] #[should_panic(expected: ('League: player not subscribed',))] fn test_unsubscribe_twice() { - let mut player = PlayerTrait::new(REGISTER_ID, PLAYER()); - let mut league = LeagueTrait::new(REGISTER_ID, LEAGUE_ID); + let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER(), PLAYER_NAME); + let mut league = LeagueTrait::new(REGISTRY_ID, LEAGUE_ID); LeagueTrait::subscribe(ref league, ref player); LeagueTrait::unsubscribe(ref league, ref player); LeagueTrait::unsubscribe(ref league, ref player); diff --git a/examples/matchmaker/src/models/player.cairo b/examples/matchmaker/src/models/player.cairo index d6da4540..69d7e826 100644 --- a/examples/matchmaker/src/models/player.cairo +++ b/examples/matchmaker/src/models/player.cairo @@ -29,6 +29,7 @@ struct Player { registry_id: u32, #[key] id: ContractAddress, + name: felt252, league_id: u8, index: u32, rating: u32, @@ -37,8 +38,8 @@ struct Player { #[generate_trait] impl PlayerImpl of PlayerTrait { #[inline(always)] - fn new(registry_id: u32, id: ContractAddress) -> Player { - Player { registry_id, id, league_id: 0, index: 0, rating: DEFAULT_RATING, } + fn new(registry_id: u32, id: ContractAddress, name: felt252) -> Player { + Player { registry_id, id, name, league_id: 0, index: 0, rating: DEFAULT_RATING, } } #[inline(always)] @@ -89,7 +90,7 @@ impl PlayerAssert of AssertTrait { impl PlayerZeroable of Zeroable { #[inline(always)] fn zero() -> Player { - Player { registry_id: 0, id: ZERO(), league_id: 0, index: 0, rating: 0, } + Player { registry_id: 0, id: ZERO(), name: 0, league_id: 0, index: 0, rating: 0, } } #[inline(always)] @@ -119,13 +120,14 @@ mod tests { starknet::contract_address_const::<'PLAYER'>() } - const REGISTER_ID: u32 = 1; + const PLAYER_NAME: felt252 = 'NAME'; + const REGISTRY_ID: u32 = 1; #[test] fn test_new() { let player_id = PLAYER(); - let player = PlayerTrait::new(REGISTER_ID, player_id); - assert_eq!(player.registry_id, REGISTER_ID); + let player = PlayerTrait::new(REGISTRY_ID, player_id, PLAYER_NAME); + assert_eq!(player.registry_id, REGISTRY_ID); assert_eq!(player.id, player_id); assert_eq!(player.league_id, 0); assert_eq!(player.index, 0); @@ -135,7 +137,7 @@ mod tests { #[test] fn test_subscribable() { let player_id = PLAYER(); - let player = PlayerTrait::new(REGISTER_ID, player_id); + let player = PlayerTrait::new(REGISTRY_ID, player_id, PLAYER_NAME); AssertTrait::assert_subscribable(player); } @@ -143,7 +145,7 @@ mod tests { #[should_panic(expected: ('Player: not subscribable',))] fn test_subscribable_revert_not_subscribable() { let player_id = PLAYER(); - let mut player = PlayerTrait::new(REGISTER_ID, player_id); + let mut player = PlayerTrait::new(REGISTRY_ID, player_id, PLAYER_NAME); player.league_id = 1; AssertTrait::assert_subscribable(player); } @@ -151,7 +153,7 @@ mod tests { #[test] fn test_subscribed() { let player_id = PLAYER(); - let mut player = PlayerTrait::new(REGISTER_ID, player_id); + let mut player = PlayerTrait::new(REGISTRY_ID, player_id, PLAYER_NAME); player.league_id = 1; AssertTrait::assert_subscribed(player); } @@ -160,7 +162,7 @@ mod tests { #[should_panic(expected: ('Player: not subscribed',))] fn test_subscribed_revert_not_subscribed() { let player_id = PLAYER(); - let player = PlayerTrait::new(REGISTER_ID, player_id); + let player = PlayerTrait::new(REGISTRY_ID, player_id, PLAYER_NAME); AssertTrait::assert_subscribed(player); } } diff --git a/examples/matchmaker/src/models/registry.cairo b/examples/matchmaker/src/models/registry.cairo index 1904d1a6..46544a3f 100644 --- a/examples/matchmaker/src/models/registry.cairo +++ b/examples/matchmaker/src/models/registry.cairo @@ -47,7 +47,7 @@ impl RegistryImpl of RegistryTrait { } #[inline(always)] - fn search_league(mut self: Registry, mut league: League, mut player: Player) -> u8 { + fn search_league(ref self: Registry, ref league: League, ref player: Player) -> u8 { // [Check] Player has subscribed PlayerAssert::assert_subscribed(player); // [Effect] Unsubcribe player from his league @@ -110,6 +110,7 @@ mod tests { starknet::contract_address_const::<'TARGET'>() } + const PLAYER_NAME: felt252 = 'NAME'; const REGISTRY_ID: u32 = 1; const LEAGUE_ID: u8 = 1; const CLOSEST_LEAGUE_ID: u8 = 2; @@ -127,7 +128,7 @@ mod tests { #[test] fn test_subscribe() { let mut registry = RegistryTrait::new(REGISTRY_ID); - let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER()); + let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER(), PLAYER_NAME); let mut league = LeagueTrait::new(REGISTRY_ID, LEAGUE_ID); registry.subscribe(ref league, ref player); // [Assert] Registry @@ -137,7 +138,7 @@ mod tests { #[test] fn test_unsubscribe() { let mut registry = RegistryTrait::new(REGISTRY_ID); - let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER()); + let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER(), PLAYER_NAME); let mut league = LeagueTrait::new(REGISTRY_ID, LEAGUE_ID); registry.subscribe(ref league, ref player); registry.unsubscribe(ref league, ref player); @@ -149,11 +150,11 @@ mod tests { fn test_search_league_same() { let mut registry = RegistryTrait::new(REGISTRY_ID); let mut league = LeagueTrait::new(REGISTRY_ID, LEAGUE_ID); - let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER()); + let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER(), PLAYER_NAME); registry.subscribe(ref league, ref player); - let mut foe = PlayerTrait::new(REGISTRY_ID, TARGET()); + let mut foe = PlayerTrait::new(REGISTRY_ID, TARGET(), PLAYER_NAME); registry.subscribe(ref league, ref foe); - let league_id = registry.search_league(league, player); + let league_id = registry.search_league(ref league, ref player); // [Assert] Registry assert(league_id == LEAGUE_ID, 'Registry: wrong search league'); } @@ -162,12 +163,12 @@ mod tests { fn test_search_league_close() { let mut registry = RegistryTrait::new(REGISTRY_ID); let mut league = LeagueTrait::new(REGISTRY_ID, LEAGUE_ID); - let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER()); + let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER(), PLAYER_NAME); registry.subscribe(ref league, ref player); let mut foe_league = LeagueTrait::new(REGISTRY_ID, CLOSEST_LEAGUE_ID); - let mut foe = PlayerTrait::new(REGISTRY_ID, TARGET()); + let mut foe = PlayerTrait::new(REGISTRY_ID, TARGET(), PLAYER_NAME); registry.subscribe(ref foe_league, ref foe); - let league_id = registry.search_league(league, player); + let league_id = registry.search_league(ref league, ref player); // [Assert] Registry assert(league_id == CLOSEST_LEAGUE_ID, 'Registry: wrong search league'); } @@ -176,12 +177,12 @@ mod tests { fn test_search_league_target() { let mut registry = RegistryTrait::new(REGISTRY_ID); let mut league = LeagueTrait::new(REGISTRY_ID, LEAGUE_ID); - let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER()); + let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER(), PLAYER_NAME); registry.subscribe(ref league, ref player); let mut foe_league = LeagueTrait::new(REGISTRY_ID, TARGET_LEAGUE_ID); - let mut foe = PlayerTrait::new(REGISTRY_ID, TARGET()); + let mut foe = PlayerTrait::new(REGISTRY_ID, TARGET(), PLAYER_NAME); registry.subscribe(ref foe_league, ref foe); - let league_id = registry.search_league(league, player); + let league_id = registry.search_league(ref league, ref player); // [Assert] Registry assert(league_id == TARGET_LEAGUE_ID, 'Registry: wrong search league'); } @@ -190,12 +191,12 @@ mod tests { fn test_search_league_far_down_top() { let mut registry = RegistryTrait::new(REGISTRY_ID); let mut league = LeagueTrait::new(REGISTRY_ID, LEAGUE_ID); - let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER()); + let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER(), PLAYER_NAME); registry.subscribe(ref league, ref player); let mut foe_league = LeagueTrait::new(REGISTRY_ID, FAREST_LEAGUE_ID); - let mut foe = PlayerTrait::new(REGISTRY_ID, TARGET()); + let mut foe = PlayerTrait::new(REGISTRY_ID, TARGET(), PLAYER_NAME); registry.subscribe(ref foe_league, ref foe); - let league_id = registry.search_league(league, player); + let league_id = registry.search_league(ref league, ref player); // [Assert] Registry assert(league_id == FAREST_LEAGUE_ID, 'Registry: wrong search league'); } @@ -204,12 +205,12 @@ mod tests { fn test_search_league_far_top_down() { let mut registry = RegistryTrait::new(REGISTRY_ID); let mut league = LeagueTrait::new(REGISTRY_ID, FAREST_LEAGUE_ID); - let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER()); + let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER(), PLAYER_NAME); registry.subscribe(ref league, ref player); let mut foe_league = LeagueTrait::new(REGISTRY_ID, LEAGUE_ID); - let mut foe = PlayerTrait::new(REGISTRY_ID, TARGET()); + let mut foe = PlayerTrait::new(REGISTRY_ID, TARGET(), PLAYER_NAME); registry.subscribe(ref foe_league, ref foe); - let league_id = registry.search_league(league, player); + let league_id = registry.search_league(ref league, ref player); // [Assert] Registry assert(league_id == LEAGUE_ID, 'Registry: wrong search league'); } @@ -219,8 +220,8 @@ mod tests { fn test_search_league_revert_empty() { let mut registry = RegistryTrait::new(REGISTRY_ID); let mut league = LeagueTrait::new(REGISTRY_ID, LEAGUE_ID); - let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER()); + let mut player = PlayerTrait::new(REGISTRY_ID, PLAYER(), PLAYER_NAME); registry.subscribe(ref league, ref player); - registry.search_league(league, player); + registry.search_league(ref league, ref player); } } diff --git a/examples/matchmaker/src/models/slot.cairo b/examples/matchmaker/src/models/slot.cairo index ab1c5056..c36490b2 100644 --- a/examples/matchmaker/src/models/slot.cairo +++ b/examples/matchmaker/src/models/slot.cairo @@ -4,6 +4,7 @@ use starknet::ContractAddress; // Internal imports +use matchmaker::constants::ZERO; use matchmaker::models::player::{Player, PlayerTrait}; #[derive(Model, Copy, Drop, Serde)] @@ -28,6 +29,11 @@ impl SlotImpl of SlotTrait { player_id: player.id, } } + + #[inline(always)] + fn nullify(ref self: Slot) { + self.player_id = ZERO(); + } } #[cfg(test)] @@ -46,18 +52,19 @@ mod tests { starknet::contract_address_const::<'PLAYER'>() } - const REGISTER_ID: u32 = 1; + const PLAYER_NAME: felt252 = 'NAME'; + const REGISTRY_ID: u32 = 1; const LEAGUE_ID: u8 = 2; const INDEX: u32 = 3; #[test] fn test_new() { let player_id = PLAYER(); - let mut player = PlayerTrait::new(REGISTER_ID, player_id); + let mut player = PlayerTrait::new(REGISTRY_ID, player_id, PLAYER_NAME); player.league_id = LEAGUE_ID; player.index = INDEX; let slot = SlotTrait::new(player); - assert_eq!(slot.registry_id, REGISTER_ID); + assert_eq!(slot.registry_id, REGISTRY_ID); assert_eq!(slot.league_id, LEAGUE_ID); assert_eq!(slot.index, INDEX); assert_eq!(slot.player_id, player_id); diff --git a/examples/matchmaker/src/store.cairo b/examples/matchmaker/src/store.cairo index e74c6425..1002bb06 100644 --- a/examples/matchmaker/src/store.cairo +++ b/examples/matchmaker/src/store.cairo @@ -17,7 +17,7 @@ use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; use matchmaker::models::league::{League, LeagueTrait}; use matchmaker::models::player::Player; use matchmaker::models::registry::Registry; -use matchmaker::models::slot::Slot; +use matchmaker::models::slot::{Slot, SlotTrait}; /// Store struct. @@ -77,15 +77,20 @@ impl StoreImpl of StoreTrait { #[inline(always)] fn remove_player_slot(self: Store, player: Player) { // [Effect] Replace the slot with the last slot if needed - let mut league = self.league(player.registry_id, player.league_id); - let mut last_slot = self.slot(league.registry_id, player.league_id, league.size - 1); - if last_slot.player_id != player.id { - last_slot.index = player.index; + let league = self.league(player.registry_id, player.league_id); + let mut player_slot = self.slot(player.registry_id, player.league_id, player.index); + let mut last_slot = self.slot(player.registry_id, player.league_id, league.size - 1); + if last_slot.index != player_slot.index { + let mut last_player = self.player(player.registry_id, last_slot.player_id); + let last_slot_index = last_slot.index; + last_slot.index = player_slot.index; + last_player.index = player_slot.index; + player_slot.index = last_slot_index; + self.set_player(last_player); self.set_slot(last_slot); } // [Effect] Remove the last slot - let mut empty_slot = self.slot(league.registry_id, player.league_id, league.size); - empty_slot.index = league.size - 1; - self.set_slot(empty_slot); + player_slot.nullify(); + self.set_slot(player_slot); } } diff --git a/examples/matchmaker/src/systems/maker.cairo b/examples/matchmaker/src/systems/maker.cairo index ae49071c..ad9f1047 100644 --- a/examples/matchmaker/src/systems/maker.cairo +++ b/examples/matchmaker/src/systems/maker.cairo @@ -10,7 +10,7 @@ use dojo::world::IWorldDispatcher; #[starknet::interface] trait IMaker { - fn create(self: @TContractState, world: IWorldDispatcher); + fn create(self: @TContractState, world: IWorldDispatcher, name: felt252); fn subscribe(self: @TContractState, world: IWorldDispatcher); fn unsubscribe(self: @TContractState, world: IWorldDispatcher); fn fight(self: @TContractState, world: IWorldDispatcher); @@ -80,7 +80,7 @@ mod maker { #[abi(embed_v0)] impl MakerImpl of IMaker { - fn create(self: @ContractState, world: IWorldDispatcher) { + fn create(self: @ContractState, world: IWorldDispatcher, name: felt252) { // [Setup] Datastore let mut store: Store = StoreTrait::new(world); @@ -90,7 +90,7 @@ mod maker { PlayerAssert::assert_not_exist(player); // [Effect] Create one - let player = PlayerTrait::new(0, caller); + let player = PlayerTrait::new(0, caller, name); store.set_player(player); } @@ -158,48 +158,45 @@ mod maker { let mut player = store.player(0, caller); PlayerAssert::assert_does_exist(player); - // [Compute] Search opponent + // [Effect] Remove slot + store.remove_player_slot(player); + + // [Effect] Search opponent which unsubscribe, then update league let seed = get_tx_info().unbox().transaction_hash; let mut registry = store.registry(0); let mut player_league = store.league(0, player.league_id); - let foe_league_id = registry.search_league(player_league, player); + let foe_league_id = registry.search_league(ref player_league, ref player); + store.set_league(player_league); + + // [Compute] Foe let mut foe_league = store.league(0, foe_league_id); let foe_slot_id = foe_league.search_player(seed); let foe_slot = store.slot(0, foe_league_id, foe_slot_id); let mut foe = store.player(0, foe_slot.player_id); - // [Effect] Fight - player.fight(ref foe, seed); - - // [Effect] Remove player slot - store.remove_player_slot(player); - - // [Effect] Update Player league and slot - registry.unsubscribe(ref player_league, ref player); - let league_id = LeagueTrait::compute_id(player.rating); - let mut player_league = store.league(0, league_id); - let player_slot = registry.subscribe(ref player_league, ref player); - - // [Effect] Remove foe slot + // [Effect] Remove foe slot, unsubscribe and update league store.remove_player_slot(foe); - - // [Effect] Update Foe league and slot registry.unsubscribe(ref foe_league, ref foe); - let foe_league_id = LeagueTrait::compute_id(foe.rating); - let mut foe_league = store.league(0, foe_league_id); - let foe_slot = registry.subscribe(ref foe_league, ref foe); + store.set_league(foe_league); - // [Effect] Update Slots - store.set_slot(player_slot); - store.set_slot(foe_slot); + // [Effect] Fight and update players + player.fight(ref foe, seed); - // [Effect] Update Players + // [Effect] Update player, league and slot + let league_id = LeagueTrait::compute_id(player.rating); + let mut league = store.league(0, league_id); + let slot = registry.subscribe(ref league, ref player); + store.set_league(league); + store.set_slot(slot); store.set_player(player); - store.set_player(foe); - // [Effect] Update League - store.set_league(player_league); - store.set_league(foe_league); + // [Effect] Update Foe, league and slot + let league_id = LeagueTrait::compute_id(foe.rating); + let mut league = store.league(0, league_id); + let slot = registry.subscribe(ref league, ref foe); + store.set_league(league); + store.set_slot(slot); + store.set_player(foe); // [Effect] Update Registry store.set_registry(registry); diff --git a/examples/matchmaker/src/tests/setup.cairo b/examples/matchmaker/src/tests/setup.cairo index cee0866a..9fc86243 100644 --- a/examples/matchmaker/src/tests/setup.cairo +++ b/examples/matchmaker/src/tests/setup.cairo @@ -27,18 +27,12 @@ mod setup { starknet::contract_address_const::<'PLAYER'>() } - fn ANYONE() -> ContractAddress { - starknet::contract_address_const::<'ANYONE'>() - } - fn SOMEONE() -> ContractAddress { starknet::contract_address_const::<'SOMEONE'>() } - fn NOONE() -> ContractAddress { - starknet::contract_address_const::<'NOONE'>() - } - + const PLAYER_NAME: felt252 = 'PLAYER'; + const SOMEONE_NAME: felt252 = 'SOMEONE'; const REGISTRY_ID: u32 = 0; #[derive(Drop)] @@ -51,8 +45,8 @@ mod setup { registry_id: u32, player_id: ContractAddress, someone_id: ContractAddress, - anyone_id: ContractAddress, - noone_id: ContractAddress, + player_name: felt252, + someone_name: felt252, } #[inline(always)] @@ -71,21 +65,15 @@ mod setup { // [Setup] Context set_contract_address(SOMEONE()); - systems.maker.create(world); - systems.maker.subscribe(world); - set_contract_address(ANYONE()); - systems.maker.create(world); - systems.maker.subscribe(world); - set_contract_address(NOONE()); - systems.maker.create(world); + systems.maker.create(world, SOMEONE_NAME); systems.maker.subscribe(world); set_contract_address(PLAYER()); let context = Context { registry_id: REGISTRY_ID, player_id: PLAYER(), someone_id: SOMEONE(), - anyone_id: ANYONE(), - noone_id: NOONE() + player_name: PLAYER_NAME, + someone_name: SOMEONE_NAME, }; // [Return] diff --git a/examples/matchmaker/src/tests/test_create.cairo b/examples/matchmaker/src/tests/test_create.cairo index 6a41fbae..f77607a7 100644 --- a/examples/matchmaker/src/tests/test_create.cairo +++ b/examples/matchmaker/src/tests/test_create.cairo @@ -27,7 +27,7 @@ fn test_maker_create() { let store = StoreTrait::new(world); // [Create] - systems.maker.create(world); + systems.maker.create(world, context.player_name); // [Assert] Player let player = store.player(context.registry_id, context.player_id); diff --git a/examples/matchmaker/src/tests/test_fight.cairo b/examples/matchmaker/src/tests/test_fight.cairo index 3c8d83f5..ca682dbf 100644 --- a/examples/matchmaker/src/tests/test_fight.cairo +++ b/examples/matchmaker/src/tests/test_fight.cairo @@ -4,7 +4,7 @@ use core::debug::PrintTrait; // Starknet imports -use starknet::testing::set_contract_address; +use starknet::testing::{set_contract_address, set_transaction_hash}; // Dojo imports @@ -12,6 +12,7 @@ use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; // Internal imports +use matchmaker::constants::DEFAULT_RATING; use matchmaker::store::{Store, StoreTrait}; use matchmaker::models::player::{Player, PlayerTrait, PlayerAssert}; use matchmaker::models::registry::{Registry, RegistryTrait, RegistryAssert}; @@ -21,23 +22,117 @@ use matchmaker::systems::maker::IMakerDispatcherTrait; use matchmaker::tests::setup::{setup, setup::Systems}; #[test] -fn test_maker_fight() { +fn test_maker_fight_lose() { // [Setup] let (world, systems, context) = setup::spawn(); let store = StoreTrait::new(world); // [Create] - systems.maker.create(world); + systems.maker.create(world, context.player_name); // [Subscribe] systems.maker.subscribe(world); // [Fight] + set_transaction_hash(0); + systems.maker.fight(world); + + // [Assert] Player let player = store.player(context.registry_id, context.player_id); - let rating = player.rating; + assert(player.rating != DEFAULT_RATING, 'Fight: wrong player rating'); + + // [Assert] Someone + let someone = store.player(context.registry_id, context.someone_id); + assert(someone.rating != DEFAULT_RATING, 'Fight: wrong player rating'); + + // [Assert] Global rating + let total = 2 * DEFAULT_RATING; + assert(player.rating + someone.rating == total, 'Fight: wrong global rating'); +} + +#[test] +fn test_maker_fight_draw() { + // [Setup] + let (world, systems, context) = setup::spawn(); + let store = StoreTrait::new(world); + + // [Create] + systems.maker.create(world, context.player_name); + + // [Subscribe] + systems.maker.subscribe(world); + + // [Fight] + set_transaction_hash(1); + systems.maker.fight(world); + + // [Assert] Player + let player = store.player(context.registry_id, context.player_id); + assert(player.rating == DEFAULT_RATING, 'Fight: wrong player rating'); + + // [Assert] Someone + let someone = store.player(context.registry_id, context.someone_id); + assert(someone.rating == DEFAULT_RATING, 'Fight: wrong player rating'); + + // [Assert] Global rating + let total = 2 * DEFAULT_RATING; + assert(player.rating + someone.rating == total, 'Fight: wrong global rating'); +} + +#[test] +fn test_maker_fight_win() { + // [Setup] + let (world, systems, context) = setup::spawn(); + let store = StoreTrait::new(world); + + // [Create] + systems.maker.create(world, context.player_name); + + // [Subscribe] + systems.maker.subscribe(world); + + // [Fight] + set_transaction_hash(2); systems.maker.fight(world); // [Assert] Player let player = store.player(context.registry_id, context.player_id); - assert(player.rating != rating, 'Fight: wrong player rating'); + assert(player.rating != DEFAULT_RATING, 'Fight: wrong player rating'); + + // [Assert] Someone + let someone = store.player(context.registry_id, context.someone_id); + assert(someone.rating != DEFAULT_RATING, 'Fight: wrong player rating'); + + // [Assert] Global rating + let total = 2 * DEFAULT_RATING; + assert(player.rating + someone.rating == total, 'Fight: wrong global rating'); +} + +#[test] +fn test_maker_fight_several_times() { + // [Setup] + let (world, systems, context) = setup::spawn(); + let store = StoreTrait::new(world); + + // [Create] + systems.maker.create(world, context.player_name); + + // [Subscribe] + systems.maker.subscribe(world); + + // [Fight] + let mut iter = 2; + loop { + if iter == 0 { + break; + } + systems.maker.fight(world); + iter -= 1; + }; + + // [Assert] Global rating + let total = 2 * DEFAULT_RATING; + let player = store.player(context.registry_id, context.player_id); + let someone = store.player(context.registry_id, context.someone_id); + assert(player.rating + someone.rating == total, 'Fight: wrong global rating'); } diff --git a/examples/matchmaker/src/tests/test_subscribe.cairo b/examples/matchmaker/src/tests/test_subscribe.cairo index 5c5555d4..b208ba37 100644 --- a/examples/matchmaker/src/tests/test_subscribe.cairo +++ b/examples/matchmaker/src/tests/test_subscribe.cairo @@ -27,7 +27,7 @@ fn test_maker_subscribe() { let store = StoreTrait::new(world); // [Create] - systems.maker.create(world); + systems.maker.create(world, context.player_name); // [Subscribe] systems.maker.subscribe(world); diff --git a/examples/matchmaker/src/tests/test_unsubscribe.cairo b/examples/matchmaker/src/tests/test_unsubscribe.cairo index dab14f06..8aa89cbc 100644 --- a/examples/matchmaker/src/tests/test_unsubscribe.cairo +++ b/examples/matchmaker/src/tests/test_unsubscribe.cairo @@ -27,7 +27,7 @@ fn test_maker_unsubscribe() { let store = StoreTrait::new(world); // [Create] - systems.maker.create(world); + systems.maker.create(world, context.player_name); // [Subscribe] systems.maker.subscribe(world);