Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions packages/interfaces/src/distribution.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,55 @@ pub enum Distribution {
Exponential: u16,
Uniform,
Custom: Span<u16>,
/// Geometric decay as a rational ratio `(a, b)`: each position receives
/// `b / a` of the one above it, so `W(p) = a^(n-p) * b^(p-1)`. Requires
/// `a > b > 0` — e.g. `(10, 7)` is "each place gets 70% of the previous".
///
/// Unlike `Exponential` — which is a power law, and whose winner share
/// falls off as roughly `(k+1)/n` — a geometric curve's shape does not
/// depend on the size of the field: first place takes about `1 - b/a` of
/// the pool whether there are 10 paid places or 100. That is the shape a
/// headline first prize actually needs, and no `Exponential` weight
/// produces it over a large field.
///
/// The trade is reach: the weights span `(a/b)^n`, so the representable
/// field size shrinks as the ratio gets finer. See
/// `max_geometric_payouts`.
///
/// NOTE: appended deliberately. Serde indices are positional, so inserting
/// this anywhere earlier would silently reinterpret every stored and
/// indexed distribution.
Geometric: (u16, u16),
/// Two tiers: a geometric head over the first `head_count` places taking
/// `head_share_bps` of the pool, and the remaining places splitting the
/// rest evenly.
///
/// This is the only family here that works for a very large field. A
/// single curve cannot: anything steep enough to give first place a real
/// share rounds its tail to nothing, and anything flat enough to pay the
/// tail gives first place nothing. Over 10,000 places the best a single
/// curve can do for first place is ~0.06% (`Exponential` k=5); a
/// `Geometric` head of 39 on (10, 7) taking 80% pays first place 24%,
/// while every one of the other 9,961 places still receives its slice of
/// the remaining 20%.
///
/// The geometric reach bound applies to `head_count`, not the field, so
/// the head is always well inside it. Requires a fixed paid-places count
/// strictly greater than `head_count`.
Tiered: TieredConfig,
}

