Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
bd9be74
Step A1: port BattleData slot-1 repack from batched branch
sudo-owen May 28, 2026
83a990d
Step B+C: port batching (single-sig, no shadow) + real-mon replay har…
sudo-owen May 28, 2026
75dfb90
Step D: offchain-CPU passthrough (selectMoveWithCpuMove)
sudo-owen May 28, 2026
e5a37e2
Step E: desync-log -> replay-data parser (processing/parse_desync_rep…
sudo-owen May 28, 2026
818df63
docs: clean-branch gas findings (ANALYSIS_BATCHED_GAS.md)
sudo-owen May 28, 2026
aa48279
Step A2: legacy executeWithDualSignedMoves -> single-sig (msg.sender=…
sudo-owen May 29, 2026
e00f9f1
test: split FullyOptimizedInlineGasTest into its own file + add GasMe…
sudo-owen May 29, 2026
6a68609
test: convert FullyOptimizedInlineGasTest to GasMeasure format; remov…
sudo-owen May 29, 2026
08483c2
test: remove EngineGasTest (external-validator baseline, not the prod…
sudo-owen May 29, 2026
d5448f8
remove unnec measuring
sudo-owen May 29, 2026
e0fc1e8
fix(gas): re-baseline RealMonReplay on the production inline-stamina-…
sudo-owen May 29, 2026
21f7e41
docs: consolidate gas optimization docs into GAS_OPTIMIZATION.md
sudo-owen May 29, 2026
5aa9fce
wip
sudo-owen May 29, 2026
ac8eea5
clean up
sudo-owen May 29, 2026
8dba414
perf: route in-game move-facing effect lookups through getEffectData
sudo-owen May 29, 2026
b248b59
clean up engine things
sudo-owen May 29, 2026
73e3fb4
more small opt
sudo-owen May 29, 2026
0374f19
test: startBattle gas breakdown harness (real GachaTeamRegistry, all …
sudo-owen May 29, 2026
db0bdee
test: warm-steady-faithful startBattle harness (distinct mons, COLD/W…
sudo-owen May 29, 2026
7ea210d
perf: batch IMatchmaker.validateMatch(battleKey, p0, p1)
sudo-owen May 29, 2026
6598bc0
docs: record why the shared-mon-instance startBattle cache was abandoned
sudo-owen May 29, 2026
fdb60d9
perf: emit EngineExecute once per batch instead of per sub-turn
sudo-owen May 29, 2026
8130c33
docs: refresh §1 totals + record validateMatch batch and EngineExecut…
sudo-owen May 29, 2026
27cf61a
perf: emit no EngineExecute in the batched flow (single paths keep it)
sudo-owen May 29, 2026
6a57c3b
test: add one-tx CPU flow (option 3) to RealMonReplay
sudo-owen May 29, 2026
88c6575
docs: record measured one-tx CPU result (~1.1M) + why it's parked (gr…
sudo-owen May 29, 2026
f5614d1
docs: move-VM feasibility research (scoping a circuit-friendly move VM)
sudo-owen May 29, 2026
c7add22
feat: one-tx PvE flow — CPUMoveManager.executeGame (per-turn player s…
sudo-owen May 29, 2026
15852e2
docs: move-VM design sketch (conditional on inlining StatBoosts)
sudo-owen May 29, 2026
62f978e
docs: move-VM honest verdict + no-keccak variation + 1-tx CPU gains
sudo-owen Jun 1, 2026
9fea61a
wip processing fix
sudo-owen Jun 1, 2026
136f62a
clean up
sudo-owen Jun 1, 2026
93cf9fe
wip optimizations and streak fixes
sudo-owen Jun 1, 2026
7220c5c
engine opt
sudo-owen Jun 1, 2026
c9ddd85
Claude/statboosts inlining plan oq sqt (#72)
sudo-owen Jun 2, 2026
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
52 changes: 42 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,14 @@ chomp/
│ │ ├── OkayCPU.sol # Mid-tier opponent
│ │ ├── CPUMoveManager.sol # Wraps Engine.execute for CPU-driven battles
│ │ └── ICPU.sol
│ ├── effects/ # Effect system (status effects, stat boosts, battlefield)
│ ├── effects/ # Effect system (status effects, battlefield)
│ │ ├── IEffect.sol # Effect interface with lifecycle hooks
│ │ ├── BasicEffect.sol
│ │ ├── StaminaRegen.sol
│ │ ├── StatBoosts.sol
│ │ ├── status/ # Status effects (Burn, Frostbite, Panic, Sleep, Zap) + StatusEffectLib
│ │ ├── battlefield/ # Battlefield effects (Overclock)
│ │ # NOTE: stat boosts are inlined into the Engine (see "Stat Boosts" below); the math
│ │ # helpers live in src/lib/StatBoostLib.sol, there is no StatBoosts effect contract.
│ ├── game-layer/ # Team / mon registry, gacha, exp, facets, quests, gifts
│ │ ├── GachaTeamRegistry.sol # Concrete leaf: composes the abstracts below
│ │ ├── MonOwnership.sol # monsOwned set + ownership view/check helpers
Expand Down Expand Up @@ -220,6 +221,30 @@ Effects implement `IEffect` with a bitmap indicating which lifecycle steps they

Effects can be per-mon (local) or global (battlefield-wide). The `StaminaRegen` effect is a global default that regenerates 1 stamina per turn.

### Stat Boosts (inlined into the Engine)

Stat modifiers are **not** a separate effect contract — they are native Engine functions:
`addStatBoost` / `addKeyedStatBoost` / `removeStatBoost` / `removeKeyedStatBoost` / `clearAllStatBoosts`
(declared in `IEngine`). Moves, abilities, and shared effects (`BurnStatus`, `FrostbiteStatus`,
`Overclock`, `UpOnly`, `Tinderclaws`, …) call `engine.addStatBoost(...)` directly during `execute`.
The packing/aggregation math lives in `src/lib/StatBoostLib.sol`. (Historically this was an external
`StatBoosts` effect that the Engine called back into ~10–15× per application; inlining removed those
round-trips.)

How it works:
- Each boost **source** is one packed entry stored in the mon's normal effect mapping under the
`STAT_BOOST_ADDRESS` sentinel (steps bitmap `STAT_BOOST_STEPS` = `OnMonSwitchOut | ALWAYS_APPLIES`).
Sources are keyed by `msg.sender` (or `msg.sender` + a salt string), so each move/ability/effect
stacks independently and can remove its own boost. Boosts are **multiplicative** per source; `Temp`
boosts are dropped automatically on switch-out (`_inlineStatBoostSwitchOut`), `Perm` ones persist.
- Boosts apply only to the 5 stat deltas: `Speed`, `Attack`, `Defense`, `SpecialAttack`,
`SpecialDefense`. There is **no globalKV snapshot** — the Engine telescopes off the live `monState`
delta (`new boosted − base − currentDelta`) and fires `OnUpdateMonState` like any other delta write.
- **Ownership invariant:** the stat-boost system is the *sole* writer of those 5 deltas. External
`updateMonState` calls with `Speed`..`SpecialDefense` **revert with `StatRequiresStatBoost`** — to
change a stat you must go through `add`/`removeStatBoost`, never `updateMonState`. (`Hp`, `Stamina`,
`IsKnockedOut`, `ShouldSkipTurn` remain writable via `updateMonState`.)

### Type System

16 types: Yin, Yang, Earth, Liquid, Fire, Metal, Ice, Nature, Lightning, Mythic, Air, Math, Cyber, Wild, Cosmic, None. Type effectiveness is calculated by `ITypeCalculator`.
Expand Down Expand Up @@ -254,19 +279,26 @@ playerData[address] (1 slot per player):
bit 254 isWhitelistedAsOpponent (admin-set; replaces a separate mapping)
bit 253 isHardCpu (only meaningful when bit 254 is set)
bits 250-252 streakDay (1..STREAK_FLAT_BONUS_MAX; 0 = no streak yet)
bits 224-249 (reserved)
bits 192-223 lastQuestCompletedDay (uint32 calendar day)
bits 128-159 lastFirstGameTimestamp (uint32 seconds since epoch)
bits 160-191 lastSeenTimestamp (uint32 seconds; last battle of ANY kind — drives streak grace/reset)
bits 128-159 lastFirstGameTimestamp (uint32 seconds; last streak-bonus game — gates the 24h cooldown)
bits 0-127 pointsBalance (uint128)

packedExpForMon[player][monId / 16]: 16 mons × 16 bits each, capped at 65535.
facetData[player][monId / 16]: 16 mons × 16 bits each
(bits 0-11 unlockedBitmap, bits 12-15 assignedFacetId).
```

Streak is timestamp-driven (not calendar-day): a battle counts as "first of day"
when ≥24h have passed since `lastFirstGameTimestamp`. A gap >36h
(`STREAK_GRACE_WINDOW`) resets `streakDay` to 1; otherwise it ratchets up
toward the cap of `STREAK_FLAT_BONUS_MAX` (= 5).
Streak is timestamp-driven (not calendar-day): a battle qualifies for the streak bonus
when ≥24h have passed since `lastFirstGameTimestamp` (the last *bonus-earning* game). On a
qualifying battle the ratchet-vs-reset decision is measured from `lastSeenTimestamp` (the
last battle of *any* kind, advanced every battle): a gap >36h (`STREAK_GRACE_WINDOW`) of
genuine inactivity resets `streakDay` to 1, otherwise it ratchets up toward the cap of
`STREAK_FLAT_BONUS_MAX` (= 5). Splitting the two anchors is deliberate — measuring the
reset from the bonus anchor instead would strand players who play slightly more often than
once per 24h (their sub-24h plays advance no anchor, so the next day reads a phantom ~46h
gap and resets the streak forever).

Both per-mon mappings share the same 16-mon bucketing so `_applyExpAndFacetDraws` walks the team in one pass and coalesces SSTOREs by bucket.

Expand All @@ -293,8 +325,8 @@ Both per-mon mappings share the same 16-mon bucketing so `_applyExpAndFacetDraws
### Storage Architecture

- `BattleData` and `BattleConfig` are stored per battle key (derived from player addresses)
- `MonState` tracks deltas from base stats (hpDelta, staminaDelta, etc.)
- Effects stored in per-mon mappings with stride-based indexing (64 slots per mon)
- `MonState` tracks deltas from base stats (hpDelta, staminaDelta, etc.). The 5 stat deltas are written only by the inlined stat-boost path (see "Stat Boosts"); other deltas via `updateMonState`.
- Effects stored in per-mon mappings with stride-based indexing (64 slots per mon). Stat-boost sources reuse these same mappings under the `STAT_BOOST_ADDRESS` sentinel.
- Heavy use of bit packing for gas efficiency (KO bitmaps, effect counts, active mon indices)
- Transient storage used for per-transaction state (`battleKeyForWrite`, `tempRNG`)
- `GachaTeamRegistry`'s storage is the union of its abstract bases; each base owns its own mappings/constants so the leaf is integration-only. Reordering the inheritance list would shift slot layout — keep the order in `GachaTeamRegistry.sol` stable across deploys.
Expand Down Expand Up @@ -417,7 +449,7 @@ Effects fall into several categories depending on scope:

- **Status effects** (`src/effects/status/`): Extend `StatusEffect` which enforces one-status-per-mon via a KV flag. Shared across mons — deployed once, injected into moves via constructor parameters. (e.g., `BurnStatus`, `FrostbiteStatus`, `SleepStatus`)
- **Battlefield effects** (`src/effects/battlefield/`): Extend `BasicEffect`, use `targetIndex=2` for global scope. (e.g., `Overclock`)
- **Shared utility effects** (`src/effects/`): Deployed once, used by many contracts. (e.g., `StatBoosts` for stat modifiers, `StaminaRegen` for per-turn recovery)
- **Shared utility effects** (`src/effects/`): Deployed once, used by many contracts. (e.g., `StaminaRegen` for per-turn recovery). NOTE: stat modifiers are **not** an effect — they are inlined Engine functions (`addStatBoost`/`removeStatBoost`/…); see "Stat Boosts" above.
- **Mon-local effects** (`src/mons/<monname>/`): Abilities or move-effect hybrids that only apply to one mon. These live in the mon's directory, not in `src/effects/`.

To implement a new effect:
Expand Down
Loading