XP earned should scale with CHARM bought (1× at min band → 10× at 10 CHARM), not flat per buy #304

Closed
opened 2026-06-10 03:30:11 +00:00 by PlasticDigits · 6 comments
PlasticDigits commented 2026-06-10 03:30:11 +00:00 (Migrated from gitlab.com)

Problem

Player XP is perceived / documented as flat per buy (one progression tick per transaction), which rewards many small buys the same as fewer large buys. XP should scale with how much CHARM the player clears in that buy — same economic intent as CRED_PER_CHARM_WAD (burn scales with weight) rather than CRED_PER_BUY (flat epoch pool tranche per tx).

Desired rule: at the minimum allowed CHARM band, award the base XP; at 10 CHARM (CHARM_MAX_WAD), award up to 10× that base (integer floor, documented rounding).

Example targets (band ends):

CHARM bought XP gained (this buy)
0.99 (CHARM_MIN_WAD) 1
5 (mid-band) ~5
10 (CHARM_MAX_WAD) 10

Formula (integer floor):

xp = 1 + (charmWad - CHARM_MIN_WAD) * 9 / (CHARM_MAX_WAD - CHARM_MIN_WAD)

Applies to both buy(charmWad) (DOUB) and buyWithCred(charmWad) — XP must follow charmWad passed into _finishBuy, not buyCount or a flat constant.

Related closed work: #250 (XP accrual), #265 (cached buy-path progression), #299 (level cap + onboarding). This issue tracks CHARM-weight scaling explicitly and end-to-end verification.


Acceptance criteria

  • Onchain: each _finishBuy awards xpGain = ArenaXp.xpForCharm(charmWad); min band → 1 XP, max band → 10 XP; no code path awards a flat +1 regardless of charmWad.
  • DOUB and CRED paths: buy and buyWithCred produce identical XP for the same charmWad (only payment rail differs).
  • Events / views: XpGained(player, amount, newLevel) emits the charm-scaled amount; xp, level, xpTowardNext, xpToNextLevel reflect cumulative charm-scaled gains.
  • Frontend preview: buy checkout “projected effects” +Xxp chip updates live with the CHARM slider / cleared weight (uses xpForCharm, not a constant).
  • Indexer: idx_player_xp.xp_gained stores the event amount; wallet stats xp is SUM(xp_gained) and matches onchain xp() after sync.
  • Docs / copy: product docs and player-facing copy describe CHARM-scaled XP (1–10), not “XP per buy” without mentioning weight.
  • Invariants: add or extend INV-TIME-ARENA-XP-CHARM-SCALE (or tighten INV-TIME-ARENA-XP) with min/mid/max band table tests.

Verification criteria

Layer Command / check Pass
Contracts — library cd contracts && forge test --match-contract ArenaXp -vv xpForCharm(99e16)==1, xpForCharm(10e18)==10, mid-band spot checks
Contracts — integration cd contracts && forge test --match-test test_xp_ -vv test_xp_emits_XpGained (10 CHARM → 10 XP), test_xp_buy_with_cred_same_as_doub, test_xp_max_charm_first_buy
Frontend math cd frontend && npm test -- --run src/lib/arenaXpMath.test.ts TS mirrors Solidity band
Frontend preview cd frontend && npm test -- --run src/pages/arena/arenaBuyProjectedEffects.test.ts +Xxp tracks xpForCharm(charmWad)
Indexer cd indexer && cargo test arena_wallet_stats + integration_stage2 XP fixtures xp aggregate matches summed XpGained
Manual (Anvil) After bash scripts/verify-evm-dev-wallet-seed-anvil.sh + buy min vs max CHARM cast call TimeArena xp(address) differs by ~10× between min and max single buys; XpGained log amount matches

  1. Contracts (if any gap): confirm _finishBuy uses ArenaXp.xpForCharm(charmWad) — see TimeArena.sol ~L547. Remove any legacy flat-per-buy XP constant if present on deployed branches.
  2. Frontend: ensure all buy previews call xpForCharm(charmWadSelected) (or charmWeightTotalWad when referral/presale weight bonuses apply) via formatBuyProjectedXpLine / buildArenaBuyProjectedEffectLines.
  3. Indexer: decoder maps XpGained.amount → xp_gained; wallet stats must not derive XP from COUNT(buys).
  4. Docs: replace ambiguous “per buy” XP wording in docs/product/time-arena.md / arena-v2.md with “per buy, scaled 1–10 by CHARM weight”.
  5. Parameters: document CHARM_MIN_WAD / CHARM_MAX_WAD XP mapping in contracts/PARAMETERS.md alongside CRED constants.