/// Configuration for `Distribution::Tiered`.
#[derive(Drop, Copy, Serde, PartialEq)]
pub struct TieredConfig {
/// Geometric decay `(a, b)` for the head: each place gets `b / a` of the
/// one above. Same semantics and validity rules as `Geometric`.
pub head_ratio: (u16, u16),
/// How many places the head covers. Must be under the paid-places count
/// and within `max_geometric_payouts(a)`.
pub head_count: u16,
/// The head's slice of the pool, in basis points. Strictly between 0 and
/// 10000 — at either extreme one of the tiers would round to an
/// unclaimable zero, and the single-curve variants cover those shapes.
pub head_share_bps: u16,
}
53 changes: 44 additions & 9 deletions packages/metagame/src/entry_fee/entry_fee_store.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use game_components_utilities::distribution::packed_shares::{
calculate_slot_position,
};
use game_components_utilities::distribution::structs::{
DIST_TYPE_CUSTOM, DIST_TYPE_EXPONENTIAL, DIST_TYPE_LINEAR, DIST_TYPE_UNIFORM,
PackedDistribution,
DIST_TYPE_CUSTOM, DIST_TYPE_EXPONENTIAL, DIST_TYPE_GEOMETRIC, DIST_TYPE_LINEAR,
DIST_TYPE_TIERED, DIST_TYPE_UNIFORM, PackedDistribution, TieredConfig,
};
use starknet::ContractAddress;
use crate::entry_fee::store::Store;
Expand Down Expand Up @@ -98,6 +98,30 @@ pub impl EntryFeeStoreImpl<T, +Store<T>, +Drop<T>> of EntryFeeStoreTrait<T> {
(Option::Some(Distribution::Exponential(packed_dist.dist_param)), packed_dist.positions)
} else if packed_dist.dist_type == DIST_TYPE_UNIFORM {
(Option::Some(Distribution::Uniform), packed_dist.positions)
} else if packed_dist.dist_type == DIST_TYPE_TIERED {
(
Option::Some(
Distribution::Tiered(
TieredConfig {
head_ratio: (
packed_dist.dist_param / 256, packed_dist.dist_param % 256,
),
head_count: packed_dist.dist_param2,
head_share_bps: packed_dist.dist_param3,
},
),
),
packed_dist.positions,
)
} else if packed_dist.dist_type == DIST_TYPE_GEOMETRIC {
(
Option::Some(
Distribution::Geometric(
(packed_dist.dist_param / 256, packed_dist.dist_param % 256),
),
),
packed_dist.positions,
)
} else {
// DIST_TYPE_CUSTOM — return with empty shares span. Loading the
// full array is O(N/15) storage reads and is only needed for UI
Expand Down Expand Up @@ -216,13 +240,20 @@ pub impl EntryFeeStoreImpl<T, +Store<T>, +Drop<T>> of EntryFeeStoreTrait<T> {

// Persist the distribution config (shape + paid-places count), and
// for Custom, the shares array in packed out-of-band storage.
let (dist_type, dist_param) = match config.distribution {
Option::None => (DIST_TYPE_LINEAR, 0_u16),
let (dist_type, dist_param, dist_param2, dist_param3) = match config.distribution {
Option::None => (DIST_TYPE_LINEAR, 0_u16, 0_u16, 0_u16),
Option::Some(dist) => match dist {
Distribution::Linear(w) => (DIST_TYPE_LINEAR, *w),
Distribution::Exponential(w) => (DIST_TYPE_EXPONENTIAL, *w),
Distribution::Uniform => (DIST_TYPE_UNIFORM, 0_u16),
Distribution::Custom(_) => (DIST_TYPE_CUSTOM, 0_u16),
Distribution::Linear(w) => (DIST_TYPE_LINEAR, *w, 0_u16, 0_u16),
Distribution::Exponential(w) => (DIST_TYPE_EXPONENTIAL, *w, 0_u16, 0_u16),
Distribution::Uniform => (DIST_TYPE_UNIFORM, 0_u16, 0_u16, 0_u16),
Distribution::Custom(_) => (DIST_TYPE_CUSTOM, 0_u16, 0_u16, 0_u16),
Distribution::Geometric((
a, b,
)) => (DIST_TYPE_GEOMETRIC, *a * 256 + *b, 0_u16, 0_u16),
Distribution::Tiered(cfg) => {
let (a, b) = *cfg.head_ratio;
(DIST_TYPE_TIERED, a * 256 + b, *cfg.head_count, *cfg.head_share_bps)
},
},
};
// For Custom, paid places are defined by the shares array length;
Expand All @@ -235,7 +266,11 @@ pub impl EntryFeeStoreImpl<T, +Store<T>, +Drop<T>> of EntryFeeStoreTrait<T> {
Option::None => 0_u32,
};

self.set_distribution(context_id, PackedDistribution { dist_type, dist_param, positions });
self
.set_distribution(
context_id,
PackedDistribution { dist_type, dist_param, positions, dist_param2, dist_param3 },
);

if let Option::Some(dist) = config.distribution {
if let Distribution::Custom(shares) = dist {
Expand Down
11 changes: 11 additions & 0 deletions packages/metagame/src/prize/prize_component.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,17 @@ pub mod PrizeComponent {
PrizeStoreTrait::get_custom_shares(self, prize_id)
}

/// One custom share by 1-indexed position — a single storage read.
///
/// `_get_prize` deliberately returns Custom with an empty span, so a
/// claim settling one position reads its share through here instead of
/// paying to rebuild the whole curve.
fn _get_custom_share_at(
self: @ComponentState<TContractState>, prize_id: u64, position: u32,
) -> u16 {
PrizeStoreTrait::get_custom_share_at(self, prize_id, position)
}

/// Store a token-prize record (converts to StoredPrize for storage).
/// Extension prizes are not persisted via this path.
fn set_token_record(
Expand Down
30 changes: 27 additions & 3 deletions packages/metagame/src/prize/prize_store.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,16 @@ pub trait PrizeStoreTrait<T> {
/// are routed via the component's `resolve_prize` before this is
/// called and never reach the store bridge.
fn get_token_record(self: @T, prize_id: u64) -> PrizeRecord;
/// Get custom shares for a prize (reconstructs from packed storage)
/// Get custom shares for a prize (reconstructs from packed storage).
///
/// O(count/15) storage reads. Only view surfaces need the whole curve —
/// a claim wants exactly one share, so it calls `get_custom_share_at`.
fn get_custom_shares(self: @T, prize_id: u64) -> Array<u16>;
/// One share by 1-indexed position, without rebuilding the array.
///
/// Mirrors `EntryFeeStoreTrait::get_custom_share_at`. Shares are packed
/// 15 to a felt, so this is a single storage read at any position.
fn get_custom_share_at(self: @T, prize_id: u64, position: u32) -> u16;
/// Store a token prize. Takes the host-assigned context + sponsor
/// alongside the variant payload; converts to StoredPrize for
/// storage.
Expand Down Expand Up @@ -67,10 +75,19 @@ pub impl PrizeStoreImpl<T, +Store<T>, +Drop<T>> of PrizeStoreTrait<T> {
Option::Some(dist) => {
match dist {
game_components_utilities::distribution::structs::Distribution::Custom(_) => {
let shares = self.get_custom_shares(prize_id);
// Return the shape with an empty span
// rather than rebuilding the curve.
// Loading it is O(count/15) storage
// reads on a path that runs on every
// claim, and a claim needs exactly one
// share — `get_custom_share_at`.
// View surfaces call
// `get_custom_shares` explicitly.
// Mirrors the entry-fee store, which
// has always done this.
Option::Some(
game_components_utilities::distribution::structs::Distribution::Custom(
shares.span(),
array![].span(),
Comment on lines +78 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 8 \
  'get_prize\(|get_token_record\(|Distribution::Custom|_get_custom_share_at|get_custom_share_at|get_custom_shares' \
  packages --glob '*.cairo'

Repository: Provable-Games/game-components

Length of output: 50387


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== prize interface excerpt =="
sed -n '80,105p' packages/interfaces/src/prize.cairo

echo "== prize_store getter excerpt =="
sed -n '60,115p' packages/metagame/src/prize/prize_store.cairo
sed -n '190,225p' packages/metagame/src/prize/prize_store.cairo

echo "== prize_component/prize wrapper candidates =="
fd -a 'prize.*\.cairo|prize_impl.*\.cairo|prize_store.*\.cairo' packages/metagame/src 2>/dev/null || true
rg -n --glob '*.cairo' 'get_prize|to_token_record|PrizeStoreTrait|IPrize|IPrizeImpl|impl .*Prize' packages/metagame/src packages/interfaces/src

echo "== tests excerpt around custom get_prize =="
sed -n '380,415p' packages/metagame/src/prize/tests/test_prize_store.cairo

echo "== all get_prize references outside distributions =="
rg -n --glob '*.cairo' 'get_prize\(' packages --glob '!packages/utilities/src/distribution/**'

Repository: Provable-Games/game-components

Length of output: 23459


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== prize_component implementation excerpt =="
sed -n '212,275p' packages/metagame/src/prize/prize_component.cairo
sed -n '530,558p' packages/metagame/src/prize/prize_component.cairo

echo "== structs to_token_record and custom packing excerpts =="
sed -n '125,155p' packages/metagame/src/prize/structs.cairo
sed -n '198,220p' packages/metagame/src/prize/structs.cairo

echo "== external call references to IPrize/get_prize =="
rg -n --glob '*.cairo' 'IPrizeDispatcher|IPrizeDispatcherTrait|get_prize\(' packages --glob '!packages/metagame/src/prize/**'

echo "== broader references to custom distribution shape expectation =="
rg -n --glob '*.cairo' 'get_prize.*Custom|Custom.*get_prize|reread\.len|get_prize must not rebuild|get_token_record|get_custom_shares|get_custom_share_at' packages --glob '!packages/utilities/src/distribution/**'

Repository: Provable-Games/game-components

Length of output: 13998


Preserve the full custom share span on IPrize.get_prize.

PrizeComponentImpl::get_prize returns the result of get_token_record, which converts every stored Distribution::Custom to Custom(array![].span()). The public IPrize view therefore exposes an incomplete PrizeRecord. Keep get_prize returning the reconstructed custom shares, and have claim paths call the indexed share helper instead.

📍 Affects 2 files
  • packages/metagame/src/prize/prize_store.cairo#L78-L90 (this comment)
  • packages/metagame/src/prize/tests/test_prize_store.cairo#L401-L411
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/metagame/src/prize/prize_store.cairo` around lines 78 - 90, Update
packages/metagame/src/prize/prize_store.cairo:78-90 so
PrizeComponentImpl::get_prize returns the fully reconstructed custom share span
through get_token_record, while claim paths use get_custom_share_at for the
indexed share. Update
packages/metagame/src/prize/tests/test_prize_store.cairo:401-411 to assert that
IPrize.get_prize exposes all stored custom shares.

),
)
},
Expand Down Expand Up @@ -99,6 +116,13 @@ pub impl PrizeStoreImpl<T, +Store<T>, +Drop<T>> of PrizeStoreTrait<T> {
record
}

fn get_custom_share_at(self: @T, prize_id: u64, position: u32) -> u16 {
let index: u32 = position - 1;
let slot_index: u8 = (index / CUSTOM_SHARES_PER_SLOT.into()).try_into().unwrap();
let index_in_slot: u8 = (index % CUSTOM_SHARES_PER_SLOT.into()).try_into().unwrap();
Store::get_custom_shares_packed(self, prize_id, slot_index).get_share(index_in_slot)
}
Comment on lines +119 to +124

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 8 \
  'get_custom_share_at|_get_custom_share_at|PrizeType::Distributed|set_prize_claimed|payout' \
  packages --glob '*.cairo'

Repository: Provable-Games/game-components

Length of output: 50387


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
fd -a 'prize_store.cail|test_prize_store\.cairo|prize_component\.cairo|prize.*\.cairo' packages/metagame/src/prize packages/metagame/src 2>/dev/null | sed 's#^\./##'

echo
echo "prize_store outline:"
ast-grep outline packages/metagame/src/prize/prize_store.cairo --view expanded || true

echo
echo "Relevant store implementation:"
sed -n '1,190p' packages/metagame/src/prize/prize_store.cairo | cat -n

echo
echo "Relevant tests around custom share:"
sed -n '330,420p' packages/metagame/src/prize/tests/test_prize_store.cairo | cat -n

echo
echo "Claim/payout references in prize files only:"
rg -n -C 10 'set_prize_claimed|payout_prize|claim|Custom|get_custom_share_at|get_custom_shares|positions' packages/metagame/src/prize --glob '*.cairo'

Repository: Provable-Games/game-components

Length of output: 50387


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Packed share behavior probe:"
python3 - <<'PY'
# Mirrors the documented packed-share arithmetic in packages/metagame/src/prize/prize_store.cairo.
CUSTOM_SHARES_PER_SLOT = 15
positions = [0, 1, 15, 16, 17, 18, 384, 385]
count = 17
for position in positions:
    index = position - 1
    if index < 0:
        print(f"position {position}: underflows u32; no direct slot conversion")
        continue
    slot_index_u8 = (index // CUSTOM_SHARES_PER_SLOT)
    index_in_slot_u8 = (index % CUSTOM_SHARES_PER_SLOT)
    readable = 0 <= slot_index_u8 <= 255 and 0 <= index_in_slot_u8 <= 14
    status = "readable slot if slot exists" if readable else f"u8 conversion failure ({slot_index_u8=})"
    print(f"position {position}: zindex={index}, slot={slot_index_u8}, offset={index_in_slot_u8}, status={status}, in_stored_range={0 < position <= count}")
PY

echo
echo "Error constants and custom share tests:"
rg -n -C 4 'Prize:|Custom share|get_custom_share_at|position + 1|position - 1|CUSTOM_SHARE|SHARES_PER_SLOT' packages/metagame/src/prize --glob '*.cairo'

echo
echo "Read claim resolution in prize component around custom shares:"
rg -n -C 12 'get_custom_share_at|get_payout_position|set_prize_claimed|claim|Distributed|pout' packages/metagame/src/prize/prize_component.cairo | sed -n '1,260p'

Repository: Provable-Games/game-components

Length of output: 50386


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Determine claim-related code shape around get_custom_share_at callers"
rg -n 'get_custom_share_at|_get_custom_share_at|calculate_share|payout_prize|set_prize_claimed|get_claims|Claimable|claim_position|position' packages/metagame/src/prize/prize_component.cairo
echo
sed -n '1,340p' packages/metagame/src/prize/prize_component.cairo | cat -n
echo
sed -n '340,660p' packages/metagame/src/prize/prize_component.cairo | cat -n

echo
echo "Interface and claim type definitions in package interfaces"
fd -a 'prize.*\.cairo|claim.*\.cairo' packages packages/metagame 2>/dev/null | sed -n '1,200p'
rg -n 'iprize|IPRIZE|claim_prize|claims|ClaimType|position' packages --glob '*.cairo' | sed -n '1,260p'

Repository: Provable-Games/game-components

Length of output: 50386


Reject out-of-range custom shares before packed lookup.

position - 1 can underflow for position = 0, and values above count read beyond the configured distribution. Validate 1 <= position <= count before subtraction, keep the packed lookup limited to supported slot indices, and add coverages for invalid custom-share positions including empty prizes and slot boundaries.

📍 Affects 2 files
  • packages/metagame/src/prize/prize_store.cairo#L119-L124 (this comment)
  • packages/metagame/src/prize/tests/test_prize_store.cairo#L395-L399
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/metagame/src/prize/prize_store.cairo` around lines 119 - 124, Update
get_custom_share_at to validate that position is within 1..=count before
subtracting one, reject invalid positions without performing a packed lookup,
and restrict computed slot indices to supported slots. In
packages/metagame/src/prize/tests/test_prize_store.cairo:395-399, add coverage
for position zero, positions above count, empty prizes, and slot-boundary
positions.

Source: Coding guidelines


fn get_custom_shares(self: @T, prize_id: u64) -> Array<u16> {
let count = Store::get_custom_shares_count(self, prize_id);
let mut shares = ArrayTrait::new();
Expand Down
74 changes: 63 additions & 11 deletions packages/metagame/src/prize/structs.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use starknet::storage_access::StorePacking;
mod nz128 {
pub const TWO_POW_8: NonZero<u128> = 0x100;
pub const TWO_POW_16: NonZero<u128> = 0x10000;
pub const TWO_POW_32: NonZero<u128> = 0x100000000;
}

// Payout type constants for storage
Expand All @@ -24,16 +25,21 @@ pub const PAYOUT_TYPE_LINEAR: u8 = 1;
pub const PAYOUT_TYPE_EXPONENTIAL: u8 = 2;
pub const PAYOUT_TYPE_UNIFORM: u8 = 3;
pub const PAYOUT_TYPE_CUSTOM: u8 = 4;
pub const PAYOUT_TYPE_GEOMETRIC: u8 = 5;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Exercise the new payout type in the packing tests.

test_fuzz_realistic_payout_types treats only values 0-4 as valid and uses % 5. It never exercises PAYOUT_TYPE_GEOMETRIC == 5. Include type 5 in the fuzz domain and add an API-level round-trip for Distribution::Geometric((10, 7)).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/metagame/src/prize/structs.cairo` at line 27, Update
test_fuzz_realistic_payout_types to treat PAYOUT_TYPE_GEOMETRIC as valid by
expanding the generated payout-type domain from 0–4 to 0–5. Add an API-level
packing/unpacking round-trip that covers Distribution::Geometric((10, 7)),
preserving the existing round-trip coverage for other distribution variants.

pub const PAYOUT_TYPE_TIERED: u8 = 6;

/// Internal packed representation for ERC20 data storage
/// Layout: [amount: 128 bits][payout_type: 8 bits][param: 16 bits][count: 32 bits] = 184 bits
/// Layout: [amount: 128 bits][payout_type: 8][param: 16][count: 32][param2: 16][param3: 16]
/// = 216 bits. param2/param3 carry Tiered's head_count and head_share_bps; 0 otherwise.
/// This is used internally by StorePacking and not exposed in the API
#[derive(Copy, Drop)]
struct PackedERC20Data {
amount: u128,
payout_type: u8,
param: u16,
count: u32,
param2: u16,
param3: u16,
}

/// u128-aligned StorePacking for PackedERC20Data.
Expand All @@ -53,7 +59,9 @@ impl PackedERC20DataPacking of StorePacking<PackedERC20Data, felt252> {

let high: u128 = value.payout_type.into()
+ value.param.into() * 0x100_u128 // shift 8
+ value.count.into() * 0x1000000_u128; // shift 24
+ value.count.into() * 0x1000000_u128 // shift 24
+ value.param2.into() * 0x100000000000000_u128 // shift 56
+ value.param3.into() * 0x1000000000000000000_u128; // shift 72
Comment thread
coderabbitai[bot] marked this conversation as resolved.

let packed = u256 { low, high };
packed.try_into().unwrap()
Expand All @@ -66,13 +74,17 @@ impl PackedERC20DataPacking of StorePacking<PackedERC20Data, felt252> {

let high = packed.high;
let (hi, payout_type) = DivRem::div_rem(high, nz128::TWO_POW_8);
let (count, param) = DivRem::div_rem(hi, nz128::TWO_POW_16);
let (hi2, param) = DivRem::div_rem(hi, nz128::TWO_POW_16);
let (hi3, count) = DivRem::div_rem(hi2, nz128::TWO_POW_32);
let (param3, param2) = DivRem::div_rem(hi3, nz128::TWO_POW_16);

PackedERC20Data {
amount,
payout_type: payout_type.try_into().unwrap(),
param: param.try_into().unwrap(),
count: count.try_into().unwrap(),
param2: param2.try_into().unwrap(),
param3: param3.try_into().unwrap(),
}
}
}
Expand Down Expand Up @@ -115,30 +127,39 @@ fn pack_token_type(token_type: TokenTypeData) -> PackedTokenTypeData {
match token_type {
TokenTypeData::erc20(erc20_data) => {
// Convert ERC20Data to packed format
let (payout_type, param) = match erc20_data.distribution {
Option::None => (PAYOUT_TYPE_POSITION, 0_u16),
let (payout_type, param, param2, param3) = match erc20_data.distribution {
Option::None => (PAYOUT_TYPE_POSITION, 0_u16, 0_u16, 0_u16),
Option::Some(dist) => {
match dist {
game_components_utilities::distribution::structs::Distribution::Linear(w) => (
PAYOUT_TYPE_LINEAR, w,
PAYOUT_TYPE_LINEAR, w, 0_u16, 0_u16,
),
game_components_utilities::distribution::structs::Distribution::Exponential(w) => (
PAYOUT_TYPE_EXPONENTIAL, w,
PAYOUT_TYPE_EXPONENTIAL, w, 0_u16, 0_u16,
),
game_components_utilities::distribution::structs::Distribution::Uniform => (
PAYOUT_TYPE_UNIFORM, 0_u16,
PAYOUT_TYPE_UNIFORM, 0_u16, 0_u16, 0_u16,
),
game_components_utilities::distribution::structs::Distribution::Custom(_) => (
PAYOUT_TYPE_CUSTOM, 0_u16,
PAYOUT_TYPE_CUSTOM, 0_u16, 0_u16, 0_u16,
),
game_components_utilities::distribution::structs::Distribution::Geometric((
a, b,
)) => (PAYOUT_TYPE_GEOMETRIC, a * 256 + b, 0_u16, 0_u16),
game_components_utilities::distribution::structs::Distribution::Tiered(cfg) => {
let (a, b) = cfg.head_ratio;
(PAYOUT_TYPE_TIERED, a * 256 + b, cfg.head_count, cfg.head_share_bps)
},
}
},
};
let count = match erc20_data.distribution_count {
Option::Some(c) => c,
Option::None => 0_u32,
};
let packed = PackedERC20Data { amount: erc20_data.amount, payout_type, param, count };
let packed = PackedERC20Data {
amount: erc20_data.amount, payout_type, param, count, param2, param3,
};
PackedTokenTypeData::erc20(PackedERC20DataPacking::pack(packed))
},
TokenTypeData::erc721(erc721_data) => PackedTokenTypeData::erc721(erc721_data),
Expand Down Expand Up @@ -170,6 +191,22 @@ fn unpack_token_type(packed_token_type: PackedTokenTypeData) -> TokenTypeData {
Option::Some(
game_components_utilities::distribution::structs::Distribution::Uniform,
)
} else if packed.payout_type == PAYOUT_TYPE_GEOMETRIC {
Option::Some(
game_components_utilities::distribution::structs::Distribution::Geometric(
(packed.param / 256, packed.param % 256),
),
)
} else if packed.payout_type == PAYOUT_TYPE_TIERED {
Option::Some(
game_components_utilities::distribution::structs::Distribution::Tiered(
game_components_utilities::distribution::structs::TieredConfig {
head_ratio: (packed.param / 256, packed.param % 256),
head_count: packed.param2,
head_share_bps: packed.param3,
},
),
)
} else {
Option::Some(
game_components_utilities::distribution::structs::Distribution::Custom(
Expand Down Expand Up @@ -240,7 +277,7 @@ mod packed_erc20_data_tests {
fn build_packed_erc20(
amount: u128, payout_type: u8, param: u16, count: u32,
) -> PackedERC20Data {
PackedERC20Data { amount, payout_type, param, count }
PackedERC20Data { amount, payout_type, param, count, param2: 0, param3: 0 }
}

fn assert_roundtrip(data: PackedERC20Data) {
Expand All @@ -250,6 +287,21 @@ mod packed_erc20_data_tests {
assert!(unpacked.payout_type == data.payout_type, "payout_type mismatch");
assert!(unpacked.param == data.param, "param mismatch");
assert!(unpacked.count == data.count, "count mismatch");
assert!(unpacked.param2 == data.param2, "param2 mismatch");
assert!(unpacked.param3 == data.param3, "param3 mismatch");
}

#[test]
fn test_tiered_params_roundtrip_in_the_widened_slots() {
let data = PackedERC20Data {
amount: 0xffffffffffffffffffffffffffffffff, // u128::MAX alongside full params
payout_type: 6,
param: 10 * 256 + 7,
count: 10000,
param2: 39,
param3: 8000,
};
assert_roundtrip(data);
}

// -------------------------------------------------------------------------
Expand Down
6 changes: 6 additions & 0 deletions packages/metagame/src/prize/tests/mocks/prize_mock.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ pub mod PrizeMock {
self.prize._get_custom_shares(prize_id)
}

/// One custom share by 1-indexed position, without rebuilding the array
#[external(v0)]
fn get_custom_share_at(self: @ContractState, prize_id: u64, position: u32) -> u16 {
self.prize._get_custom_share_at(prize_id, position)
}

/// Get extension address for a context and prize
#[external(v0)]
fn get_extension_address(
Expand Down
Loading
Loading