Relevant files

Contracts

  • contracts/src/arena/libraries/ArenaXp.sol — xpForCharm, applyXpGain, band constants
  • contracts/src/arena/TimeArena.sol — _finishBuy, XpGained emit, buy / buyWithCred
  • contracts/test/ArenaXp.t.sol, contracts/test/TimeArena.t.sol — test_xp_*

Frontend

  • frontend/src/lib/arenaXpMath.ts — TS mirror of ArenaXp
  • frontend/src/pages/arena/arenaBuyProjectedEffects.ts — checkout +Xxp chip
  • frontend/src/components/ArenaXpHero.tsx — level / XP progress bar
  • frontend/src/lib/arenaProgression.ts — level gates (#299)

Indexer

  • indexer/src/decoder.rs — XpGained decode
  • indexer/src/persist.rs — idx_player_xp insert
  • indexer/src/arena_wallet_stats.rs — SUM(xp_gained) for profile API

Docs / invariants

  • docs/product/arena-v2.md (§ XP), docs/product/time-arena.md (§ XP)
  • docs/testing/invariants-and-business-logic.md — INV-TIME-ARENA-XP, INV-TIME-ARENA-XP-GAS
  • skills/play-time-arena-doub/SKILL.md

Notes

  • Level thresholds unchanged: L1→L2 needs 20 cumulative XP; step min(20 + (L-1)×5, 100) per level (#250).
  • Max 5 level-ups per buy; surplus banks in xpTowardNext at level cap (#265, #299).
  • Out of scope: changing CRED_PER_BUY epoch pool (still flat 35 CRED/buy) — separate from XP scaling.
## Problem Player XP is perceived / documented as **flat per buy** (one progression tick per transaction), which rewards many small buys the same as fewer large buys. XP should scale with **how much CHARM** the player clears in that buy — same economic intent as **`CRED_PER_CHARM_WAD`** (burn scales with weight) rather than **`CRED_PER_BUY`** (flat epoch pool tranche per tx). **Desired rule:** at the minimum allowed CHARM band, award the **base** XP; at **10 CHARM** (`CHARM_MAX_WAD`), award **up to 10×** that base (integer floor, documented rounding). Example targets (band ends): | CHARM bought | XP gained (this buy) | |--------------|----------------------| | 0.99 (`CHARM_MIN_WAD`) | 1 | | 5 (mid-band) | ~5 | | 10 (`CHARM_MAX_WAD`) | 10 | Formula (integer floor): ``` xp = 1 + (charmWad - CHARM_MIN_WAD) * 9 / (CHARM_MAX_WAD - CHARM_MIN_WAD) ``` Applies to **both** `buy(charmWad)` (DOUB) and `buyWithCred(charmWad)` — XP must follow **`charmWad` passed into `_finishBuy`**, not `buyCount` or a flat constant. Related closed work: #250 (XP accrual), #265 (cached buy-path progression), #299 (level cap + onboarding). This issue tracks **CHARM-weight scaling** explicitly and end-to-end verification. --- ## Acceptance criteria - [ ] **Onchain:** each `_finishBuy` awards `xpGain = ArenaXp.xpForCharm(charmWad)`; min band → **1 XP**, max band → **10 XP**; no code path awards a flat `+1` regardless of `charmWad`. - [ ] **DOUB and CRED paths:** `buy` and `buyWithCred` produce identical XP for the same `charmWad` (only payment rail differs). - [ ] **Events / views:** `XpGained(player, amount, newLevel)` emits the charm-scaled `amount`; `xp`, `level`, `xpTowardNext`, `xpToNextLevel` reflect cumulative charm-scaled gains. - [ ] **Frontend preview:** buy checkout “projected effects” `+Xxp` chip updates live with the CHARM slider / cleared weight (uses `xpForCharm`, not a constant). - [ ] **Indexer:** `idx_player_xp.xp_gained` stores the event `amount`; wallet stats `xp` is `SUM(xp_gained)` and matches onchain `xp()` after sync. - [ ] **Docs / copy:** product docs and player-facing copy describe **CHARM-scaled** XP (1–10), not “XP per buy” without mentioning weight. - [ ] **Invariants:** add or extend `INV-TIME-ARENA-XP-CHARM-SCALE` (or tighten `INV-TIME-ARENA-XP`) with min/mid/max band table tests. --- ## Verification criteria | Layer | Command / check | Pass | |-------|-----------------|------| | Contracts — library | `cd contracts && forge test --match-contract ArenaXp -vv` | `xpForCharm(99e16)==1`, `xpForCharm(10e18)==10`, mid-band spot checks | | Contracts — integration | `cd contracts && forge test --match-test test_xp_ -vv` | `test_xp_emits_XpGained` (10 CHARM → 10 XP), `test_xp_buy_with_cred_same_as_doub`, `test_xp_max_charm_first_buy` | | Frontend math | `cd frontend && npm test -- --run src/lib/arenaXpMath.test.ts` | TS mirrors Solidity band | | Frontend preview | `cd frontend && npm test -- --run src/pages/arena/arenaBuyProjectedEffects.test.ts` | `+Xxp` tracks `xpForCharm(charmWad)` | | Indexer | `cd indexer && cargo test arena_wallet_stats` + `integration_stage2` XP fixtures | `xp` aggregate matches summed `XpGained` | | Manual (Anvil) | After `bash scripts/verify-evm-dev-wallet-seed-anvil.sh` + buy min vs max CHARM | `cast call TimeArena xp(address)` differs by ~10× between min and max single buys; `XpGained` log `amount` matches | --- ## Recommended fixes 1. **Contracts (if any gap):** confirm `_finishBuy` uses `ArenaXp.xpForCharm(charmWad)` — see `TimeArena.sol` ~L547. Remove any legacy flat-per-buy XP constant if present on deployed branches. 2. **Frontend:** ensure all buy previews call `xpForCharm(charmWadSelected)` (or `charmWeightTotalWad` when referral/presale weight bonuses apply) via `formatBuyProjectedXpLine` / `buildArenaBuyProjectedEffectLines`. 3. **Indexer:** decoder maps `XpGained.amount` → `xp_gained`; wallet stats must not derive XP from `COUNT(buys)`. 4. **Docs:** replace ambiguous “per buy” XP wording in `docs/product/time-arena.md` / `arena-v2.md` with “per buy, scaled 1–10 by CHARM weight”. 5. **Parameters:** document `CHARM_MIN_WAD` / `CHARM_MAX_WAD` XP mapping in `contracts/PARAMETERS.md` alongside CRED constants. --- ## Relevant files **Contracts** - `contracts/src/arena/libraries/ArenaXp.sol` — `xpForCharm`, `applyXpGain`, band constants - `contracts/src/arena/TimeArena.sol` — `_finishBuy`, `XpGained` emit, `buy` / `buyWithCred` - `contracts/test/ArenaXp.t.sol`, `contracts/test/TimeArena.t.sol` — `test_xp_*` **Frontend** - `frontend/src/lib/arenaXpMath.ts` — TS mirror of `ArenaXp` - `frontend/src/pages/arena/arenaBuyProjectedEffects.ts` — checkout `+Xxp` chip - `frontend/src/components/ArenaXpHero.tsx` — level / XP progress bar - `frontend/src/lib/arenaProgression.ts` — level gates (#299) **Indexer** - `indexer/src/decoder.rs` — `XpGained` decode - `indexer/src/persist.rs` — `idx_player_xp` insert - `indexer/src/arena_wallet_stats.rs` — `SUM(xp_gained)` for profile API **Docs / invariants** - `docs/product/arena-v2.md` (§ XP), `docs/product/time-arena.md` (§ XP) - `docs/testing/invariants-and-business-logic.md` — `INV-TIME-ARENA-XP`, `INV-TIME-ARENA-XP-GAS` - `skills/play-time-arena-doub/SKILL.md` --- ## Notes - Level thresholds unchanged: L1→L2 needs **20** cumulative XP; step `min(20 + (L-1)×5, 100)` per level (#250). - Max **5** level-ups per buy; surplus banks in `xpTowardNext` at level cap (#265, #299). - Out of scope: changing **`CRED_PER_BUY`** epoch pool (still flat 35 CRED/buy) — separate from XP scaling.
PlasticDigits commented 2026-06-10 09:20:32 +00:00 (Migrated from gitlab.com)

mentioned in commit f0a6a6a90f

mentioned in commit f0a6a6a90f70bd183c90d8853649817707ce7dcf
PlasticDigits commented 2026-06-10 09:20:49 +00:00 (Migrated from gitlab.com)

mentioned in merge request !452

mentioned in merge request !452
PlasticDigits commented 2026-06-10 09:20:53 +00:00 (Migrated from gitlab.com)

Implementation summary (MR !452)

CHARM-scaled XP was already wired onchain (ArenaXp.xpForCharm in _finishBuy), frontend (arenaXpMath.ts, buy preview +Xxp), and indexer (XpGained.amount → idx_player_xp.xp_gained). MR !452 documents and verifies the full stack.

Changes:

  • Added INV-TIME-ARENA-XP-CHARM-SCALE + detail section in invariants-and-business-logic.md
  • Crosslinked arena-v2.md, time-arena.md, PARAMETERS.md, skills/play-time-arena-doub/SKILL.md
  • Added test_xp_min_vs_max_charm_single_buy (1 XP min band vs 10 XP max band)
  • Mid-band spot check in arenaXpMath.test.ts

Verification checklist

  • cd contracts && forge test --match-contract ArenaXp -vv — min/mid/max band
  • cd contracts && forge test --match-test test_xp_ -vv — emit, CRED parity, min vs max
  • cd frontend && npm test -- --run src/lib/arenaXpMath.test.ts src/pages/arena/arenaBuyProjectedEffects.test.ts
  • cd indexer && cargo test arena_wallet_stats
  • Manual Anvil: min-band buy vs max-band buy → cast call TimeArena xp(address) differs ~10×; XpGained log amount matches

MR: https://gitlab.com/PlasticDigits/yieldomega/-/merge_requests/82

## Implementation summary (MR !452) CHARM-scaled XP was already wired onchain (`ArenaXp.xpForCharm` in `_finishBuy`), frontend (`arenaXpMath.ts`, buy preview `+Xxp`), and indexer (`XpGained.amount` → `idx_player_xp.xp_gained`). MR !452 documents and verifies the full stack. **Changes:** - Added **`INV-TIME-ARENA-XP-CHARM-SCALE`** + detail section in `invariants-and-business-logic.md` - Crosslinked `arena-v2.md`, `time-arena.md`, `PARAMETERS.md`, `skills/play-time-arena-doub/SKILL.md` - Added `test_xp_min_vs_max_charm_single_buy` (1 XP min band vs 10 XP max band) - Mid-band spot check in `arenaXpMath.test.ts` ## Verification checklist - [ ] `cd contracts && forge test --match-contract ArenaXp -vv` — min/mid/max band - [ ] `cd contracts && forge test --match-test test_xp_ -vv` — emit, CRED parity, min vs max - [ ] `cd frontend && npm test -- --run src/lib/arenaXpMath.test.ts src/pages/arena/arenaBuyProjectedEffects.test.ts` - [ ] `cd indexer && cargo test arena_wallet_stats` - [ ] Manual Anvil: min-band buy vs max-band buy → `cast call TimeArena xp(address)` differs ~10×; `XpGained` log `amount` matches MR: https://gitlab.com/PlasticDigits/yieldomega/-/merge_requests/82
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-06-10 10:59:15 +00:00
PlasticDigits commented 2026-06-10 10:59:16 +00:00 (Migrated from gitlab.com)

mentioned in commit cd85e51056

mentioned in commit cd85e51056df5616d4199f99df0e0e76d46b0b98
PlasticDigits commented 2026-06-13 13:35:52 +00:00 (Migrated from gitlab.com)

mentioned in merge request !475

mentioned in merge request !475
PlasticDigits commented 2026-07-03 13:36:15 +00:00 (Migrated from gitlab.com)

mentioned in commit 09937b3d34

mentioned in commit 09937b3d348d40b6ef918b5d421b87e0450ee187
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
code/yieldomega#304
No description provided.