diff --git a/OPT_PLAN.md b/OPT_PLAN.md new file mode 100644 index 00000000..ad353844 --- /dev/null +++ b/OPT_PLAN.md @@ -0,0 +1,472 @@ +# OPT_PLAN — Batched Execute Gas Optimization + +## 1. Goal + +Amortize per-turn cold-storage access in `Engine.execute()` by: +1. Submitting each turn's signed moves on-chain immediately to a per-turn buffer (no execute). +2. Executing **all currently buffered turns** in one tx with engine state held in **transient shadow storage**, flushed to persistent storage once at the end. + +Secondary goal: route `Engine` state access through helpers so the single-turn path can also use the shadow layer. + +--- + +## 2. Mechanism + +### 2.1 Per-turn submission (PvP) + +`SignedCommitManager.submitTurnMoves(battleKey, TurnSubmission entry)`: +- Uniform shape every turn: **two EIP-712 signatures** (committer + revealer), committer preimage in calldata. Roles derived from `turnId % 2` (matching `getCommitAuthForDualSigned`). +- Switch turns use the same shape. The non-acting player signs a `NO_OP` (move 126); engine ignores their half at batch time using the live `playerSwitchForTurnFlag`. +- Manager hashes committer preimage, verifies committer sig over `SignedCommit{committerMoveHash, …}` and revealer sig over `DualSignedReveal{committerMoveHash, …}`, writes to `moveBuffer[storageKey][turnId]`. **No execute runs.** +- Updates `lastSubmitTimestamp` for timeout tracking. + +**Why two sigs.** Without a committer sig, a malicious revealer could pick any preimage `P*`, sign `DualSignedReveal{committerMoveHash: keccak(P*), …}`, and submit unilaterally — the contract would play `P*` as the committer's move with no committer involvement. Today's `executeWithDualSignedMoves` blocks this only via `msg.sender == committer`, which is fragile and not relayer-friendly. Phase 0 (§9) lifts the same fix into the existing function before any batching ships, so both paths share one security model. + +### 2.2 Per-batch execute + +`Engine.executeBatch(battleKey)`: +- Anyone can call (sigs were checked at submission). +- Reads every currently buffered entry `[startTurn, startTurn + numTurnsBuffered)`, runs each in sequence inside transient shadow storage, flushes once at end. +- The **transient mirror** of `turnId` advances inside the loop. Persistent `BattleData.turnId` advances only during the final flush. +- Batch execution always consumes the full pending buffer. There is no partial-batch mode in v1. +- Processed buffer slots are not cleared — the unbounded mapping leaves them for on-chain replay. Slot reuse across battles comes from `MappingAllocator`. + +### 2.3 Fallback / stalls + +Fully separate write paths. Legacy `DefaultCommitManager.commitMove`/`revealMove` writes `config.p0Move` etc. and triggers `execute()` immediately; the batched path never reads that storage. A battle can alternate between modes turn-by-turn. Timeout via `Engine.end()` covers full stalls. + +--- + +## 3. Buffer layout + +One 256-bit slot per turn: + +```solidity +// [ p0MoveIndex (8) | p0ExtraData (16) | p0Salt (104) | p1MoveIndex (8) | p1ExtraData (16) | p1Salt (104) ] +struct PackedTurnEntry { + uint8 p0MoveIndex; + uint16 p0ExtraData; + uint104 p0Salt; + uint8 p1MoveIndex; + uint16 p1ExtraData; + uint104 p1Salt; +} + +mapping(bytes32 storageKey => mapping(uint64 turnId => PackedTurnEntry)) moveBuffer; +``` + +Steady-state cost per turn: 1 SSTORE (5k, nonzero→nonzero from prior battle's slot reuse) + 1 SLOAD inside batch (2.1k) = ~7.1k. + +Buffer validity is tracked by two packed `uint8` counters: +- `numTurnsBuffered`: number of currently pending buffered turns. +- `numTurnsExecuted`: cumulative number of buffered turns consumed for the current battle/storage key. + +Submit rule: +- If `numTurnsBuffered == 0`, the manager first syncs `numTurnsExecuted` to the engine's current `BattleData.turnId`. This keeps the batched buffer compatible with legacy single-turn execution when the battle alternates modes. +- A new entry must have `entry.turnId == numTurnsExecuted + numTurnsBuffered`. +- After storing the entry, increment `numTurnsBuffered`. + +Execute rule: +- `executeBatch` requires `numTurnsBuffered > 0`. +- It attempts the full pending range of `numTurnsBuffered` turns, starting at `numTurnsExecuted`. +- At flush, persistent `BattleData.turnId` becomes the shadowed turn id, `numTurnsExecuted += executedTurns`, and `numTurnsBuffered = 0`. + +This means stale slots from a prior battle or earlier batch cannot be treated as valid pending moves: only the contiguous range described by `(numTurnsExecuted, numTurnsBuffered)` is live. + +**Width changes (clean break):** +- `extraData`: 240 → 16 bits. Audit confirmed all production consumers read ≤8 bits. Narrow `IMoveSet.move()`'s `extraData` param to `uint16`; repack test helpers (`_packStatBoost`, `StatBoostsMove` mock). +- `Salt`: 256 → 104 bits. 2^104 brute-force resistance is sufficient for the seconds-to-minutes commit-reveal window. + +--- + +## 4. API + +### 4.1 Submission + +```solidity +struct TurnSubmission { + uint64 turnId; + // Committer preimage: + uint8 committerMoveIndex; + uint16 committerExtraData; + uint104 committerSalt; + // Revealer reveal: + uint8 revealerMoveIndex; + uint16 revealerExtraData; + uint104 revealerSalt; + // Sigs: + bytes committerSig; // EIP-712 over SignedCommit{committerMoveHash, battleKey, turnId} + bytes revealerSig; // EIP-712 over DualSignedReveal +} + +// Existing SignedCommitLib struct, reused unchanged. +struct SignedCommit { + bytes32 moveHash; + bytes32 battleKey; + uint64 turnId; +} + +struct DualSignedReveal { + bytes32 battleKey; + uint64 turnId; + bytes32 committerMoveHash; // keccak(committerMoveIndex, committerSalt, committerExtraData) + uint8 revealerMoveIndex; + uint16 revealerExtraData; + uint104 revealerSalt; +} + +function submitTurnMoves(bytes32 battleKey, TurnSubmission calldata entry) external; +``` + +Manager flow: +1. Battle is in dual-signed mode and not over. +2. `entry.turnId` equals next append position. +3. Derive `(committer, revealer)` from `turnId % 2`. +4. `committerMoveHash = keccak(committerMoveIndex, committerSalt, committerExtraData)`. +5. Recover `committerSig` over `SignedCommit{committerMoveHash, battleKey, turnId}`; require equality with `committer`. +6. Recover `revealerSig` over `DualSignedReveal{committerMoveHash, …}`; require equality with `revealer`. +7. Map fields to `(p0, p1)` by parity; SSTORE `PackedTurnEntry`. + +### 4.2 Batch execute + +```solidity +function executeBatch(bytes32 battleKey) external; +``` + +1. Read `startTurn = numTurnsExecuted`; require `numTurnsBuffered > 0`. +2. Hydrate shadow. +3. For each pending buffered turn: read buffer slot, populate per-turn move/salt transient, run `_executeOneTurn()`, break on game-over. +4. Flush shadow → storage. +5. Set `numTurnsBuffered = 0` and increment `numTurnsExecuted` by the number of turns actually executed. + +--- + +## 5. Transient shadow storage + +### 5.1 Shadowed state + +| Storage | Shadow form | +|---|---| +| `MonState` (per mon) | Per-`(playerIndex, monIndex)` mirror, lazy-loaded. Dirty bit per slot. | +| `koBitmaps` (16 bits in `BattleConfig` slot 2) | `uint16` mirror, loaded flag. | +| `winnerIndex` / `prevPlayerSwitchForTurnFlag` / `playerSwitchForTurnFlag` / `activeMonIndex` / `turnId` / `lastExecuteTimestamp` | Single packed `uint256` mirror. | +| Effect list slots (`globalEffects[i]`, `pXEffects[i]`) | Fixed numeric transient keys, mirrors the full `EffectInstance` (`effect`, `stepsBitmap`, `data`). | +| `packedP0EffectsCount` / `packedP1EffectsCount` / `globalEffectsLength` | Three small mirrors, flushed with effect-list shadow. | +| `globalKV[storageKey][key]` | Per-`key` mirror, lazy-loaded. | +| `BattleConfig.p0Move` / `p1Move` / salts | Re-populated per sub-turn from buffer slot. | + +Hydrate strategy: +- **Eager**: `BattleData` slot 1 + `BattleConfig` slot 2 (always touched). +- **Lazy**: `MonState`, effect slots/counts, `globalKV` (sparse — pay only for slots touched). + +Loaded-flag strategy: +- **Bitmap** for fixed-shape slots (MonState, effects, slot-2 packed fields). +- **Per-key transient hash-set** for `globalKV` (dynamic keys). + +### 5.1.1 Effect shadow key layout + +Effects are bounded and already partitioned, so use numeric transient keys and bitmaps instead of hashed keys. + +Assumptions: +- Up to 8 mons per side. +- Up to 8 effects per mon. +- Up to 16 global effects. + +Flat effect-slot keys: + +```solidity +uint256 constant EFFECTS_PER_MON = 8; +uint256 constant MONS_PER_SIDE = 8; +uint256 constant MAX_GLOBAL_EFFECTS = 16; + +uint256 constant EFFECT_P0_OFFSET = 0; // keys 0..63 +uint256 constant EFFECT_P1_OFFSET = 64; // keys 64..127 +uint256 constant EFFECT_GLOBAL_OFFSET = 128; // keys 128..143 + +function _effectShadowKey(uint256 targetIndex, uint256 monIndex, uint256 localEffectIndex) + internal + pure + returns (uint256) +{ + if (targetIndex == 2) return EFFECT_GLOBAL_OFFSET + localEffectIndex; + uint256 sideOffset = targetIndex == 0 ? EFFECT_P0_OFFSET : EFFECT_P1_OFFSET; + return sideOffset + monIndex * EFFECTS_PER_MON + localEffectIndex; +} +``` + +For player effects, `localEffectIndex` is `0..7` and the storage slot remains +`_getEffectSlotIndex(monIndex, localEffectIndex)`. For global effects, `monIndex` is ignored and +`localEffectIndex` is the global effect index. + +Loaded/dirty bitmaps: + +```solidity +uint256 transient effectSlotLoadedBitmap; +uint256 transient effectSlotDirtyBitmap; + +function _effectBit(uint256 key) internal pure returns (uint256) { + return 1 << key; +} +``` + +Shadow values can use numeric transient key regions, one region per `EffectInstance` field: + +```solidity +uint256 constant T_EFFECT_ADDR_BASE = 0x1000; +uint256 constant T_EFFECT_STEPS_BASE = 0x2000; +uint256 constant T_EFFECT_DATA_BASE = 0x3000; + +// tstore(T_EFFECT_ADDR_BASE + key, address(effect)) +// tstore(T_EFFECT_STEPS_BASE + key, stepsBitmap) +// tstore(T_EFFECT_DATA_BASE + key, data) +``` + +Counts use a separate compact key space: + +```solidity +// 0 = globalEffectsLength +// 1..8 = p0 mon counts +// 9..16 = p1 mon counts +function _effectCountKey(uint256 targetIndex, uint256 monIndex) internal pure returns (uint256) { + if (targetIndex == 2) return 0; + if (targetIndex == 0) return 1 + monIndex; + return 9 + monIndex; +} +``` + +Use separate loaded/dirty bitmaps for counts. Flush scans only dirty effect-slot bits in `0..143` and dirty count bits in `0..16`, so flush work is bounded and independent of calldata shape. + +### 5.2 Helper boundary + +Mirrored helpers in `Engine.sol`: + +```solidity +function _shadowReadMonState(BattleConfig storage cfg, uint256 playerIndex, uint256 monIndex) internal returns (MonState memory); +function _shadowWriteMonState(uint256 playerIndex, uint256 monIndex, MonState memory state) internal; +function _shadowReadKV(bytes32 storageKey, uint64 key) internal returns (uint192); +function _shadowWriteKV(bytes32 storageKey, uint64 key, uint192 value) internal; +function _shadowReadEffectSlot(uint256 effectList, uint256 monIndex, uint256 slotIndex) internal returns (EffectInstance memory); +function _shadowWriteEffectSlot(uint256 effectList, uint256 monIndex, uint256 slotIndex, EffectInstance memory eff) internal; +function _shadowReadEffectCount(uint256 effectList, uint256 monIndex) internal returns (uint256); +function _shadowWriteEffectCount(uint256 effectList, uint256 monIndex, uint256 count) internal; +``` + +When `_shadowActive == false`, helpers SLOAD/SSTORE storage directly. When `true`, they read/write the transient mirror with lazy-load and dirty-bit bookkeeping. + +External `IEngine` writers (`updateMonState`, `dealDamage`, `addEffect`, `removeEffect`, `editEffect`, `setGlobalKV`, `switchActiveMon`, `dispatchStandardAttack`, `setMove`) and external readers (`getMonStateForBattle`, `getEffects`, `getGlobalKV`, etc.) all route through these helpers. The `battleKeyForWrite != bytes32(0)` gate stays. + +Effect-list shadowing must preserve these same-batch visibility rules: +- `addEffect` writes a full shadow `EffectInstance` and increments the shadow count, so later effect loops / `getEffects` calls in the same batch see the new effect. +- `editEffect` updates shadow `data`; later hooks see the edited value. +- `removeEffect` tombstones the shadow `effect` address and keeps the slot index stable; later loops skip it. +- `_handleEffects` loads counts and slots from shadow, not storage, and keeps the existing `effectsDirtyBitmap` pattern so effects added while iterating can extend the current loop when today’s logic would. +- `getEffects` builds its return arrays from shadow while `_shadowActive == true`, so external moves/effects that inspect active effects observe the live batch state. + +### 5.3 Batch loop + +``` +executeBatch(battleKey): + storageKey = _getStorageKey(battleKey) + storageKeyForWrite = storageKey + battleKeyForWrite = battleKey + _shadowActive = true + + _hydrateBattleData(battleKey) + _hydrateConfigSlot2(storageKey) + + startTurn = numTurnsExecuted + turnsToExecute = numTurnsBuffered + for t in [startTurn .. startTurn + turnsToExecute): + bufferEntry = _readMoveBufferSlot(storageKey, t) + _populateTurnMoveTransient(bufferEntry) + _executeOneTurn() + if winnerIndex != 2: break + _resetPerTurnTransients() + + _flushBattleData(battleKey) + _flushConfigSlot2(storageKey) + _flushDirtyMonStates(storageKey) + _flushDirtyEffectSlots(storageKey) + _flushDirtyGlobalKV(storageKey) + _flushBufferCounters(executedTurns) + + _shadowActive = false +``` + +Per sub-turn, `tempRNG = keccak(p0Salt, p1Salt)` (or single signed salt for switch turns). Engine hooks (`onRoundStart`, `onRoundEnd`) fire per sub-turn and read shadow state via the routed getters. + +--- + +## 6. Forced switches and game-over + +### 6.1 Forced switch (KO without game-over) + +Both players sign for every turn. The non-acting player signs `NO_OP`. At batch time, the engine reads the live `playerSwitchForTurnFlag` (cheap — in shadow state) and dispatches: +- `flag == 2`: process both halves. +- `flag == 0`: process p0 only, ignore p1's NO_OP. +- `flag == 1`: mirror. + +A player who maliciously signs a non-NO_OP on a turn they shouldn't act has bound themselves cryptographically, but the engine ignores the move. A player who refuses to sign stalls the batched flow; legacy single-turn paths remain as fallback. + +Submission validates only cheap invariants (battle exists, not over at last flush, append position, sig). It does **not** project `playerSwitchForTurnFlag`, since that would require replaying every unprocessed turn. + +### 6.2 Game-over mid-batch + +`_executeInternal` already breaks when `winnerIndex != 2`. Same check stops the batch loop. Because batch execution consumes the full pending buffer, any unexecuted buffered entries after game-over remain in storage for replay but are no longer live; `numTurnsBuffered` is set to zero at flush. + +### 6.3 Status-induced skip-turn + +`shouldSkipTurn` already auto-clears in `_handleMove`. No special batch handling. + +--- + +## 7. CPU mode (trusted-state batched) + +Same per-turn buffer + `executeBatch` as PvP. CPU manager packs `(Alice move, computed CPU move)` into the same `PackedTurnEntry` layout. **Zero engine changes.** + +### 7.1 Trusted state hint + +Alice supplies the projected post-prior-turn `CPUContext` in calldata. Not verified. Lying never benefits Alice — it makes the CPU's chosen move suboptimal against her, which she absorbs. This replaces the dozen-plus cold SLOADs `engine.getCPUContext(battleKey)` does today with a single calldata struct. + +### 7.2 No signature + +Alice calls directly from her wallet. Manager checks `msg.sender == alice` (same as today's `CPUMoveManager.selectMove`). The tx is the proof — no relay path needed for a single-human flow. + +### 7.3 Off-chain protocol + +Each turn, locally on Alice's client: +1. Hold current `CPUContext`-shaped state. Turn 0 = post-`startBattle` state; later turns = output of last local sim. +2. Pick Alice's move. +3. Run the transpiled engine locally to produce the post-turn state, used as next turn's hint. +4. Submit on-chain with the **current-turn** hint. + +### 7.4 Submission + +```solidity +function selectMoveWithStateHint( + bytes32 battleKey, + uint8 aliceMoveIndex, + uint16 aliceExtraData, + uint104 aliceSalt, + CPUContext calldata projectedState +) external; +``` + +1. Read/sync the next append `turnId` from `numTurnsExecuted + numTurnsBuffered` using the same buffer counter rules as PvP. +2. Require `msg.sender == alice`. +3. Route on `projectedState.playerSwitchForTurnFlag` (single-player vs two-player CPU branch). +4. `ICPU(cpuAddr).calculateMove(projectedState, aliceMoveIndex, aliceExtraData)` → `(cpuMove, cpuExtra)`. CPU reads from calldata only. +5. Derive CPU salt: `uint104(uint256(keccak256(abi.encode(block.timestamp, aliceSalt, turnId))))`. Emit `CPUTurnSalt(battleKey, turnId, timestamp)` so off-chain replay can reconstruct it. `turnId` in the hash prevents collision when Alice submits multiple CPU turns in the same block. +6. Pack into `PackedTurnEntry` and SSTORE into `moveBuffer[storageKey][turnId]`. + +`executeBatch` is shared with PvP — the engine doesn't know whether the buffer came from PvP or CPU submissions. + +### 7.5 Coexistence + +Battles select via the `moveManager` they're started with: +- `signedCommitManager` (extended) → PvP batched +- `cpuMoveManager` (extended) → CPU batched +- Today's unmodified managers → legacy single-turn paths + +Today's `CPUMoveManager.selectMove` stays callable for any battle that doesn't opt into batching. + +--- + +## 8. Migration + +Add new entry points alongside existing ones. No "batch mode" flag on a battle — `executeBatch` works on any battle that has buffered turns. + +Touched contracts: +- `Engine.sol`: `executeBatch` + shadow-transient layer + helper routing + flag-based per-turn dispatch. +- `IEngine.sol`: new function signatures. +- `SignedCommitManager.sol`: `submitTurnMoves` (sharing existing EIP-712 domain). +- `CPUMoveManager.sol`: `selectMoveWithStateHint`. +- `IMoveSet.sol`: narrow `extraData` to `uint16`. ~40 mon files take mechanical edits. + +Validator/legality is unchanged: signature recovery proves player intent (or `msg.sender == alice` for CPU); state-dependent illegality silently no-ops in `_handleMove`. Timeout reads `lastSubmitTimestamp` and `lastExecuteTimestamp` — whichever is more recent. + +--- + +## 9. Phased rollout + +**Phase 0 — Dual-sig security fix (preflight, ships first, independent of batching).** The existing `executeWithDualSignedMoves` relies on `msg.sender == committer` as the committer's binding. Without that check, a malicious revealer could sign `DualSignedReveal{committerMoveHash: keccak(P*), …}` for any preimage `P*` they choose and submit unilaterally — the contract would happily compute `committerMoveHash = keccak(P*)`, recover the revealer's sig, and play `P*` as the committer's move. The check is load-bearing today, but it's also fragile: any future evolution of the flow that drops or weakens it (relayers, batching, alt entry points) silently re-opens the hole. + +Fix: require an explicit committer signature over the existing `SignedCommit{moveHash, battleKey, turnId}` struct (already used by `commitWithSignature`). + +- Modify `executeWithDualSignedMoves` to take an additional `bytes calldata committerSignature` parameter. +- Recover `committerSignature` over `SignedCommit{committerMoveHash, battleKey, turnId}`; require equality with `committer`. +- Drop the `msg.sender == committer` check; the function becomes relayer-friendly (anyone with both sigs + the preimage can submit). +- Breaking signature change. Update all callers (tests, `BattleHelper`, anything off-chain that calls this function) in the same PR. No deployed callers in production yet. +- New tests: missing committer sig reverts; wrong committer signer reverts; submission by a third party with both valid sigs succeeds; revealer cannot submit a self-chosen committer preimage (regression). + +This phase ships before any batching work. It hardens the existing flow on its own merits and unifies the security model so the batched path in Phase 2 inherits the same shape (§4.1) without surprises. + +**Phase 0.1 — Instrumentation refresh.** `test/BatchInstrumentationTest.sol` already wires `vm.startStateDiffRecording` for the clean damage-trade case. Add scenarios: effect-heavy turn (status DOT + StatBoosts active), forced-switch turn, multi-mon turn. Lock final batch-size guidance. + +**Phase 0.5 — Helper extraction (no behavior change).** Replace direct `MonState`/`globalKV`/effect-data SLOAD/SSTORE in `Engine.sol` with §5.2 helpers, with `_shadowActive` permanently `false`. Snapshot diff should be roughly flat. + +**Phase 1 — Single-turn shadow.** Implement transient mirrors + lazy-load/dirty-flag bookkeeping. Wire helpers to consult `_shadowActive`. Add `executeShadowed(bytes32 battleKey)` that does `execute()`'s work inside the shadow layer (hydrate → run one turn → flush). Existing test suite should pass against it. B=1 will be slightly *worse* than today's `execute()` due to bookkeeping overhead; expected. + +**Phase 2 — PvP per-turn submission + batch execute.** Extend `SignedCommitManager` with `submitTurnMoves`. Add per-turn move buffer mapping and `numTurnsBuffered` / `numTurnsExecuted` counters. Add `Engine.executeBatch` with flag-based dispatch (§6.1), requiring execution of all currently buffered turns. Equivalence tests + gas snapshots. + +**Phase 2.5 — CPU mode.** Extend `CPUMoveManager` with `selectMoveWithStateHint` (§7.4). Reuse Phase-2 buffer + `executeBatch`. Equivalence test: 24-turn CPU game via legacy `selectMove × 24` vs `selectMoveWithStateHint × 24 + executeBatch × 3` produces identical end state. + +**Phase 3 — Transpiler parity (deferred).** Local TS engine continues running single-turn `execute()` against hydrated state. Eventual batched parity desired but not v1. + +**Phase 4 — Optional cutover.** If `executeShadowed` (B=1) is gas-neutral or better, consider redirecting. Otherwise keep the legacy fast path. + +--- + +## 10. Test surface + +New `BattleHelper` helpers: +- `_submitTurnMoves(battleKey, turnId, p0Move, p1Move)` — synthesizes signatures and calls `submitTurnMoves`. +- `_executeBuffered(battleKey)` — calls `executeBatch` for all currently buffered turns. + +New tests: +- **Submission validation**: wrong committer signer, wrong revealer signer (parity), wrong turnId, wrong battleKey, replay, committer preimage hash mismatch, missing committer sig (regression for unilateral-revealer attack), missing revealer sig. +- **Buffer ordering**: out-of-order rejected; batch executes in turnId order. +- **Switch-turn dispatch**: `flag == 0` and `flag == 1` ignore the non-acting half; non-acting player signing a non-NO_OP has no effect. +- **Equivalence (core gate)**: B turns through legacy path vs `submitTurnMoves × B + executeBatch` produce byte-identical state. +- **Game-over short-circuit** mid-batch: remaining stored buffer entries are no longer live after `numTurnsBuffered` resets to zero. +- **Effect lifecycle parity**: BurnStatus DOT over a 4-turn batch matches per-turn execution. +- **Multi-batch in one battle**: submit 4 then execute, submit 4 then execute, submit 6 then execute — `turnId`, `numTurnsBuffered`, and `numTurnsExecuted` advance correctly. +- **Shadow flush**: post-batch `getMonStateForBattle` / `getGlobalKV` / `getEffects` match equivalent per-turn execution. +- **CPU equivalence**: 24-turn CPU game via legacy vs trusted-state batched produces identical end state. + +Existing tests stay untouched — they use the legacy entry points. + +Targeted equivalence tests for v1; differential fuzzing as a follow-up. + +### 10.1 Effect-shadow correctness tests + +Correctness target: for any scripted turn sequence, batched execution produces the same final battle state and the same mid-execution observations as legacy single-turn execution would produce after each turn. + +Use a small purpose-built mock effect/move suite instead of relying only on production mons: + +- `AddEffectOnRun`: during a hook, calls `engine.addEffect` to append another effect to the same list. +- `EditSelfOnRun`: calls `engine.editEffect` on its own slot and increments a counter in `data`. +- `RemoveSelfOnRun`: returns `removeAfterRun = true`. +- `RemoveOtherOnRun`: calls `engine.removeEffect` for another slot. +- `InspectEffectsOnRun`: calls `engine.getEffects` during the batch and records/validates the visible list. +- `SingletonAbilityRegister`: exercises ability-triggered self-registration through `_activateAbility`. + +Required cases: + +- **Add visibility:** an effect added on sub-turn `T` is visible to `getEffects` and to `_handleEffects` on sub-turn `T+1`. +- **Add during iteration:** when an effect adds another effect while `_handleEffects` is iterating, the shadow count + `effectsDirtyBitmap` behavior matches legacy storage behavior. +- **Edit visibility:** data written by `editEffect` or returned from a hook is visible to later hooks in the same batch. +- **Remove visibility:** a removed effect is tombstoned in shadow, skipped by later `_handleEffects`, and omitted from `getEffects`, with slot indices preserved. +- **OnRemove callback:** removing an effect with `OnRemove` sees shadowed active mon indices and can perform shadowed writes. +- **Singleton/idempotency:** ability self-registration checks the shadow list, so repeated activation in one batch does not duplicate an effect. +- **Global effects:** repeat add/edit/remove/getEffects cases for global effects, including index `15` to cover the `MAX_GLOBAL_EFFECTS = 16` boundary. +- **Per-player boundaries:** cover p0 mon 0, p0 mon 7, p1 mon 0, and p1 mon 7 to exercise numeric key offsets. +- **Capacity:** adding a ninth effect to one mon or a seventeenth global effect fails/no-ops according to the chosen production behavior, and never corrupts adjacent shadow keys. +- **Flush parity:** after batch flush, storage `EffectInstance` slots and counts match the legacy run byte-for-byte, including tombstones. + +Test shape: + +1. Start two identical battles. +2. Run the same scripted turns through legacy single-turn execution in battle A. +3. Submit all turns, execute one full batch in battle B. +4. Compare `BattleData`, mon states, `globalKV`, `getEffects` for all relevant lists, and any mock-recorded observations. diff --git a/docs/extruder.md b/docs/extruder.md new file mode 100644 index 00000000..8a1c0aaf --- /dev/null +++ b/docs/extruder.md @@ -0,0 +1,32 @@ +1) +introducing extruder. + +extruder is an **experimental** **vibe-coded** transpiler that converts Solidity into TypeScript. + +if this sounds insane to you, let me explain why. if you're already insane, feel free to jump to the end, download the package, and start extruding. + +2) +okay, so WHY do something this atrocious? + +two main reasons: gas efficiency, and observability. + +3) +traditionally, devs have been hesitant to include verbose events in smart contracts because of the extra gas costs. we've had projects like shadow or ghost logs try to fix this with "fake" logs that don't get shipped to prod. but this locks you into their infra, and it still requires you to write events at the end of the day. + +event logs are elegant, but also very blunt. you cannot substitute them for traces without emitting a ton of data + +4) +extruder lets you run your contracts anywhere you have a JS runtime. on your client, on your server, etc. importantly, it gives you an entire trace of any transaction. + +5) +how well does it work? i'm already dogfooding extruder in prod! it's used on the game client for @stompdotgg. stomp was a very events heavy game--any given round might produce 10-20 logs for stats being altered, statuses being applied, etc. etc. + +using extruder has saved around 15-20% in gas costs! + +6) +one extra bonus is having your contracts in TypeScript makes them more legible to LLMs. most agents have some JS sandbox, so they can easily fuzz and understand your integrations. + +7) +note that extrude does NOT SUPPORT ALL OF SOLIDITY OR THE EVM. for example, it uses the native Map and does NOT calculate storage slots correctly. some Yul will break! the goal here was to support a good enough fast enough subset of Solidity. + +anyway, it's in alpha here under AGPL v3. try it out! \ No newline at end of file diff --git a/drool/imgs/aurox_front_damage.gif b/drool/imgs/aurox_front_damage.gif new file mode 100644 index 00000000..e745a340 Binary files /dev/null and b/drool/imgs/aurox_front_damage.gif differ diff --git a/drool/imgs/ekineki_front_damage.gif b/drool/imgs/ekineki_front_damage.gif new file mode 100644 index 00000000..edcec9ce Binary files /dev/null and b/drool/imgs/ekineki_front_damage.gif differ diff --git a/drool/imgs/embursa_front_damage.gif b/drool/imgs/embursa_front_damage.gif new file mode 100644 index 00000000..c6a6d061 Binary files /dev/null and b/drool/imgs/embursa_front_damage.gif differ diff --git a/drool/imgs/ghouliath_front_damage.gif b/drool/imgs/ghouliath_front_damage.gif new file mode 100644 index 00000000..0f52a713 Binary files /dev/null and b/drool/imgs/ghouliath_front_damage.gif differ diff --git a/drool/imgs/gorillax_front_damage.gif b/drool/imgs/gorillax_front_damage.gif new file mode 100644 index 00000000..965664f5 Binary files /dev/null and b/drool/imgs/gorillax_front_damage.gif differ diff --git a/drool/imgs/iblivion_front_damage.gif b/drool/imgs/iblivion_front_damage.gif new file mode 100644 index 00000000..fd8191a1 Binary files /dev/null and b/drool/imgs/iblivion_front_damage.gif differ diff --git a/drool/imgs/inutia_front_damage.gif b/drool/imgs/inutia_front_damage.gif new file mode 100644 index 00000000..f7baa662 Binary files /dev/null and b/drool/imgs/inutia_front_damage.gif differ diff --git a/drool/imgs/malalien_front_damage.gif b/drool/imgs/malalien_front_damage.gif new file mode 100644 index 00000000..4f2ea67e Binary files /dev/null and b/drool/imgs/malalien_front_damage.gif differ diff --git a/drool/imgs/mon_spritesheet.png b/drool/imgs/mon_spritesheet.png index b0927afe..79d90a7e 100644 Binary files a/drool/imgs/mon_spritesheet.png and b/drool/imgs/mon_spritesheet.png differ diff --git a/drool/imgs/mon_switch.png b/drool/imgs/mon_switch.png index fef9edaf..18e3cc3b 100644 Binary files a/drool/imgs/mon_switch.png and b/drool/imgs/mon_switch.png differ diff --git a/drool/imgs/pengym_front_damage.gif b/drool/imgs/pengym_front_damage.gif new file mode 100644 index 00000000..967fe055 Binary files /dev/null and b/drool/imgs/pengym_front_damage.gif differ diff --git a/drool/imgs/sofabbi_front_damage.gif b/drool/imgs/sofabbi_front_damage.gif new file mode 100644 index 00000000..e18d25ec Binary files /dev/null and b/drool/imgs/sofabbi_front_damage.gif differ diff --git a/drool/imgs/spritesheet.json b/drool/imgs/spritesheet.json index a0e2e180..79f8608a 100644 --- a/drool/imgs/spritesheet.json +++ b/drool/imgs/spritesheet.json @@ -13,7 +13,7 @@ }, "Nirvamma_Front.gif": { "msPerFrame": 100, - "frames": [[768, 0], [864, 0], [960, 0], [0, 96], [96, 96], [192, 96], [288, 96], [384, 96]], + "frames": [[768, 0], [864, 0], [960, 0], [1056, 0], [0, 96], [96, 96], [192, 96], [288, 96]], "switchOut": { "msPerFrame": 100, "frames": [[768, 0], [864, 0], [960, 0], [1056, 0], [1152, 0], [1248, 0], [1344, 0], [0, 96]] @@ -23,9 +23,9 @@ "frames": [[0, 96], [1344, 0], [1248, 0], [1152, 0], [1056, 0], [960, 0], [864, 0], [768, 0]] } }, - "Nirvamma_Front_Shapes.gif": { - "msPerFrame": 100, - "frames": [[480, 96], [576, 96], [672, 96], [768, 96], [864, 96], [960, 96], [0, 192], [96, 192]], + "aurox_back.gif": { + "msPerFrame": 200, + "frames": [[384, 96], [480, 96]], "switchOut": { "msPerFrame": 100, "frames": [[96, 96], [192, 96], [288, 96], [384, 96], [480, 96], [576, 96], [672, 96], [768, 96]] @@ -35,9 +35,9 @@ "frames": [[768, 96], [672, 96], [576, 96], [480, 96], [384, 96], [288, 96], [192, 96], [96, 96]] } }, - "Nirvamma_Only_Back.gif": { + "aurox_front.gif": { "msPerFrame": 100, - "frames": [[192, 192], [288, 192], [384, 192], [480, 192]], + "frames": [[576, 96], [672, 96], [768, 96], [864, 96]], "switchOut": { "msPerFrame": 100, "frames": [[864, 96], [960, 96], [1056, 96], [1152, 96], [1248, 96], [1344, 96], [0, 192], [96, 192]] @@ -45,11 +45,15 @@ "switchIn": { "msPerFrame": 100, "frames": [[96, 192], [0, 192], [1344, 96], [1248, 96], [1152, 96], [1056, 96], [960, 96], [864, 96]] + }, + "damageFront": { + "msPerFrame": 100, + "frames": [[960, 96], [1056, 96], [0, 192], [96, 192]] } }, - "aurox_back.gif": { - "msPerFrame": 200, - "frames": [[576, 192], [672, 192]], + "ekineki_back.gif": { + "msPerFrame": 100, + "frames": [[192, 192], [288, 192], [384, 192], [480, 192]], "switchOut": { "msPerFrame": 100, "frames": [[192, 192], [288, 192], [384, 192], [480, 192], [576, 192], [672, 192], [768, 192], [864, 192]] @@ -59,9 +63,9 @@ "frames": [[864, 192], [768, 192], [672, 192], [576, 192], [480, 192], [384, 192], [288, 192], [192, 192]] } }, - "aurox_front.gif": { + "ekineki_front.gif": { "msPerFrame": 100, - "frames": [[768, 192], [864, 192], [960, 192], [0, 288]], + "frames": [[576, 192], [672, 192], [768, 192], [864, 192]], "switchOut": { "msPerFrame": 100, "frames": [[960, 192], [1056, 192], [1152, 192], [1248, 192], [1344, 192], [0, 288], [96, 288], [192, 288]] @@ -69,11 +73,15 @@ "switchIn": { "msPerFrame": 100, "frames": [[192, 288], [96, 288], [0, 288], [1344, 192], [1248, 192], [1152, 192], [1056, 192], [960, 192]] + }, + "damageFront": { + "msPerFrame": 100, + "frames": [[960, 192], [1056, 192], [0, 288], [96, 288]] } }, - "ekineki_back.gif": { - "msPerFrame": 100, - "frames": [[96, 288], [192, 288], [288, 288], [384, 288]], + "embursa_back.gif": { + "msPerFrame": 200, + "frames": [[192, 288], [288, 288]], "switchOut": { "msPerFrame": 100, "frames": [[288, 288], [384, 288], [480, 288], [576, 288], [672, 288], [768, 288], [864, 288], [960, 288]] @@ -83,9 +91,9 @@ "frames": [[960, 288], [864, 288], [768, 288], [672, 288], [576, 288], [480, 288], [384, 288], [288, 288]] } }, - "ekineki_front.gif": { - "msPerFrame": 100, - "frames": [[480, 288], [576, 288], [672, 288], [768, 288]], + "embursa_front.gif": { + "msPerFrame": 200, + "frames": [[384, 288], [480, 288], [576, 288], [672, 288]], "switchOut": { "msPerFrame": 100, "frames": [[1056, 288], [1152, 288], [1248, 288], [1344, 288], [0, 384], [96, 384], [192, 384], [288, 384]] @@ -93,11 +101,15 @@ "switchIn": { "msPerFrame": 100, "frames": [[288, 384], [192, 384], [96, 384], [0, 384], [1344, 288], [1248, 288], [1152, 288], [1056, 288]] + }, + "damageFront": { + "msPerFrame": 100, + "frames": [[768, 288], [864, 288], [960, 288], [1056, 288]] } }, - "embursa_back.gif": { + "ghouliath_back.gif": { "msPerFrame": 200, - "frames": [[864, 288], [960, 288]], + "frames": [[0, 384], [96, 384]], "switchOut": { "msPerFrame": 100, "frames": [[384, 384], [480, 384], [576, 384], [672, 384], [768, 384], [864, 384], [960, 384], [1056, 384]] @@ -107,9 +119,9 @@ "frames": [[1056, 384], [960, 384], [864, 384], [768, 384], [672, 384], [576, 384], [480, 384], [384, 384]] } }, - "embursa_front.gif": { - "msPerFrame": 200, - "frames": [[0, 384], [96, 384], [192, 384], [288, 384]], + "ghouliath_front.gif": { + "msPerFrame": 100, + "frames": [[192, 384], [288, 384], [384, 384], [480, 384]], "switchOut": { "msPerFrame": 100, "frames": [[1152, 384], [1248, 384], [1344, 384], [0, 480], [96, 480], [192, 480], [288, 480], [384, 480]] @@ -117,11 +129,15 @@ "switchIn": { "msPerFrame": 100, "frames": [[384, 480], [288, 480], [192, 480], [96, 480], [0, 480], [1344, 384], [1248, 384], [1152, 384]] + }, + "damageFront": { + "msPerFrame": 100, + "frames": [[576, 384], [672, 384], [768, 384], [864, 384]] } }, - "ghouliath_back.gif": { + "gorillax_back.gif": { "msPerFrame": 200, - "frames": [[384, 384], [480, 384]], + "frames": [[960, 384], [1056, 384]], "switchOut": { "msPerFrame": 100, "frames": [[480, 480], [576, 480], [672, 480], [768, 480], [864, 480], [960, 480], [1056, 480], [1152, 480]] @@ -131,9 +147,9 @@ "frames": [[1152, 480], [1056, 480], [960, 480], [864, 480], [768, 480], [672, 480], [576, 480], [480, 480]] } }, - "ghouliath_front.gif": { + "gorillax_front.gif": { "msPerFrame": 100, - "frames": [[576, 384], [672, 384], [768, 384], [864, 384]], + "frames": [[0, 480], [96, 480], [192, 480], [288, 480]], "switchOut": { "msPerFrame": 100, "frames": [[1248, 480], [1344, 480], [0, 576], [96, 576], [192, 576], [288, 576], [384, 576], [480, 576]] @@ -141,11 +157,15 @@ "switchIn": { "msPerFrame": 100, "frames": [[480, 576], [384, 576], [288, 576], [192, 576], [96, 576], [0, 576], [1344, 480], [1248, 480]] + }, + "damageFront": { + "msPerFrame": 100, + "frames": [[384, 480], [480, 480], [576, 480], [672, 480]] } }, - "gorillax_back.gif": { + "iblivion_back.gif": { "msPerFrame": 200, - "frames": [[960, 384], [0, 480]], + "frames": [[768, 480], [864, 480]], "switchOut": { "msPerFrame": 100, "frames": [[576, 576], [672, 576], [768, 576], [864, 576], [960, 576], [1056, 576], [1152, 576], [1248, 576]] @@ -155,9 +175,9 @@ "frames": [[1248, 576], [1152, 576], [1056, 576], [960, 576], [864, 576], [768, 576], [672, 576], [576, 576]] } }, - "gorillax_front.gif": { + "iblivion_front.gif": { "msPerFrame": 100, - "frames": [[96, 480], [192, 480], [288, 480], [384, 480]], + "frames": [[960, 480], [1056, 480], [0, 576], [96, 576]], "switchOut": { "msPerFrame": 100, "frames": [[1344, 576], [0, 672], [96, 672], [192, 672], [288, 672], [384, 672], [480, 672], [576, 672]] @@ -165,11 +185,15 @@ "switchIn": { "msPerFrame": 100, "frames": [[576, 672], [480, 672], [384, 672], [288, 672], [192, 672], [96, 672], [0, 672], [1344, 576]] + }, + "damageFront": { + "msPerFrame": 100, + "frames": [[192, 576], [288, 576], [384, 576], [480, 576]] } }, - "iblivion_back.gif": { + "inutia_back.gif": { "msPerFrame": 200, - "frames": [[480, 480], [576, 480]], + "frames": [[576, 576], [672, 576]], "switchOut": { "msPerFrame": 100, "frames": [[672, 672], [768, 672], [864, 672], [960, 672], [1056, 672], [1152, 672], [1248, 672], [1344, 672]] @@ -179,9 +203,9 @@ "frames": [[1344, 672], [1248, 672], [1152, 672], [1056, 672], [960, 672], [864, 672], [768, 672], [672, 672]] } }, - "iblivion_front.gif": { + "inutia_front.gif": { "msPerFrame": 100, - "frames": [[672, 480], [768, 480], [864, 480], [960, 480]], + "frames": [[768, 576], [864, 576], [960, 576], [1056, 576]], "switchOut": { "msPerFrame": 100, "frames": [[0, 768], [96, 768], [192, 768], [288, 768], [384, 768], [480, 768], [576, 768], [672, 768]] @@ -189,11 +213,15 @@ "switchIn": { "msPerFrame": 100, "frames": [[672, 768], [576, 768], [480, 768], [384, 768], [288, 768], [192, 768], [96, 768], [0, 768]] + }, + "damageFront": { + "msPerFrame": 100, + "frames": [[0, 672], [96, 672], [192, 672], [288, 672]] } }, - "inutia_back.gif": { + "malalien_back.gif": { "msPerFrame": 200, - "frames": [[0, 576], [96, 576]], + "frames": [[384, 672], [480, 672]], "switchOut": { "msPerFrame": 100, "frames": [[768, 768], [864, 768], [960, 768], [1056, 768], [1152, 768], [1248, 768], [1344, 768], [0, 864]] @@ -203,9 +231,9 @@ "frames": [[0, 864], [1344, 768], [1248, 768], [1152, 768], [1056, 768], [960, 768], [864, 768], [768, 768]] } }, - "inutia_front.gif": { + "malalien_front.gif": { "msPerFrame": 100, - "frames": [[192, 576], [288, 576], [384, 576], [480, 576]], + "frames": [[576, 672], [672, 672], [768, 672], [864, 672]], "switchOut": { "msPerFrame": 100, "frames": [[96, 864], [192, 864], [288, 864], [384, 864], [480, 864], [576, 864], [672, 864], [768, 864]] @@ -213,11 +241,15 @@ "switchIn": { "msPerFrame": 100, "frames": [[768, 864], [672, 864], [576, 864], [480, 864], [384, 864], [288, 864], [192, 864], [96, 864]] + }, + "damageFront": { + "msPerFrame": 100, + "frames": [[960, 672], [1056, 672], [0, 768], [96, 768]] } }, - "malalien_back.gif": { + "pengym_back.gif": { "msPerFrame": 200, - "frames": [[576, 576], [672, 576]], + "frames": [[192, 768], [288, 768]], "switchOut": { "msPerFrame": 100, "frames": [[864, 864], [960, 864], [1056, 864], [1152, 864], [1248, 864], [1344, 864], [0, 960], [96, 960]] @@ -227,9 +259,9 @@ "frames": [[96, 960], [0, 960], [1344, 864], [1248, 864], [1152, 864], [1056, 864], [960, 864], [864, 864]] } }, - "malalien_front.gif": { + "pengym_front.gif": { "msPerFrame": 100, - "frames": [[768, 576], [864, 576], [960, 576], [0, 672]], + "frames": [[384, 768], [480, 768], [576, 768], [672, 768]], "switchOut": { "msPerFrame": 100, "frames": [[192, 960], [288, 960], [384, 960], [480, 960], [576, 960], [672, 960], [768, 960], [864, 960]] @@ -237,11 +269,15 @@ "switchIn": { "msPerFrame": 100, "frames": [[864, 960], [768, 960], [672, 960], [576, 960], [480, 960], [384, 960], [288, 960], [192, 960]] + }, + "damageFront": { + "msPerFrame": 100, + "frames": [[768, 768], [864, 768], [960, 768], [1056, 768]] } }, - "pengym_back.gif": { + "sofabbi_back.gif": { "msPerFrame": 200, - "frames": [[96, 672], [192, 672]], + "frames": [[0, 864], [96, 864]], "switchOut": { "msPerFrame": 100, "frames": [[960, 960], [1056, 960], [1152, 960], [1248, 960], [1344, 960], [0, 1056], [96, 1056], [192, 1056]] @@ -251,9 +287,9 @@ "frames": [[192, 1056], [96, 1056], [0, 1056], [1344, 960], [1248, 960], [1152, 960], [1056, 960], [960, 960]] } }, - "pengym_front.gif": { - "msPerFrame": 100, - "frames": [[288, 672], [384, 672], [480, 672], [576, 672]], + "sofabbi_front.gif": { + "msPerFrame": 200, + "frames": [[192, 864], [288, 864], [384, 864], [480, 864]], "switchOut": { "msPerFrame": 100, "frames": [[288, 1056], [384, 1056], [480, 1056], [576, 1056], [672, 1056], [768, 1056], [864, 1056], [960, 1056]] @@ -261,11 +297,15 @@ "switchIn": { "msPerFrame": 100, "frames": [[960, 1056], [864, 1056], [768, 1056], [672, 1056], [576, 1056], [480, 1056], [384, 1056], [288, 1056]] + }, + "damageFront": { + "msPerFrame": 100, + "frames": [[576, 864], [672, 864], [768, 864], [864, 864]] } }, - "sofabbi_back.gif": { + "volthare_back.gif": { "msPerFrame": 200, - "frames": [[672, 672], [768, 672]], + "frames": [[960, 864], [1056, 864]], "switchOut": { "msPerFrame": 100, "frames": [[1056, 1056], [1152, 1056], [1248, 1056], [1344, 1056], [0, 1152], [96, 1152], [192, 1152], [288, 1152]] @@ -275,9 +315,9 @@ "frames": [[288, 1152], [192, 1152], [96, 1152], [0, 1152], [1344, 1056], [1248, 1056], [1152, 1056], [1056, 1056]] } }, - "sofabbi_front.gif": { - "msPerFrame": 200, - "frames": [[864, 672], [960, 672], [0, 768], [96, 768]], + "volthare_front.gif": { + "msPerFrame": 100, + "frames": [[0, 960], [96, 960], [192, 960], [288, 960]], "switchOut": { "msPerFrame": 100, "frames": [[384, 1152], [480, 1152], [576, 1152], [672, 1152], [768, 1152], [864, 1152], [960, 1152], [1056, 1152]] @@ -285,11 +325,15 @@ "switchIn": { "msPerFrame": 100, "frames": [[1056, 1152], [960, 1152], [864, 1152], [768, 1152], [672, 1152], [576, 1152], [480, 1152], [384, 1152]] + }, + "damageFront": { + "msPerFrame": 100, + "frames": [[384, 960], [480, 960], [576, 960], [672, 960]] } }, - "volthare_back.gif": { + "xmon_back.gif": { "msPerFrame": 200, - "frames": [[192, 768], [288, 768]], + "frames": [[768, 960], [864, 960]], "switchOut": { "msPerFrame": 100, "frames": [[1152, 1152], [1248, 1152], [1344, 1152], [0, 1248], [96, 1248], [192, 1248], [288, 1248], [384, 1248]] @@ -299,9 +343,9 @@ "frames": [[384, 1248], [288, 1248], [192, 1248], [96, 1248], [0, 1248], [1344, 1152], [1248, 1152], [1152, 1152]] } }, - "volthare_front.gif": { + "xmon_front.gif": { "msPerFrame": 100, - "frames": [[384, 768], [480, 768], [576, 768], [672, 768]], + "frames": [[960, 960], [1056, 960], [0, 1056], [96, 1056]], "switchOut": { "msPerFrame": 100, "frames": [[480, 1248], [576, 1248], [672, 1248], [768, 1248], [864, 1248], [960, 1248], [1056, 1248], [1152, 1248]] @@ -309,30 +353,10 @@ "switchIn": { "msPerFrame": 100, "frames": [[1152, 1248], [1056, 1248], [960, 1248], [864, 1248], [768, 1248], [672, 1248], [576, 1248], [480, 1248]] - } - }, - "xmon_back.gif": { - "msPerFrame": 200, - "frames": [[768, 768], [864, 768]], - "switchOut": { - "msPerFrame": 100, - "frames": [[1248, 1248], [1344, 1248], [0, 1344], [96, 1344], [192, 1344], [288, 1344], [384, 1344], [480, 1344]] }, - "switchIn": { - "msPerFrame": 100, - "frames": [[480, 1344], [384, 1344], [288, 1344], [192, 1344], [96, 1344], [0, 1344], [1344, 1248], [1248, 1248]] - } - }, - "xmon_front.gif": { - "msPerFrame": 100, - "frames": [[960, 768], [0, 864], [96, 864], [192, 864]], - "switchOut": { - "msPerFrame": 100, - "frames": [[576, 1344], [672, 1344], [768, 1344], [864, 1344], [960, 1344], [1056, 1344], [1152, 1344], [1248, 1344]] - }, - "switchIn": { + "damageFront": { "msPerFrame": 100, - "frames": [[1248, 1344], [1152, 1344], [1056, 1344], [960, 1344], [864, 1344], [768, 1344], [672, 1344], [576, 1344]] + "frames": [[192, 1056], [288, 1056], [384, 1056], [480, 1056]] } } } \ No newline at end of file diff --git a/drool/imgs/volthare_front_damage.gif b/drool/imgs/volthare_front_damage.gif new file mode 100644 index 00000000..1c4c9859 Binary files /dev/null and b/drool/imgs/volthare_front_damage.gif differ diff --git a/drool/imgs/xmon_front_damage.gif b/drool/imgs/xmon_front_damage.gif new file mode 100644 index 00000000..2d78de23 Binary files /dev/null and b/drool/imgs/xmon_front_damage.gif differ diff --git a/processing/buildDamageGifs.py b/processing/buildDamageGifs.py new file mode 100644 index 00000000..af60fd8f --- /dev/null +++ b/processing/buildDamageGifs.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +"""Rename *_damage_front.png sheets to *_front_damage.png and convert them to +4-frame 96x96 animated GIFs that match the existing front-gif format.""" + +import sys +from pathlib import Path +from PIL import Image + +FRAME_SIZE = 96 +NUM_FRAMES = 4 +FRAME_DURATION_MS = 100 # matches existing {mon}_front.gif frame timing + + +def rename_damage_pngs(source_dir: Path) -> list[Path]: + """Rename *_damage_front.png -> *_front_damage.png in place. Returns the + full set of *_front_damage.png paths (including any that were already + correctly named).""" + for old in sorted(source_dir.glob("*_damage_front.png")): + new = old.with_name(old.name.replace("_damage_front.png", "_front_damage.png")) + if new.exists() and new != old: + print(f" ⚠ Skipping rename, target exists: {new.name}") + continue + old.rename(new) + print(f" ✓ Renamed {old.name} -> {new.name}") + return sorted(source_dir.glob("*_front_damage.png")) + + +def png_sheet_to_gif(png_path: Path, gif_path: Path) -> None: + """Slice a horizontal NUM_FRAMES x 1 sprite sheet into individual frames and + save as an animated GIF.""" + sheet = Image.open(png_path).convert("RGBA") + expected = (FRAME_SIZE * NUM_FRAMES, FRAME_SIZE) + if sheet.size != expected: + raise ValueError(f"{png_path.name}: expected {expected[0]}x{expected[1]}, got {sheet.size[0]}x{sheet.size[1]}") + + frames = [ + sheet.crop((i * FRAME_SIZE, 0, (i + 1) * FRAME_SIZE, FRAME_SIZE)) + for i in range(NUM_FRAMES) + ] + frames[0].save( + gif_path, + save_all=True, + append_images=frames[1:], + duration=FRAME_DURATION_MS, + loop=0, + disposal=2, + transparency=0, + optimize=False, + ) + + +def run(source_dir: str = None, output_dir: str = None) -> bool: + base = Path(__file__).parent.parent + src = Path(source_dir) if source_dir else base / "drool" / "imgs" + out = Path(output_dir) if output_dir else base / "drool" / "imgs" + + if not src.is_dir(): + print(f"Error: source dir '{src}' does not exist") + return False + out.mkdir(parents=True, exist_ok=True) + + print(f"Renaming damage sheets in: {src}") + pngs = rename_damage_pngs(src) + if not pngs: + print("No *_damage_front.png or *_front_damage.png files found") + return False + + print(f"\nConverting {len(pngs)} sheets to GIFs in: {out}") + for png in pngs: + gif_path = out / (png.stem + ".gif") + png_sheet_to_gif(png, gif_path) + print(f" ✓ {png.name} -> {gif_path.name}") + + print("\n✅ Done!") + return True + + +def main() -> None: + src = sys.argv[1] if len(sys.argv) >= 2 else None + out = sys.argv[2] if len(sys.argv) >= 3 else None + if not run(src, out): + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/processing/createMonSpritesheets.py b/processing/createMonSpritesheets.py index da442048..fb925129 100644 --- a/processing/createMonSpritesheets.py +++ b/processing/createMonSpritesheets.py @@ -132,7 +132,17 @@ def create_spritesheets(gif_files: list[str], output_dir: str): metadata = {} all_frames, all_switch_frames = [], [] - for gif_path in gif_files: + # Damage gifs (e.g. aurox_front_damage.gif) are not standalone entries — + # their frames get attached to the matching {mon}_front.gif entry as a + # `damageFront` sub-key, and are packed into the spritesheet immediately + # after the parent's idle frames so related frames stay contiguous. + regular_gifs = [g for g in gif_files if "_front_damage" not in Path(g).name.lower()] + damage_gifs = [g for g in gif_files if "_front_damage" in Path(g).name.lower()] + damage_by_parent = { + Path(g).name.replace("_front_damage.gif", "_front.gif"): g for g in damage_gifs + } + + for gif_path in regular_gifs: name = Path(gif_path).name frames, frame_rate = extract_frames(gif_path) print(f"Extracted {len(frames)} frames from {name} (frame rate: {frame_rate}ms)") @@ -142,6 +152,18 @@ def create_spritesheets(gif_files: list[str], output_dir: str): all_frames.extend(frames) metadata[name] = {"msPerFrame": frame_rate, "_main_start": frame_start, "_main_count": len(frames)} + # Damage frames immediately follow the idle frames for this mon + if name in damage_by_parent: + damage_path = damage_by_parent.pop(name) + damage_name = Path(damage_path).name + d_frames, d_rate = extract_frames(damage_path) + damage_start = len(all_frames) + all_frames.extend(d_frames) + metadata[name]["_damage_start"] = damage_start + metadata[name]["_damage_count"] = len(d_frames) + metadata[name]["_damage_rate"] = d_rate + print(f"Extracted {len(d_frames)} damage frames from {damage_name} -> {name} (frame rate: {d_rate}ms)") + # Switch animation frames white = to_white_silhouette(frames[0]) morph = create_morph_animation(white, is_back="back" in name.lower()) @@ -151,6 +173,9 @@ def create_spritesheets(gif_files: list[str], output_dir: str): metadata[name]["_switch_count"] = len(morph) print(f"Generated {len(morph)} switch frames for {name}") + for parent in damage_by_parent: + print(f"⚠ No matching {parent} entry for {parent.replace('_front.gif', '_front_damage.gif')}, skipping") + # Build main spritesheet sheet, positions = build_spritesheet(all_frames) output_path = Path(output_dir) @@ -189,6 +214,14 @@ def create_spritesheets(gif_files: list[str], output_dir: str): switch_frames = [list(switch_positions[switch_start + i]) for i in range(switch_count)] data["switchOut"] = {"msPerFrame": 100, "frames": switch_frames} data["switchIn"] = {"msPerFrame": 100, "frames": switch_frames[::-1]} + if "_damage_start" in data: + d_start = data.pop("_damage_start") + d_count = data.pop("_damage_count") + d_rate = data.pop("_damage_rate") + data["damageFront"] = { + "msPerFrame": d_rate, + "frames": [list(positions[d_start + i]) for i in range(d_count)], + } # Save JSON with compact coordinate arrays json_path = Path(output_dir) / "spritesheet.json" diff --git a/processing/generateMonsTypeScript.py b/processing/generateMonsTypeScript.py index bde93295..9194d37e 100644 --- a/processing/generateMonsTypeScript.py +++ b/processing/generateMonsTypeScript.py @@ -95,6 +95,7 @@ def build_sprites( ("front", "frontIdle", None, "mon_spritesheet.png", True), ("front", "frontSwitchIn", "switchIn", "mon_switch.png", False), ("front", "frontSwitchOut", "switchOut", "mon_switch.png", False), + ("front", "frontHurt", "damageFront", "mon_spritesheet.png", False), ("back", "backIdle", None, "mon_spritesheet.png", True), ("back", "backSwitchIn", "switchIn", "mon_switch.png", False), ("back", "backSwitchOut", "switchOut", "mon_switch.png", False), @@ -353,9 +354,11 @@ def generate_typescript_const(data: Dict[int, Dict[str, Any]], output_file: str) readonly frontIdle: SpriteAnimationConfig; readonly frontSwitchIn: SpriteAnimationConfig; readonly frontSwitchOut: SpriteAnimationConfig; + readonly frontHurt?: SpriteAnimationConfig; readonly backIdle: SpriteAnimationConfig; readonly backSwitchIn: SpriteAnimationConfig; readonly backSwitchOut: SpriteAnimationConfig; + readonly backHurt?: SpriteAnimationConfig; }}; readonly stats: {{ readonly hp: number; diff --git a/script/EngineAndPeriphery.s.sol b/script/EngineAndPeriphery.s.sol index 2a4eb4bd..8a041b7d 100644 --- a/script/EngineAndPeriphery.s.sol +++ b/script/EngineAndPeriphery.s.sol @@ -6,15 +6,11 @@ import "../src/Constants.sol"; // Fundamental entities import {SignedCommitManager} from "../src/commit-manager/SignedCommitManager.sol"; -import {DefaultRuleset} from "../src/DefaultRuleset.sol"; import {Engine} from "../src/Engine.sol"; import {DefaultValidator} from "../src/DefaultValidator.sol"; import {OkayCPU} from "../src/cpu/OkayCPU.sol"; import {BetterCPU} from "../src/cpu/BetterCPU.sol"; -import {IEffect} from "../src/effects/IEffect.sol"; -import {StaminaRegen} from "../src/effects/StaminaRegen.sol"; import {GachaRegistry, IGachaRNG} from "../src/gacha/GachaRegistry.sol"; -import {DefaultRandomnessOracle} from "../src/rng/DefaultRandomnessOracle.sol"; import {ICPURNG} from "../src/rng/ICPURNG.sol"; import {DefaultMonRegistry} from "../src/teams/DefaultMonRegistry.sol"; import {GachaTeamRegistry} from "../src/teams/GachaTeamRegistry.sol"; diff --git a/snapshots/BetterCPUInlineGasTest.json b/snapshots/BetterCPUInlineGasTest.json index ae3a2b0a..b2978ec6 100644 --- a/snapshots/BetterCPUInlineGasTest.json +++ b/snapshots/BetterCPUInlineGasTest.json @@ -1,8 +1,8 @@ { - "Flag0_P0ForcedSwitch": "22701", - "Turn0_Lead": "104410", - "Turn1_BothAttack": "262381", - "Turn2_BothAttack": "236457", - "Turn3_BothAttack": "232481", - "Turn4_BothAttack": "232485" + "Flag0_P0ForcedSwitch": "22763", + "Turn0_Lead": "104184", + "Turn1_BothAttack": "264088", + "Turn2_BothAttack": "238164", + "Turn3_BothAttack": "234188", + "Turn4_BothAttack": "234192" } \ No newline at end of file diff --git a/snapshots/EngineGasTest.json b/snapshots/EngineGasTest.json index 844a9603..8d0b40ce 100644 --- a/snapshots/EngineGasTest.json +++ b/snapshots/EngineGasTest.json @@ -1,21 +1,21 @@ { - "B1_Execute": "889458", - "B1_Setup": "816408", - "B2_Execute": "645719", - "B2_Setup": "282218", - "Battle1_Execute": "449877", - "Battle1_Setup": "791633", - "Battle2_Execute": "367369", - "Battle2_Setup": "233097", - "External_Execute": "456527", - "External_Setup": "782348", - "FirstBattle": "2906000", - "Inline_Execute": "318805", - "Inline_Setup": "225023", + "B1_Execute": "889886", + "B1_Setup": "816459", + "B2_Execute": "648291", + "B2_Setup": "282290", + "Battle1_Execute": "449213", + "Battle1_Setup": "791685", + "Battle2_Execute": "368905", + "Battle2_Setup": "233149", + "External_Execute": "456719", + "External_Setup": "782400", + "FirstBattle": "2911711", + "Inline_Execute": "320015", + "Inline_Setup": "225083", "Intermediary stuff": "44458", - "SecondBattle": "2927592", - "Setup 1": "1670484", - "Setup 2": "292493", - "Setup 3": "332770", - "ThirdBattle": "2274723" + "SecondBattle": "2936650", + "Setup 1": "1670530", + "Setup 2": "292539", + "Setup 3": "332805", + "ThirdBattle": "2282494" } \ No newline at end of file diff --git a/snapshots/EngineOptimizationTest.json b/snapshots/EngineOptimizationTest.json index 883b347d..09b37c45 100644 --- a/snapshots/EngineOptimizationTest.json +++ b/snapshots/EngineOptimizationTest.json @@ -1,4 +1,4 @@ { - "ExternalStaminaRegen": "390772", - "InlineStaminaRegen": "999942" + "ExternalStaminaRegen": "392424", + "InlineStaminaRegen": "999051" } \ No newline at end of file diff --git a/snapshots/FullyOptimizedInlineGasTest.json b/snapshots/FullyOptimizedInlineGasTest.json index 3edf6547..66ef6ab8 100644 --- a/snapshots/FullyOptimizedInlineGasTest.json +++ b/snapshots/FullyOptimizedInlineGasTest.json @@ -1,8 +1,8 @@ { - "Fast_Battle1": "1802444", - "Fast_Battle2": "1704099", - "Fast_Battle3": "1221705", - "Fast_Setup_1": "1321867", - "Fast_Setup_2": "216314", - "Fast_Setup_3": "212698" + "Fast_Battle1": "1867564", + "Fast_Battle2": "1777864", + "Fast_Battle3": "1288656", + "Fast_Setup_1": "1322088", + "Fast_Setup_2": "216706", + "Fast_Setup_3": "212909" } \ No newline at end of file diff --git a/snapshots/InlineEngineGasTest.json b/snapshots/InlineEngineGasTest.json index ffc94073..d4833e98 100644 --- a/snapshots/InlineEngineGasTest.json +++ b/snapshots/InlineEngineGasTest.json @@ -1,16 +1,16 @@ { - "B1_Execute": "870357", - "B1_Setup": "758497", - "B2_Execute": "604941", - "B2_Setup": "271670", - "Battle1_Execute": "402355", - "Battle1_Setup": "733715", - "Battle2_Execute": "318757", - "Battle2_Setup": "224451", - "FirstBattle": "2595237", - "SecondBattle": "2580140", - "Setup 1": "1612152", - "Setup 2": "318169", - "Setup 3": "314901", - "ThirdBattle": "1964987" + "B1_Execute": "870101", + "B1_Setup": "758557", + "B2_Execute": "606857", + "B2_Setup": "271758", + "Battle1_Execute": "401230", + "Battle1_Setup": "733775", + "Battle2_Execute": "319832", + "Battle2_Setup": "224511", + "FirstBattle": "2595770", + "SecondBattle": "2583418", + "Setup 1": "1612191", + "Setup 2": "318239", + "Setup 3": "314704", + "ThirdBattle": "1967495" } \ No newline at end of file diff --git a/snapshots/MatchmakerTest.json b/snapshots/MatchmakerTest.json index 9b3b0b0c..344f7a99 100644 --- a/snapshots/MatchmakerTest.json +++ b/snapshots/MatchmakerTest.json @@ -1,5 +1,5 @@ { - "Accept1": "307765", - "Accept2": "34130", - "Propose1": "197286" + "Accept1": "307809", + "Accept2": "34162", + "Propose1": "197318" } \ No newline at end of file diff --git a/snapshots/StandardAttackPvPGasTest.json b/snapshots/StandardAttackPvPGasTest.json index 020b58ba..ea0ef873 100644 --- a/snapshots/StandardAttackPvPGasTest.json +++ b/snapshots/StandardAttackPvPGasTest.json @@ -1,7 +1,7 @@ { - "Turn0_Lead": "64905", - "Turn1_BothAttack": "117165", - "Turn2_BothAttack": "77337", - "Turn3_BothAttack": "77398", - "Turn4_BothAttack": "77347" + "Turn0_Lead": "154149", + "Turn1_BothAttack": "218306", + "Turn2_BothAttack": "184102", + "Turn3_BothAttack": "190651", + "Turn4_BothAttack": "190600" } \ No newline at end of file diff --git a/src/DefaultValidator.sol b/src/DefaultValidator.sol index d2659ed1..e9f35c71 100644 --- a/src/DefaultValidator.sol +++ b/src/DefaultValidator.sol @@ -103,7 +103,7 @@ contract DefaultValidator is IValidator { bytes32 battleKey, uint256 moveIndex, uint256 playerIndex, - uint240 extraData + uint16 extraData ) public view returns (bool) { BattleContext memory ctx = ENGINE.getBattleContext(battleKey); uint256 activeMonIndex = (playerIndex == 0) ? ctx.p0ActiveMonIndex : ctx.p1ActiveMonIndex; @@ -127,7 +127,7 @@ contract DefaultValidator is IValidator { } // Validates that you can't switch to the same mon, you have enough stamina, the move isn't disabled, etc. - function validatePlayerMove(bytes32 battleKey, uint256 moveIndex, uint256 playerIndex, uint240 extraData) + function validatePlayerMove(bytes32 battleKey, uint256 moveIndex, uint256 playerIndex, uint16 extraData) external view returns (bool) @@ -191,7 +191,7 @@ contract DefaultValidator is IValidator { bytes32 battleKey, uint256 moveIndex, uint256 playerIndex, - uint240 extraData, + uint16 extraData, uint256 activeMonIndex, ValidationContext memory vctx ) internal view returns (bool) { diff --git a/src/Engine.sol b/src/Engine.sol index 4f608183..df1be320 100644 --- a/src/Engine.sol +++ b/src/Engine.sol @@ -15,7 +15,6 @@ import {StaminaRegenLogic} from "./lib/StaminaRegenLogic.sol"; import {TimeoutCheckParams, ValidatorLogic} from "./lib/ValidatorLogic.sol"; import {IMatchmaker} from "./matchmaker/IMatchmaker.sol"; import {AttackCalculator} from "./moves/AttackCalculator.sol"; -import {MoveSlotLib} from "./moves/MoveSlotLib.sol"; import {TypeCalcLib} from "./types/TypeCalcLib.sol"; contract Engine is IEngine, MappingAllocator { @@ -46,8 +45,8 @@ contract Engine is IEngine, MappingAllocator { // A non-zero encoded move is the "transient is populated for this call" signal. uint256 private transient _turnP0MoveEncoded; uint256 private transient _turnP1MoveEncoded; - bytes32 private transient _turnP0Salt; - bytes32 private transient _turnP1Salt; + uint104 private transient _turnP0Salt; + uint104 private transient _turnP1Salt; // Errors error NoWriteAllowed(); @@ -65,7 +64,7 @@ contract Engine is IEngine, MappingAllocator { // Events event BattleStart(bytes32 indexed battleKey, address p0, address p1); event MonMove( - bytes32 indexed battleKey, uint256 packedPlayerIndexMonIndex, uint256 packedMoveIndexExtraData, bytes32 salt + bytes32 indexed battleKey, uint256 packedPlayerIndexMonIndex, uint256 packedMoveIndexExtraData, uint104 salt ); event EngineExecute(bytes32 indexed battleKey); event BattleComplete(bytes32 indexed battleKey, address winner); @@ -300,11 +299,11 @@ contract Engine is IEngine, MappingAllocator { function executeWithMoves( bytes32 battleKey, uint8 p0MoveIndex, - bytes32 p0Salt, - uint240 p0ExtraData, + uint104 p0Salt, + uint16 p0ExtraData, uint8 p1MoveIndex, - bytes32 p1Salt, - uint240 p1ExtraData + uint104 p1Salt, + uint16 p1ExtraData ) external { bytes32 storageKey = _getStorageKey(battleKey); storageKeyForWrite = storageKey; @@ -329,7 +328,7 @@ contract Engine is IEngine, MappingAllocator { /// @notice Combined single-player setMove + execute for forced switch turns /// @dev Only callable by moveManager. The acting player is inferred from battle.playerSwitchForTurnFlag. - function executeWithSingleMove(bytes32 battleKey, uint8 moveIndex, bytes32 salt, uint240 extraData) external { + function executeWithSingleMove(bytes32 battleKey, uint8 moveIndex, uint104 salt, uint16 extraData) external { bytes32 storageKey = _getStorageKey(battleKey); storageKeyForWrite = storageKey; @@ -358,12 +357,12 @@ contract Engine is IEngine, MappingAllocator { _executeInternal(battleKey, storageKey); } - /// @dev Decodes a transient-encoded move (layout: [extraData:240 | packedMoveIndex:8]) into a + /// @dev Decodes a transient-encoded move (layout: [extraData:16 | packedMoveIndex:8]) into a /// MoveDecision. Encoded == 0 means "no current turn move" since packedMoveIndex always has /// IS_REAL_TURN_BIT set for a real move. function _decodeMove(uint256 encoded) private pure returns (MoveDecision memory m) { m.packedMoveIndex = uint8(encoded & 0xFF); - m.extraData = uint240(encoded >> 8); + m.extraData = uint16(encoded >> 8); } /// @dev Returns the current turn's MoveDecision for `playerIndex`. During an active @@ -381,7 +380,7 @@ contract Engine is IEngine, MappingAllocator { } /// @dev Salt companion to `_getCurrentTurnMove`. - function _getCurrentTurnSalt(BattleConfig storage config, uint256 playerIndex) internal view returns (bytes32) { + function _getCurrentTurnSalt(BattleConfig storage config, uint256 playerIndex) internal view returns (uint104) { uint256 encoded = playerIndex == 0 ? _turnP0MoveEncoded : _turnP1MoveEncoded; if (encoded != 0) { return playerIndex == 0 ? _turnP0Salt : _turnP1Salt; @@ -481,12 +480,12 @@ contract Engine is IEngine, MappingAllocator { // Update the temporary RNG to the newest value // Inline RNG computation when oracle is address(0) to avoid external call uint256 rng; - bytes32 p0TurnSalt = _getCurrentTurnSalt(config, 0); - bytes32 p1TurnSalt = _getCurrentTurnSalt(config, 1); + uint104 p0TurnSalt = _getCurrentTurnSalt(config, 0); + uint104 p1TurnSalt = _getCurrentTurnSalt(config, 1); if (address(config.rngOracle) == address(0)) { rng = uint256(keccak256(abi.encode(p0TurnSalt, p1TurnSalt))); } else { - rng = config.rngOracle.getRNG(p0TurnSalt, p1TurnSalt); + rng = config.rngOracle.getRNG(bytes32(uint256(p0TurnSalt)), bytes32(uint256(p1TurnSalt))); } tempRNG = rng; @@ -730,8 +729,8 @@ contract Engine is IEngine, MappingAllocator { function resetCallContext() external { _turnP0MoveEncoded = 0; _turnP1MoveEncoded = 0; - _turnP0Salt = bytes32(0); - _turnP1Salt = bytes32(0); + _turnP0Salt = 0; + _turnP1Salt = 0; } function end(bytes32 battleKey) external { @@ -1377,8 +1376,8 @@ contract Engine is IEngine, MappingAllocator { BattleConfig storage config, uint256 playerIndex, uint8 moveIndex, - bytes32 salt, - uint240 extraData + uint104 salt, + uint16 extraData ) internal { // Pack moveIndex with isRealTurn bit and apply +1 offset for regular moves // Regular moves (< SWITCH_MOVE_INDEX) are stored as moveIndex + 1 to avoid zero ambiguity @@ -1395,7 +1394,7 @@ contract Engine is IEngine, MappingAllocator { } } - function setMove(bytes32 battleKey, uint256 playerIndex, uint8 moveIndex, bytes32 salt, uint240 extraData) + function setMove(bytes32 battleKey, uint256 playerIndex, uint8 moveIndex, uint104 salt, uint16 extraData) external { bool isInsideExecute = _turnP0MoveEncoded != 0 || _turnP1MoveEncoded != 0; @@ -1565,7 +1564,7 @@ contract Engine is IEngine, MappingAllocator { // check below silently no-ops and timeout handles the stuck player. if ((battle.turnId == 0 || currentMonState.isKnockedOut) && moveIndex != SWITCH_MOVE_INDEX) { moveIndex = SWITCH_MOVE_INDEX; - move.extraData = uint240(0); + move.extraData = uint16(0); } // Handle a switch, no-op, or regular move. @@ -2391,7 +2390,7 @@ contract Engine is IEngine, MappingAllocator { /// @notice Validates a player move, handling both inline validation (when validator is address(0)) and external validators /// @dev This allows callers like CPU to validate moves without needing to handle the address(0) case themselves - function validatePlayerMoveForBattle(bytes32 battleKey, uint256 moveIndex, uint256 playerIndex, uint240 extraData) + function validatePlayerMoveForBattle(bytes32 battleKey, uint256 moveIndex, uint256 playerIndex, uint16 extraData) external returns (bool) { diff --git a/src/IEngine.sol b/src/IEngine.sol index 3dd7273f..a75b3e1d 100644 --- a/src/IEngine.sol +++ b/src/IEngine.sol @@ -37,18 +37,18 @@ interface IEngine { uint256 rng ) external returns (int32 damage, bytes32 eventType); function switchActiveMon(uint256 playerIndex, uint256 monToSwitchIndex) external; - function setMove(bytes32 battleKey, uint256 playerIndex, uint8 moveIndex, bytes32 salt, uint240 extraData) external; + function setMove(bytes32 battleKey, uint256 playerIndex, uint8 moveIndex, uint104 salt, uint16 extraData) external; function execute(bytes32 battleKey) external; function executeWithMoves( bytes32 battleKey, uint8 p0MoveIndex, - bytes32 p0Salt, - uint240 p0ExtraData, + uint104 p0Salt, + uint16 p0ExtraData, uint8 p1MoveIndex, - bytes32 p1Salt, - uint240 p1ExtraData + uint104 p1Salt, + uint16 p1ExtraData ) external; - function executeWithSingleMove(bytes32 battleKey, uint8 moveIndex, bytes32 salt, uint240 extraData) external; + function executeWithSingleMove(bytes32 battleKey, uint8 moveIndex, uint104 salt, uint16 extraData) external; function resetCallContext() external; // Getters @@ -94,7 +94,7 @@ interface IEngine { function getPlayerSwitchForTurnFlagForBattleState(bytes32 battleKey) external view returns (uint256); function getGlobalKV(bytes32 battleKey, uint64 key) external view returns (uint192); function getBattleValidator(bytes32 battleKey) external view returns (IValidator); - function validatePlayerMoveForBattle(bytes32 battleKey, uint256 moveIndex, uint256 playerIndex, uint240 extraData) + function validatePlayerMoveForBattle(bytes32 battleKey, uint256 moveIndex, uint256 playerIndex, uint16 extraData) external returns (bool); function getEffects(bytes32 battleKey, uint256 targetIndex, uint256 monIndex) diff --git a/src/IValidator.sol b/src/IValidator.sol index 8dea5f1d..2f71e565 100644 --- a/src/IValidator.sol +++ b/src/IValidator.sol @@ -11,7 +11,7 @@ interface IValidator { ) external returns (bool); // Validates that you can't switch to the same mon, you have enough stamina, the move isn't disabled, etc. - function validatePlayerMove(bytes32 battleKey, uint256 moveIndex, uint256 playerIndex, uint240 extraData) + function validatePlayerMove(bytes32 battleKey, uint256 moveIndex, uint256 playerIndex, uint16 extraData) external returns (bool); @@ -20,7 +20,7 @@ interface IValidator { bytes32 battleKey, uint256 moveIndex, uint256 playerIndex, - uint240 extraData + uint16 extraData ) external returns (bool); // Validates that a switch is valid diff --git a/src/Structs.sol b/src/Structs.sol index 327a52ec..0504a330 100644 --- a/src/Structs.sol +++ b/src/Structs.sol @@ -47,11 +47,11 @@ struct Battle { IEngineHook[] engineHooks; } -// Packed into 1 storage slot (8 + 240 = 248 bits) +// Packed into 1 storage slot (8 + 16 = 24 bits) // packedMoveIndex: lower 7 bits = moveIndex (0-127), bit 7 = isRealTurn (1 = real, 0 = not set) struct MoveDecision { uint8 packedMoveIndex; - uint240 extraData; + uint16 extraData; } // Stored by the Engine, tracks immutable battle data and battle state @@ -81,8 +81,8 @@ struct BattleConfig { uint40 startTimestamp; // 40 — battle start time; overflows in year ~36825 (shrunk from uint48 for slot-2 packing) bool hasInlineStaminaRegen; // 8 uint8 globalKVCount; // 8 — live entry count in the current battle's globalKV key buffer - bytes32 p0Salt; - bytes32 p1Salt; + uint104 p0Salt; + uint104 p1Salt; MoveDecision p0Move; MoveDecision p1Move; mapping(uint256 index => Mon) p0Team; @@ -118,8 +118,8 @@ struct BattleConfigView { uint96 packedP1EffectsCount; uint8 teamSizes; uint40 startTimestamp; // Needed client-side for the getGlobalKV freshness gate - bytes32 p0Salt; - bytes32 p1Salt; + uint104 p0Salt; + uint104 p1Salt; MoveDecision p0Move; MoveDecision p1Move; EffectInstance[] globalEffects; @@ -185,8 +185,8 @@ struct PlayerDecisionData { struct RevealedMove { uint8 moveIndex; - uint240 extraData; - bytes32 salt; + uint16 extraData; + uint104 salt; } // Used for StatBoosts diff --git a/src/commit-manager/DefaultCommitManager.sol b/src/commit-manager/DefaultCommitManager.sol index e2190275..d681cbeb 100644 --- a/src/commit-manager/DefaultCommitManager.sol +++ b/src/commit-manager/DefaultCommitManager.sol @@ -118,7 +118,7 @@ contract DefaultCommitManager is ICommitManager { emit MoveCommit(battleKey, caller); } - function revealMove(bytes32 battleKey, uint8 moveIndex, bytes32 salt, uint240 extraData, bool autoExecute) + function revealMove(bytes32 battleKey, uint8 moveIndex, uint104 salt, uint16 extraData, bool autoExecute) external { // Get all battle context in one call diff --git a/src/commit-manager/ICommitManager.sol b/src/commit-manager/ICommitManager.sol index f9e25bb5..f666c1d8 100644 --- a/src/commit-manager/ICommitManager.sol +++ b/src/commit-manager/ICommitManager.sol @@ -5,7 +5,7 @@ import "../Structs.sol"; interface ICommitManager { function commitMove(bytes32 battleKey, bytes32 moveHash) external; - function revealMove(bytes32 battleKey, uint8 moveIndex, bytes32 salt, uint240 extraData, bool autoExecute) + function revealMove(bytes32 battleKey, uint8 moveIndex, uint104 salt, uint16 extraData, bool autoExecute) external; function getCommitment(bytes32 battleKey, address player) external view returns (bytes32 moveHash, uint256 turnId); function getMoveCountForBattleState(bytes32 battleKey, address player) external view returns (uint256); diff --git a/src/commit-manager/SignedCommitLib.sol b/src/commit-manager/SignedCommitLib.sol index 4646d4ee..3342a749 100644 --- a/src/commit-manager/SignedCommitLib.sol +++ b/src/commit-manager/SignedCommitLib.sol @@ -40,14 +40,14 @@ library SignedCommitLib { uint64 turnId; bytes32 committerMoveHash; // A's hash that B signs over uint8 revealerMoveIndex; - bytes32 revealerSalt; - uint240 revealerExtraData; + uint104 revealerSalt; + uint16 revealerExtraData; } /// @notice Computes the type hash for DualSignedReveal function computeDualSignedRevealTypehash() internal pure returns (bytes32) { return keccak256( - "DualSignedReveal(bytes32 battleKey,uint64 turnId,bytes32 committerMoveHash,uint8 revealerMoveIndex,bytes32 revealerSalt,uint240 revealerExtraData)" + "DualSignedReveal(bytes32 battleKey,uint64 turnId,bytes32 committerMoveHash,uint8 revealerMoveIndex,uint104 revealerSalt,uint16 revealerExtraData)" ); } @@ -58,7 +58,7 @@ library SignedCommitLib { return keccak256( abi.encode( keccak256( - "DualSignedReveal(bytes32 battleKey,uint64 turnId,bytes32 committerMoveHash,uint8 revealerMoveIndex,bytes32 revealerSalt,uint240 revealerExtraData)" + "DualSignedReveal(bytes32 battleKey,uint64 turnId,bytes32 committerMoveHash,uint8 revealerMoveIndex,uint104 revealerSalt,uint16 revealerExtraData)" ), reveal.battleKey, reveal.turnId, diff --git a/src/commit-manager/SignedCommitManager.sol b/src/commit-manager/SignedCommitManager.sol index 20bd4aaf..aa0831fa 100644 --- a/src/commit-manager/SignedCommitManager.sol +++ b/src/commit-manager/SignedCommitManager.sol @@ -20,13 +20,16 @@ import {SignedCommitLib} from "./SignedCommitLib.sol"; /// 3. Alice reveals (TX 3) /// /// Dual-signed flow (1 transaction): -/// 1. Alice signs her move hash off-chain, sends to Bob -/// 2. Bob signs his move + Alice's hash off-chain, sends to Alice -/// 3. Alice calls executeWithDualSignedMoves with both moves + Bob's signature (TX 1) +/// 1. Alice signs her move hash off-chain (SignedCommit), sends to Bob +/// 2. Bob signs his move + Alice's hash off-chain (DualSignedReveal), sends back +/// 3. Anyone (Alice, Bob, or a relayer) calls executeWithDualSignedMoves with +/// both signatures + Alice's preimage (TX 1) /// -/// Security: Alice commits to her hash before seeing Bob's move. Bob signs over -/// Alice's hash, so even though Alice sees Bob's move before submitting, Alice -/// cannot change her move (must provide valid preimage for the hash Bob signed). +/// Security: Alice commits to her hash before seeing Bob's move (binding Alice +/// cryptographically via her SignedCommit). Bob signs over Alice's hash (binding +/// Bob via his DualSignedReveal). Both signatures together prove both players' +/// intent without trusting msg.sender — submission can be relayed without +/// reopening any unilateral-revealer attack. /// /// Fallback if Alice stalls: Bob can use commitWithSignature() to publish Alice's /// signed commitment on-chain, then continue with the normal reveal flow. @@ -36,9 +39,6 @@ contract SignedCommitManager is DefaultCommitManager, EIP712 { /// @notice Thrown when the signature verification fails error InvalidSignature(); - /// @notice Thrown when caller is not the committing player for this turn - error CallerNotCommitter(); - /// @notice Thrown when trying to use dual-signed flow on a single-player turn error NotTwoPlayerTurn(); @@ -54,8 +54,12 @@ contract SignedCommitManager is DefaultCommitManager, EIP712 { } /// @notice Executes a turn using dual-signed moves from both players (gas-optimized) - /// @dev The committer (A) submits both moves. The revealer (B) has signed over - /// their move and A's move hash, binding both players to their moves. + /// @dev Both players sign off-chain — committer over `SignedCommit{committerMoveHash, …}` + /// and revealer over `DualSignedReveal{committerMoveHash, …, revealerMove…}`. Anyone + /// can submit (relayer-friendly) since both signatures are required and bind each + /// player independently. Without the explicit committer signature, a malicious + /// revealer could pick any preimage `P*`, sign `DualSignedReveal{keccak(P*), …}` + /// and play `P*` as the committer's move — the committer signature closes that. /// @param battleKey The battle identifier /// @param committerMoveIndex The committer's move index /// @param committerSalt The committer's salt @@ -63,48 +67,54 @@ contract SignedCommitManager is DefaultCommitManager, EIP712 { /// @param revealerMoveIndex The revealer's move index /// @param revealerSalt The revealer's salt /// @param revealerExtraData The revealer's extra data + /// @param committerSignature EIP-712 signature from the committer over + /// SignedCommit(committerMoveHash, battleKey, turnId) /// @param revealerSignature EIP-712 signature from the revealer over - /// DualSignedReveal(battleKey, turnId, committerMoveHash, revealerMove...) + /// DualSignedReveal(battleKey, turnId, committerMoveHash, revealerMove…) function executeWithDualSignedMoves( bytes32 battleKey, uint8 committerMoveIndex, - bytes32 committerSalt, - uint240 committerExtraData, + uint104 committerSalt, + uint16 committerExtraData, uint8 revealerMoveIndex, - bytes32 revealerSalt, - uint240 revealerExtraData, + uint104 revealerSalt, + uint16 revealerExtraData, + bytes calldata committerSignature, bytes calldata revealerSignature ) external { - // Use lightweight getter (validates internally, reverts on bad state) (address committer, address revealer, uint64 turnId) = ENGINE.getCommitAuthForDualSigned(battleKey); - // Caller must be the committing player - if (msg.sender != committer) { - revert CallerNotCommitter(); - } - - // Compute the committer's move hash bytes32 committerMoveHash = keccak256(abi.encodePacked(committerMoveIndex, committerSalt, committerExtraData)); - // Verify the revealer's signature over DualSignedReveal - SignedCommitLib.DualSignedReveal memory reveal = SignedCommitLib.DualSignedReveal({ - battleKey: battleKey, - turnId: turnId, - committerMoveHash: committerMoveHash, - revealerMoveIndex: revealerMoveIndex, - revealerSalt: revealerSalt, - revealerExtraData: revealerExtraData - }); - - bytes32 digest = _hashTypedData(SignedCommitLib.hashDualSignedReveal(reveal)); - if (ECDSA.recoverCalldata(digest, revealerSignature) != revealer) { - revert InvalidSignature(); + // Scoped to keep `commit`/`reveal` structs from sharing stack space across recoveries. + { + SignedCommitLib.SignedCommit memory commit = SignedCommitLib.SignedCommit({ + moveHash: committerMoveHash, + battleKey: battleKey, + turnId: turnId + }); + bytes32 commitDigest = _hashTypedData(SignedCommitLib.hashSignedCommit(commit)); + if (ECDSA.recoverCalldata(commitDigest, committerSignature) != committer) { + revert InvalidSignature(); + } + } + + { + SignedCommitLib.DualSignedReveal memory reveal = SignedCommitLib.DualSignedReveal({ + battleKey: battleKey, + turnId: turnId, + committerMoveHash: committerMoveHash, + revealerMoveIndex: revealerMoveIndex, + revealerSalt: revealerSalt, + revealerExtraData: revealerExtraData + }); + bytes32 revealDigest = _hashTypedData(SignedCommitLib.hashDualSignedReveal(reveal)); + if (ECDSA.recoverCalldata(revealDigest, revealerSignature) != revealer) { + revert InvalidSignature(); + } } - // Execute with moves in a single call (engine validates during execution) - // No playerData updates needed - engine tracks lastExecuteTimestamp for timeouts if (turnId % 2 == 0) { - // Committer is p0 ENGINE.executeWithMoves( battleKey, committerMoveIndex, @@ -115,7 +125,6 @@ contract SignedCommitManager is DefaultCommitManager, EIP712 { revealerExtraData ); } else { - // Committer is p1 ENGINE.executeWithMoves( battleKey, revealerMoveIndex, @@ -130,7 +139,7 @@ contract SignedCommitManager is DefaultCommitManager, EIP712 { /// @notice Executes a forced single-player move, usually a switch after a KO, in one transaction. /// @dev The acting player is inferred from the engine's switch flag and must be msg.sender. - function executeSinglePlayerMove(bytes32 battleKey, uint8 moveIndex, bytes32 salt, uint240 extraData) external { + function executeSinglePlayerMove(bytes32 battleKey, uint8 moveIndex, uint104 salt, uint16 extraData) external { CommitContext memory ctx = ENGINE.getCommitContext(battleKey); if (ctx.startTimestamp == 0) { diff --git a/src/cpu/BetterCPU.sol b/src/cpu/BetterCPU.sol index 68077cc9..4a1cbec0 100644 --- a/src/cpu/BetterCPU.sol +++ b/src/cpu/BetterCPU.sol @@ -56,10 +56,10 @@ contract BetterCPU is CPU { // ============ CORE DECISION TREE ============ - function calculateMove(CPUContext memory ctx, uint8 playerMoveIndex, uint240 playerExtraData) + function calculateMove(CPUContext memory ctx, uint8 playerMoveIndex, uint16 playerExtraData) external override - returns (uint128 moveIndex, uint240 extraData) + returns (uint128 moveIndex, uint16 extraData) { (RevealedMove[] memory noOp, RevealedMove[] memory moves, RevealedMove[] memory switches) = _calculateValidMoves(ctx); @@ -380,9 +380,9 @@ contract BetterCPU is CPU { // ============ LEAD SELECTION ============ /// @notice Select lead with dual-type scoring (defensive + offensive) - function _selectLead(bytes32 battleKey, uint240 opponentMonExtraData, RevealedMove[] memory switches) + function _selectLead(bytes32 battleKey, uint16 opponentMonExtraData, RevealedMove[] memory switches) internal - returns (uint128, uint240) + returns (uint128, uint16) { MonStats memory oppStats = ENGINE.getMonStatsForBattle(battleKey, 0, uint256(opponentMonExtraData)); Type oppType1 = oppStats.type1; @@ -445,7 +445,7 @@ contract BetterCPU is CPU { uint256 opponentMonIndex, uint8 opponentMoveIndex, RevealedMove[] memory switches - ) internal view returns (uint128, uint240) { + ) internal view returns (uint128, uint16) { // If opponent isn't attacking, fall back to random if (opponentMoveIndex >= SWITCH_MOVE_INDEX) { return (switches[0].moveIndex, switches[0].extraData); diff --git a/src/cpu/CPU.sol b/src/cpu/CPU.sol index e5553cc8..b48b1a93 100644 --- a/src/cpu/CPU.sol +++ b/src/cpu/CPU.sol @@ -34,10 +34,10 @@ abstract contract CPU is CPUMoveManager, ICPU, ICPURNG, IMatchmaker { * If it's turn 0, randomly selects a mon index to swap to * Otherwise, randomly selects a valid move, switch index, or no op */ - function calculateMove(CPUContext memory ctx, uint8 playerMoveIndex, uint240 playerExtraData) + function calculateMove(CPUContext memory ctx, uint8 playerMoveIndex, uint16 playerExtraData) external virtual - returns (uint128 moveIndex, uint240 extraData); + returns (uint128 moveIndex, uint16 extraData); /** * Public test-friendly wrapper: fetches context and forwards. playerIndex is ignored @@ -63,7 +63,7 @@ abstract contract CPU is CPUMoveManager, ICPU, ICPURNG, IMatchmaker { uint256 teamSize = ctx.p1TeamSize; RevealedMove[] memory switchChoices = new RevealedMove[](teamSize); for (uint256 i = 0; i < teamSize; i++) { - switchChoices[i] = RevealedMove({moveIndex: SWITCH_MOVE_INDEX, salt: "", extraData: uint240(i)}); + switchChoices[i] = RevealedMove({moveIndex: SWITCH_MOVE_INDEX, salt: 0, extraData: uint16(i)}); } return (new RevealedMove[](0), new RevealedMove[](0), switchChoices); } @@ -77,7 +77,7 @@ abstract contract CPU is CPUMoveManager, ICPU, ICPURNG, IMatchmaker { validSwitchIndices = new uint256[](teamSize); for (uint256 i = 0; i < teamSize; i++) { if (i != activeMonIndex) { - if (_validateCPUMove(ctx, SWITCH_MOVE_INDEX, uint240(i))) { + if (_validateCPUMove(ctx, SWITCH_MOVE_INDEX, uint16(i))) { validSwitchIndices[validSwitchCount++] = i; } } @@ -89,19 +89,19 @@ abstract contract CPU is CPUMoveManager, ICPU, ICPURNG, IMatchmaker { for (uint256 i = 0; i < validSwitchCount; i++) { switchChoices[i] = RevealedMove({ moveIndex: SWITCH_MOVE_INDEX, - salt: "", - extraData: uint240(validSwitchIndices[i]) + salt: 0, + extraData: uint16(validSwitchIndices[i]) }); } return (new RevealedMove[](0), new RevealedMove[](0), switchChoices); } uint8[] memory validMoveIndices = new uint8[](NUM_MOVES); - uint240[] memory validMoveExtraData = new uint240[](NUM_MOVES); + uint16[] memory validMoveExtraData = new uint16[](NUM_MOVES); uint256 validMoveCount; // Check for valid moves for (uint256 i = 0; i < NUM_MOVES; i++) { uint256 rawMoveSlot = ctx.cpuActiveMonMoveSlots[i]; - uint240 extraDataToUse = 0; + uint16 extraDataToUse = 0; // Inline moves always have ExtraDataType.None — skip extraData logic if (!MoveSlotLib.isInline(rawMoveSlot)) { IMoveSet move = MoveSlotLib.toIMoveSet(rawMoveSlot); @@ -112,7 +112,7 @@ abstract contract CPU is CPUMoveManager, ICPU, ICPURNG, IMatchmaker { } uint256 randomIndex = _sampleRNG(keccak256(abi.encode(nonce++, ctx.battleKey, block.timestamp))) % validSwitchCount; - extraDataToUse = uint240(validSwitchIndices[randomIndex]); + extraDataToUse = uint16(validSwitchIndices[randomIndex]); validMoveExtraData[validMoveCount] = extraDataToUse; } else if (move.extraDataType() == ExtraDataType.OpponentNonKOTeamIndex) { uint256 opponentTeamSize = ctx.p0TeamSize; @@ -129,7 +129,7 @@ abstract contract CPU is CPUMoveManager, ICPU, ICPURNG, IMatchmaker { } uint256 randomIndex = _sampleRNG(keccak256(abi.encode(nonce++, ctx.battleKey, block.timestamp))) % validTargetCount; - extraDataToUse = uint240(validTargets[randomIndex]); + extraDataToUse = uint16(validTargets[randomIndex]); validMoveExtraData[validMoveCount] = extraDataToUse; } } @@ -141,15 +141,15 @@ abstract contract CPU is CPUMoveManager, ICPU, ICPURNG, IMatchmaker { RevealedMove[] memory validMovesArray = new RevealedMove[](validMoveCount); for (uint256 i = 0; i < validMoveCount; i++) { validMovesArray[i] = - RevealedMove({moveIndex: validMoveIndices[i], salt: "", extraData: validMoveExtraData[i]}); + RevealedMove({moveIndex: validMoveIndices[i], salt: 0, extraData: validMoveExtraData[i]}); } RevealedMove[] memory validSwitchesArray = new RevealedMove[](validSwitchCount); for (uint256 i = 0; i < validSwitchCount; i++) { validSwitchesArray[i] = - RevealedMove({moveIndex: SWITCH_MOVE_INDEX, salt: "", extraData: uint240(validSwitchIndices[i])}); + RevealedMove({moveIndex: SWITCH_MOVE_INDEX, salt: 0, extraData: uint16(validSwitchIndices[i])}); } RevealedMove[] memory noOpArray = new RevealedMove[](1); - noOpArray[0] = RevealedMove({moveIndex: NO_OP_MOVE_INDEX, salt: "", extraData: 0}); + noOpArray[0] = RevealedMove({moveIndex: NO_OP_MOVE_INDEX, salt: 0, extraData: 0}); nonceToUse = nonce; return (noOpArray, validMovesArray, validSwitchesArray); @@ -172,7 +172,7 @@ abstract contract CPU is CPUMoveManager, ICPU, ICPURNG, IMatchmaker { /// DEFAULT_MOVES_PER_MON / DEFAULT_MONS_PER_TEAM; the CPU's own iteration already bounds /// moveIndex below NUM_MOVES and monToSwitchIndex below p1TeamSize, so the basic/switch /// checks are equivalent to the engine-side versions. - function _validateCPUMove(CPUContext memory ctx, uint8 moveIndex, uint240 extraData) + function _validateCPUMove(CPUContext memory ctx, uint8 moveIndex, uint16 extraData) internal returns (bool) { @@ -182,7 +182,7 @@ abstract contract CPU is CPUMoveManager, ICPU, ICPURNG, IMatchmaker { return ENGINE.validatePlayerMoveForBattle(ctx.battleKey, moveIndex, 1, extraData); } - function _inlineValidateCPUMove(CPUContext memory ctx, uint8 moveIndex, uint240 extraData) + function _inlineValidateCPUMove(CPUContext memory ctx, uint8 moveIndex, uint16 extraData) private view returns (bool) diff --git a/src/cpu/CPUMoveManager.sol b/src/cpu/CPUMoveManager.sol index 734a03e6..1f286113 100644 --- a/src/cpu/CPUMoveManager.sol +++ b/src/cpu/CPUMoveManager.sol @@ -22,7 +22,7 @@ abstract contract CPUMoveManager { engine.updateMatchmakers(self, empty); } - function selectMove(bytes32 battleKey, uint8 moveIndex, bytes32 salt, uint240 extraData) external { + function selectMove(bytes32 battleKey, uint8 moveIndex, uint104 salt, uint16 extraData) external { // Cheap routing staticcall: one SLOAD for p0 / winnerIndex / playerSwitchForTurnFlag. // When the turn is "p0 forced switch" (flag == 0) or the game is already over we return // without ever paying for the full CPUContext (which would load team sizes, KO bitmaps, @@ -44,8 +44,10 @@ abstract contract CPUMoveManager { // P1's turn or both players move: CPU calculates its move. Fetch the full context now. CPUContext memory ctx = ENGINE.getCPUContext(battleKey); - (uint128 cpuMoveIndex, uint240 cpuExtraData) = ICPU(address(this)).calculateMove(ctx, moveIndex, extraData); - bytes32 p1Salt = keccak256(abi.encode(battleKey, msg.sender, block.timestamp)); + (uint128 cpuMoveIndex, uint16 cpuExtraData) = ICPU(address(this)).calculateMove(ctx, moveIndex, extraData); + // Salt narrows to 104 bits to match the engine's storage; ample for an unpredictable + // RNG source within the seconds-to-minutes commit-reveal window. + uint104 p1Salt = uint104(uint256(keccak256(abi.encode(battleKey, msg.sender, block.timestamp)))); if (playerSwitchForTurnFlag == 1) { ENGINE.executeWithSingleMove(battleKey, uint8(cpuMoveIndex), p1Salt, cpuExtraData); diff --git a/src/cpu/ICPU.sol b/src/cpu/ICPU.sol index 01e1b381..49e6c1c3 100644 --- a/src/cpu/ICPU.sol +++ b/src/cpu/ICPU.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.0; import {CPUContext, ProposedBattle} from "../Structs.sol"; interface ICPU { - function calculateMove(CPUContext memory ctx, uint8 playerMoveIndex, uint240 playerExtraData) + function calculateMove(CPUContext memory ctx, uint8 playerMoveIndex, uint16 playerExtraData) external - returns (uint128 moveIndex, uint240 extraData); + returns (uint128 moveIndex, uint16 extraData); function startBattle(ProposedBattle memory proposal) external returns (bytes32 battleKey); } diff --git a/src/cpu/OkayCPU.sol b/src/cpu/OkayCPU.sol index 0b38edd4..c5eccddb 100644 --- a/src/cpu/OkayCPU.sol +++ b/src/cpu/OkayCPU.sol @@ -21,10 +21,10 @@ contract OkayCPU is CPU { /** * If it's turn 0, swap in a mon that resists the other player's type1 (if possible) */ - function calculateMove(CPUContext memory ctx, uint8, uint240) + function calculateMove(CPUContext memory ctx, uint8, uint16) external override - returns (uint128 moveIndex, uint240 extraData) + returns (uint128 moveIndex, uint16 extraData) { (RevealedMove[] memory noOp, RevealedMove[] memory moves, RevealedMove[] memory switches) = _calculateValidMoves(ctx); @@ -126,7 +126,7 @@ contract OkayCPU is CPU { RevealedMove[] memory noOp, RevealedMove[] memory moves, RevealedMove[] memory switches - ) internal returns (uint128, uint240) { + ) internal returns (uint128, uint16) { // Single RNG call - use different bit ranges for different random decisions uint256 rng = _getRNG(battleKey); uint256 movesLen = moves.length; diff --git a/src/cpu/PlayerCPU.sol b/src/cpu/PlayerCPU.sol index 9cb710e4..7c0712e4 100644 --- a/src/cpu/PlayerCPU.sol +++ b/src/cpu/PlayerCPU.sol @@ -13,21 +13,21 @@ contract PlayerCPU is CPU { constructor(uint256 numMoves, IEngine engine, ICPURNG rng) CPU(numMoves, engine, rng) {} - function setMove(bytes32 battleKey, uint8 moveIndex, uint240 extraData) external { + function setMove(bytes32 battleKey, uint8 moveIndex, uint16 extraData) external { if (msg.sender != ENGINE.getPlayersForBattle(battleKey)[0]) { revert NotP0(); } - declaredMoveForBattle[battleKey] = RevealedMove({moveIndex: moveIndex, salt: "", extraData: extraData}); + declaredMoveForBattle[battleKey] = RevealedMove({moveIndex: moveIndex, salt: 0, extraData: extraData}); } /** * Returns the move that p0 declared for this CPU, ignoring the rest of the context. */ - function calculateMove(CPUContext memory ctx, uint8, uint240) + function calculateMove(CPUContext memory ctx, uint8, uint16) external view override - returns (uint128 moveIndex, uint240 extraData) + returns (uint128 moveIndex, uint16 extraData) { return (declaredMoveForBattle[ctx.battleKey].moveIndex, declaredMoveForBattle[ctx.battleKey].extraData); } diff --git a/src/cpu/RandomCPU.sol b/src/cpu/RandomCPU.sol index 83f37d4c..0627d40d 100644 --- a/src/cpu/RandomCPU.sol +++ b/src/cpu/RandomCPU.sol @@ -15,10 +15,10 @@ contract RandomCPU is CPU { * If it's turn 0, randomly selects a mon index to swap to * Otherwise, randomly selects a valid move, switch index, or no op */ - function calculateMove(CPUContext memory ctx, uint8, uint240) + function calculateMove(CPUContext memory ctx, uint8, uint16) external override - returns (uint128 moveIndex, uint240 extraData) + returns (uint128 moveIndex, uint16 extraData) { (RevealedMove[] memory noOp, RevealedMove[] memory moves, RevealedMove[] memory switches) = _calculateValidMoves(ctx); diff --git a/src/effects/status/SleepStatus.sol b/src/effects/status/SleepStatus.sol index 564ae40a..a7e679da 100644 --- a/src/effects/status/SleepStatus.sol +++ b/src/effects/status/SleepStatus.sol @@ -41,7 +41,7 @@ contract SleepStatus is StatusEffect { MoveDecision memory moveDecision = engine.getMoveDecisionForBattleState(battleKey, targetIndex); uint8 moveIndex = moveDecision.packedMoveIndex & MOVE_INDEX_MASK; if (moveIndex != SWITCH_MOVE_INDEX) { - engine.setMove(battleKey, targetIndex, NO_OP_MOVE_INDEX, "", 0); + engine.setMove(battleKey, targetIndex, NO_OP_MOVE_INDEX, 0, 0); } } diff --git a/src/lib/ValidatorLogic.sol b/src/lib/ValidatorLogic.sol index 172229fb..060d9be2 100644 --- a/src/lib/ValidatorLogic.sol +++ b/src/lib/ValidatorLogic.sol @@ -43,7 +43,7 @@ library ValidatorLogic { uint256 rawMoveSlot, uint256 playerIndex, uint256 activeMonIndex, - uint240 extraData, + uint16 extraData, uint32 baseStamina, int32 staminaDelta ) internal view returns (bool valid) { diff --git a/src/mons/aurox/BullRush.sol b/src/mons/aurox/BullRush.sol index 7902cee2..21e747e6 100644 --- a/src/mons/aurox/BullRush.sol +++ b/src/mons/aurox/BullRush.sol @@ -40,7 +40,7 @@ contract BullRush is StandardAttack { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, - uint240, + uint16, uint256 rng ) public override { // Deal the damage to opponent diff --git a/src/mons/aurox/GildedRecovery.sol b/src/mons/aurox/GildedRecovery.sol index be90436a..66b939c1 100644 --- a/src/mons/aurox/GildedRecovery.sol +++ b/src/mons/aurox/GildedRecovery.sol @@ -24,10 +24,10 @@ contract GildedRecovery is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240 extraData, + uint16 extraData, uint256 ) external { - // extraData contains the mon index as raw uint240 + // extraData contains the mon index as raw uint16 uint256 targetMonIndex = uint256(extraData); // Check if the target mon has a status effect @@ -84,7 +84,7 @@ contract GildedRecovery is IMoveSet { return MoveClass.Self; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/aurox/IronWall.sol b/src/mons/aurox/IronWall.sol index 397d3d6c..3c8d807f 100644 --- a/src/mons/aurox/IronWall.sol +++ b/src/mons/aurox/IronWall.sol @@ -25,7 +25,7 @@ contract IronWall is IMoveSet, BasicEffect { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240, + uint16, uint256 ) external { // Check to see if the effect is already active @@ -68,7 +68,7 @@ contract IronWall is IMoveSet, BasicEffect { return MoveClass.Self; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/aurox/VolatilePunch.sol b/src/mons/aurox/VolatilePunch.sol index fb288cad..772eaa2e 100644 --- a/src/mons/aurox/VolatilePunch.sol +++ b/src/mons/aurox/VolatilePunch.sol @@ -46,7 +46,7 @@ contract VolatilePunch is StandardAttack { uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, - uint240, + uint16, uint256 rng ) public override { // Deal the damage to opponent diff --git a/src/mons/ekineki/BubbleBop.sol b/src/mons/ekineki/BubbleBop.sol index cb18cc3f..5aa35f92 100644 --- a/src/mons/ekineki/BubbleBop.sol +++ b/src/mons/ekineki/BubbleBop.sol @@ -39,7 +39,7 @@ contract BubbleBop is StandardAttack { uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, - uint240, + uint16, uint256 rng ) public override { uint32 effectiveCritRate = NineNineNineLib._getEffectiveCritRate(engine, battleKey, attackerPlayerIndex); diff --git a/src/mons/ekineki/NineNineNine.sol b/src/mons/ekineki/NineNineNine.sol index 0c1a6f41..614311c5 100644 --- a/src/mons/ekineki/NineNineNine.sol +++ b/src/mons/ekineki/NineNineNine.sol @@ -17,7 +17,7 @@ contract NineNineNine is IMoveSet { return "Nine Nine Nine"; } - function move(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256, uint256, uint240, uint256) external { + function move(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256, uint256, uint16, uint256) external { // Set crit boost for the next turn uint256 currentTurn = engine.getTurnIdForBattleState(battleKey); uint64 key = NineNineNineLib._getKey(attackerPlayerIndex); @@ -40,7 +40,7 @@ contract NineNineNine is IMoveSet { return MoveClass.Self; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/ekineki/Overflow.sol b/src/mons/ekineki/Overflow.sol index 7d3fa135..f75c87bd 100644 --- a/src/mons/ekineki/Overflow.sol +++ b/src/mons/ekineki/Overflow.sol @@ -39,7 +39,7 @@ contract Overflow is StandardAttack { uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, - uint240, + uint16, uint256 rng ) public override { uint32 effectiveCritRate = NineNineNineLib._getEffectiveCritRate(engine, battleKey, attackerPlayerIndex); diff --git a/src/mons/ekineki/SneakAttack.sol b/src/mons/ekineki/SneakAttack.sol index 3c669924..2682e158 100644 --- a/src/mons/ekineki/SneakAttack.sol +++ b/src/mons/ekineki/SneakAttack.sol @@ -35,7 +35,7 @@ contract SneakAttack is IMoveSet, BasicEffect { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240 extraData, + uint16 extraData, uint256 rng ) external { // Check if already used this switch-in (effect present = already used) @@ -107,7 +107,7 @@ contract SneakAttack is IMoveSet, BasicEffect { return MoveClass.Special; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/embursa/HeatBeacon.sol b/src/mons/embursa/HeatBeacon.sol index b6d3f0e2..7d60ab69 100644 --- a/src/mons/embursa/HeatBeacon.sol +++ b/src/mons/embursa/HeatBeacon.sol @@ -28,7 +28,7 @@ contract HeatBeacon is IMoveSet { uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, - uint240, + uint16, uint256 ) external { // Apply burn to opposing mon @@ -56,7 +56,7 @@ contract HeatBeacon is IMoveSet { return Type.Fire; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/embursa/HoneyBribe.sol b/src/mons/embursa/HoneyBribe.sol index a9a68614..653d4d05 100644 --- a/src/mons/embursa/HoneyBribe.sol +++ b/src/mons/embursa/HoneyBribe.sol @@ -51,7 +51,7 @@ contract HoneyBribe is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, - uint240, + uint16, uint256 ) external { // Heal active mon by max HP / 2**bribeLevel @@ -110,7 +110,7 @@ contract HoneyBribe is IMoveSet { return MoveClass.Self; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/embursa/Q5.sol b/src/mons/embursa/Q5.sol index f6bf895a..fb5df4e0 100644 --- a/src/mons/embursa/Q5.sol +++ b/src/mons/embursa/Q5.sol @@ -36,7 +36,7 @@ contract Q5 is IMoveSet, BasicEffect { attackerPlayerIndex = uint256(data) & type(uint128).max; } - function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256, uint240, uint256) external { + function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256, uint16, uint256) external { // Add effect to global effects engine.addEffect(2, attackerPlayerIndex, this, _packExtraData(1, attackerPlayerIndex)); @@ -58,7 +58,7 @@ contract Q5 is IMoveSet, BasicEffect { return Type.Fire; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/embursa/SetAblaze.sol b/src/mons/embursa/SetAblaze.sol index af8ae952..36cdf255 100644 --- a/src/mons/embursa/SetAblaze.sol +++ b/src/mons/embursa/SetAblaze.sol @@ -40,7 +40,7 @@ contract SetAblaze is StandardAttack { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, - uint240 args, + uint16 args, uint256 rng ) public override { engine.dispatchStandardAttack( diff --git a/src/mons/ghouliath/EternalGrudge.sol b/src/mons/ghouliath/EternalGrudge.sol index ad78b202..adecae58 100644 --- a/src/mons/ghouliath/EternalGrudge.sol +++ b/src/mons/ghouliath/EternalGrudge.sol @@ -30,7 +30,7 @@ contract EternalGrudge is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, - uint240, + uint16, uint256 ) external { // Apply the debuff (50% debuff to both attack and special attack) @@ -69,7 +69,7 @@ contract EternalGrudge is IMoveSet { return Type.Yin; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/ghouliath/WitherAway.sol b/src/mons/ghouliath/WitherAway.sol index 8115f38c..d8094642 100644 --- a/src/mons/ghouliath/WitherAway.sol +++ b/src/mons/ghouliath/WitherAway.sol @@ -39,7 +39,7 @@ contract WitherAway is StandardAttack { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, - uint240 extraData, + uint16 extraData, uint256 rng ) public override { // Deal the damage and inflict panic diff --git a/src/mons/gorillax/RockPull.sol b/src/mons/gorillax/RockPull.sol index 68457ad5..f86b3094 100644 --- a/src/mons/gorillax/RockPull.sol +++ b/src/mons/gorillax/RockPull.sol @@ -40,7 +40,7 @@ contract RockPull is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240, + uint16, uint256 rng ) external { if (_didOtherPlayerChooseSwitch(engine, battleKey, attackerPlayerIndex)) { @@ -98,7 +98,7 @@ contract RockPull is IMoveSet { return MoveClass.Physical; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/iblivion/Brightback.sol b/src/mons/iblivion/Brightback.sol index 46f42998..e9712c1c 100644 --- a/src/mons/iblivion/Brightback.sol +++ b/src/mons/iblivion/Brightback.sol @@ -41,7 +41,7 @@ contract Brightback is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240, + uint16, uint256 rng ) external { (int32 damageDealt,) = AttackCalculator._calculateDamage( @@ -95,7 +95,7 @@ contract Brightback is IMoveSet { return MoveClass.Physical; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/iblivion/Loop.sol b/src/mons/iblivion/Loop.sol index 8c1b4c2e..6b418083 100644 --- a/src/mons/iblivion/Loop.sol +++ b/src/mons/iblivion/Loop.sol @@ -70,7 +70,7 @@ contract Loop is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240, + uint16, uint256 ) external { // Check if Loop is already active @@ -134,7 +134,7 @@ contract Loop is IMoveSet { return Type.Yang; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/iblivion/Renormalize.sol b/src/mons/iblivion/Renormalize.sol index 656dd9f6..81ac7114 100644 --- a/src/mons/iblivion/Renormalize.sol +++ b/src/mons/iblivion/Renormalize.sol @@ -42,7 +42,7 @@ contract Renormalize is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240, + uint16, uint256 ) external { // Set Baselight level to 3 @@ -67,7 +67,7 @@ contract Renormalize is IMoveSet { return Type.Yang; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/iblivion/UnboundedStrike.sol b/src/mons/iblivion/UnboundedStrike.sol index 6225859e..f4a3d461 100644 --- a/src/mons/iblivion/UnboundedStrike.sol +++ b/src/mons/iblivion/UnboundedStrike.sol @@ -45,7 +45,7 @@ contract UnboundedStrike is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240, + uint16, uint256 rng ) external { uint256 baselightLevel = BASELIGHT.getBaselightLevel(engine, battleKey, attackerPlayerIndex, attackerMonIndex); @@ -95,7 +95,7 @@ contract UnboundedStrike is IMoveSet { return MoveClass.Physical; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/inutia/ChainExpansion.sol b/src/mons/inutia/ChainExpansion.sol index a462fc3f..7d673aad 100644 --- a/src/mons/inutia/ChainExpansion.sol +++ b/src/mons/inutia/ChainExpansion.sol @@ -33,7 +33,7 @@ contract ChainExpansion is IMoveSet, BasicEffect { return keccak256(abi.encode(playerIndex, monIndex, name())); } - function move(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256, uint256, uint240, uint256) external { + function move(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256, uint256, uint16, uint256) external { // Check if the ability is already applied globally (EffectInstance[] memory effects, ) = engine.getEffects(battleKey, 2, 2); for (uint256 i = 0; i < effects.length; i++) { @@ -61,7 +61,7 @@ contract ChainExpansion is IMoveSet, BasicEffect { return MoveClass.Other; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/inutia/HitAndDip.sol b/src/mons/inutia/HitAndDip.sol index 36357b77..dc087ec4 100644 --- a/src/mons/inutia/HitAndDip.sol +++ b/src/mons/inutia/HitAndDip.sol @@ -39,7 +39,7 @@ contract HitAndDip is StandardAttack { uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, - uint240 extraData, + uint16 extraData, uint256 rng ) public override { // Deal the damage @@ -51,7 +51,7 @@ contract HitAndDip is StandardAttack { ); if (damage > 0) { - // extraData contains the swap index as raw uint240 + // extraData contains the swap index as raw uint16 uint256 swapIndex = uint256(extraData); engine.switchActiveMon(attackerPlayerIndex, swapIndex); } diff --git a/src/mons/inutia/Initialize.sol b/src/mons/inutia/Initialize.sol index 427147c0..d9fb1cfe 100644 --- a/src/mons/inutia/Initialize.sol +++ b/src/mons/inutia/Initialize.sol @@ -35,7 +35,7 @@ contract Initialize is IMoveSet, BasicEffect { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240, + uint16, uint256 ) external { // Check if global KV is set @@ -83,7 +83,7 @@ contract Initialize is IMoveSet, BasicEffect { return MoveClass.Self; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/malalien/TripleThink.sol b/src/mons/malalien/TripleThink.sol index 95a24830..5b0a8d0b 100644 --- a/src/mons/malalien/TripleThink.sol +++ b/src/mons/malalien/TripleThink.sol @@ -29,7 +29,7 @@ contract TripleThink is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240, + uint16, uint256 ) external { // Apply the buff @@ -58,7 +58,7 @@ contract TripleThink is IMoveSet { return MoveClass.Self; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/pengym/Deadlift.sol b/src/mons/pengym/Deadlift.sol index 120974e0..065843f3 100644 --- a/src/mons/pengym/Deadlift.sol +++ b/src/mons/pengym/Deadlift.sol @@ -30,7 +30,7 @@ contract Deadlift is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240, + uint16, uint256 ) external { // Apply the buffs @@ -64,7 +64,7 @@ contract Deadlift is IMoveSet { return MoveClass.Self; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/pengym/DeepFreeze.sol b/src/mons/pengym/DeepFreeze.sol index 6f841a39..9dba3bd8 100644 --- a/src/mons/pengym/DeepFreeze.sol +++ b/src/mons/pengym/DeepFreeze.sol @@ -47,7 +47,7 @@ contract DeepFreeze is IMoveSet { uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, - uint240, + uint16, uint256 rng ) external { uint256 otherPlayerIndex = (attackerPlayerIndex + 1) % 2; @@ -90,7 +90,7 @@ contract DeepFreeze is IMoveSet { return MoveClass.Physical; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/pengym/PistolSquat.sol b/src/mons/pengym/PistolSquat.sol index 9bb24bee..b234624d 100644 --- a/src/mons/pengym/PistolSquat.sol +++ b/src/mons/pengym/PistolSquat.sol @@ -60,7 +60,7 @@ contract PistolSquat is StandardAttack { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, - uint240 extraData, + uint16 extraData, uint256 rng ) public override { // Deal the damage diff --git a/src/mons/sofabbi/Gachachacha.sol b/src/mons/sofabbi/Gachachacha.sol index a25180f8..1d49a9bb 100644 --- a/src/mons/sofabbi/Gachachacha.sol +++ b/src/mons/sofabbi/Gachachacha.sol @@ -41,7 +41,7 @@ contract Gachachacha is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, - uint240, + uint16, uint256 rng ) external { uint256 chance = rng % OPP_KO_THRESHOLD_R; @@ -91,7 +91,7 @@ contract Gachachacha is IMoveSet { return MoveClass.Physical; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/sofabbi/GuestFeature.sol b/src/mons/sofabbi/GuestFeature.sol index 83fb7d6a..1b73f2f9 100644 --- a/src/mons/sofabbi/GuestFeature.sol +++ b/src/mons/sofabbi/GuestFeature.sol @@ -30,7 +30,7 @@ contract GuestFeature is IMoveSet { uint256 attackerPlayerIndex, uint256, uint256, - uint240 extraData, + uint16 extraData, uint256 rng ) external { uint256 monIndex = uint256(extraData); @@ -67,7 +67,7 @@ contract GuestFeature is IMoveSet { return MoveClass.Physical; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/sofabbi/SnackBreak.sol b/src/mons/sofabbi/SnackBreak.sol index ec40b197..7b78752b 100644 --- a/src/mons/sofabbi/SnackBreak.sol +++ b/src/mons/sofabbi/SnackBreak.sol @@ -42,7 +42,7 @@ contract SnackBreak is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240, + uint16, uint256 ) external { uint256 snackLevel = _getSnackLevel(engine, battleKey, attackerPlayerIndex, attackerMonIndex); @@ -78,7 +78,7 @@ contract SnackBreak is IMoveSet { return MoveClass.Self; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/volthare/DualShock.sol b/src/mons/volthare/DualShock.sol index be91b0fc..bc541f89 100644 --- a/src/mons/volthare/DualShock.sol +++ b/src/mons/volthare/DualShock.sol @@ -45,7 +45,7 @@ contract DualShock is StandardAttack { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, - uint240 extraData, + uint16 extraData, uint256 rng ) public override { // Deal the damage diff --git a/src/mons/volthare/MegaStarBlast.sol b/src/mons/volthare/MegaStarBlast.sol index 9ff0f9de..df7c0aa7 100644 --- a/src/mons/volthare/MegaStarBlast.sol +++ b/src/mons/volthare/MegaStarBlast.sol @@ -52,7 +52,7 @@ contract MegaStarBlast is IMoveSet { uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, - uint240, + uint16, uint256 rng ) external { // Check if Overclock is active @@ -104,7 +104,7 @@ contract MegaStarBlast is IMoveSet { return MoveClass.Special; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/volthare/RoundTrip.sol b/src/mons/volthare/RoundTrip.sol index 8880e455..1fa154fe 100644 --- a/src/mons/volthare/RoundTrip.sol +++ b/src/mons/volthare/RoundTrip.sol @@ -39,7 +39,7 @@ contract RoundTrip is StandardAttack { uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, - uint240 extraData, + uint16 extraData, uint256 rng ) public override { // Deal the damage @@ -51,7 +51,7 @@ contract RoundTrip is StandardAttack { ); if (damage > 0) { - // extraData contains the swap index as raw uint240 + // extraData contains the swap index as raw uint16 uint256 swapIndex = uint256(extraData); engine.switchActiveMon(attackerPlayerIndex, swapIndex); } diff --git a/src/mons/xmon/ContagiousSlumber.sol b/src/mons/xmon/ContagiousSlumber.sol index 5153ae73..5c349615 100644 --- a/src/mons/xmon/ContagiousSlumber.sol +++ b/src/mons/xmon/ContagiousSlumber.sol @@ -27,7 +27,7 @@ contract ContagiousSlumber is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, - uint240, + uint16, uint256 ) external { // Apply sleep to self @@ -54,7 +54,7 @@ contract ContagiousSlumber is IMoveSet { return MoveClass.Other; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/xmon/NightTerrors.sol b/src/mons/xmon/NightTerrors.sol index 399b053e..fb7984a7 100644 --- a/src/mons/xmon/NightTerrors.sol +++ b/src/mons/xmon/NightTerrors.sol @@ -36,7 +36,7 @@ contract NightTerrors is IMoveSet, BasicEffect { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240, + uint16, uint256 ) external { uint256 defenderPlayerIndex = (attackerPlayerIndex + 1) % 2; @@ -96,7 +96,7 @@ contract NightTerrors is IMoveSet, BasicEffect { return MoveClass.Special; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/xmon/Somniphobia.sol b/src/mons/xmon/Somniphobia.sol index 7916a432..628e3553 100644 --- a/src/mons/xmon/Somniphobia.sol +++ b/src/mons/xmon/Somniphobia.sol @@ -18,7 +18,7 @@ contract Somniphobia is IMoveSet, BasicEffect { return "Somniphobia"; } - function move(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256, uint256, uint240, uint256) external { + function move(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256, uint256, uint16, uint256) external { // Add effect globally for 6 turns (only if it's not already in global effects) (EffectInstance[] memory effects, ) = engine.getEffects(battleKey, 2, 2); for (uint256 i = 0; i < effects.length; i++) { @@ -45,7 +45,7 @@ contract Somniphobia is IMoveSet, BasicEffect { return MoveClass.Other; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/xmon/VitalSiphon.sol b/src/mons/xmon/VitalSiphon.sol index d2f472d7..1f85f0d8 100644 --- a/src/mons/xmon/VitalSiphon.sol +++ b/src/mons/xmon/VitalSiphon.sol @@ -40,7 +40,7 @@ contract VitalSiphon is StandardAttack { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, - uint240 extraData, + uint16 extraData, uint256 rng ) public override { // Deal the damage diff --git a/src/moves/IMoveSet.sol b/src/moves/IMoveSet.sol index 3ca381fe..cba4b69b 100644 --- a/src/moves/IMoveSet.sol +++ b/src/moves/IMoveSet.sol @@ -13,7 +13,7 @@ interface IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, - uint240 extraData, + uint16 extraData, uint256 rng ) external; function priority(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex) external view returns (uint32); @@ -22,7 +22,7 @@ interface IMoveSet { view returns (uint32); function moveType(IEngine engine, bytes32 battleKey) external view returns (Type); - function isValidTarget(IEngine engine, bytes32 battleKey, uint240 extraData) external view returns (bool); + function isValidTarget(IEngine engine, bytes32 battleKey, uint16 extraData) external view returns (bool); function moveClass(IEngine engine, bytes32 battleKey) external view returns (MoveClass); function extraDataType() external view returns (ExtraDataType); diff --git a/src/moves/StandardAttack.sol b/src/moves/StandardAttack.sol index 47bc9f51..18223e9e 100644 --- a/src/moves/StandardAttack.sol +++ b/src/moves/StandardAttack.sol @@ -10,7 +10,6 @@ import {IEffect} from "../effects/IEffect.sol"; import {Ownable} from "../lib/Ownable.sol"; import {ITypeCalculator} from "../types/ITypeCalculator.sol"; -import {AttackCalculator} from "./AttackCalculator.sol"; import {IMoveSet} from "./IMoveSet.sol"; import {ATTACK_PARAMS} from "./StandardAttackStructs.sol"; @@ -51,7 +50,7 @@ contract StandardAttack is IMoveSet, Ownable { uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, - uint240, + uint16, uint256 rng ) public virtual { _move(engine, battleKey, attackerPlayerIndex, defenderMonIndex, rng); @@ -83,7 +82,7 @@ contract StandardAttack is IMoveSet, Ownable { ); } - function isValidTarget(IEngine, bytes32, uint240) public pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) public pure returns (bool) { return true; } diff --git a/test/BatchInstrumentationTest.sol b/test/BatchInstrumentationTest.sol new file mode 100644 index 00000000..1d1fb6d2 --- /dev/null +++ b/test/BatchInstrumentationTest.sol @@ -0,0 +1,323 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import "../lib/forge-std/src/Test.sol"; +import "../src/Constants.sol"; +import "../src/Enums.sol"; +import "../src/Structs.sol"; + +import {Engine} from "../src/Engine.sol"; +import {SignedCommitManager} from "../src/commit-manager/SignedCommitManager.sol"; +import {SignedMatchmaker} from "../src/matchmaker/SignedMatchmaker.sol"; +import {BattleOfferLib} from "../src/matchmaker/BattleOfferLib.sol"; +import {StandardAttackFactory} from "../src/moves/StandardAttackFactory.sol"; +import {ATTACK_PARAMS} from "../src/moves/StandardAttackStructs.sol"; + +import {IEngine} from "../src/IEngine.sol"; +import {IEngineHook} from "../src/IEngineHook.sol"; +import {IEffect} from "../src/effects/IEffect.sol"; +import {IMoveSet} from "../src/moves/IMoveSet.sol"; +import {IRandomnessOracle} from "../src/rng/IRandomnessOracle.sol"; +import {IRuleset} from "../src/IRuleset.sol"; +import {IValidator} from "../src/IValidator.sol"; + +import {TypeCalculator} from "../src/types/TypeCalculator.sol"; +import {ITypeCalculator} from "../src/types/ITypeCalculator.sol"; + +import {SignedCommitHelper} from "./abstract/SignedCommitHelper.sol"; +import {TestTeamRegistry} from "./mocks/TestTeamRegistry.sol"; + +/// Counts SLOAD / SSTORE access patterns on a warm steady-state turn, to ground the PLAN_OPT.md +/// gas math in real data instead of estimates. +contract BatchInstrumentationTest is SignedCommitHelper { + + uint256 constant MONS_PER_TEAM = 4; + uint256 constant MOVES_PER_MON = 4; + + uint256 constant P0_PK = 0xA11CE; + uint256 constant P1_PK = 0xB0B; + address p0; + address p1; + + Engine engine; + SignedCommitManager signedCommitManager; + SignedMatchmaker signedMatchmaker; + ITypeCalculator typeCalc; + TestTeamRegistry defaultRegistry; + StandardAttackFactory attackFactory; + + function setUp() public { + p0 = vm.addr(P0_PK); + p1 = vm.addr(P1_PK); + + engine = new Engine(MONS_PER_TEAM, MOVES_PER_MON, 1); + signedCommitManager = new SignedCommitManager(IEngine(address(engine))); + signedMatchmaker = new SignedMatchmaker(engine); + typeCalc = new TypeCalculator(); + defaultRegistry = new TestTeamRegistry(); + attackFactory = new StandardAttackFactory(typeCalc); + } + + function _startBattle(IRuleset ruleset) internal returns (bytes32) { + address[] memory makersToAdd = new address[](1); + makersToAdd[0] = address(signedMatchmaker); + address[] memory makersToRemove = new address[](0); + vm.prank(p0); + engine.updateMatchmakers(makersToAdd, makersToRemove); + vm.prank(p1); + engine.updateMatchmakers(makersToAdd, makersToRemove); + + (bytes32 battleKey, bytes32 pairHash) = engine.computeBattleKey(p0, p1); + uint256 nonce = engine.pairHashNonces(pairHash); + + BattleOffer memory offer = BattleOffer({ + battle: Battle({ + p0: p0, + p0TeamIndex: 0, + p1: p1, + p1TeamIndex: 0, + teamRegistry: defaultRegistry, + validator: IValidator(address(0)), + rngOracle: IRandomnessOracle(address(0)), + ruleset: ruleset, + moveManager: address(signedCommitManager), + matchmaker: signedMatchmaker, + engineHooks: new IEngineHook[](0) + }), + pairHashNonce: nonce + }); + + bytes32 structHash = BattleOfferLib.hashBattleOffer(offer); + bytes32 digest = signedMatchmaker.hashTypedData(structHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(P0_PK, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.prank(p1); + signedMatchmaker.startGame(offer, signature); + + return battleKey; + } + + function _fastTurn( + bytes32 battleKey, + uint8 p0MoveIndex, + uint8 p1MoveIndex, + uint16 p0ExtraData, + uint16 p1ExtraData + ) internal { + uint64 turnId = uint64(engine.getTurnIdForBattleState(battleKey)); + uint104 committerSalt = uint104(uint256(keccak256(abi.encode("committer", battleKey, turnId)))); + uint104 revealerSalt = uint104(uint256(keccak256(abi.encode("revealer", battleKey, turnId)))); + + uint8 committerMoveIndex; + uint16 committerExtraData; + uint8 revealerMoveIndex; + uint16 revealerExtraData; + uint256 committerPk; + uint256 revealerPk; + + if (turnId % 2 == 0) { + committerMoveIndex = p0MoveIndex; + committerExtraData = p0ExtraData; + revealerMoveIndex = p1MoveIndex; + revealerExtraData = p1ExtraData; + committerPk = P0_PK; + revealerPk = P1_PK; + } else { + committerMoveIndex = p1MoveIndex; + committerExtraData = p1ExtraData; + revealerMoveIndex = p0MoveIndex; + revealerExtraData = p0ExtraData; + committerPk = P1_PK; + revealerPk = P0_PK; + } + + bytes32 committerMoveHash = + keccak256(abi.encodePacked(committerMoveIndex, committerSalt, committerExtraData)); + bytes memory committerSig = + _signCommit(address(signedCommitManager), committerPk, committerMoveHash, battleKey, turnId); + bytes memory revealerSig = _signDualReveal( + address(signedCommitManager), + revealerPk, + battleKey, + turnId, + committerMoveHash, + revealerMoveIndex, + revealerSalt, + revealerExtraData + ); + + signedCommitManager.executeWithDualSignedMoves( + battleKey, + committerMoveIndex, committerSalt, committerExtraData, + revealerMoveIndex, revealerSalt, revealerExtraData, + committerSig, + revealerSig + ); + engine.resetCallContext(); + } + + function _createMon(Type t1) internal pure returns (Mon memory) { + return Mon({ + stats: MonStats({ + hp: 10000, + stamina: 50, + speed: 10, + attack: 30, + defense: 10, + specialAttack: 30, + specialDefense: 10, + type1: t1, + type2: Type.None + }), + moves: new uint256[](0), + ability: 0 + }); + } + + /// @dev Iterates account accesses returned by stopAndReturnStateDiff and counts SLOAD/SSTORE + /// per (account, slot) — distinguishing first-touch (cold) from subsequent (warm), and + /// for SSTORE distinguishing zero→nonzero / nonzero→nonzero / no-op. + function _summarizeAccesses(Vm.AccountAccess[] memory accesses) + internal + pure + returns ( + uint256 totalSloadCount, + uint256 totalSstoreCount, + uint256 coldSloads, + uint256 warmSloads, + uint256 coldSstores, + uint256 warmSstores, + uint256 zeroToNonzeroSstores, + uint256 nonzeroToNonzeroSstores, + uint256 noopSstores, + uint256 uniqueSlotsTouched, + uint256 multiWriteSlots + ) + { + // Count slot-touch frequencies via a small fixed-capacity table (we don't expect many uniques) + bytes32[] memory keys = new bytes32[](256); + uint8[] memory writes = new uint8[](256); + bool[] memory reads = new bool[](256); + uint256 keyCount; + + for (uint256 i = 0; i < accesses.length; i++) { + Vm.StorageAccess[] memory storageAccesses = accesses[i].storageAccesses; + for (uint256 j = 0; j < storageAccesses.length; j++) { + Vm.StorageAccess memory a = storageAccesses[j]; + bytes32 key = keccak256(abi.encode(a.account, a.slot)); + + // Locate or create entry + uint256 idx = keyCount; + for (uint256 k = 0; k < keyCount; k++) { + if (keys[k] == key) { + idx = k; + break; + } + } + if (idx == keyCount) { + keys[idx] = key; + keyCount++; + } + + if (a.isWrite) { + totalSstoreCount++; + writes[idx]++; + if (a.previousValue == bytes32(0) && a.newValue != bytes32(0)) zeroToNonzeroSstores++; + else if (a.previousValue != bytes32(0) && a.newValue != bytes32(0) && a.previousValue != a.newValue) + nonzeroToNonzeroSstores++; + else if (a.previousValue == a.newValue) noopSstores++; + + if (writes[idx] == 1 && !reads[idx]) { + coldSstores++; + } else { + warmSstores++; + } + } else { + totalSloadCount++; + if (!reads[idx] && writes[idx] == 0) { + coldSloads++; + reads[idx] = true; + } else { + warmSloads++; + } + } + } + } + + uniqueSlotsTouched = keyCount; + for (uint256 i = 0; i < keyCount; i++) { + if (writes[i] >= 2) multiWriteSlots++; + } + } + + /// @notice Per-turn storage-access profile for a clean PvP damage-trade turn (steady state). + function test_storageAccessProfile_cleanDamageTradeTurn() public { + IMoveSet moveA = attackFactory.createAttack( + ATTACK_PARAMS({ + BASE_POWER: 30, STAMINA_COST: 1, ACCURACY: 100, PRIORITY: 1, + MOVE_TYPE: Type.Fire, EFFECT_ACCURACY: 0, MOVE_CLASS: MoveClass.Physical, + CRIT_RATE: 0, VOLATILITY: 0, NAME: "AttackA", EFFECT: IEffect(address(0)) + }) + ); + IMoveSet moveB = attackFactory.createAttack( + ATTACK_PARAMS({ + BASE_POWER: 25, STAMINA_COST: 1, ACCURACY: 100, PRIORITY: 1, + MOVE_TYPE: Type.Fire, EFFECT_ACCURACY: 0, MOVE_CLASS: MoveClass.Special, + CRIT_RATE: 0, VOLATILITY: 0, NAME: "AttackB", EFFECT: IEffect(address(0)) + }) + ); + + Mon memory mon = _createMon(Type.Fire); + mon.moves = new uint256[](MOVES_PER_MON); + mon.moves[0] = uint256(uint160(address(moveA))); + mon.moves[1] = uint256(uint160(address(moveB))); + mon.moves[2] = uint256(uint160(address(moveA))); + mon.moves[3] = uint256(uint160(address(moveB))); + + Mon[] memory team = new Mon[](MONS_PER_TEAM); + for (uint256 i; i < MONS_PER_TEAM; i++) team[i] = mon; + defaultRegistry.setTeam(p0, team); + defaultRegistry.setTeam(p1, team); + + IRuleset ruleset = IRuleset(INLINE_STAMINA_REGEN_RULESET); + bytes32 battleKey = _startBattle(ruleset); + vm.warp(vm.getBlockTimestamp() + 1); + + // Warm-up: lead-in switch + 1 damage trade. + _fastTurn(battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); + _fastTurn(battleKey, 0, 0, 0, 0); + + // Now profile a steady-state warm turn. + vm.startStateDiffRecording(); + _fastTurn(battleKey, 1, 1, 0, 0); + Vm.AccountAccess[] memory diffs = vm.stopAndReturnStateDiff(); + + ( + uint256 totalSload, + uint256 totalSstore, + uint256 coldSload, + uint256 warmSload, + uint256 coldSstore, + uint256 warmSstore, + uint256 z2nz, + uint256 nz2nz, + uint256 noop, + uint256 unique, + uint256 multiWrite + ) = _summarizeAccesses(diffs); + + console.log("=== CLEAN DAMAGE-TRADE TURN - STORAGE PROFILE ==="); + console.log("Total SLOADs :", totalSload); + console.log(" Cold (first-touch in tx) :", coldSload); + console.log(" Warm :", warmSload); + console.log("Total SSTOREs :", totalSstore); + console.log(" Cold (first-touch in tx) :", coldSstore); + console.log(" Warm :", warmSstore); + console.log(" zero -> nonzero :", z2nz); + console.log(" nonzero -> nonzero (diff) :", nz2nz); + console.log(" no-op (same value) :", noop); + console.log("Unique slots touched :", unique); + console.log("Slots written 2+ times in turn :", multiWrite); + } +} diff --git a/test/BattleHistoryTest.sol b/test/BattleHistoryTest.sol index 005af49a..2aed9ea2 100644 --- a/test/BattleHistoryTest.sol +++ b/test/BattleHistoryTest.sol @@ -166,10 +166,10 @@ contract BattleHistoryTest is Test, BattleHelper { address p1, uint8 p0MoveIndex, uint8 p1MoveIndex, - uint240 p0ExtraData, - uint240 p1ExtraData + uint16 p0ExtraData, + uint16 p1ExtraData ) internal { - bytes32 salt = ""; + uint104 salt = 0; bytes32 p0MoveHash = keccak256(abi.encodePacked(p0MoveIndex, salt, p0ExtraData)); bytes32 p1MoveHash = keccak256(abi.encodePacked(p1MoveIndex, salt, p1ExtraData)); @@ -199,7 +199,7 @@ contract BattleHistoryTest is Test, BattleHelper { // First move - both players switch to their mon _commitRevealExecute( - battleKey, battleData.p0, battleData.p1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + battleKey, battleData.p0, battleData.p1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Second move - both attack (faster mon wins) @@ -216,7 +216,7 @@ contract BattleHistoryTest is Test, BattleHelper { assertEq(battleHistory.getNumBattles(BOB), 0, "Bob should have 0 battles before completion"); // First turn - both switch to their mons - _commitRevealExecute(battleKey, ALICE, BOB, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecute(battleKey, ALICE, BOB, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); // Stats should still be 0 after first turn assertEq(battleHistory.getNumBattles(ALICE), 0, "Alice should have 0 battles after switch"); diff --git a/test/BetterCPUInlineGasTest.sol b/test/BetterCPUInlineGasTest.sol index 9cccd4e5..e7b4112e 100644 --- a/test/BetterCPUInlineGasTest.sol +++ b/test/BetterCPUInlineGasTest.sol @@ -159,28 +159,28 @@ contract BetterCPUInlineGasTest is Test { // Turn 0: lead selection. Both players "switch in" a starting mon. vm.startSnapshotGas("Turn0_Lead"); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); uint256 turn0Gas = vm.stopSnapshotGas("Turn0_Lead"); engine.resetCallContext(); // Turns 1-4: both attack with move 1. Every one is flag == 2, no KOs. vm.startSnapshotGas("Turn1_BothAttack"); - cpu.selectMove(battleKey, 1, "", 0); + cpu.selectMove(battleKey, 1, uint104(0), 0); uint256 turn1Gas = vm.stopSnapshotGas("Turn1_BothAttack"); engine.resetCallContext(); vm.startSnapshotGas("Turn2_BothAttack"); - cpu.selectMove(battleKey, 1, "", 0); + cpu.selectMove(battleKey, 1, uint104(0), 0); uint256 turn2Gas = vm.stopSnapshotGas("Turn2_BothAttack"); engine.resetCallContext(); vm.startSnapshotGas("Turn3_BothAttack"); - cpu.selectMove(battleKey, 1, "", 0); + cpu.selectMove(battleKey, 1, uint104(0), 0); uint256 turn3Gas = vm.stopSnapshotGas("Turn3_BothAttack"); engine.resetCallContext(); vm.startSnapshotGas("Turn4_BothAttack"); - cpu.selectMove(battleKey, 1, "", 0); + cpu.selectMove(battleKey, 1, uint104(0), 0); uint256 turn4Gas = vm.stopSnapshotGas("Turn4_BothAttack"); engine.resetCallContext(); @@ -225,11 +225,11 @@ contract BetterCPUInlineGasTest is Test { mockCPURNG.setRNG(0); // Turn 0: lead. - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: both attack. CPU's move 1 (BP=40, attack=200, defense=10) should KO Alice. - cpu.selectMove(battleKey, 1, "", 0); + cpu.selectMove(battleKey, 1, uint104(0), 0); engine.resetCallContext(); // After the KO we should be in flag==0 (Alice forced switch). @@ -238,7 +238,7 @@ contract BetterCPUInlineGasTest is Test { // Measure the cheap-router path: Alice submits her switch via the CPU manager. vm.startSnapshotGas("Flag0_P0ForcedSwitch"); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(1)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(1)); uint256 flag0Gas = vm.stopSnapshotGas("Flag0_P0ForcedSwitch"); engine.resetCallContext(); diff --git a/test/BetterCPUTest.sol b/test/BetterCPUTest.sol index e331a72a..5965ae46 100644 --- a/test/BetterCPUTest.sol +++ b/test/BetterCPUTest.sol @@ -198,13 +198,13 @@ contract BetterCPUTest is Test { bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); // Turn 0: Both select mon 0 - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Deal some damage to Alice's mon so CPU sees it can KO // The CPU should select the high power move (index 1) to secure the KO // Set RNG to not trigger random selection mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Check that Alice's mon took massive damage (from high power attack) int32 aliceHpDelta = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); @@ -243,7 +243,7 @@ contract BetterCPUTest is Test { bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); // Alice selects mon 0 (Fire type) - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // CPU should have selected Liquid mon (index 1) uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); @@ -298,7 +298,7 @@ contract BetterCPUTest is Test { // Since neither mon resists Liquid (Alice's lead could be Liquid), CPU picks randomly // We force Fire mon to be selected mockCPURNG.setRNG(0); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Get active mons - CPU might have selected based on type calcs uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); @@ -306,7 +306,7 @@ contract BetterCPUTest is Test { // Turn 1: CPU should detect kill threat from Fire attack and switch to Liquid if currently Fire mockCPURNG.setRNG(1); // Don't trigger random selection - cpu.selectMove(battleKey, 0, "", 0); // Alice attacks + cpu.selectMove(battleKey, 0, uint104(0), 0); // Alice attacks // If CPU started with Fire, it should switch to Liquid to survive // If CPU started with Liquid, it should stay (already resists Fire) @@ -354,12 +354,12 @@ contract BetterCPUTest is Test { bytes32 battleKey = _startBattleWithCPU(team, team); // Turn 0: Both select mon 0 - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Use the expensive attack (costs 5 stamina) // RNG = 1 won't trigger random selection (1 % 10 != 0) mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); // Stamina delta should be -5 int32 staminaDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina); @@ -367,7 +367,7 @@ contract BetterCPUTest is Test { // Turn 2: Opponent rests (P4 path). New BetterCPU attacks on free turns even at low stamina. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Stamina should be -10 (attacked again with the 5-cost move on the free turn) staminaDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina); @@ -409,12 +409,12 @@ contract BetterCPUTest is Test { bytes32 battleKey = _startBattleWithCPU(team, team); // Turn 0: Both select mon 0 - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: At full HP, CPU should prefer setup move // Set RNG to not trigger random selection mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Check stamina consumed (setup move costs 1) int32 staminaDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina); @@ -434,28 +434,34 @@ contract BetterCPUTest is Test { moves[1] = uint256(uint160(address(mediumAttack))); moves[2] = uint256(uint160(address(strongAttack))); - Mon memory mon = _createMon(Type.Fire, 100, 50, 10); - mon.moves = moves; + // CPU mon is faster than Alice's so attack ordering is deterministic regardless + // of the engine's RNG-based speed-tie breaker. + Mon memory aliceMon = _createMonWithSpeed(Type.Fire, 100, 50, 10, 10); + aliceMon.moves = moves; + Mon memory cpuMon = _createMonWithSpeed(Type.Fire, 100, 50, 10, 100); + cpuMon.moves = moves; - // Need 2 mons per team - Mon[] memory team = new Mon[](2); - team[0] = mon; - team[1] = mon; + Mon[] memory aliceTeam = new Mon[](2); + aliceTeam[0] = aliceMon; + aliceTeam[1] = aliceMon; + Mon[] memory cpuTeam = new Mon[](2); + cpuTeam[0] = cpuMon; + cpuTeam[1] = cpuMon; - bytes32 battleKey = _startBattleWithCPU(team, team); + bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); // Turn 0: Both select mon 0 - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: CPU is at full HP, so attack first with Alice to damage CPU // Then CPU will be at non-full HP and prefer attack moves mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 2, "", 0); // Alice uses strong attack on CPU + cpu.selectMove(battleKey, 2, uint104(0), 0); // Alice uses strong attack on CPU // Now CPU's HP is damaged, next turn it should use highest damage move // Turn 2: CPU should select the strongest attack mockCPURNG.setRNG(1); // Don't trigger random - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Verify significant damage was dealt (strong attack) - Alice took damage both turns int32 aliceHpDelta = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); @@ -492,7 +498,7 @@ contract BetterCPUTest is Test { bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); // Alice selects mon 0 (Fire/Nature) - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 1, "CPU should select Liquid mon (resists both Fire and Nature)"); @@ -521,7 +527,7 @@ contract BetterCPUTest is Test { typeCalc.setTypeEffectiveness(Type.Fire, Type.Nature, 20); // 2x bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 0, "CPU should select Fire mon (2x offensive vs Nature)"); @@ -546,7 +552,7 @@ contract BetterCPUTest is Test { } bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Just verify no crash and valid index uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); @@ -601,7 +607,7 @@ contract BetterCPUTest is Test { bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); // Turn 0: lead selection. All neutral → picks mon 0 (first). - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 0, "CPU should lead with mon 0"); @@ -609,12 +615,12 @@ contract BetterCPUTest is Test { // Turn 1: Alice uses Fire move (move 0). All CPU mons take equal Fire damage. // P5 materiality fails. CPU stays. Mon0 KO'd. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); // Turn 2 (forced switch): Alice signals move 1 (Liquid). CPU evaluates Liquid damage. // Mon2(Nature) resists Liquid → takes less damage → picked. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 1, "", 0); + cpu.selectMove(battleKey, 1, uint104(0), 0); engine.resetCallContext(); activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 2, "CPU should switch to Nature (resists Liquid attack)"); @@ -649,11 +655,11 @@ contract BetterCPUTest is Test { bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); // Turn 0: lead selection - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: CPU should use KO move. Alice attacks weakly. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); // Alice's mon should be KO'd int32 aliceKO = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.IsKnockedOut); @@ -680,11 +686,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = moves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Both attack. CPU outspeeds → KOs Alice first. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); int32 aliceKO = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.IsKnockedOut); assertEq(aliceKO, 1, "CPU should KO Alice when outspeeding"); @@ -716,11 +722,11 @@ contract BetterCPUTest is Test { typeCalc.setTypeEffectiveness(Type.Fire, Type.Liquid, 5); // 0.5x bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: CPU outsped and opponent can KO → CPU should switch to Liquid. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 1, "CPU should switch to Liquid when outsped in KO race"); @@ -755,11 +761,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = aliceMoves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: CPU should pick the cheaper KO move (cost=1). mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); // Stamina delta should be -1 (cheap move used) int32 staminaDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina); @@ -792,11 +798,11 @@ contract BetterCPUTest is Test { typeCalc.setTypeEffectiveness(Type.Fire, Type.Nature, 20); // 2x bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Alice switches to mon 1 (Nature, hp=20). CPU should KO it. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, "", uint240(1)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1)); engine.resetCallContext(); // Alice's mon 1 should be KO'd int32 aliceMon1KO = engine.getMonStateForBattle(battleKey, 0, 1, MonStateIndexName.IsKnockedOut); @@ -837,11 +843,11 @@ contract BetterCPUTest is Test { typeCalc.setTypeEffectiveness(Type.Liquid, Type.Nature, 5); // 0.5x bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Alice switches to Nature mon. CPU should use Fire attack (best damage). mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, "", uint240(1)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1)); engine.resetCallContext(); // Alice's Nature mon should take Fire damage (500) int32 aliceHpDelta = engine.getMonStateForBattle(battleKey, 0, 1, MonStateIndexName.Hp); @@ -873,11 +879,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = aliceMoves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Alice switches. CPU has no affordable moves → rests. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, "", uint240(1)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1)); engine.resetCallContext(); // CPU stamina should be unchanged (rested) int32 staminaDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina); @@ -915,11 +921,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = aliceMoves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Alice rests. No KO possible (hp=500). CPU should use strongest move in P4. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); int32 aliceHpDelta = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); assertEq(aliceHpDelta, -400, "CPU should use bp=80 move for 400 damage"); @@ -949,11 +955,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = aliceMoves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Alice rests. CPU has no affordable moves → also rests. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); int32 staminaDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina); assertEq(staminaDelta, 0, "CPU should rest when no affordable moves"); @@ -994,14 +1000,14 @@ contract BetterCPUTest is Test { bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); // Lead selection: Alice is Liquid type. Liquid→Metal=1x, Liquid→Liquid=1x. Neutral. Mon0 leads. - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 0, "CPU should lead with Mon0 (Metal)"); // Turn 1: Alice uses Fire attack. Lethal to Metal. CPU switches to Liquid. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 1, "CPU should switch to Liquid to survive lethal Fire attack"); @@ -1027,11 +1033,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = moves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Alice attacks weakly. CPU stays and attacks back. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 0, "CPU should stay when damage is low"); @@ -1056,10 +1062,11 @@ contract BetterCPUTest is Test { uint256[] memory moves = new uint256[](1); moves[0] = uint256(uint160(address(strongAttack))); + // CPU mons are faster so they attack first and get to deal damage before being KO'd. Mon[] memory cpuTeam = new Mon[](2); - cpuTeam[0] = _createMon(Type.Fire, 100, 50, 10); + cpuTeam[0] = _createMonWithSpeed(Type.Fire, 100, 50, 10, 100); cpuTeam[0].moves = moves; - cpuTeam[1] = _createMon(Type.Fire, 100, 50, 10); + cpuTeam[1] = _createMonWithSpeed(Type.Fire, 100, 50, 10, 100); cpuTeam[1].moves = moves; Mon[] memory aliceTeam = new Mon[](2); @@ -1069,11 +1076,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = moves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Both lethal, no material improvement → CPU stays and attacks. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); // CPU should have stayed (attacked, not switched) int32 aliceHpDelta = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); @@ -1111,14 +1118,14 @@ contract BetterCPUTest is Test { typeCalc.setTypeEffectiveness(Type.Fire, Type.Liquid, 5); // 0.5x bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 0, "CPU should lead with Mon0"); // Turn 1: Alice Fire attack → lethal to Metal, Liquid survives → switch. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 1, "CPU should switch to Liquid (materially better)"); @@ -1148,11 +1155,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = aliceMoves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Alice uses Self move. CPU skips P5, attacks in P6. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 0, "CPU should stay when opponent uses Self move"); @@ -1196,11 +1203,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = aliceMoves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Alice attacks weakly. CPU uses best move in P6. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); int32 aliceHpDelta = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); assertEq(aliceHpDelta, -400, "CPU should use bp=80 for 400 damage"); @@ -1237,11 +1244,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = aliceMoves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: CPU should pick cheaper move (cost=1). mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); int32 staminaDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina); assertEq(staminaDelta, -1, "CPU should pick cheaper move within damage threshold"); @@ -1278,11 +1285,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = aliceMoves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: CPU should pick bp=100 (cost=3) since bp=50 is outside threshold. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); int32 staminaDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina); assertEq(staminaDelta, -3, "CPU should pick strongest move when cheap one is outside threshold"); @@ -1314,11 +1321,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = aliceMoves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: CPU can't afford moves → rests. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); int32 staminaDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina); assertEq(staminaDelta, 0, "CPU should rest when no affordable moves"); @@ -1349,11 +1356,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = aliceMoves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: CPU exhausted, switches to Mon1. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 1, "CPU should switch when exhausted and switch available"); @@ -1397,12 +1404,12 @@ contract BetterCPUTest is Test { // Set preferred move: monIndex=0, key=CONFIG_PREFERRED_MOVE(0), value=2 (moveIndex 1 + 1) cpu.setMonConfig(0, 0, 2); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: CPU should use preferred move (bp=90). Damage = 90*50/10 = 450. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); int32 aliceHpDelta = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); assertEq(aliceHpDelta, -450, "CPU should use preferred move (bp=90) within threshold"); @@ -1444,12 +1451,12 @@ contract BetterCPUTest is Test { // Set preferred move: monIndex=0, key=CONFIG_PREFERRED_MOVE(0), value=2 (moveIndex 1 + 1) cpu.setMonConfig(0, 0, 2); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Preferred too weak → CPU uses bp=100. Damage = 100*50/10 = 500. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); int32 aliceHpDelta = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); assertEq(aliceHpDelta, -500, "CPU should ignore preferred move when too weak"); @@ -1491,19 +1498,19 @@ contract BetterCPUTest is Test { // Set switch-in move: monIndex=0, key=CONFIG_SWITCH_IN_MOVE(1), value=2 (moveIndex 1 + 1) cpu.setMonConfig(0, 1, 2); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Alice rests (P4 safe turn). CPU uses switch-in move (Self, bp=0). mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); int32 aliceHpDeltaTurn1 = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); assertEq(aliceHpDeltaTurn1, 0, "Turn 1: CPU should use Self switch-in move (no damage)"); // Turn 2: Alice rests again. Switch-in move already used → normal P4 (best damage). mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); int32 aliceHpDeltaTurn2 = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); assertEq(aliceHpDeltaTurn2, -250, "Turn 2: CPU should use attack move (damage 250)"); @@ -1545,19 +1552,19 @@ contract BetterCPUTest is Test { // Set switch-in move for Mon0 cpu.setMonConfig(0, 1, 2); // moveIndex 1 + 1 - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Alice rests. CPU uses switch-in Self move. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); int32 aliceHpDelta = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); assertEq(aliceHpDelta, 0, "Turn 1: switch-in Self move fires (no damage)"); // Turn 2: Alice rests. CPU attacks normally (switch-in already used). mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Turn 3: Alice switches to mon 1. CPU re-evaluates. // On the switch turn, the CPU gets the switch-in move bit cleared for Mon0 when switching. @@ -1597,11 +1604,11 @@ contract BetterCPUTest is Test { typeCalc.setTypeEffectiveness(Type.Fire, Type.Liquid, 5); // 0.5x so Liquid is a viable switch bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Both can KO. Speed tie → _weGoFirst returns false → CPU should switch. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 1, "CPU should switch on speed tie (play it safe)"); @@ -1631,11 +1638,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = aliceMoves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: CPU priority 5 > Alice priority 1 → CPU goes first, KOs Alice. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); int32 aliceKO = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.IsKnockedOut); assertEq(aliceKO, 1, "CPU should KO Alice with higher priority move"); @@ -1670,7 +1677,7 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = aliceMoves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: CPU can't afford moves. Has switch option but P5 won't trigger (10 damage = 5%). // Falls through to P6: no moves. Switch available → might switch. @@ -1680,7 +1687,7 @@ contract BetterCPUTest is Test { // To force no-op, make team size 1? Can't, validator requires >= 2 for MONS_PER_TEAM. // Alternative: test that stamina is unchanged (CPU didn't attack). mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); int32 staminaDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina); assertEq(staminaDelta, 0, "CPU stamina should be unchanged (couldn't afford attack)"); diff --git a/test/CPUTest.sol b/test/CPUTest.sol index 6cfaffd3..3b20acba 100644 --- a/test/CPUTest.sol +++ b/test/CPUTest.sol @@ -217,7 +217,7 @@ contract CPUTest is Test { // Alice selects mon 2, CPU selects mon 1 mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(2)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(2)); engine.resetCallContext(); // Assert active mon index for both p0 and p1 are correct assertEq(engine.getActiveMonIndexForBattleState(battleKey)[0], 2); @@ -232,7 +232,7 @@ contract CPUTest is Test { // Alice KO's the CPU's mon, the CPU chooses no op mockCPURNG.setRNG(0); // [no op, move 1, move 2, swap 0, swap 2, swap 3] and we want no op at index 0 - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); // Check that the CPU now has 3 moves, all of which are switching to mon index 0, 2, or 3 { @@ -250,7 +250,7 @@ contract CPUTest is Test { } // Alice chooses no op (choice is irrelevant here), CPU chooses to switch to mon index 0 - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Assert the CPU now has mon index 0 as the active mon assertEq(engine.getActiveMonIndexForBattleState(battleKey)[1], 0); @@ -266,7 +266,7 @@ contract CPUTest is Test { mockCPURNG.setRNG(2); // [no op, move 1, move 2, swap 2, swap 3] and we want move 2 at index 2 // (note that the swaps are 0-indexed, and the moves are 1-indexed to refer to the above variable // naming convention, sorry D: ) - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Assert that there are now 3 moves, switching to mon index 2, 3, and no op (all stamina has been consumed) { @@ -277,7 +277,7 @@ contract CPUTest is Test { // Alice chooses no op, CPU chooses swapping to mon index 3 mockCPURNG.setRNG(2); // [no op, swap 2, swap 3] and we want swap 3 at index 2 - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Assert the CPU now has mon index 3 as the active mon assertEq(engine.getActiveMonIndexForBattleState(battleKey)[1], 3); @@ -370,15 +370,15 @@ contract CPUTest is Test { // Verify that calculateMove returns the correct move CPUContext memory cpuCtx = engine.getCPUContext(battleKey); - (uint128 moveIndex, uint240 extraData) = playerCPU.calculateMove(cpuCtx, 0, 0); + (uint128 moveIndex, uint16 extraData) = playerCPU.calculateMove(cpuCtx, 0, 0); assertEq(moveIndex, 0); assertEq(extraData, 0); // Execute the turn - playerCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(1)); + playerCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(1)); engine.resetCallContext(); // Second turn: p0 sets move 1 for PlayerCPU (should override previous move) - playerCPU.setMove(battleKey, 1, uint240(42)); + playerCPU.setMove(battleKey, 1, uint16(42)); // Verify that calculateMove now returns the new move cpuCtx = engine.getCPUContext(battleKey); @@ -387,7 +387,7 @@ contract CPUTest is Test { assertEq(uint256(extraData), 42); // Execute another turn to verify the flow continues to work - playerCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + playerCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); } @@ -455,7 +455,7 @@ contract CPUTest is Test { bytes32 battleKey = okayCPU.startBattle(proposal); // Player switches in mon index 0 (Fire type) - okayCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + okayCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Get active index for battle, it should be the resisted mon uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); @@ -506,11 +506,11 @@ contract CPUTest is Test { bytes32 battleKey = okayCPU.startBattle(proposal); // Turn 0, both player send in mon index 0 - okayCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + okayCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1, player rests, CPU should select no op because the move costs too much stamina mockCPURNG.setRNG(1); - okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); } @@ -559,17 +559,17 @@ contract CPUTest is Test { bytes32 battleKey = okayCPU.startBattle(proposal); // Turn 0, both player send in mon index 0 - okayCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + okayCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1, player rests, CPU should select move index 0 mockCPURNG.setRNG(1); // This triggers the OkayCPU to select a move, which should set its stamina delta to be -3 - okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Assert the stamina delta for P1's active mon is -3 assertEq(engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina), -3); // Turn 2, player rests, CPU should rest as well - okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Assert the stamina delta for P1's active mon is still -3 (it didn't go down more) assertEq(engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina), -3); @@ -621,10 +621,10 @@ contract CPUTest is Test { bytes32 battleKey = okayCPU.startBattle(proposal); // Turn 0, both player send in mon index 0 - okayCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + okayCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1, p0 rests, CPU should select move index 1 (self move) - okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Assert that the stamina delta is -1 for p1's active mon int32 staminaDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina); @@ -678,10 +678,10 @@ contract CPUTest is Test { bytes32 battleKey = okayCPU.startBattle(proposal); // Turn 0, both player send in mon index 0 - okayCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + okayCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1, p0 rests, CPU should select move index 1 (self move) - okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Assert that the stamina delta is -1 for p1's active mon int32 staminaDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina); @@ -735,14 +735,14 @@ contract CPUTest is Test { bytes32 battleKey = okayCPU.startBattle(proposal); // Turn 0, both player send in mon index 0 - okayCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + okayCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1, set RNG to trigger smart random select and pick move index 1 // RNG needs: (RNG % 6 == 5) to trigger smart random, (RNG % 3 != 0) to not switch, ((RNG >> 8) % 2 == 1) to pick move 1 // 257 satisfies all: 257 % 6 = 5, 257 % 3 = 2, (257 >> 8) = 1 // So both mons should take 1 damage, as p0 also selects the damage move mockCPURNG.setRNG(257); - okayCPU.selectMove(battleKey, 1, "", 0); + okayCPU.selectMove(battleKey, 1, uint104(0), 0); engine.resetCallContext(); // Assert that the hp delta is -1 for p0's active mon and p1's active mon int32 hpDelta = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); @@ -753,7 +753,7 @@ contract CPUTest is Test { // Turn 2, set RNG to be 0 (do not trigger short circuit) // CPU should select no-op because no type advantage is currently set mockCPURNG.setRNG(0); - okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Assert that the hp delta is still -1 for p0's active mon hpDelta = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); @@ -763,7 +763,7 @@ contract CPUTest is Test { typeCalc.setTypeEffectiveness(Type.Liquid, Type.Liquid, 2); // Now the CPU should select the damage move (move index 1) because it has a type advantage - okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Assert that the hp delta is -2 for p0's active mon hpDelta = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); diff --git a/test/DefaultCommitManagerTest.sol b/test/DefaultCommitManagerTest.sol index 8ba783fe..b3da8de8 100644 --- a/test/DefaultCommitManagerTest.sol +++ b/test/DefaultCommitManagerTest.sol @@ -85,19 +85,19 @@ contract DefaultCommitManagerTest is Test, BattleHelper { // Alice commits vm.startPrank(ALICE); uint8 moveIndex = SWITCH_MOVE_INDEX; - bytes32 moveHash = keccak256(abi.encodePacked(moveIndex, bytes32(""), uint240(0))); + bytes32 moveHash = keccak256(abi.encodePacked(moveIndex, uint104(0), uint16(0))); commitManager.commitMove(battleKey, moveHash); // Alice tries to reveal vm.expectRevert(DefaultCommitManager.NotYetRevealed.selector); - commitManager.revealMove(battleKey, moveIndex, bytes32(""), uint240(0), false); + commitManager.revealMove(battleKey, moveIndex, uint104(0), uint16(0), false); } function test_RevealBeforeSelfCommit() public { bytes32 battleKey = _startBattle(validator, engine, defaultOracle, defaultRegistry, matchmaker, address(commitManager)); // Alice sets commitment _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Bob sets commitment _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, NO_OP_MOVE_INDEX, 0, 0); @@ -108,13 +108,13 @@ contract DefaultCommitManagerTest is Test, BattleHelper { // Alice's turn again to move vm.startPrank(ALICE); vm.expectRevert(DefaultCommitManager.RevealBeforeSelfCommit.selector); - commitManager.revealMove(battleKey, NO_OP_MOVE_INDEX, bytes32(""), 0, false); + commitManager.revealMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0, false); } function test_BattleNotYetStarted() public { vm.startPrank(ALICE); vm.expectRevert(DefaultCommitManager.BattleNotYetStarted.selector); - commitManager.revealMove(bytes32(0), NO_OP_MOVE_INDEX, bytes32(""), 0, false); + commitManager.revealMove(bytes32(0), NO_OP_MOVE_INDEX, uint104(0), 0, false); vm.startPrank(BOB); vm.expectRevert(DefaultCommitManager.BattleNotYetStarted.selector); commitManager.commitMove(bytes32(0), bytes32(0)); @@ -127,7 +127,7 @@ contract DefaultCommitManagerTest is Test, BattleHelper { engine.end(battleKey); vm.startPrank(ALICE); vm.expectRevert(DefaultCommitManager.BattleAlreadyComplete.selector); - commitManager.revealMove(battleKey, NO_OP_MOVE_INDEX, bytes32(""), 0, false); + commitManager.revealMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0, false); vm.startPrank(BOB); vm.expectRevert(DefaultCommitManager.BattleAlreadyComplete.selector); commitManager.commitMove(battleKey, bytes32(0)); @@ -157,7 +157,7 @@ contract DefaultCommitManagerTest is Test, BattleHelper { vm.startPrank(ALICE); commitManager.commitMove(battleKey, bytes32("1")); vm.startPrank(BOB); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, bytes32(""), uint240(0), false); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(0), false); vm.warp(TIMEOUT * validator.PREV_TURN_MULTIPLIER() + 1); engine.end(battleKey); assertEq(engine.getWinner(battleKey), BOB); diff --git a/test/EngineGasTest.sol b/test/EngineGasTest.sol index c80bd59a..1dae1d6e 100644 --- a/test/EngineGasTest.sol +++ b/test/EngineGasTest.sol @@ -49,11 +49,6 @@ contract EngineGasTest is Test, BattleHelper { TestTeamRegistry defaultRegistry; DefaultMatchmaker matchmaker; - // Helper to pack StatBoostsMove extraData: lower 60 bits = playerIndex, next 60 bits = monIndex, next 60 bits = statIndex, upper 60 bits = boostAmount - function _packStatBoost(uint256 playerIndex, uint256 monIndex, uint256 statIndex, int32 boostAmount) internal pure returns (uint240) { - return uint240(playerIndex | (monIndex << 60) | (statIndex << 120) | (uint256(uint32(boostAmount)) << 180)); - } - function setUp() public { defaultOracle = new DefaultRandomnessOracle(); engine = new Engine(0, 0, 0); @@ -142,16 +137,16 @@ contract EngineGasTest is Test, BattleHelper { // - Alice swaps in mon index 3 // - Alice rests, Bob KOs vm.startSnapshotGas("FirstBattle"); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); // Alice uses burn, Bob uses frostbite _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 1, 0, 0); // Bob is mon index 0, we boost attack by 90% - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 2, uint240(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 2, uint16(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); // Alice is now mon index 1, Bob is mon index 0 _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 2, 3, _packStatBoost(0, 1, uint256(MonStateIndexName.Attack), int32(90)), 0); // Alice swaps in mon index 0 vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0), true); engine.resetCallContext(); // Alice is now mon index 0, Bob rests _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 2, NO_OP_MOVE_INDEX, _packStatBoost(0, 0, uint256(MonStateIndexName.Attack), int32(90)), 0); @@ -159,7 +154,7 @@ contract EngineGasTest is Test, BattleHelper { _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 3, NO_OP_MOVE_INDEX, 0, 0); // Bob sends in mon index 1 vm.startPrank(BOB); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); // Alice rests, Bob uses self-stat boost _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 2, 0, _packStatBoost(1, 1, uint256(MonStateIndexName.Attack), int32(90))); @@ -167,13 +162,13 @@ contract EngineGasTest is Test, BattleHelper { _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 3, 0, 0); // Alice swaps in mon index 2 vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(2), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(2), true); engine.resetCallContext(); // Alice rests, Bob KOs _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 3, 0, 0); // Alice swaps in mon index 3 vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(3), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(3), true); engine.resetCallContext(); // Alice rests, Bob KOs _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 3, 0, 0); @@ -223,24 +218,24 @@ contract EngineGasTest is Test, BattleHelper { // - Both players send in mon 0 vm.startSnapshotGas("SecondBattle"); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); // - Alice sets up self-stat boost (move 3), Bob sets up Burn (move 1) _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 3, 1, _packStatBoost(0, 0, uint256(MonStateIndexName.Attack), int32(90)), 0); // - Alice KOs Bob (move 0 = damage) _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); // - Bob swaps in mon index 1 vm.startPrank(BOB); - commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); // - Alice swaps in mon index 1, Bob sets up Frostbite (move 2) - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, 2, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, 2, uint16(1), 0); // - Alice sets up self-stat boost (move 3, playerIndex=0, monIndex=1), Bob rests _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 3, NO_OP_MOVE_INDEX, _packStatBoost(0, 1, uint256(MonStateIndexName.Attack), int32(90)), 0); // - Alice KOs Bob (move 0) _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); // - Bob sends in mon index 2 vm.startPrank(BOB); - commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint240(2), true); + commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint16(2), true); engine.resetCallContext(); // - Alice rests, Bob uses self-stat boost (move 3, playerIndex=1, monIndex=2) _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, NO_OP_MOVE_INDEX, 3, 0, _packStatBoost(1, 2, uint256(MonStateIndexName.Attack), int32(90))); @@ -248,7 +243,7 @@ contract EngineGasTest is Test, BattleHelper { _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, NO_OP_MOVE_INDEX, 0, 0, 0); // - Alice swaps in mon index 2 vm.startPrank(ALICE); - commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint240(2), true); + commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint16(2), true); engine.resetCallContext(); // - Alice uses self-stat boost (move 3, p0 mon2), Bob uses self-stat boost (move 3, p1 mon2) _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 3, 3, _packStatBoost(0, 2, uint256(MonStateIndexName.Attack), int32(90)), _packStatBoost(1, 2, uint256(MonStateIndexName.Attack), int32(90))); @@ -256,7 +251,7 @@ contract EngineGasTest is Test, BattleHelper { _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); // - Bob sends in mon index 3 vm.startPrank(BOB); - commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint240(3), true); + commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint16(3), true); engine.resetCallContext(); // - Alice KOs Bob (move 0) _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); @@ -283,16 +278,16 @@ contract EngineGasTest is Test, BattleHelper { // Battle 3: Exact same sequence as Battle 1 vm.startSnapshotGas("ThirdBattle"); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); // Alice uses burn, Bob uses frostbite _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, 0, 1, 0, 0); // Bob is mon index 0, we boost attack by 90% - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, SWITCH_MOVE_INDEX, 2, uint240(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, SWITCH_MOVE_INDEX, 2, uint16(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); // Alice is now mon index 1, Bob is mon index 0 _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, 2, 3, _packStatBoost(0, 1, uint256(MonStateIndexName.Attack), int32(90)), 0); // Alice swaps in mon index 0 vm.startPrank(ALICE); - commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint240(0), true); + commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint16(0), true); engine.resetCallContext(); // Alice is now mon index 0, Bob rests _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, 2, NO_OP_MOVE_INDEX, _packStatBoost(0, 0, uint256(MonStateIndexName.Attack), int32(90)), 0); @@ -300,7 +295,7 @@ contract EngineGasTest is Test, BattleHelper { _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, 3, NO_OP_MOVE_INDEX, 0, 0); // Bob sends in mon index 1 vm.startPrank(BOB); - commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); // Alice rests, Bob uses self-stat boost _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, NO_OP_MOVE_INDEX, 2, 0, _packStatBoost(1, 1, uint256(MonStateIndexName.Attack), int32(90))); @@ -308,13 +303,13 @@ contract EngineGasTest is Test, BattleHelper { _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, NO_OP_MOVE_INDEX, 3, 0, 0); // Alice swaps in mon index 2 vm.startPrank(ALICE); - commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint240(2), true); + commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint16(2), true); engine.resetCallContext(); // Alice rests, Bob KOs _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, NO_OP_MOVE_INDEX, 3, 0, 0); // Alice swaps in mon index 3 vm.startPrank(ALICE); - commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint240(3), true); + commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint16(3), true); engine.resetCallContext(); // Alice rests, Bob KOs _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, NO_OP_MOVE_INDEX, 3, 0, 0); @@ -390,7 +385,7 @@ contract EngineGasTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("Battle1_Execute"); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); // Both switch in mon 0 + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); // Both switch in mon 0 _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey1, 0, 0, 0, 0); // Both attack - one dies // After this, battle should end uint256 execute1 = vm.stopSnapshotGas("Battle1_Execute"); @@ -404,7 +399,7 @@ contract EngineGasTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("Battle2_Execute"); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); // Both switch in mon 0 + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); // Both switch in mon 0 _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 0, 0, 0, 0); // Both attack - one dies uint256 execute2 = vm.stopSnapshotGas("Battle2_Execute"); @@ -469,7 +464,7 @@ contract EngineGasTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("B1_Execute"); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); // Check after switch (BattleConfigView memory cfgAfterSwitch,) = engine.getBattle(battleKey1); @@ -510,7 +505,7 @@ contract EngineGasTest is Test, BattleHelper { console.log("After B2 setup - packedP1EffectsCount:", cfg2.packedP1EffectsCount); vm.startSnapshotGas("B2_Execute"); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); // Both apply effect to each other (adds 2 effects - should REUSE slots) _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 0, 0, 0, 0); // Both attack - KO @@ -582,7 +577,7 @@ contract EngineGasTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("External_Execute"); - _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, externalBattleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, externalBattleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, externalBattleKey, 0, 0, 0, 0); uint256 externalExecute = vm.stopSnapshotGas("External_Execute"); @@ -603,7 +598,7 @@ contract EngineGasTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("Inline_Execute"); - _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, inlineBattleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, inlineBattleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, inlineBattleKey, 0, 0, 0, 0); uint256 inlineExecute = vm.stopSnapshotGas("Inline_Execute"); @@ -664,7 +659,7 @@ contract EngineGasTest is Test, BattleHelper { new IEngineHook[](0), simpleRuleset, address(commitManager) ); vm.warp(vm.getBlockTimestamp() + 1); - _commitRevealExecuteForEngine(engine, commitManager, battleKey1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForEngine(engine, commitManager, battleKey1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _commitRevealExecuteForEngine(engine, commitManager, battleKey1, 0, 0, 0, 0); // Get final HP deltas @@ -684,7 +679,7 @@ contract EngineGasTest is Test, BattleHelper { new IEngineHook[](0), inlineRuleset, address(inlineCM) ); vm.warp(vm.getBlockTimestamp() + 1); - _commitRevealExecuteForEngine(inlineEngine, inlineCM, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForEngine(inlineEngine, inlineCM, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _commitRevealExecuteForEngine(inlineEngine, inlineCM, battleKey2, 0, 0, 0, 0); // Get final HP deltas @@ -756,10 +751,10 @@ contract EngineGasTest is Test, BattleHelper { bytes32 battleKey, uint8 aliceMoveIndex, uint8 bobMoveIndex, - uint240 aliceExtraData, - uint240 bobExtraData + uint16 aliceExtraData, + uint16 bobExtraData ) internal { - bytes32 salt = ""; + uint104 salt = 0; bytes32 aliceMoveHash = keccak256(abi.encodePacked(aliceMoveIndex, salt, aliceExtraData)); bytes32 bobMoveHash = keccak256(abi.encodePacked(bobMoveIndex, salt, bobExtraData)); uint256 turnId = eng.getTurnIdForBattleState(battleKey); diff --git a/test/EngineGlobalKVTest.sol b/test/EngineGlobalKVTest.sol index f3d42257..91a8cd5a 100644 --- a/test/EngineGlobalKVTest.sol +++ b/test/EngineGlobalKVTest.sol @@ -9,7 +9,6 @@ import "../src/Structs.sol"; import {DefaultCommitManager} from "../src/commit-manager/DefaultCommitManager.sol"; import {DefaultValidator} from "../src/DefaultValidator.sol"; import {Engine} from "../src/Engine.sol"; -import {MonStateIndexName, Type} from "../src/Enums.sol"; import {IEngine} from "../src/IEngine.sol"; import {DefaultMatchmaker} from "../src/matchmaker/DefaultMatchmaker.sol"; @@ -30,12 +29,12 @@ contract EngineGlobalKVTest is Test, BattleHelper { DefaultValidator validator; // Arbitrary keys used throughout the tests. - uint240 constant KEY_A = 1001; - uint240 constant KEY_B = 1002; - uint240 constant KEY_C = 1003; - uint240 constant KEY_D = 1004; - uint240 constant KEY_E = 1005; - uint240 constant KEY_F = 1006; + uint16 constant KEY_A = 1001; + uint16 constant KEY_B = 1002; + uint16 constant KEY_C = 1003; + uint16 constant KEY_D = 1004; + uint16 constant KEY_E = 1005; + uint16 constant KEY_F = 1006; function setUp() public { mockOracle = new MockRandomnessOracle(); @@ -71,7 +70,7 @@ contract EngineGlobalKVTest is Test, BattleHelper { defaultRegistry.setTeam(BOB, team); battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); } @@ -82,22 +81,22 @@ contract EngineGlobalKVTest is Test, BattleHelper { defaultRegistry.setTeam(BOB, team); battleKey2 = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); } /// @dev Both players use the mock write-move with their respective keys. - function _bothWrite(bytes32 battleKey, uint240 aliceKey, uint240 bobKey) internal { + function _bothWrite(bytes32 battleKey, uint16 aliceKey, uint16 bobKey) internal { _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 0, aliceKey, bobKey); } /// @dev Alice writes; Bob rests. - function _aliceWrites(bytes32 battleKey, uint240 aliceKey) internal { + function _aliceWrites(bytes32 battleKey, uint16 aliceKey) internal { _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, aliceKey, 0); } /// @dev Bob writes; Alice rests. - function _bobWrites(bytes32 battleKey, uint240 bobKey) internal { + function _bobWrites(bytes32 battleKey, uint16 bobKey) internal { _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 0, 0, bobKey); } @@ -127,7 +126,8 @@ contract EngineGlobalKVTest is Test, BattleHelper { bytes32 firstPacked = view1.globalKVEntries[0].value; // Second call re-writes via encoded value bump so we can confirm it changed. - uint240 extraData = KEY_A | (uint240(42) << 64); + // Layout: bits 0..9 = key, bits 10..15 = value. + uint16 extraData = KEY_A | (uint16(42) << 10); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, extraData, 0); (BattleConfigView memory view2,) = engine.getBattle(battleKey); diff --git a/test/EngineOptimizationTest.sol b/test/EngineOptimizationTest.sol index 943bdb22..607f235a 100644 --- a/test/EngineOptimizationTest.sol +++ b/test/EngineOptimizationTest.sol @@ -116,7 +116,7 @@ contract EngineOptimizationTest is Test, BattleHelper { // Switch in mons _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses effect move on Bob (Bob does NoOp) @@ -175,7 +175,7 @@ contract EngineOptimizationTest is Test, BattleHelper { ); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, 0, 0); @@ -248,7 +248,7 @@ contract EngineOptimizationTest is Test, BattleHelper { // Switch in mons _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both use 5-stamina move. After: -5 from move + 1 from RoundEnd regen = -4 @@ -318,7 +318,7 @@ contract EngineOptimizationTest is Test, BattleHelper { ); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both use 5-stamina move: staminaDelta = -5 + 1 (RoundEnd) = -4 @@ -393,7 +393,7 @@ contract EngineOptimizationTest is Test, BattleHelper { ); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Use 1-stamina move: -1 from move + 1 from RoundEnd regen = 0 (not +1) @@ -414,7 +414,7 @@ contract EngineOptimizationTest is Test, BattleHelper { _forceP1Switch(testEngine, signedManager, battleKey); - _executeSinglePlayerMoveAndReset(testEngine, signedManager, battleKey, BOB, uint240(1)); + _executeSinglePlayerMoveAndReset(testEngine, signedManager, battleKey, BOB, uint16(1)); uint256[] memory activeMons = testEngine.getActiveMonIndexForBattleState(battleKey); assertEq(activeMons[1], 1, "P1 should switch to mon 1"); @@ -433,7 +433,7 @@ contract EngineOptimizationTest is Test, BattleHelper { vm.startPrank(ALICE); vm.expectRevert(DefaultCommitManager.PlayerNotAllowed.selector); - signedManager.executeSinglePlayerMove(battleKey, SWITCH_MOVE_INDEX, bytes32(0), uint240(1)); + signedManager.executeSinglePlayerMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1)); vm.stopPrank(); } @@ -442,12 +442,12 @@ contract EngineOptimizationTest is Test, BattleHelper { _startSignedInlineSwitchBattle(false); _commitRevealExecuteForAliceAndBob( - testEngine, signedManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + testEngine, signedManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); vm.startPrank(BOB); vm.expectRevert(SignedCommitManager.NotSinglePlayerTurn.selector); - signedManager.executeSinglePlayerMove(battleKey, SWITCH_MOVE_INDEX, bytes32(0), uint240(1)); + signedManager.executeSinglePlayerMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1)); vm.stopPrank(); } @@ -456,11 +456,11 @@ contract EngineOptimizationTest is Test, BattleHelper { _startSignedInlineSwitchBattle(false); _forceP1Switch(testEngine, signedManager, battleKey); - _executeSinglePlayerMoveAndReset(testEngine, signedManager, battleKey, BOB, uint240(1)); + _executeSinglePlayerMoveAndReset(testEngine, signedManager, battleKey, BOB, uint16(1)); uint256 turnBefore = testEngine.getTurnIdForBattleState(battleKey); _commitRevealExecuteForAliceAndBob( - testEngine, signedManager, battleKey, NO_OP_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + testEngine, signedManager, battleKey, NO_OP_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); assertEq(testEngine.getTurnIdForBattleState(battleKey), turnBefore + 1, "Normal fallback should execute"); @@ -473,7 +473,7 @@ contract EngineOptimizationTest is Test, BattleHelper { _forceP1Switch(testEngine, signedManager, battleKey); vm.prank(BOB); - signedManager.revealMove(battleKey, SWITCH_MOVE_INDEX, bytes32(0), uint240(1), true); + signedManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1), true); testEngine.resetCallContext(); uint256[] memory activeMons = testEngine.getActiveMonIndexForBattleState(battleKey); @@ -489,11 +489,11 @@ contract EngineOptimizationTest is Test, BattleHelper { (Engine testEngine, SignedCommitManager signedManager, bytes32 battleKey) = _startSignedInlineSwitchBattle(true); _forceP1Switch(testEngine, signedManager, battleKey); - _executeSinglePlayerMoveAndReset(testEngine, signedManager, battleKey, BOB, uint240(1)); + _executeSinglePlayerMoveAndReset(testEngine, signedManager, battleKey, BOB, uint16(1)); assertEq(testEngine.getPlayerSwitchForTurnFlagForBattleState(battleKey), 0, "P0 should be forced to switch"); - _executeSinglePlayerMoveAndReset(testEngine, signedManager, battleKey, ALICE, uint240(1)); + _executeSinglePlayerMoveAndReset(testEngine, signedManager, battleKey, ALICE, uint16(1)); assertEq( testEngine.getPlayerSwitchForTurnFlagForBattleState(battleKey), 2, @@ -502,7 +502,7 @@ contract EngineOptimizationTest is Test, BattleHelper { uint256 turnBefore = testEngine.getTurnIdForBattleState(battleKey); _commitRevealExecuteForAliceAndBob( - testEngine, signedManager, battleKey, NO_OP_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + testEngine, signedManager, battleKey, NO_OP_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); assertEq( @@ -514,10 +514,10 @@ contract EngineOptimizationTest is Test, BattleHelper { function _forceP1Switch(Engine testEngine, SignedCommitManager signedManager, bytes32 battleKey) internal { _commitRevealExecuteForAliceAndBob( - testEngine, signedManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + testEngine, signedManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); _commitRevealExecuteForAliceAndBob( - testEngine, signedManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + testEngine, signedManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); assertEq(testEngine.getPlayerSwitchForTurnFlagForBattleState(battleKey), 1, "P1 should be forced to switch"); @@ -528,10 +528,10 @@ contract EngineOptimizationTest is Test, BattleHelper { SignedCommitManager signedManager, bytes32 battleKey, address player, - uint240 monIndex + uint16 monIndex ) internal { vm.prank(player); - signedManager.executeSinglePlayerMove(battleKey, SWITCH_MOVE_INDEX, bytes32(0), monIndex); + signedManager.executeSinglePlayerMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), monIndex); testEngine.resetCallContext(); } @@ -665,7 +665,7 @@ contract EngineOptimizationTest is Test, BattleHelper { ); vm.warp(vm.getBlockTimestamp() + 1); _commitRevealExecuteForAliceAndBob( - engine, commitManager, warmupKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, warmupKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); _commitRevealExecuteForAliceAndBob(engine, commitManager, warmupKey, 0, 0, 0, 0); _commitRevealExecuteForAliceAndBob(engine, commitManager, warmupKey, NO_OP_MOVE_INDEX, NO_OP_MOVE_INDEX, 0, 0); @@ -683,7 +683,7 @@ contract EngineOptimizationTest is Test, BattleHelper { ); vm.warp(vm.getBlockTimestamp() + 1); _commitRevealExecuteForAliceAndBob( - engine, commitManager, externalKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, externalKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); vm.startSnapshotGas("ExternalStaminaRegen"); @@ -704,7 +704,7 @@ contract EngineOptimizationTest is Test, BattleHelper { ); vm.warp(vm.getBlockTimestamp() + 1); _commitRevealExecuteForAliceAndBob( - engine, commitManager, inlineKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, inlineKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); vm.startSnapshotGas("InlineStaminaRegen"); diff --git a/test/EngineTest.sol b/test/EngineTest.sol index 3579cdfa..7ff48a80 100644 --- a/test/EngineTest.sol +++ b/test/EngineTest.sol @@ -62,9 +62,9 @@ contract EngineTest is Test, BattleHelper { Mon dummyMon; IMoveSet dummyAttack; - // Helper to pack ForceSwitchMove extraData: lower 120 bits = playerIndex, upper 120 bits = monToSwitchIndex - function _packForceSwitch(uint256 playerIndex, uint256 monToSwitchIndex) internal pure returns (uint240) { - return uint240(playerIndex | (monToSwitchIndex << 120)); + // Pack ForceSwitchMove extraData: bit 0 = playerIndex (0/1), bits 1..15 = monToSwitchIndex + function _packForceSwitch(uint256 playerIndex, uint256 monToSwitchIndex) internal pure returns (uint16) { + return uint16((playerIndex & 0x1) | (monToSwitchIndex << 1)); } function setUp() public { @@ -123,7 +123,7 @@ contract EngineTest is Test, BattleHelper { // Let Alice and Bob both commit to switching _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Let Alice and Bob do a no-op @@ -190,7 +190,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Let Alice and Bob commit and reveal to both choosing attack (move index 0) @@ -266,7 +266,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Let Alice and Bob commit and reveal to both choosing attack (move index 0) @@ -348,7 +348,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Let Alice and Bob commit and reveal to both choosing attack (move index 0) @@ -374,7 +374,7 @@ contract EngineTest is Test, BattleHelper { // Reveal Alice's move, and advance game state vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, bytes32(""), uint240(1), false); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1), false); engine.execute(battleKey); engine.resetCallContext(); @@ -400,7 +400,7 @@ contract EngineTest is Test, BattleHelper { // Attempt to reveal Alice's move, and assert that we cannot advance the game state vm.startPrank(ALICE); vm.expectRevert(abi.encodeWithSignature("InvalidMove(address)", ALICE)); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, bytes32(""), uint240(0), false); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(0), false); // Attempt to forcibly advance the game state vm.expectRevert(); @@ -476,7 +476,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Let Alice and Bob commit and reveal to both choosing attack (move index 0) @@ -534,11 +534,11 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Second move, have Alice swap out to mon at index 1, have Bob use attack - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), 0); // Assert that mon index for Alice is 1 // Assert that the mon state for Alice has -5 applied to the switched in mon @@ -587,11 +587,11 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Second move, have Alice swap out to mon at index 1, have Bob use fast attack - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), 0); // Assert that mon index for Alice is 1 // Assert that the mon state for Alice has -5 applied to the previous mon @@ -640,11 +640,11 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Second move, have Alice swap out to mon at index 1, have Bob use fast attack which supersedes Switch - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), 0); // Given that it's a KO (even though Alice chose switch), // check that now they have the priority flag again @@ -699,7 +699,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Let Alice and Bob commit and reveal to both choosing attack (move index 0) @@ -752,7 +752,7 @@ contract EngineTest is Test, BattleHelper { // Select mons _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both attack (costs 1 stamina each) @@ -829,7 +829,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Let Alice and Bob commit and reveal to both choosing attack (move index 0) @@ -900,19 +900,19 @@ contract EngineTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, defaultOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Commit move index 0 for Bob uint8 moveIndex = 0; vm.startPrank(BOB); - bytes32 bobMoveHash = keccak256(abi.encodePacked(moveIndex, bytes32(""), uint240(0))); + bytes32 bobMoveHash = keccak256(abi.encodePacked(moveIndex, uint104(0), uint16(0))); commitManager.commitMove(battleKey, bobMoveHash); // Assert that Alice cannot reveal anything because of the stamina cost (she has the high stamina cost mon) vm.startPrank(ALICE); vm.expectRevert(abi.encodeWithSignature("InvalidMove(address)", ALICE)); - commitManager.revealMove(battleKey, moveIndex, bytes32(""), uint240(0), false); + commitManager.revealMove(battleKey, moveIndex, uint104(0), uint16(0), false); } // Ensure that we cannot write to mon state when there is no active execute() call in the call stack @@ -997,13 +997,13 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both player pick move index 0, which for Bob afflicts the instant death condition on the // opposing mon (Alice's) and knocks it out uint8 moveIndex = 0; - uint240 extraData = 0; + uint16 extraData = 0; _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, moveIndex, moveIndex, extraData, extraData); // Assert Bob wins @@ -1076,20 +1076,20 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both player pick move index 0, which for Bob afflicts the instant death condition on the // opposing mon (Alice's) and knocks it out uint8 moveIndex = 0; - uint240 extraData = 0; + uint16 extraData = 0; _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, moveIndex, moveIndex, extraData, extraData); // Now only Alice should be able to switch vm.startPrank(ALICE); // Alice should be able to reveal because she is the only player (player flag should be set) - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, bytes32(""), uint240(1), false); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1), false); // Execute the switch engine.execute(battleKey); @@ -1135,13 +1135,13 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice swaps to mon index 1, and Bob applies the effect // The effect should be applied to mon index 1 for Alice but only during the duration of the turn // (We have a check for 2 instead of 1 to avoid confusing it with the base case state) - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), 0); // Assert that the temporary stat boost effect is updated to 2 because the roundEnd hook also runs assertEq(engine.getMonStateForBattle(battleKey, 0, 1, MonStateIndexName.Attack), 2); @@ -1209,14 +1209,14 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both player pick move index 0, which for Bob afflicts the instant death condition on the // opposing mon (Alice's) // But Alice's mon should KO Bob's before the end of round takes place uint8 moveIndex = 0; - uint240 extraData = 0; + uint16 extraData = 0; _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, moveIndex, moveIndex, extraData, extraData); // Assert Alice wins @@ -1289,26 +1289,26 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both player pick move index 0, which for Bob afflicts the instant death condition on the // opposing mon (Alice's) and knocks it out // But Bob moves first (higher priority), so he gets the instant death affliction uint8 moveIndex = 0; - uint240 extraData = 0; + uint16 extraData = 0; _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, moveIndex, moveIndex, extraData, extraData); // Now if Alice tries to pick a non-switch move, the engine should revert vm.startPrank(ALICE); - bytes32 salt = ""; + uint104 salt = 0; uint8 aliceMoveIndex = 0; bytes32 aliceMoveHash = keccak256(abi.encodePacked(aliceMoveIndex, salt, extraData)); commitManager.commitMove(battleKey, aliceMoveHash); // Bob reveals a swap vm.startPrank(BOB); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, uint240(1), false); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, uint16(1), false); // Alice's reveal will revert (must choose switch) vm.startPrank(ALICE); @@ -1382,19 +1382,19 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both player pick move index 0, which for Bob afflicts the instant death condition on the // opposing mon (Alice's) and knocks it out // But Bob moves first (higher priority), so he gets the instant death affliction uint8 moveIndex = 0; - uint240 extraData = 0; + uint16 extraData = 0; _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, moveIndex, moveIndex, extraData, extraData); // Now both moves have to swap to index 1 for their mons _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(1), uint240(1) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(1), uint16(1) ); } @@ -1456,7 +1456,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both player pick move index 0 @@ -1464,7 +1464,7 @@ contract EngineTest is Test, BattleHelper { // Alice tries to go fast for a lethal effect // Bob should win priority and inflict skip turn effect uint8 moveIndex = 0; - uint240 extraData = 0; + uint16 extraData = 0; _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, moveIndex, moveIndex, extraData, extraData); // Assert no winner, and no damage dealt @@ -1538,7 +1538,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both player pick move index 0, but Alice encodes a swap to mon index 1 for player index 1 (Bob) @@ -1617,7 +1617,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both player pick move index 0, but Alice encodes a swap to mon index 1 for player index 0 (Alice) @@ -1696,12 +1696,12 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Let Bob commit and reveal to attack (move index 0) - uint240 extraData = 0; - bytes32 salt = ""; + uint16 extraData = 0; + uint104 salt = 0; uint8 moveIndex = 0; vm.startPrank(BOB); commitManager.commitMove(battleKey, keccak256(abi.encodePacked(moveIndex, salt, extraData))); @@ -1788,12 +1788,12 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Let Bob commit and reveal to attack (move index 0) - uint240 extraData = 0; - bytes32 salt = ""; + uint16 extraData = 0; + uint104 salt = 0; uint8 moveIndex = 0; vm.startPrank(BOB); commitManager.commitMove(battleKey, keccak256(abi.encodePacked(moveIndex, salt, extraData))); @@ -1885,7 +1885,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Let both players select move index 0 @@ -1955,7 +1955,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // (Have Alice force Bob to switch to mon index 1, have Bob select the instant death switch in effect @@ -2024,12 +2024,12 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Have Alice switch to their second mon, have Bob select the instant death switch in effect // (But swapping to mon index 1 for Alice will trigger on switch in and kill the mon) - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), 0); // Assert that the player switch for turn flag is now 0, indicating Alice has to switch (, BattleData memory state) = engine.getBattle(battleKey); @@ -2099,7 +2099,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // After this, Alice's mon should be dead and Bob should be the winner @@ -2193,7 +2193,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice switches themselves to mon index 1, while Bob chooses move index 0 @@ -2293,11 +2293,11 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice switches themselves to mon index 1, while Bob chooses move index 0 - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), 0); // Assert that Alice's new mon is now KOed assertEq(engine.getMonStateForBattle(battleKey, 0, 1, MonStateIndexName.IsKnockedOut), 1); @@ -2349,7 +2349,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice and Bob both select attacks (they should apply the single instance effect on hit) @@ -2395,19 +2395,19 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Bob commits to NO_OP vm.startPrank(BOB); uint8 moveIndex = NO_OP_MOVE_INDEX; - bytes32 bobMoveHash = keccak256(abi.encodePacked(moveIndex, bytes32(""), uint240(0))); + bytes32 bobMoveHash = keccak256(abi.encodePacked(moveIndex, uint104(0), uint16(0))); commitManager.commitMove(battleKey, bobMoveHash); // Alice should revert when revealing vm.startPrank(ALICE); vm.expectRevert(abi.encodeWithSignature("InvalidMove(address)", ALICE)); - commitManager.revealMove(battleKey, 0, bytes32(""), uint240(0), false); + commitManager.revealMove(battleKey, 0, uint104(0), uint16(0), false); } function test_onMonSwitchOutHookWorksWithTempStatBoost() public { @@ -2451,7 +2451,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice and Bob both select attacks (they should apply the temporary stat boost effect) @@ -2463,7 +2463,7 @@ contract EngineTest is Test, BattleHelper { // Alice and Bob both switch to mon index 1 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(1), uint240(1) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(1), uint16(1) ); // Assert that the temporary stat boost effect was removed from both mons @@ -2517,7 +2517,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice and Bob both select attacks, both of them are move index 0 (do damage rebound) @@ -2568,8 +2568,8 @@ contract EngineTest is Test, BattleHelper { bytes32 battleKey = _startBattle(twoMoveValidator, engine, defaultOracle, defaultRegistry, matchmaker, address(commitManager)); // Alice commits to swapping in mon index 1 - bytes32 salt = ""; - bytes32 aliceMoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(1))); + uint104 salt = 0; + bytes32 aliceMoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint16(1))); vm.startPrank(ALICE); commitManager.commitMove(battleKey, aliceMoveHash); @@ -2579,19 +2579,19 @@ contract EngineTest is Test, BattleHelper { // Bob reveals vm.startPrank(BOB); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, uint240(1), false); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, uint16(1), false); // Bob cannot reveal twice vm.expectRevert(DefaultCommitManager.AlreadyRevealed.selector); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, uint240(1), false); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, uint16(1), false); // Alice reveals but does not execute vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, uint240(1), false); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, uint16(1), false); // Second reveal should also fail vm.expectRevert(DefaultCommitManager.AlreadyRevealed.selector); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, uint240(1), false); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, uint16(1), false); } function test_cannotCommitToEndedBattle() public { @@ -2622,7 +2622,7 @@ contract EngineTest is Test, BattleHelper { // Both players send in mon index 0 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Bob does nothing (it's his turn to move, so he'll lose by timeout) @@ -2671,7 +2671,7 @@ contract EngineTest is Test, BattleHelper { // Alice commits to switch to mon index 0 vm.startPrank(ALICE); - commitManager.commitMove(battleKey, keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(""), uint240(0)))); + commitManager.commitMove(battleKey, keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, uint104(0), uint16(0)))); // Attempt to end the battle immediately (same block as start) // Bob hasn't committed and timeout is 0, so Bob loses, but game should revert @@ -2722,10 +2722,10 @@ contract EngineTest is Test, BattleHelper { bytes32 battleKey, uint8 aliceMoveIndex, uint8 bobMoveIndex, - uint240 aliceExtraData, - uint240 bobExtraData + uint16 aliceExtraData, + uint16 bobExtraData ) internal { - bytes32 salt = ""; + uint104 salt = 0; bytes32 aliceMoveHash = keccak256(abi.encodePacked(aliceMoveIndex, salt, aliceExtraData)); bytes32 bobMoveHash = keccak256(abi.encodePacked(bobMoveIndex, salt, bobExtraData)); // Decide which player commits @@ -2830,8 +2830,8 @@ contract EngineTest is Test, BattleHelper { */ function test_turn0DefaultCommitManagerValidPreimage() public { bytes32 battleKey = _startDummyBattleWithTwoMons(); - bytes32 salt = ""; - uint240 extraData = uint240(0); + uint104 salt = 0; + uint16 extraData = uint16(0); bytes32 moveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, extraData)); // Ensure Alice cannot reveal yet because Alice has not yet committed @@ -2889,8 +2889,8 @@ contract EngineTest is Test, BattleHelper { bytes32 battleKey = _startDummyBattleWithTwoMons(); // Let Alice commit to choosing switch - bytes32 salt = ""; - uint240 extraData = uint240(0); + uint104 salt = 0; + uint16 extraData = uint16(0); bytes32 moveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, extraData)); // Let Alice commit the first move (switching in mon index 0) @@ -2910,7 +2910,7 @@ contract EngineTest is Test, BattleHelper { commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, extraData, true); engine.resetCallContext(); // New turn, both players swap to mon index 1 - extraData = uint240(1); + extraData = uint16(1); moveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, extraData)); // Ensure Bob cannot reveal yet because Bob has not yet committed @@ -2936,7 +2936,7 @@ contract EngineTest is Test, BattleHelper { // Let Alice reveal their invalid move index of 0 vm.startPrank(ALICE); vm.expectRevert(abi.encodeWithSignature("InvalidMove(address)", ALICE)); - uint240 invalidExtraData = uint240(0); + uint16 invalidExtraData = uint16(0); commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, invalidExtraData, true); engine.resetCallContext(); // Now let Alice reveal a valid move @@ -2971,8 +2971,8 @@ contract EngineTest is Test, BattleHelper { bytes32 battleKey = _startDummyBattleWithTwoMons(); // Let Alice commit to choosing switch - bytes32 salt = ""; - uint240 extraData = uint240(0); + uint104 salt = 0; + uint16 extraData = uint16(0); bytes32 moveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, extraData)); vm.startPrank(ALICE); commitManager.commitMove(battleKey, moveHash); @@ -2991,8 +2991,8 @@ contract EngineTest is Test, BattleHelper { bytes32 battleKey = _startDummyBattleWithTwoMons(); // Let Alice commit to choosing switch - bytes32 salt = ""; - uint240 extraData = uint240(0); + uint104 salt = 0; + uint16 extraData = uint16(0); bytes32 moveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, extraData)); vm.startPrank(ALICE); commitManager.commitMove(battleKey, moveHash); @@ -3067,7 +3067,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Let Alice and Bob commit and reveal to both choosing attack (move index 0) @@ -3111,9 +3111,9 @@ contract EngineTest is Test, BattleHelper { ); bytes32 battleKey = _startBattle(validatorToUse, engine, defaultOracle, defaultRegistry, matchmaker, address(commitManager)); // Alice sends in mon index 0, Bob sends in the fast mon - _commitRevealExecuteForAliceAndBob(battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(1)); + _commitRevealExecuteForAliceAndBob(battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(1)); // Both players pick move index 0 - _commitRevealExecuteForAliceAndBob(battleKey, 0, 0, uint240(1), uint240(0)); + _commitRevealExecuteForAliceAndBob(battleKey, 0, 0, uint16(1), uint16(0)); // Switch for turn flag should be 0, Bob's active mon index should now be 0 (, BattleData memory state) = engine.getBattle(battleKey); assertEq(state.playerSwitchForTurnFlag, 0); @@ -3139,15 +3139,15 @@ contract EngineTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validatorToUse, engine, defaultOracle, defaultRegistry, matchmaker, address(commitManager)); // Alice and Bob send in their first mon - _commitRevealExecuteForAliceAndBob(battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); // Verify the dummy effect is applied to Alice's mon (EffectInstance[] memory effects, uint256[] memory indices) = engine.getEffects(battleKey, 1, 0); assertEq(effects.length, 1); // Alice uses the edit effect attack to change the extra data to 69 on Bob - // Pack extraData: lower 80 bits = targetIndex (1), next 80 bits = monIndex (0), upper 80 bits = effectIndex - uint240 editExtraData = uint240(1 | (0 << 80) | (indices[0] << 160)); + // Pack extraData: bits 0..1 = targetIndex (1), bits 2..5 = monIndex (0), bits 6..15 = effectIndex + uint16 editExtraData = uint16(uint256(1) | (uint256(0) << 2) | (uint256(indices[0]) << 6)); _commitRevealExecuteForAliceAndBob(battleKey, 0, NO_OP_MOVE_INDEX, editExtraData, 0); (effects, ) = engine.getEffects(battleKey, 1, 0); assertEq(effects[0].data, bytes32(uint256(69))); diff --git a/test/GachaTest.sol b/test/GachaTest.sol index ba3af067..87abb7ae 100644 --- a/test/GachaTest.sol +++ b/test/GachaTest.sol @@ -10,7 +10,6 @@ import {DefaultValidator} from "../src/DefaultValidator.sol"; import {GachaRegistry} from "../src/gacha/GachaRegistry.sol"; import {DefaultRandomnessOracle} from "../src/rng/DefaultRandomnessOracle.sol"; -import {IGachaRNG} from "../src/rng/IGachaRNG.sol"; import {DefaultMonRegistry} from "../src/teams/DefaultMonRegistry.sol"; import {BattleHelper} from "./abstract/BattleHelper.sol"; @@ -125,7 +124,7 @@ contract GachaTest is Test, BattleHelper { // Alice commits switching to mon index 0 vm.startPrank(ALICE); - commitManager.commitMove(battleKey, keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(""), uint240(0)))); + commitManager.commitMove(battleKey, keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(""), uint16(0)))); // Alice wins the battle (inactivity for Bob), we skip ahead mockRNG.setRNG(1); // No extra bonus for points @@ -197,7 +196,7 @@ contract GachaTest is Test, BattleHelper { // Alice commits switching to mon index 0 vm.startPrank(ALICE); - commitManager.commitMove(battleKey, keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(""), uint240(0)))); + commitManager.commitMove(battleKey, keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(""), uint16(0)))); // Alice wins the battle engine.end(battleKey); @@ -295,7 +294,7 @@ contract GachaTest is Test, BattleHelper { _startBattle(validator, engine, defaultOracle, defaultRegistry, matchmaker, hooks, address(commitManager)); vm.warp(vm.getBlockTimestamp() + 1); vm.startPrank(ALICE); - commitManager.commitMove(battleKey, keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(""), uint240(0)))); + commitManager.commitMove(battleKey, keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(""), uint16(0)))); vm.stopPrank(); mockRNG.setRNG(1); engine.end(battleKey); @@ -318,7 +317,7 @@ contract GachaTest is Test, BattleHelper { _startBattle(validator, engine, defaultOracle, defaultRegistry, matchmaker, hooks, address(commitManager)); vm.warp(vm.getBlockTimestamp() + 1); vm.startPrank(ALICE); - commitManager.commitMove(battleKey, keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(""), uint240(0)))); + commitManager.commitMove(battleKey, keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(""), uint16(0)))); vm.stopPrank(); mockRNG.setRNG(1); engine.end(battleKey); diff --git a/test/InlineAbilityParityTest.sol b/test/InlineAbilityParityTest.sol index 7320fff7..0bbf6940 100644 --- a/test/InlineAbilityParityTest.sol +++ b/test/InlineAbilityParityTest.sol @@ -117,7 +117,7 @@ contract InlineAbilityParityTest is Test, BattleHelper { // Turn 0: both switch in _commitRevealExecuteForAliceAndBob( engine, commitManager, battleKey, - SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); assertTrue(_hasEffect(battleKey, 0, 0, address(singletonAbility)), "p0 should have effect"); @@ -139,18 +139,18 @@ contract InlineAbilityParityTest is Test, BattleHelper { // Turn 0: switch in mon 0 _commitRevealExecuteForAliceAndBob( engine, commitManager, battleKey, - SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); assertEq(_countEffect(battleKey, 0, 0, address(singletonAbility)), 1, "should have 1 effect"); // Switch to mon 1 then back to mon 0 _commitRevealExecuteForAliceAndBob( engine, commitManager, battleKey, - SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(1), 0 + SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(1), 0 ); _commitRevealExecuteForAliceAndBob( engine, commitManager, battleKey, - SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(0), 0 + SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(0), 0 ); assertEq(_countEffect(battleKey, 0, 0, address(singletonAbility)), 1, "should still have exactly 1 effect"); @@ -170,7 +170,7 @@ contract InlineAbilityParityTest is Test, BattleHelper { // Turn 0 _commitRevealExecuteForAliceAndBob( engine, commitManager, battleKey, - SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Execute 2 attack turns — AfterDamage should increment counter each time @@ -205,7 +205,7 @@ contract InlineAbilityParityTest is Test, BattleHelper { // Turn 0 _commitRevealExecuteForAliceAndBob( engine, commitManager, battleKey, - SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // EffectAbility adds dummyEffect — verify it's registered @@ -233,14 +233,14 @@ contract InlineAbilityParityTest is Test, BattleHelper { // Turn 0: switch in mon 0 (inline) _commitRevealExecuteForAliceAndBob( engine, commitManager, battleKey, - SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); assertTrue(_hasEffect(battleKey, 0, 0, address(singletonAbility)), "alice mon 0 should have inline effect"); // Alice switches to mon 1 (external) _commitRevealExecuteForAliceAndBob( engine, commitManager, battleKey, - SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(1), 0 + SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(1), 0 ); assertTrue(_hasEffect(battleKey, 0, 1, address(dummyEffect)), "alice mon 1 should have external effect"); } @@ -260,7 +260,7 @@ contract InlineAbilityParityTest is Test, BattleHelper { _commitRevealExecuteForAliceAndBob( engine, commitManager, battleKey, - SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); assertTrue(_hasEffect(battleKey, 0, 0, address(singletonAbility)), "p0 should have inline effect"); diff --git a/test/InlineEngineGasTest.sol b/test/InlineEngineGasTest.sol index 579b6e95..5d61f5d3 100644 --- a/test/InlineEngineGasTest.sol +++ b/test/InlineEngineGasTest.sol @@ -10,12 +10,11 @@ import "../src/Structs.sol"; import {DefaultRuleset} from "../src/DefaultRuleset.sol"; import {DefaultCommitManager} from "../src/commit-manager/DefaultCommitManager.sol"; -import {SignedCommitLib} from "../src/commit-manager/SignedCommitLib.sol"; import {SignedCommitManager} from "../src/commit-manager/SignedCommitManager.sol"; import {Engine} from "../src/Engine.sol"; import {IEngine} from "../src/IEngine.sol"; import {IValidator} from "../src/IValidator.sol"; -import {EIP712} from "../src/lib/EIP712.sol"; +import {SignedCommitHelper} from "./abstract/SignedCommitHelper.sol"; import {IEffect} from "../src/effects/IEffect.sol"; import {StaminaRegen} from "../src/effects/StaminaRegen.sol"; @@ -60,10 +59,6 @@ contract InlineEngineGasTest is Test, BattleHelper { uint256 constant MONS_PER_TEAM = 4; uint256 constant MOVES_PER_MON = 4; - function _packStatBoost(uint256 playerIndex, uint256 monIndex, uint256 statIndex, int32 boostAmount) internal pure returns (uint240) { - return uint240(playerIndex | (monIndex << 60) | (statIndex << 120) | (uint256(uint32(boostAmount)) << 180)); - } - function setUp() public { defaultOracle = new DefaultRandomnessOracle(); // Create engine with inline validation defaults @@ -162,26 +157,26 @@ contract InlineEngineGasTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("FirstBattle"); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 1, 0, 0); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 2, uint240(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 2, uint16(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 2, 3, _packStatBoost(0, 1, uint256(MonStateIndexName.Attack), int32(90)), 0); vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0), true); engine.resetCallContext(); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 2, NO_OP_MOVE_INDEX, _packStatBoost(0, 0, uint256(MonStateIndexName.Attack), int32(90)), 0); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 3, NO_OP_MOVE_INDEX, 0, 0); vm.startPrank(BOB); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 2, 0, _packStatBoost(1, 1, uint256(MonStateIndexName.Attack), int32(90))); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 3, 0, 0); vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(2), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(2), true); engine.resetCallContext(); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 3, 0, 0); vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(3), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(3), true); engine.resetCallContext(); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 3, 0, 0); uint256 firstBattleGas = vm.stopSnapshotGas("FirstBattle"); @@ -209,27 +204,27 @@ contract InlineEngineGasTest is Test, BattleHelper { console.log("After setup 2 - packedP1EffectsCount:", cfgAfterSetup2.packedP1EffectsCount); vm.startSnapshotGas("SecondBattle"); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 3, 1, _packStatBoost(0, 0, uint256(MonStateIndexName.Attack), int32(90)), 0); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); vm.startPrank(BOB); - commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, 2, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, 2, uint16(1), 0); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 3, NO_OP_MOVE_INDEX, _packStatBoost(0, 1, uint256(MonStateIndexName.Attack), int32(90)), 0); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); vm.startPrank(BOB); - commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint240(2), true); + commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint16(2), true); engine.resetCallContext(); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, NO_OP_MOVE_INDEX, 3, 0, _packStatBoost(1, 2, uint256(MonStateIndexName.Attack), int32(90))); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, NO_OP_MOVE_INDEX, 0, 0, 0); vm.startPrank(ALICE); - commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint240(2), true); + commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint16(2), true); engine.resetCallContext(); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 3, 3, _packStatBoost(0, 2, uint256(MonStateIndexName.Attack), int32(90)), _packStatBoost(1, 2, uint256(MonStateIndexName.Attack), int32(90))); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); vm.startPrank(BOB); - commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint240(3), true); + commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint16(3), true); engine.resetCallContext(); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); uint256 secondBattleGas = vm.stopSnapshotGas("SecondBattle"); @@ -252,26 +247,26 @@ contract InlineEngineGasTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("ThirdBattle"); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, 0, 1, 0, 0); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, SWITCH_MOVE_INDEX, 2, uint240(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, SWITCH_MOVE_INDEX, 2, uint16(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, 2, 3, _packStatBoost(0, 1, uint256(MonStateIndexName.Attack), int32(90)), 0); vm.startPrank(ALICE); - commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint240(0), true); + commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint16(0), true); engine.resetCallContext(); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, 2, NO_OP_MOVE_INDEX, _packStatBoost(0, 0, uint256(MonStateIndexName.Attack), int32(90)), 0); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, 3, NO_OP_MOVE_INDEX, 0, 0); vm.startPrank(BOB); - commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, NO_OP_MOVE_INDEX, 2, 0, _packStatBoost(1, 1, uint256(MonStateIndexName.Attack), int32(90))); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, NO_OP_MOVE_INDEX, 3, 0, 0); vm.startPrank(ALICE); - commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint240(2), true); + commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint16(2), true); engine.resetCallContext(); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, NO_OP_MOVE_INDEX, 3, 0, 0); vm.startPrank(ALICE); - commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint240(3), true); + commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint16(3), true); engine.resetCallContext(); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, NO_OP_MOVE_INDEX, 3, 0, 0); uint256 thirdBattleGas = vm.stopSnapshotGas("ThirdBattle"); @@ -338,7 +333,7 @@ contract InlineEngineGasTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("Battle1_Execute"); - _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey1, 0, 0, 0, 0); uint256 execute1 = vm.stopSnapshotGas("Battle1_Execute"); @@ -350,7 +345,7 @@ contract InlineEngineGasTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("Battle2_Execute"); - _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey2, 0, 0, 0, 0); uint256 execute2 = vm.stopSnapshotGas("Battle2_Execute"); @@ -412,7 +407,7 @@ contract InlineEngineGasTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("B1_Execute"); - _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); (BattleConfigView memory cfgAfterSwitch,) = inlineEngine.getBattle(battleKey1); console.log("After B1 switch - globalEffectsLength:", cfgAfterSwitch.globalEffectsLength); @@ -445,7 +440,7 @@ contract InlineEngineGasTest is Test, BattleHelper { console.log("After B2 setup - packedP1EffectsCount:", cfg2.packedP1EffectsCount); vm.startSnapshotGas("B2_Execute"); - _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey2, 0, 0, 0, 0); _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey2, 1, 1, 0, 0); uint256 execute2 = vm.stopSnapshotGas("B2_Execute"); @@ -525,10 +520,10 @@ contract InlineEngineGasTest is Test, BattleHelper { bytes32 battleKey, uint8 aliceMoveIndex, uint8 bobMoveIndex, - uint240 aliceExtraData, - uint240 bobExtraData + uint16 aliceExtraData, + uint16 bobExtraData ) internal { - bytes32 salt = ""; + uint104 salt = 0; bytes32 aliceMoveHash = keccak256(abi.encodePacked(aliceMoveIndex, salt, aliceExtraData)); bytes32 bobMoveHash = keccak256(abi.encodePacked(bobMoveIndex, salt, bobExtraData)); uint256 turnId = eng.getTurnIdForBattleState(battleKey); @@ -563,7 +558,7 @@ contract InlineEngineGasTest is Test, BattleHelper { /// SignedMatchmaker (no propose/accept/confirm storage), and /// SignedCommitManager::executeWithDualSignedMoves (1 TX per two-player turn). /// @dev Forced single-player switches after KOs use SignedCommitManager::executeSinglePlayerMove. -contract FullyOptimizedInlineGasTest is Test, BattleHelper, EIP712 { +contract FullyOptimizedInlineGasTest is BattleHelper, SignedCommitHelper { uint256 constant MONS_PER_TEAM = 4; uint256 constant MOVES_PER_MON = 4; @@ -584,14 +579,6 @@ contract FullyOptimizedInlineGasTest is Test, BattleHelper, EIP712 { mapping(bytes32 => bool) private _seenSlot; bytes32[] private _seenKeys; - function _domainNameAndVersion() internal pure override returns (string memory, string memory) { - return ("SignedCommitManager", "1"); - } - - function _packStatBoost(uint256 playerIndex, uint256 monIndex, uint256 statIndex, int32 boostAmount) internal pure returns (uint240) { - return uint240(playerIndex | (monIndex << 60) | (statIndex << 120) | (uint256(uint32(boostAmount)) << 180)); - } - function setUp() public { p0 = vm.addr(P0_PK); p1 = vm.addr(P1_PK); @@ -645,39 +632,6 @@ contract FullyOptimizedInlineGasTest is Test, BattleHelper, EIP712 { return battleKey; } - function _signDualReveal( - uint256 privateKey, - bytes32 battleKey, - uint64 turnId, - bytes32 committerMoveHash, - uint8 revealerMoveIndex, - bytes32 revealerSalt, - uint240 revealerExtraData - ) internal view returns (bytes memory) { - bytes32 domainSeparator = keccak256( - abi.encode( - _DOMAIN_TYPEHASH, - keccak256("SignedCommitManager"), - keccak256("1"), - block.chainid, - address(signedCommitManager) - ) - ); - bytes32 structHash = SignedCommitLib.hashDualSignedReveal( - SignedCommitLib.DualSignedReveal({ - battleKey: battleKey, - turnId: turnId, - committerMoveHash: committerMoveHash, - revealerMoveIndex: revealerMoveIndex, - revealerSalt: revealerSalt, - revealerExtraData: revealerExtraData - }) - ); - bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); - return abi.encodePacked(r, s, v); - } - /// @dev Executes a two-player turn in 1 TX via executeWithDualSignedMoves. /// p0Move/p1Move semantics match _commitRevealExecuteForAliceAndBob so the /// battle scripts can be transcribed directly from the non-optimized test. @@ -685,17 +639,18 @@ contract FullyOptimizedInlineGasTest is Test, BattleHelper, EIP712 { bytes32 battleKey, uint8 p0MoveIndex, uint8 p1MoveIndex, - uint240 p0ExtraData, - uint240 p1ExtraData + uint16 p0ExtraData, + uint16 p1ExtraData ) internal { uint64 turnId = uint64(engine.getTurnIdForBattleState(battleKey)); - bytes32 committerSalt = keccak256(abi.encode("committer", battleKey, turnId)); - bytes32 revealerSalt = keccak256(abi.encode("revealer", battleKey, turnId)); + uint104 committerSalt = uint104(uint256(keccak256(abi.encode("committer", battleKey, turnId)))); + uint104 revealerSalt = uint104(uint256(keccak256(abi.encode("revealer", battleKey, turnId)))); uint8 committerMoveIndex; - uint240 committerExtraData; + uint16 committerExtraData; uint8 revealerMoveIndex; - uint240 revealerExtraData; + uint16 revealerExtraData; + uint256 committerPk; uint256 revealerPk; address committer; @@ -704,6 +659,7 @@ contract FullyOptimizedInlineGasTest is Test, BattleHelper, EIP712 { committerExtraData = p0ExtraData; revealerMoveIndex = p1MoveIndex; revealerExtraData = p1ExtraData; + committerPk = P0_PK; revealerPk = P1_PK; committer = p0; } else { @@ -711,14 +667,17 @@ contract FullyOptimizedInlineGasTest is Test, BattleHelper, EIP712 { committerExtraData = p1ExtraData; revealerMoveIndex = p0MoveIndex; revealerExtraData = p0ExtraData; + committerPk = P1_PK; revealerPk = P0_PK; committer = p1; } bytes32 committerMoveHash = keccak256(abi.encodePacked(committerMoveIndex, committerSalt, committerExtraData)); + address mgr = address(signedCommitManager); + bytes memory committerSig = _signCommit(mgr, committerPk, committerMoveHash, battleKey, turnId); bytes memory revealerSig = _signDualReveal( - revealerPk, battleKey, turnId, committerMoveHash, + mgr, revealerPk, battleKey, turnId, committerMoveHash, revealerMoveIndex, revealerSalt, revealerExtraData ); @@ -727,16 +686,16 @@ contract FullyOptimizedInlineGasTest is Test, BattleHelper, EIP712 { battleKey, committerMoveIndex, committerSalt, committerExtraData, revealerMoveIndex, revealerSalt, revealerExtraData, - revealerSig + committerSig, revealerSig ); engine.resetCallContext(); } /// @dev Single-player forced switch after a KO. This uses the optimized /// SignedCommitManager path because there is no hidden opponent move to reveal. - function _fastSwitchReveal(bytes32 battleKey, bool isP0, uint240 extraData) internal { + function _fastSwitchReveal(bytes32 battleKey, bool isP0, uint16 extraData) internal { vm.prank(isP0 ? p0 : p1); - signedCommitManager.executeSinglePlayerMove(battleKey, SWITCH_MOVE_INDEX, bytes32(0), extraData); + signedCommitManager.executeSinglePlayerMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), extraData); engine.resetCallContext(); } @@ -768,43 +727,43 @@ contract FullyOptimizedInlineGasTest is Test, BattleHelper, EIP712 { bytes32 oldFlowBattleKey = _startBattleFullyOptimized(ruleset); vm.warp(vm.getBlockTimestamp() + 1); - _fastTurn(oldFlowBattleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); - _fastTurn(oldFlowBattleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0)); + _fastTurn(oldFlowBattleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); + _fastTurn(oldFlowBattleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0)); assertEq(engine.getPlayerSwitchForTurnFlagForBattleState(oldFlowBattleKey), 1); vm.prank(p1); uint256 gasBefore = gasleft(); - signedCommitManager.revealMove(oldFlowBattleKey, SWITCH_MOVE_INDEX, bytes32(0), uint240(1), true); + signedCommitManager.revealMove(oldFlowBattleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1), true); uint256 oldFlowGas = gasBefore - gasleft(); engine.resetCallContext(); - _fastTurn(oldFlowBattleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0)); + _fastTurn(oldFlowBattleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0)); assertEq(engine.getPlayerSwitchForTurnFlagForBattleState(oldFlowBattleKey), 1); vm.prank(p1); gasBefore = gasleft(); - signedCommitManager.revealMove(oldFlowBattleKey, SWITCH_MOVE_INDEX, bytes32(0), uint240(2), true); + signedCommitManager.revealMove(oldFlowBattleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(2), true); uint256 oldFlowSecondGas = gasBefore - gasleft(); engine.resetCallContext(); bytes32 fastPathBattleKey = _startBattleFullyOptimized(ruleset); vm.warp(vm.getBlockTimestamp() + 1); - _fastTurn(fastPathBattleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); - _fastTurn(fastPathBattleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0)); + _fastTurn(fastPathBattleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); + _fastTurn(fastPathBattleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0)); assertEq(engine.getPlayerSwitchForTurnFlagForBattleState(fastPathBattleKey), 1); vm.prank(p1); gasBefore = gasleft(); - signedCommitManager.executeSinglePlayerMove(fastPathBattleKey, SWITCH_MOVE_INDEX, bytes32(0), uint240(1)); + signedCommitManager.executeSinglePlayerMove(fastPathBattleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1)); uint256 fastPathGas = gasBefore - gasleft(); engine.resetCallContext(); - _fastTurn(fastPathBattleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0)); + _fastTurn(fastPathBattleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0)); assertEq(engine.getPlayerSwitchForTurnFlagForBattleState(fastPathBattleKey), 1); vm.prank(p1); gasBefore = gasleft(); - signedCommitManager.executeSinglePlayerMove(fastPathBattleKey, SWITCH_MOVE_INDEX, bytes32(0), uint240(2)); + signedCommitManager.executeSinglePlayerMove(fastPathBattleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(2)); uint256 fastPathSecondGas = gasBefore - gasleft(); engine.resetCallContext(); @@ -857,19 +816,19 @@ contract FullyOptimizedInlineGasTest is Test, BattleHelper, EIP712 { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("Fast_Battle1"); - _fastTurn(battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _fastTurn(battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _fastTurn(battleKey, 0, 1, 0, 0); - _fastTurn(battleKey, SWITCH_MOVE_INDEX, 2, uint240(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); + _fastTurn(battleKey, SWITCH_MOVE_INDEX, 2, uint16(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); _fastTurn(battleKey, 2, 3, _packStatBoost(0, 1, uint256(MonStateIndexName.Attack), int32(90)), 0); - _fastSwitchReveal(battleKey, true, uint240(0)); + _fastSwitchReveal(battleKey, true, uint16(0)); _fastTurn(battleKey, 2, NO_OP_MOVE_INDEX, _packStatBoost(0, 0, uint256(MonStateIndexName.Attack), int32(90)), 0); _fastTurn(battleKey, 3, NO_OP_MOVE_INDEX, 0, 0); - _fastSwitchReveal(battleKey, false, uint240(1)); + _fastSwitchReveal(battleKey, false, uint16(1)); _fastTurn(battleKey, NO_OP_MOVE_INDEX, 2, 0, _packStatBoost(1, 1, uint256(MonStateIndexName.Attack), int32(90))); _fastTurn(battleKey, NO_OP_MOVE_INDEX, 3, 0, 0); - _fastSwitchReveal(battleKey, true, uint240(2)); + _fastSwitchReveal(battleKey, true, uint16(2)); _fastTurn(battleKey, NO_OP_MOVE_INDEX, 3, 0, 0); - _fastSwitchReveal(battleKey, true, uint240(3)); + _fastSwitchReveal(battleKey, true, uint16(3)); _fastTurn(battleKey, NO_OP_MOVE_INDEX, 3, 0, 0); uint256 firstBattleGas = vm.stopSnapshotGas("Fast_Battle1"); @@ -891,20 +850,20 @@ contract FullyOptimizedInlineGasTest is Test, BattleHelper, EIP712 { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("Fast_Battle2"); - _fastTurn(battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _fastTurn(battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _fastTurn(battleKey2, 3, 1, _packStatBoost(0, 0, uint256(MonStateIndexName.Attack), int32(90)), 0); _fastTurn(battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); - _fastSwitchReveal(battleKey2, false, uint240(1)); - _fastTurn(battleKey2, SWITCH_MOVE_INDEX, 2, uint240(1), 0); + _fastSwitchReveal(battleKey2, false, uint16(1)); + _fastTurn(battleKey2, SWITCH_MOVE_INDEX, 2, uint16(1), 0); _fastTurn(battleKey2, 3, NO_OP_MOVE_INDEX, _packStatBoost(0, 1, uint256(MonStateIndexName.Attack), int32(90)), 0); _fastTurn(battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); - _fastSwitchReveal(battleKey2, false, uint240(2)); + _fastSwitchReveal(battleKey2, false, uint16(2)); _fastTurn(battleKey2, NO_OP_MOVE_INDEX, 3, 0, _packStatBoost(1, 2, uint256(MonStateIndexName.Attack), int32(90))); _fastTurn(battleKey2, NO_OP_MOVE_INDEX, 0, 0, 0); - _fastSwitchReveal(battleKey2, true, uint240(2)); + _fastSwitchReveal(battleKey2, true, uint16(2)); _fastTurn(battleKey2, 3, 3, _packStatBoost(0, 2, uint256(MonStateIndexName.Attack), int32(90)), _packStatBoost(1, 2, uint256(MonStateIndexName.Attack), int32(90))); _fastTurn(battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); - _fastSwitchReveal(battleKey2, false, uint240(3)); + _fastSwitchReveal(battleKey2, false, uint16(3)); _fastTurn(battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); uint256 secondBattleGas = vm.stopSnapshotGas("Fast_Battle2"); @@ -926,19 +885,19 @@ contract FullyOptimizedInlineGasTest is Test, BattleHelper, EIP712 { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("Fast_Battle3"); - _fastTurn(battleKey3, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _fastTurn(battleKey3, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _fastTurn(battleKey3, 0, 1, 0, 0); - _fastTurn(battleKey3, SWITCH_MOVE_INDEX, 2, uint240(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); + _fastTurn(battleKey3, SWITCH_MOVE_INDEX, 2, uint16(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); _fastTurn(battleKey3, 2, 3, _packStatBoost(0, 1, uint256(MonStateIndexName.Attack), int32(90)), 0); - _fastSwitchReveal(battleKey3, true, uint240(0)); + _fastSwitchReveal(battleKey3, true, uint16(0)); _fastTurn(battleKey3, 2, NO_OP_MOVE_INDEX, _packStatBoost(0, 0, uint256(MonStateIndexName.Attack), int32(90)), 0); _fastTurn(battleKey3, 3, NO_OP_MOVE_INDEX, 0, 0); - _fastSwitchReveal(battleKey3, false, uint240(1)); + _fastSwitchReveal(battleKey3, false, uint16(1)); _fastTurn(battleKey3, NO_OP_MOVE_INDEX, 2, 0, _packStatBoost(1, 1, uint256(MonStateIndexName.Attack), int32(90))); _fastTurn(battleKey3, NO_OP_MOVE_INDEX, 3, 0, 0); - _fastSwitchReveal(battleKey3, true, uint240(2)); + _fastSwitchReveal(battleKey3, true, uint16(2)); _fastTurn(battleKey3, NO_OP_MOVE_INDEX, 3, 0, 0); - _fastSwitchReveal(battleKey3, true, uint240(3)); + _fastSwitchReveal(battleKey3, true, uint16(3)); _fastTurn(battleKey3, NO_OP_MOVE_INDEX, 3, 0, 0); uint256 thirdBattleGas = vm.stopSnapshotGas("Fast_Battle3"); diff --git a/test/InlineMoveParityTest.sol b/test/InlineMoveParityTest.sol index 019395f0..7ffaeaf4 100644 --- a/test/InlineMoveParityTest.sol +++ b/test/InlineMoveParityTest.sol @@ -111,9 +111,9 @@ contract InlineMoveParityTest is Test, BattleHelper { } function _doSwitchTurn(bytes32 battleKey) internal { - bytes32 salt = ""; + uint104 salt = 0; uint256 turnId = engine.getTurnIdForBattleState(battleKey); - bytes32 moveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); + bytes32 moveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint16(0))); if (turnId % 2 == 0) { vm.startPrank(ALICE); commitManager.commitMove(battleKey, moveHash); @@ -134,10 +134,10 @@ contract InlineMoveParityTest is Test, BattleHelper { } function _doAttackTurn(bytes32 battleKey, uint8 aliceMove, uint8 bobMove) internal { - bytes32 salt = ""; + uint104 salt = 0; uint256 turnId = engine.getTurnIdForBattleState(battleKey); if (turnId % 2 == 0) { - bytes32 moveHash = keccak256(abi.encodePacked(aliceMove, salt, uint240(0))); + bytes32 moveHash = keccak256(abi.encodePacked(aliceMove, salt, uint16(0))); vm.startPrank(ALICE); commitManager.commitMove(battleKey, moveHash); vm.startPrank(BOB); @@ -145,7 +145,7 @@ contract InlineMoveParityTest is Test, BattleHelper { vm.startPrank(ALICE); commitManager.revealMove(battleKey, aliceMove, salt, 0, true); } else { - bytes32 moveHash = keccak256(abi.encodePacked(bobMove, salt, uint240(0))); + bytes32 moveHash = keccak256(abi.encodePacked(bobMove, salt, uint16(0))); vm.startPrank(BOB); commitManager.commitMove(battleKey, moveHash); vm.startPrank(ALICE); diff --git a/test/InlineValidationTest.sol b/test/InlineValidationTest.sol index 823bd40c..229f9083 100644 --- a/test/InlineValidationTest.sol +++ b/test/InlineValidationTest.sol @@ -125,9 +125,9 @@ contract InlineValidationTest is Test, BattleHelper { bytes32 battleKey = _startBattleWithInlineValidation(); // Both players switch in mon 0 - bytes32 salt = ""; - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); - bytes32 p1MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); + uint104 salt = 0; + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint16(0))); + bytes32 p1MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint16(0))); vm.startPrank(p0); commitManager.commitMove(battleKey, p0MoveHash); @@ -150,8 +150,8 @@ contract InlineValidationTest is Test, BattleHelper { bytes32 battleKey = _startBattleWithInlineValidation(); // Both players switch in mon 0 - bytes32 salt = ""; - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); + uint104 salt = 0; + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint16(0))); vm.startPrank(p0); commitManager.commitMove(battleKey, p0MoveHash); @@ -161,7 +161,7 @@ contract InlineValidationTest is Test, BattleHelper { commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, 0, true); engine.resetCallContext(); // Now use move 0 (attack) - bytes32 p0AttackHash = keccak256(abi.encodePacked(uint8(0), salt, uint240(0))); + bytes32 p0AttackHash = keccak256(abi.encodePacked(uint8(0), salt, uint16(0))); vm.startPrank(p1); commitManager.commitMove(battleKey, p0AttackHash); @@ -180,8 +180,8 @@ contract InlineValidationTest is Test, BattleHelper { bytes32 battleKey = _startBattleWithInlineValidation(); // Both players switch in mon 0 - bytes32 salt = ""; - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); + uint104 salt = 0; + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint16(0))); vm.startPrank(p0); commitManager.commitMove(battleKey, p0MoveHash); @@ -192,7 +192,7 @@ contract InlineValidationTest is Test, BattleHelper { engine.resetCallContext(); // P1 commits turn 1 - try to switch to mon 0 again (invalid - already active) // The inline validation should treat this as invalid and fall through - bytes32 p1InvalidSwitchHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); + bytes32 p1InvalidSwitchHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint16(0))); vm.startPrank(p1); commitManager.commitMove(battleKey, p1InvalidSwitchHash); @@ -213,10 +213,10 @@ contract InlineValidationTest is Test, BattleHelper { /// @notice Test multiple turns with inline validation function test_inlineValidation_multipleRounds() public { bytes32 battleKey = _startBattleWithInlineValidation(); - bytes32 salt = ""; + uint104 salt = 0; // Turn 0: Both switch in mon 0 - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint16(0))); vm.startPrank(p0); commitManager.commitMove(battleKey, p0MoveHash); vm.startPrank(p1); @@ -233,7 +233,7 @@ contract InlineValidationTest is Test, BattleHelper { if (bd.playerSwitchForTurnFlag != 2) break; uint256 turnId = engine.getTurnIdForBattleState(battleKey); - bytes32 attackHash = keccak256(abi.encodePacked(uint8(0), salt, uint240(0))); + bytes32 attackHash = keccak256(abi.encodePacked(uint8(0), salt, uint16(0))); if (turnId % 2 == 0) { vm.startPrank(p0); @@ -290,8 +290,8 @@ contract InlineValidationTest is Test, BattleHelper { bytes32 battleKey = _startBattleWithInlineValidation(); // Complete turn 0 switches - bytes32 salt = ""; - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); + uint104 salt = 0; + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint16(0))); vm.startPrank(p0); commitManager.commitMove(battleKey, p0MoveHash); vm.startPrank(p1); @@ -321,8 +321,8 @@ contract InlineValidationTest is Test, BattleHelper { bytes32 battleKey = _startBattleWithInlineValidation(); // Complete turn 0 switches - bytes32 salt = ""; - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); + uint104 salt = 0; + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint16(0))); vm.startPrank(p0); commitManager.commitMove(battleKey, p0MoveHash); vm.startPrank(p1); @@ -419,8 +419,8 @@ contract InlineValidationTest is Test, BattleHelper { (bytes32 battleKey, DefaultValidator externalValidator) = _startBattleWithExternalValidator(); // Complete turn 0 switches - bytes32 salt = ""; - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); + uint104 salt = 0; + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint16(0))); vm.startPrank(p0); commitManager.commitMove(battleKey, p0MoveHash); vm.startPrank(p1); @@ -488,7 +488,7 @@ contract InlineValidationTest is Test, BattleHelper { // P0 selects mon 0, CPU will randomly select (mockRNG returns 0, so mon 0) mockRNG.setRNG(0); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), 0); // Verify both players switched in assertEq(engine.getActiveMonIndexForBattleState(battleKey)[0], 0, "P0 should have mon 0 active"); @@ -504,7 +504,7 @@ contract InlineValidationTest is Test, BattleHelper { // P0 uses attack, CPU will use attack (mockRNG selects index 1 which is the move) mockRNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); // Battle should have advanced to turn 2 uint256 turnId = engine.getTurnIdForBattleState(battleKey); diff --git a/test/MatchmakerTest.sol b/test/MatchmakerTest.sol index 8678845f..54892be9 100644 --- a/test/MatchmakerTest.sol +++ b/test/MatchmakerTest.sol @@ -393,7 +393,7 @@ contract MatchmakerTest is Test, BattleHelper { assertEq(battleData.p1, BOB); // Check that Alice and Bob can commit/reveal/reveal to switch to mon index 0 - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); } function test_fastBattleSucceedsAndNoSubsequentAccept() public { diff --git a/test/SignedCommitManager.t.sol b/test/SignedCommitManager.t.sol index f9220256..843774e9 100644 --- a/test/SignedCommitManager.t.sol +++ b/test/SignedCommitManager.t.sol @@ -17,11 +17,10 @@ import {MockRandomnessOracle} from "./mocks/MockRandomnessOracle.sol"; import {TestTeamRegistry} from "./mocks/TestTeamRegistry.sol"; import {DefaultMatchmaker} from "../src/matchmaker/DefaultMatchmaker.sol"; import {BattleHelper} from "./abstract/BattleHelper.sol"; -import {SignedCommitLib} from "../src/commit-manager/SignedCommitLib.sol"; +import {SignedCommitHelper} from "./abstract/SignedCommitHelper.sol"; import {TestMoveFactory} from "./mocks/TestMoveFactory.sol"; -import {EIP712} from "../src/lib/EIP712.sol"; -abstract contract SignedCommitManagerTestBase is Test, BattleHelper, EIP712 { +abstract contract SignedCommitManagerTestBase is BattleHelper, SignedCommitHelper { Engine engine; SignedCommitManager signedCommitManager; MockRandomnessOracle mockOracle; @@ -36,11 +35,6 @@ abstract contract SignedCommitManagerTestBase is Test, BattleHelper, EIP712 { address p0; address p1; - // Required by EIP712 inheritance (only used to access _DOMAIN_TYPEHASH) - function _domainNameAndVersion() internal pure override returns (string memory, string memory) { - return ("SignedCommitManager", "1"); - } - function setUp() public virtual { p0 = vm.addr(P0_PK); p1 = vm.addr(P1_PK); @@ -130,96 +124,19 @@ abstract contract SignedCommitManagerTestBase is Test, BattleHelper, EIP712 { return battleKey; } - /// @dev Signs a DualSignedReveal struct for the revealer - /// @param privateKey The revealer's private key - /// @param battleKey The battle identifier - /// @param turnId The current turn ID - /// @param committerMoveHash The committer's move hash (that revealer signs over) - /// @param revealerMoveIndex The revealer's move index - /// @param revealerSalt The revealer's salt - /// @param revealerExtraData The revealer's extra data - function _signDualReveal( - uint256 privateKey, - bytes32 battleKey, - uint64 turnId, - bytes32 committerMoveHash, - uint8 revealerMoveIndex, - bytes32 revealerSalt, - uint240 revealerExtraData - ) internal view returns (bytes memory) { - bytes32 domainSeparator = keccak256( - abi.encode( - _DOMAIN_TYPEHASH, - keccak256("SignedCommitManager"), - keccak256("1"), - block.chainid, - address(signedCommitManager) - ) - ); - - bytes32 structHash = SignedCommitLib.hashDualSignedReveal( - SignedCommitLib.DualSignedReveal({ - battleKey: battleKey, - turnId: turnId, - committerMoveHash: committerMoveHash, - revealerMoveIndex: revealerMoveIndex, - revealerSalt: revealerSalt, - revealerExtraData: revealerExtraData - }) - ); - bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); - return abi.encodePacked(r, s, v); - } - - /// @dev Signs a SignedCommit struct for the committer - /// @param privateKey The committer's private key - /// @param moveHash The committer's move hash - /// @param battleKey The battle identifier - /// @param turnId The current turn ID - function _signCommit( - uint256 privateKey, - bytes32 moveHash, - bytes32 battleKey, - uint64 turnId - ) internal view returns (bytes memory) { - bytes32 domainSeparator = keccak256( - abi.encode( - _DOMAIN_TYPEHASH, - keccak256("SignedCommitManager"), - keccak256("1"), - block.chainid, - address(signedCommitManager) - ) - ); - - bytes32 structHash = SignedCommitLib.hashSignedCommit( - SignedCommitLib.SignedCommit({ - moveHash: moveHash, - battleKey: battleKey, - turnId: turnId - }) - ); - bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); - return abi.encodePacked(r, s, v); - } - /// @dev Completes a turn using the normal commit-reveal flow. /// Turn 0 uses SWITCH_MOVE_INDEX; subsequent turns use NO_OP_MOVE_INDEX. function _completeTurnNormal(bytes32 battleKey, uint256 turnId) internal { - bytes32 salt = bytes32(turnId + 1); + uint104 salt = uint104(turnId + 1); uint8 moveIndex = turnId == 0 ? SWITCH_MOVE_INDEX : NO_OP_MOVE_INDEX; - bytes32 moveHash = keccak256(abi.encodePacked(moveIndex, salt, uint240(0))); + bytes32 moveHash = keccak256(abi.encodePacked(moveIndex, salt, uint16(0))); if (turnId % 2 == 0) { // p0 commits vm.startPrank(p0); signedCommitManager.commitMove(battleKey, moveHash); vm.startPrank(p1); - signedCommitManager.revealMove(battleKey, moveIndex, bytes32(0), 0, false); + signedCommitManager.revealMove(battleKey, moveIndex, uint104(0), 0, false); vm.startPrank(p0); signedCommitManager.revealMove(battleKey, moveIndex, salt, 0, true); } else { @@ -227,7 +144,7 @@ abstract contract SignedCommitManagerTestBase is Test, BattleHelper, EIP712 { vm.startPrank(p1); signedCommitManager.commitMove(battleKey, moveHash); vm.startPrank(p0); - signedCommitManager.revealMove(battleKey, moveIndex, bytes32(0), 0, false); + signedCommitManager.revealMove(battleKey, moveIndex, uint104(0), 0, false); vm.startPrank(p1); signedCommitManager.revealMove(battleKey, moveIndex, salt, 0, true); } @@ -238,44 +155,31 @@ abstract contract SignedCommitManagerTestBase is Test, BattleHelper, EIP712 { /// @dev Completes a turn using the dual-signed flow (1 TX). /// Turn 0 uses SWITCH_MOVE_INDEX; subsequent turns use NO_OP_MOVE_INDEX. function _completeTurnFast(bytes32 battleKey, uint256 turnId) internal { - bytes32 committerSalt = bytes32(turnId + 1); - bytes32 revealerSalt = bytes32(turnId + 2); + uint104 committerSalt = uint104(turnId + 1); + uint104 revealerSalt = uint104(turnId + 2); uint8 moveIndex = turnId == 0 ? SWITCH_MOVE_INDEX : NO_OP_MOVE_INDEX; - bytes32 committerMoveHash = keccak256(abi.encodePacked(moveIndex, committerSalt, uint240(0))); + bytes32 committerMoveHash = keccak256(abi.encodePacked(moveIndex, committerSalt, uint16(0))); - if (turnId % 2 == 0) { - // p0 is committer, p1 is revealer - bytes memory revealerSignature = _signDualReveal( - P1_PK, battleKey, uint64(turnId), committerMoveHash, moveIndex, revealerSalt, 0 - ); - vm.startPrank(p0); - signedCommitManager.executeWithDualSignedMoves( - battleKey, - moveIndex, - committerSalt, - 0, - moveIndex, - revealerSalt, - 0, - revealerSignature - ); - } else { - // p1 is committer, p0 is revealer - bytes memory revealerSignature = _signDualReveal( - P0_PK, battleKey, uint64(turnId), committerMoveHash, moveIndex, revealerSalt, 0 - ); - vm.startPrank(p1); - signedCommitManager.executeWithDualSignedMoves( - battleKey, - moveIndex, - committerSalt, - 0, - moveIndex, - revealerSalt, - 0, - revealerSignature - ); - } + (uint256 committerPk, uint256 revealerPk) = turnId % 2 == 0 ? (P0_PK, P1_PK) : (P1_PK, P0_PK); + bytes memory committerSignature = + _signCommit(address(signedCommitManager), committerPk, committerMoveHash, battleKey, uint64(turnId)); + bytes memory revealerSignature = _signDualReveal(address(signedCommitManager), + revealerPk, battleKey, uint64(turnId), committerMoveHash, moveIndex, revealerSalt, 0 + ); + + // Caller can be anyone; pick committer for parity with old test setup. + vm.startPrank(turnId % 2 == 0 ? p0 : p1); + signedCommitManager.executeWithDualSignedMoves( + battleKey, + moveIndex, + committerSalt, + 0, + moveIndex, + revealerSalt, + 0, + committerSignature, + revealerSignature + ); vm.stopPrank(); engine.resetCallContext(); } @@ -294,12 +198,13 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { uint64 turnId = 0; // p0 creates commitment hash off-chain - bytes32 p0Salt = bytes32(uint256(1)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint240(0))); + uint104 p0Salt = uint104(1); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); - // p1 signs their move + p0's hash - bytes32 p1Salt = bytes32(uint256(2)); - bytes memory p1Signature = _signDualReveal( + // p0 signs their commitment, p1 signs their move + p0's hash + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, turnId); + uint104 p1Salt = uint104(2); + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), P1_PK, battleKey, turnId, p0MoveHash, SWITCH_MOVE_INDEX, p1Salt, 0 ); @@ -313,6 +218,7 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { SWITCH_MOVE_INDEX, p1Salt, 0, + p0CommitSig, p1Signature ); @@ -333,11 +239,12 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { // Turn 1: p1 is committer, p0 is revealer uint64 turnId = 1; - bytes32 p1Salt = bytes32(uint256(2)); - bytes32 p1MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p1Salt, uint240(0))); + uint104 p1Salt = uint104(2); + bytes32 p1MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p1Salt, uint16(0))); + bytes memory p1CommitSig = _signCommit(address(signedCommitManager), P1_PK, p1MoveHash, battleKey, turnId); - bytes32 p0Salt = bytes32(uint256(3)); - bytes memory p0Signature = _signDualReveal( + uint104 p0Salt = uint104(3); + bytes memory p0Signature = _signDualReveal(address(signedCommitManager), P0_PK, battleKey, turnId, p1MoveHash, NO_OP_MOVE_INDEX, p0Salt, 0 ); @@ -350,6 +257,7 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { NO_OP_MOVE_INDEX, p0Salt, 0, + p1CommitSig, p0Signature ); @@ -418,7 +326,11 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { function test_revert_invalidSignature() public { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); - bytes32 p0Salt = bytes32(uint256(1)); + uint104 p0Salt = uint104(1); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); + + // Valid committer sig, but garbage revealer sig. + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 0); bytes memory invalidSignature = abi.encodePacked(bytes32(uint256(1)), bytes32(uint256(2)), uint8(27)); vm.startPrank(p0); @@ -429,8 +341,9 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { p0Salt, 0, SWITCH_MOVE_INDEX, - bytes32(0), + uint104(0), 0, + p0CommitSig, invalidSignature ); } @@ -438,12 +351,13 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { function test_revert_wrongSigner() public { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); - bytes32 p0Salt = bytes32(uint256(1)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint240(0))); + uint104 p0Salt = uint104(1); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); - // p0 signs instead of p1 (wrong signer - should be revealer p1) - bytes memory wrongSignature = _signDualReveal( - P0_PK, battleKey, 0, p0MoveHash, SWITCH_MOVE_INDEX, bytes32(0), 0 + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 0); + // p0 signs the revealer slot instead of p1 (wrong signer - should be revealer p1) + bytes memory wrongSignature = _signDualReveal(address(signedCommitManager), + P0_PK, battleKey, 0, p0MoveHash, SWITCH_MOVE_INDEX, uint104(0), 0 ); vm.startPrank(p0); @@ -454,8 +368,9 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { p0Salt, 0, SWITCH_MOVE_INDEX, - bytes32(0), + uint104(0), 0, + p0CommitSig, wrongSignature ); } @@ -467,13 +382,14 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { _completeTurnNormal(battleKey, 0); _completeTurnNormal(battleKey, 1); - // On turn 2, p0 is committer again. Try to replay a turn 0 signature. - bytes32 p0Salt = bytes32(uint256(1)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p0Salt, uint240(0))); + // On turn 2, p0 is committer again. Try to replay turn-0 signatures. + uint104 p0Salt = uint104(1); + bytes32 p0MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p0Salt, uint16(0))); - // Create signature for turn 0 (not current turn 2) - bytes memory turn0Signature = _signDualReveal( - P1_PK, battleKey, 0, p0MoveHash, NO_OP_MOVE_INDEX, bytes32(0), 0 + // Both signatures bound to turnId=0, replayed at turnId=2 + bytes memory turn0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 0); + bytes memory turn0Signature = _signDualReveal(address(signedCommitManager), + P1_PK, battleKey, 0, p0MoveHash, NO_OP_MOVE_INDEX, uint104(0), 0 ); vm.startPrank(p0); @@ -484,8 +400,9 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { p0Salt, 0, NO_OP_MOVE_INDEX, - bytes32(0), + uint104(0), 0, + turn0CommitSig, turn0Signature ); } @@ -493,15 +410,16 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { function test_revert_replayAttack_differentBattle() public { bytes32 battleKey1 = _startBattleWith(address(signedCommitManager)); - bytes32 p0Salt = bytes32(uint256(1)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint240(0))); + uint104 p0Salt = uint104(1); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); - // Create signature for battle 1 - bytes memory battle1Signature = _signDualReveal( - P1_PK, battleKey1, 0, p0MoveHash, SWITCH_MOVE_INDEX, bytes32(0), 0 + // Both signatures bound to battle 1 + bytes memory battle1CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey1, 0); + bytes memory battle1Signature = _signDualReveal(address(signedCommitManager), + P1_PK, battleKey1, 0, p0MoveHash, SWITCH_MOVE_INDEX, uint104(0), 0 ); - // Start second battle and try to use battle 1's signature + // Start second battle and try to use battle 1's signatures bytes32 battleKey2 = _startBattleWith(address(signedCommitManager)); vm.startPrank(p0); @@ -512,33 +430,146 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { p0Salt, 0, SWITCH_MOVE_INDEX, - bytes32(0), + uint104(0), 0, + battle1CommitSig, battle1Signature ); } - function test_revert_callerNotCommitter() public { + /// @notice Regression: a revealer alone (without an explicit committer signature) cannot + /// inject a self-chosen committer preimage `P*`. Previously this was blocked only by the + /// `msg.sender == committer` check; now both signatures are mandatory and bind each + /// player independently, so the check holds even under a relayer model. + function test_revert_executeWithDualSigned_unilateralRevealerAttack() public { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); - bytes32 p0Salt = bytes32(uint256(1)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint240(0))); + // Attacker (p1, the revealer for turn 0) picks a preimage P* of their choosing for p0 + uint104 attackerCommitterSalt = uint104(0xdead); + uint16 attackerCommitterExtraData = 0; + uint8 attackerCommitterMoveIndex = SWITCH_MOVE_INDEX; + bytes32 chosenCommitterMoveHash = keccak256( + abi.encodePacked(attackerCommitterMoveIndex, attackerCommitterSalt, attackerCommitterExtraData) + ); - bytes memory p1Signature = _signDualReveal( - P1_PK, battleKey, 0, p0MoveHash, SWITCH_MOVE_INDEX, bytes32(0), 0 + // p1 signs the DualSignedReveal binding themselves to a chosen committer preimage + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), + P1_PK, battleKey, 0, chosenCommitterMoveHash, SWITCH_MOVE_INDEX, uint104(0), 0 ); - // p1 (revealer) tries to call executeWithDualSignedMoves - should fail - vm.startPrank(p1); - vm.expectRevert(SignedCommitManager.CallerNotCommitter.selector); + // Attacker forges a "committer signature" (signed by themselves, P1, over the same hash). + bytes memory forgedCommitterSig = _signCommit(address(signedCommitManager), P1_PK, chosenCommitterMoveHash, battleKey, 0); + + // _startBattleWith leaves an active prank on p0; clear it. + vm.stopPrank(); + + // Submit (from any sender) — committer sig recover will return p1, not p0 → revert. + vm.expectRevert(SignedCommitManager.InvalidSignature.selector); + signedCommitManager.executeWithDualSignedMoves( + battleKey, + attackerCommitterMoveIndex, + attackerCommitterSalt, + attackerCommitterExtraData, + SWITCH_MOVE_INDEX, + uint104(0), + 0, + forgedCommitterSig, + p1Signature + ); + } + + /// @notice Drops the old `msg.sender == committer` check: anyone can submit when both + /// EIP-712 signatures are present and valid (relayer-friendly). + function test_executeWithDualSigned_thirdPartyRelay_succeeds() public { + bytes32 battleKey = _startBattleWith(address(signedCommitManager)); + + uint104 p0Salt = uint104(1); + uint104 p1Salt = uint104(2); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); + + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 0); + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), + P1_PK, battleKey, 0, p0MoveHash, SWITCH_MOVE_INDEX, p1Salt, 0 + ); + + // _startBattleWith leaves an active prank on p0; clear it before pranking the relayer. + vm.stopPrank(); + + // A random third party (neither p0 nor p1) can submit the bundle. + address relayer = address(0xCAFE); + vm.prank(relayer); + signedCommitManager.executeWithDualSignedMoves( + battleKey, + SWITCH_MOVE_INDEX, + p0Salt, + 0, + SWITCH_MOVE_INDEX, + p1Salt, + 0, + p0CommitSig, + p1Signature + ); + + assertEq(engine.getTurnIdForBattleState(battleKey), 1, "Turn should advance via relayer"); + } + + /// @notice Wrong committer signer (sig recovers to revealer's address, not committer's) reverts. + function test_revert_executeWithDualSigned_wrongCommitterSigner() public { + bytes32 battleKey = _startBattleWith(address(signedCommitManager)); + + uint104 p0Salt = uint104(1); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); + + // p1 signs the SignedCommit instead of p0 → recovers to p1, not the committer p0. + bytes memory wrongCommitSig = _signCommit(address(signedCommitManager), P1_PK, p0MoveHash, battleKey, 0); + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), + P1_PK, battleKey, 0, p0MoveHash, SWITCH_MOVE_INDEX, uint104(0), 0 + ); + + vm.startPrank(p0); + vm.expectRevert(SignedCommitManager.InvalidSignature.selector); + signedCommitManager.executeWithDualSignedMoves( + battleKey, + SWITCH_MOVE_INDEX, + p0Salt, + 0, + SWITCH_MOVE_INDEX, + uint104(0), + 0, + wrongCommitSig, + p1Signature + ); + } + + /// @notice Committer signature over a different `moveHash` than the submitted preimage + /// reverts with InvalidSignature (the recovered hash differs from what the engine computes). + function test_revert_executeWithDualSigned_committerSigForWrongHash() public { + bytes32 battleKey = _startBattleWith(address(signedCommitManager)); + + uint104 p0Salt = uint104(1); + bytes32 p0DifferentMoveHash = + keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p0Salt, uint16(0))); // committer signs over a different move + + bytes memory mismatchedCommitSig = _signCommit(address(signedCommitManager), P0_PK, p0DifferentMoveHash, battleKey, 0); + // Revealer signs the same different hash so the revealer side would have validated + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), + P1_PK, battleKey, 0, p0DifferentMoveHash, SWITCH_MOVE_INDEX, uint104(0), 0 + ); + + // p0 submits with their REAL move data (SWITCH_MOVE_INDEX, p0Salt, 0). Engine recomputes + // committerMoveHash from those fields → does not equal `p0DifferentMoveHash`. Committer sig + // recovery against the recomputed hash returns a non-p0 address → InvalidSignature. + vm.startPrank(p0); + vm.expectRevert(SignedCommitManager.InvalidSignature.selector); signedCommitManager.executeWithDualSignedMoves( battleKey, SWITCH_MOVE_INDEX, p0Salt, 0, SWITCH_MOVE_INDEX, - bytes32(0), + uint104(0), 0, + mismatchedCommitSig, p1Signature ); } @@ -550,11 +581,12 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { function test_revert_battleNotStarted() public { bytes32 fakeBattleKey = bytes32(uint256(123)); - bytes32 p0Salt = bytes32(uint256(1)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint240(0))); + uint104 p0Salt = uint104(1); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); - bytes memory p1Signature = _signDualReveal( - P1_PK, fakeBattleKey, 0, p0MoveHash, SWITCH_MOVE_INDEX, bytes32(0), 0 + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, fakeBattleKey, 0); + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), + P1_PK, fakeBattleKey, 0, p0MoveHash, SWITCH_MOVE_INDEX, uint104(0), 0 ); vm.startPrank(p0); @@ -565,8 +597,9 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { p0Salt, 0, SWITCH_MOVE_INDEX, - bytes32(0), + uint104(0), 0, + p0CommitSig, p1Signature ); } @@ -577,14 +610,16 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { // Execute turn 0 with dual-signed flow _completeTurnFast(battleKey, 0); - // After turn 0, we're now on turn 1 where p1 is committer - // Try to replay with a turn 0 signature - fails because: - // 1. Turn has advanced, so signature turnId (0) doesn't match current turnId (1) - bytes32 p1Salt = bytes32(uint256(99)); - bytes32 p1MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p1Salt, uint240(0))); + // After turn 0, we're now on turn 1 where p1 is committer. + // Try to replay with turn-0 signatures - fails because turnId in sigs (0) doesn't + // match current turnId (1). + uint104 p1Salt = uint104(99); + bytes32 p1MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p1Salt, uint16(0))); - bytes memory p0Signature = _signDualReveal( - P0_PK, battleKey, 0, p1MoveHash, NO_OP_MOVE_INDEX, bytes32(0), 0 + // Both signatures are bound to turnId=0 (replay attempt) + bytes memory p1CommitSig = _signCommit(address(signedCommitManager), P1_PK, p1MoveHash, battleKey, 0); + bytes memory p0Signature = _signDualReveal(address(signedCommitManager), + P0_PK, battleKey, 0, p1MoveHash, NO_OP_MOVE_INDEX, uint104(0), 0 ); vm.startPrank(p1); @@ -595,8 +630,9 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { p1Salt, 0, NO_OP_MOVE_INDEX, - bytes32(0), + uint104(0), 0, + p1CommitSig, p0Signature ); } @@ -604,11 +640,12 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { function test_revert_replayPrevented_sameBlockAttempt() public { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); - bytes32 p0Salt = bytes32(uint256(1)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint240(0))); + uint104 p0Salt = uint104(1); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); - bytes memory p1Signature = _signDualReveal( - P1_PK, battleKey, 0, p0MoveHash, SWITCH_MOVE_INDEX, bytes32(0), 0 + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 0); + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), + P1_PK, battleKey, 0, p0MoveHash, SWITCH_MOVE_INDEX, uint104(0), 0 ); vm.startPrank(p0); @@ -618,22 +655,24 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { p0Salt, 0, SWITCH_MOVE_INDEX, - bytes32(0), + uint104(0), 0, + p0CommitSig, p1Signature ); - // After execution, turn advances to 1. On turn 1, p1 is committer, not p0. - // So if p0 tries to call again, it fails with CallerNotCommitter (turn parity changed) - vm.expectRevert(SignedCommitManager.CallerNotCommitter.selector); + // After execution, turn advances to 1. Replaying the same signatures (turnId=0) at + // turnId=1 fails on the committer signature recovery — sig was bound to turn 0. + vm.expectRevert(SignedCommitManager.InvalidSignature.selector); signedCommitManager.executeWithDualSignedMoves( battleKey, SWITCH_MOVE_INDEX, p0Salt, 0, SWITCH_MOVE_INDEX, - bytes32(0), + uint104(0), 0, + p0CommitSig, p1Signature ); } @@ -642,16 +681,20 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); // p0's actual move data - bytes32 p0Salt = bytes32(uint256(1)); + uint104 p0Salt = uint104(1); + bytes32 p0RealMoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); - // p1 signs over a DIFFERENT hash than what p0 will submit - bytes32 fakeP0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(uint256(999)), uint240(0))); + // p0 signs the commitment for the REAL move hash (matches what they'll submit) + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0RealMoveHash, battleKey, 0); - bytes memory p1Signature = _signDualReveal( - P1_PK, battleKey, 0, fakeP0MoveHash, SWITCH_MOVE_INDEX, bytes32(0), 0 + // p1 signs over a DIFFERENT hash than what p0 will submit + bytes32 fakeP0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, uint104(999), uint16(0))); + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), + P1_PK, battleKey, 0, fakeP0MoveHash, SWITCH_MOVE_INDEX, uint104(0), 0 ); - // p0 tries to submit with their real move data, but the hash won't match what p1 signed + // p0 tries to submit with their real move data: committer sig validates (matches + // p0RealMoveHash), but revealer sig was over fakeP0MoveHash → revealer recovery fails. vm.startPrank(p0); vm.expectRevert(SignedCommitManager.InvalidSignature.selector); signedCommitManager.executeWithDualSignedMoves( @@ -660,8 +703,9 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { p0Salt, 0, SWITCH_MOVE_INDEX, - bytes32(0), + uint104(0), 0, + p0CommitSig, p1Signature ); } @@ -669,12 +713,13 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { function test_revert_revealerMoveMismatch() public { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); - bytes32 p0Salt = bytes32(uint256(1)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint240(0))); + uint104 p0Salt = uint104(1); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 0); // p1 signs with SWITCH_MOVE_INDEX - bytes memory p1Signature = _signDualReveal( - P1_PK, battleKey, 0, p0MoveHash, SWITCH_MOVE_INDEX, bytes32(0), 0 + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), + P1_PK, battleKey, 0, p0MoveHash, SWITCH_MOVE_INDEX, uint104(0), 0 ); // p0 tries to submit with different move for p1 (NO_OP instead of SWITCH) @@ -686,8 +731,9 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { p0Salt, 0, NO_OP_MOVE_INDEX, // Different from what p1 signed! - bytes32(0), + uint104(0), 0, + p0CommitSig, p1Signature ); } @@ -700,11 +746,11 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); // Turn 0: p0 is committer, p1 is revealer - bytes32 p0Salt = bytes32(uint256(1)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint240(0))); + uint104 p0Salt = uint104(1); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); // p0 signs their commitment - bytes memory p0CommitSig = _signCommit(P0_PK, p0MoveHash, battleKey, 0); + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 0); // p1 (revealer) publishes p0's commitment on-chain vm.startPrank(p1); @@ -716,7 +762,7 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { assertEq(storedTurnId, 0, "Turn ID not stored correctly"); // Now p1 can reveal normally - signedCommitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, bytes32(0), 0, false); + signedCommitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), 0, false); // p0 reveals to complete the turn vm.startPrank(p0); @@ -733,11 +779,11 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { _completeTurnNormal(battleKey, 0); // Turn 1: p1 is committer, p0 is revealer - bytes32 p1Salt = bytes32(uint256(2)); - bytes32 p1MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p1Salt, uint240(0))); + uint104 p1Salt = uint104(2); + bytes32 p1MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p1Salt, uint16(0))); // p1 signs their commitment - bytes memory p1CommitSig = _signCommit(P1_PK, p1MoveHash, battleKey, 1); + bytes memory p1CommitSig = _signCommit(address(signedCommitManager), P1_PK, p1MoveHash, battleKey, 1); // p0 (revealer) publishes p1's commitment on-chain vm.startPrank(p0); @@ -749,7 +795,7 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { assertEq(storedTurnId, 1, "Turn ID not stored correctly"); // Now p0 can reveal - signedCommitManager.revealMove(battleKey, NO_OP_MOVE_INDEX, bytes32(0), 0, false); + signedCommitManager.revealMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0, false); // p1 reveals to complete the turn vm.startPrank(p1); @@ -761,9 +807,9 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { function test_commitWithSignature_anyoneCanSubmit() public { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); - bytes32 p0Salt = bytes32(uint256(1)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint240(0))); - bytes memory p0CommitSig = _signCommit(P0_PK, p0MoveHash, battleKey, 0); + uint104 p0Salt = uint104(1); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 0); // Even p0 themselves can submit their own signed commitment // (though this is equivalent to just calling commitMove) @@ -778,10 +824,10 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { function test_commitWithSignature_revert_wrongSigner() public { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(uint256(1)), uint240(0))); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, uint104(1), uint16(0))); // p1 signs instead of p0 (wrong signer) - bytes memory wrongSig = _signCommit(P1_PK, p0MoveHash, battleKey, 0); + bytes memory wrongSig = _signCommit(address(signedCommitManager), P1_PK, p0MoveHash, battleKey, 0); vm.startPrank(p1); vm.expectRevert(SignedCommitManager.InvalidSignature.selector); @@ -791,10 +837,10 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { function test_commitWithSignature_revert_wrongTurn() public { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(uint256(1)), uint240(0))); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, uint104(1), uint16(0))); // p0 signs for turn 1 instead of turn 0 - bytes memory wrongTurnSig = _signCommit(P0_PK, p0MoveHash, battleKey, 1); + bytes memory wrongTurnSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 1); vm.startPrank(p1); vm.expectRevert(SignedCommitManager.InvalidSignature.selector); @@ -805,10 +851,10 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { bytes32 battleKey1 = _startBattleWith(address(signedCommitManager)); bytes32 battleKey2 = _startBattleWith(address(signedCommitManager)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(uint256(1)), uint240(0))); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, uint104(1), uint16(0))); // p0 signs for battle 1 - bytes memory battle1Sig = _signCommit(P0_PK, p0MoveHash, battleKey1, 0); + bytes memory battle1Sig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey1, 0); // Try to use on battle 2 vm.startPrank(p1); @@ -819,8 +865,8 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { function test_commitWithSignature_revert_alreadyCommitted() public { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(uint256(1)), uint240(0))); - bytes memory p0CommitSig = _signCommit(P0_PK, p0MoveHash, battleKey, 0); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, uint104(1), uint16(0))); + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 0); // First commit succeeds vm.startPrank(p1); @@ -833,8 +879,8 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { function test_commitWithSignature_revert_battleNotStarted() public { bytes32 fakeBattleKey = bytes32(uint256(123)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(uint256(1)), uint240(0))); - bytes memory p0CommitSig = _signCommit(P0_PK, p0MoveHash, fakeBattleKey, 0); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, uint104(1), uint16(0))); + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, fakeBattleKey, 0); vm.startPrank(p1); vm.expectRevert(DefaultCommitManager.BattleNotYetStarted.selector); @@ -844,14 +890,14 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { function test_commitWithSignature_afterNormalCommit_reverts() public { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(uint256(1)), uint240(0))); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, uint104(1), uint16(0))); // p0 commits normally vm.startPrank(p0); signedCommitManager.commitMove(battleKey, p0MoveHash); // Now trying to commit with signature should fail - bytes memory p0CommitSig = _signCommit(P0_PK, p0MoveHash, battleKey, 0); + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 0); vm.startPrank(p1); vm.expectRevert(DefaultCommitManager.AlreadyCommited.selector); signedCommitManager.commitWithSignature(battleKey, p0MoveHash, p0CommitSig); @@ -873,19 +919,21 @@ contract SignedCommitManagerEngineSafetyTest is SignedCommitManagerTestBase { bytes32 battleKey, uint64 turnId, uint8 committerMoveIndex, - uint240 committerExtraData, + uint16 committerExtraData, uint8 revealerMoveIndex, - uint240 revealerExtraData + uint16 revealerExtraData ) internal { - bytes32 committerSalt = bytes32(uint256(turnId + 1)); - bytes32 revealerSalt = bytes32(uint256(turnId + 2)); + uint104 committerSalt = uint104(turnId + 1); + uint104 revealerSalt = uint104(turnId + 2); bytes32 committerMoveHash = keccak256(abi.encodePacked(committerMoveIndex, committerSalt, committerExtraData)); // Committer is p0 on even turns, p1 on odd turns. - (uint256 revealerPk, address committerAddr) = turnId % 2 == 0 ? (P1_PK, p0) : (P0_PK, p1); + (uint256 committerPk, uint256 revealerPk, address committerAddr) = + turnId % 2 == 0 ? (P0_PK, P1_PK, p0) : (P1_PK, P0_PK, p1); - bytes memory revealerSig = _signDualReveal( + bytes memory committerSig = _signCommit(address(signedCommitManager), committerPk, committerMoveHash, battleKey, turnId); + bytes memory revealerSig = _signDualReveal(address(signedCommitManager), revealerPk, battleKey, turnId, committerMoveHash, revealerMoveIndex, revealerSalt, revealerExtraData ); @@ -898,6 +946,7 @@ contract SignedCommitManagerEngineSafetyTest is SignedCommitManagerTestBase { revealerMoveIndex, revealerSalt, revealerExtraData, + committerSig, revealerSig ); vm.stopPrank(); @@ -911,7 +960,7 @@ contract SignedCommitManagerEngineSafetyTest is SignedCommitManagerTestBase { // p0 sends a valid switch to mon 1, p1 sends NO_OP (would have been rejected at reveal in // the old flow). Engine must force p1 to switch-to-mon-0. - _executeDualSigned(battleKey, 0, SWITCH_MOVE_INDEX, uint240(1), NO_OP_MOVE_INDEX, 0); + _executeDualSigned(battleKey, 0, SWITCH_MOVE_INDEX, uint16(1), NO_OP_MOVE_INDEX, 0); assertEq(engine.getTurnIdForBattleState(battleKey), 1, "turn should advance"); uint256[] memory active = engine.getActiveMonIndexForBattleState(battleKey); @@ -971,7 +1020,7 @@ contract SignedCommitManagerEngineSafetyTest is SignedCommitManagerTestBase { // Turn 1: p1 commits SWITCH with an out-of-bounds target (team size is 2, submit 99). // p0 NO_OPs. p1's active mon should stay at 0 (switch is a no-op), not wrap to 99 & 0xFF. - _executeDualSigned(battleKey, 1, SWITCH_MOVE_INDEX, uint240(99), NO_OP_MOVE_INDEX, 0); + _executeDualSigned(battleKey, 1, SWITCH_MOVE_INDEX, uint16(99), NO_OP_MOVE_INDEX, 0); assertEq(engine.getTurnIdForBattleState(battleKey), 2, "turn should advance"); uint256[] memory active = engine.getActiveMonIndexForBattleState(battleKey); @@ -983,7 +1032,7 @@ contract SignedCommitManagerEngineSafetyTest is SignedCommitManagerTestBase { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); // Turn 0: p0 switches to mon 1, p1 to mon 0. - _executeDualSigned(battleKey, 0, SWITCH_MOVE_INDEX, uint240(1), SWITCH_MOVE_INDEX, 0); + _executeDualSigned(battleKey, 0, SWITCH_MOVE_INDEX, uint16(1), SWITCH_MOVE_INDEX, 0); assertEq(engine.getActiveMonIndexForBattleState(battleKey)[0], 1); // Turn 1: p1 is committer. p1 tries to switch to their own active mon (0). p0 NO_OP. diff --git a/test/SignedCommitManagerGasBenchmark.t.sol b/test/SignedCommitManagerGasBenchmark.t.sol index 811bbc60..06a732c3 100644 --- a/test/SignedCommitManagerGasBenchmark.t.sol +++ b/test/SignedCommitManagerGasBenchmark.t.sol @@ -26,7 +26,7 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { function test_gasBenchmark_normalFlow_cold() public { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(uint256(1)), uint240(0))); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, uint104(1), uint16(0))); vm.startPrank(p0); uint256 gasBefore = gasleft(); @@ -35,12 +35,12 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { vm.startPrank(p1); gasBefore = gasleft(); - signedCommitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, bytes32(0), 0, false); + signedCommitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), 0, false); gasUsed_normalFlow_cold_reveal1 = gasBefore - gasleft(); vm.startPrank(p0); gasBefore = gasleft(); - signedCommitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, bytes32(uint256(1)), 0, true); + signedCommitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(1), 0, true); gasUsed_normalFlow_cold_reveal2 = gasBefore - gasleft(); emit log_named_uint("Normal Flow (Cold) - Commit (Alice)", gasUsed_normalFlow_cold_commit); @@ -55,12 +55,13 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); // Prepare move data - bytes32 p0Salt = bytes32(uint256(1)); - bytes32 p1Salt = bytes32(uint256(2)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint240(0))); + uint104 p0Salt = uint104(1); + uint104 p1Salt = uint104(2); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); - // p1 (revealer) signs their move + p0's hash - bytes memory p1Signature = _signDualReveal( + // Both players sign off-chain + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 0); + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), P1_PK, battleKey, 0, p0MoveHash, SWITCH_MOVE_INDEX, p1Salt, 0 ); @@ -75,6 +76,7 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { SWITCH_MOVE_INDEX, p1Salt, 0, + p0CommitSig, p1Signature ); gasUsed_dualSignedFlow_cold = gasBefore - gasleft(); @@ -90,7 +92,7 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { _completeTurnNormal(battleKey, 1); // Turn 2 (warm storage - p0 commits again) - bytes32 p0MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, bytes32(uint256(100)), uint240(0))); + bytes32 p0MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, uint104(100), uint16(0))); vm.startPrank(p0); uint256 gasBefore = gasleft(); @@ -99,12 +101,12 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { vm.startPrank(p1); gasBefore = gasleft(); - signedCommitManager.revealMove(battleKey, NO_OP_MOVE_INDEX, bytes32(0), 0, false); + signedCommitManager.revealMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0, false); gasUsed_normalFlow_warm_reveal1 = gasBefore - gasleft(); vm.startPrank(p0); gasBefore = gasleft(); - signedCommitManager.revealMove(battleKey, NO_OP_MOVE_INDEX, bytes32(uint256(100)), 0, true); + signedCommitManager.revealMove(battleKey, NO_OP_MOVE_INDEX, uint104(100), 0, true); gasUsed_normalFlow_warm_reveal2 = gasBefore - gasleft(); emit log_named_uint("Normal Flow (Warm) - Commit (Alice)", gasUsed_normalFlow_warm_commit); @@ -122,11 +124,12 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { _completeTurnNormal(battleKey, 1); // Turn 2 with dual-signed flow (warm storage) - bytes32 p0Salt = bytes32(uint256(100)); - bytes32 p1Salt = bytes32(uint256(101)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p0Salt, uint240(0))); + uint104 p0Salt = uint104(100); + uint104 p1Salt = uint104(101); + bytes32 p0MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p0Salt, uint16(0))); - bytes memory p1Signature = _signDualReveal( + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 2); + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), P1_PK, battleKey, 2, p0MoveHash, NO_OP_MOVE_INDEX, p1Salt, 0 ); @@ -140,6 +143,7 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { NO_OP_MOVE_INDEX, p1Salt, 0, + p0CommitSig, p1Signature ); gasUsed_dualSignedFlow_warm = gasBefore - gasleft(); @@ -156,7 +160,7 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { // Normal flow cold (3 TXs) { - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(uint256(1)), uint240(0))); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, uint104(1), uint16(0))); vm.startPrank(p0); uint256 gasBefore = gasleft(); @@ -165,12 +169,12 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { vm.startPrank(p1); gasBefore = gasleft(); - signedCommitManager.revealMove(battleKey1, SWITCH_MOVE_INDEX, bytes32(0), 0, false); + signedCommitManager.revealMove(battleKey1, SWITCH_MOVE_INDEX, uint104(0), 0, false); gasUsed_normalFlow_cold_reveal1 = gasBefore - gasleft(); vm.startPrank(p0); gasBefore = gasleft(); - signedCommitManager.revealMove(battleKey1, SWITCH_MOVE_INDEX, bytes32(uint256(1)), 0, true); + signedCommitManager.revealMove(battleKey1, SWITCH_MOVE_INDEX, uint104(1), 0, true); gasUsed_normalFlow_cold_reveal2 = gasBefore - gasleft(); } @@ -178,11 +182,12 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { // Reset transient first so a stale execute from battleKey1 above doesn't pollute battleKey2's measurement. engine.resetCallContext(); { - bytes32 p0Salt = bytes32(uint256(1)); - bytes32 p1Salt = bytes32(uint256(2)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint240(0))); + uint104 p0Salt = uint104(1); + uint104 p1Salt = uint104(2); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); - bytes memory p1Signature = _signDualReveal( + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey2, 0); + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), P1_PK, battleKey2, 0, p0MoveHash, SWITCH_MOVE_INDEX, p1Salt, 0 ); @@ -196,6 +201,7 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { SWITCH_MOVE_INDEX, p1Salt, 0, + p0CommitSig, p1Signature ); gasUsed_dualSignedFlow_cold = gasBefore - gasleft(); @@ -210,7 +216,7 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { // Normal flow warm (turn 2) { - bytes32 p0MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, bytes32(uint256(100)), uint240(0))); + bytes32 p0MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, uint104(100), uint16(0))); vm.startPrank(p0); uint256 gasBefore = gasleft(); @@ -219,22 +225,23 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { vm.startPrank(p1); gasBefore = gasleft(); - signedCommitManager.revealMove(battleKey1, NO_OP_MOVE_INDEX, bytes32(0), 0, false); + signedCommitManager.revealMove(battleKey1, NO_OP_MOVE_INDEX, uint104(0), 0, false); gasUsed_normalFlow_warm_reveal1 = gasBefore - gasleft(); vm.startPrank(p0); gasBefore = gasleft(); - signedCommitManager.revealMove(battleKey1, NO_OP_MOVE_INDEX, bytes32(uint256(100)), 0, true); + signedCommitManager.revealMove(battleKey1, NO_OP_MOVE_INDEX, uint104(100), 0, true); gasUsed_normalFlow_warm_reveal2 = gasBefore - gasleft(); } // Dual-signed flow warm (turn 2) { - bytes32 p0Salt = bytes32(uint256(100)); - bytes32 p1Salt = bytes32(uint256(101)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p0Salt, uint240(0))); + uint104 p0Salt = uint104(100); + uint104 p1Salt = uint104(101); + bytes32 p0MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p0Salt, uint16(0))); - bytes memory p1Signature = _signDualReveal( + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey2, 2); + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), P1_PK, battleKey2, 2, p0MoveHash, NO_OP_MOVE_INDEX, p1Salt, 0 ); @@ -248,6 +255,7 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { NO_OP_MOVE_INDEX, p1Salt, 0, + p0CommitSig, p1Signature ); gasUsed_dualSignedFlow_warm = gasBefore - gasleft(); diff --git a/test/StandardAttackPvPGasTest.sol b/test/StandardAttackPvPGasTest.sol index 27fda2a1..cb1e91e9 100644 --- a/test/StandardAttackPvPGasTest.sol +++ b/test/StandardAttackPvPGasTest.sol @@ -9,12 +9,11 @@ import "../src/Structs.sol"; import {Engine} from "../src/Engine.sol"; import {SignedCommitManager} from "../src/commit-manager/SignedCommitManager.sol"; -import {SignedCommitLib} from "../src/commit-manager/SignedCommitLib.sol"; import {SignedMatchmaker} from "../src/matchmaker/SignedMatchmaker.sol"; import {BattleOfferLib} from "../src/matchmaker/BattleOfferLib.sol"; import {StandardAttackFactory} from "../src/moves/StandardAttackFactory.sol"; import {ATTACK_PARAMS} from "../src/moves/StandardAttackStructs.sol"; -import {EIP712} from "../src/lib/EIP712.sol"; +import {SignedCommitHelper} from "./abstract/SignedCommitHelper.sol"; import {IEngine} from "../src/IEngine.sol"; import {IEngineHook} from "../src/IEngineHook.sol"; @@ -38,7 +37,7 @@ import {TestTeamRegistry} from "./mocks/TestTeamRegistry.sol"; /// Existing PvP benchmarks (FullyOptimizedInlineGasTest) use CustomAttack / /// EffectAttack / StatBoostsMove — none of which extend StandardAttack — so the /// StandardAttack hot path doesn't show up there. -contract StandardAttackPvPGasTest is Test, EIP712 { +contract StandardAttackPvPGasTest is SignedCommitHelper { uint256 constant MONS_PER_TEAM = 4; uint256 constant MOVES_PER_MON = 4; @@ -55,10 +54,6 @@ contract StandardAttackPvPGasTest is Test, EIP712 { TestTeamRegistry defaultRegistry; StandardAttackFactory attackFactory; - function _domainNameAndVersion() internal pure override returns (string memory, string memory) { - return ("SignedCommitManager", "1"); - } - function setUp() public { p0 = vm.addr(P0_PK); p1 = vm.addr(P1_PK); @@ -116,54 +111,22 @@ contract StandardAttackPvPGasTest is Test, EIP712 { return battleKey; } - function _signDualReveal( - uint256 privateKey, - bytes32 battleKey, - uint64 turnId, - bytes32 committerMoveHash, - uint8 revealerMoveIndex, - bytes32 revealerSalt, - uint240 revealerExtraData - ) internal view returns (bytes memory) { - bytes32 domainSeparator = keccak256( - abi.encode( - _DOMAIN_TYPEHASH, - keccak256("SignedCommitManager"), - keccak256("1"), - block.chainid, - address(signedCommitManager) - ) - ); - bytes32 structHash = SignedCommitLib.hashDualSignedReveal( - SignedCommitLib.DualSignedReveal({ - battleKey: battleKey, - turnId: turnId, - committerMoveHash: committerMoveHash, - revealerMoveIndex: revealerMoveIndex, - revealerSalt: revealerSalt, - revealerExtraData: revealerExtraData - }) - ); - bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); - return abi.encodePacked(r, s, v); - } - function _fastTurn( bytes32 battleKey, uint8 p0MoveIndex, uint8 p1MoveIndex, - uint240 p0ExtraData, - uint240 p1ExtraData + uint16 p0ExtraData, + uint16 p1ExtraData ) internal { uint64 turnId = uint64(engine.getTurnIdForBattleState(battleKey)); - bytes32 committerSalt = keccak256(abi.encode("committer", battleKey, turnId)); - bytes32 revealerSalt = keccak256(abi.encode("revealer", battleKey, turnId)); + uint104 committerSalt = uint104(uint256(keccak256(abi.encode("committer", battleKey, turnId)))); + uint104 revealerSalt = uint104(uint256(keccak256(abi.encode("revealer", battleKey, turnId)))); uint8 committerMoveIndex; - uint240 committerExtraData; + uint16 committerExtraData; uint8 revealerMoveIndex; - uint240 revealerExtraData; + uint16 revealerExtraData; + uint256 committerPk; uint256 revealerPk; address committer; @@ -172,6 +135,7 @@ contract StandardAttackPvPGasTest is Test, EIP712 { committerExtraData = p0ExtraData; revealerMoveIndex = p1MoveIndex; revealerExtraData = p1ExtraData; + committerPk = P0_PK; revealerPk = P1_PK; committer = p0; } else { @@ -179,14 +143,17 @@ contract StandardAttackPvPGasTest is Test, EIP712 { committerExtraData = p1ExtraData; revealerMoveIndex = p0MoveIndex; revealerExtraData = p0ExtraData; + committerPk = P1_PK; revealerPk = P0_PK; committer = p1; } bytes32 committerMoveHash = keccak256(abi.encodePacked(committerMoveIndex, committerSalt, committerExtraData)); + address mgr = address(signedCommitManager); + bytes memory committerSig = _signCommit(mgr, committerPk, committerMoveHash, battleKey, turnId); bytes memory revealerSig = _signDualReveal( - revealerPk, battleKey, turnId, committerMoveHash, revealerMoveIndex, revealerSalt, revealerExtraData + mgr, revealerPk, battleKey, turnId, committerMoveHash, revealerMoveIndex, revealerSalt, revealerExtraData ); vm.prank(committer); @@ -198,6 +165,7 @@ contract StandardAttackPvPGasTest is Test, EIP712 { revealerMoveIndex, revealerSalt, revealerExtraData, + committerSig, revealerSig ); engine.resetCallContext(); @@ -280,7 +248,7 @@ contract StandardAttackPvPGasTest is Test, EIP712 { // Turn 0: lead-in switch. vm.startSnapshotGas("Turn0_Lead"); - _fastTurn(battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _fastTurn(battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); uint256 turn0 = vm.stopSnapshotGas("Turn0_Lead"); // Turns 1-4: pure damage trades. Both players use move 0 / move 1 alternately. diff --git a/test/abstract/BattleHelper.sol b/test/abstract/BattleHelper.sol index 45caac16..c32d6a3d 100644 --- a/test/abstract/BattleHelper.sol +++ b/test/abstract/BattleHelper.sol @@ -25,10 +25,10 @@ abstract contract BattleHelper is Test { bytes32 battleKey, uint8 aliceMoveIndex, uint8 bobMoveIndex, - uint240 aliceExtraData, - uint240 bobExtraData + uint16 aliceExtraData, + uint16 bobExtraData ) internal { - bytes32 salt = ""; + uint104 salt = 0; bytes32 aliceMoveHash = keccak256(abi.encodePacked(aliceMoveIndex, salt, aliceExtraData)); bytes32 bobMoveHash = keccak256(abi.encodePacked(bobMoveIndex, salt, bobExtraData)); // Decide which player commits @@ -64,8 +64,8 @@ abstract contract BattleHelper is Test { DefaultCommitManager commitManager, bytes32 battleKey, uint8 moveIndex, - bytes32 salt, - uint240 extraData + uint104 salt, + uint16 extraData ) internal { commitManager.revealMove(battleKey, moveIndex, salt, extraData, true); vm.stopPrank(); @@ -170,4 +170,19 @@ abstract contract BattleHelper is Test { ability: 0 }); } + + /// @dev Layout used by `test/mocks/StatBoostsMove.sol`: + /// `[boostAmount:8 (signed) | statIndex:4 | monIndex:3 | playerIndex:1]` + function _packStatBoost(uint256 playerIndex, uint256 monIndex, uint256 statIndex, int32 boostAmount) + internal + pure + returns (uint16) + { + return uint16( + (playerIndex & 0x1) + | ((monIndex & 0x7) << 1) + | ((statIndex & 0xF) << 4) + | ((uint256(uint8(int8(boostAmount))) & 0xFF) << 8) + ); + } } diff --git a/test/abstract/SignedCommitHelper.sol b/test/abstract/SignedCommitHelper.sol new file mode 100644 index 00000000..5193f7c5 --- /dev/null +++ b/test/abstract/SignedCommitHelper.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import {SignedCommitLib} from "../../src/commit-manager/SignedCommitLib.sol"; + +import {Test} from "forge-std/Test.sol"; + +/// @notice EIP-712 signing helpers for `SignedCommitManager` tests. +/// @dev Standalone (not inheriting `EIP712`) — replicates `_DOMAIN_TYPEHASH` to avoid pulling +/// the production base into test contracts. The verifying-contract address is taken as a +/// parameter so a single helper instance can sign for multiple managers. +abstract contract SignedCommitHelper is Test { + /// `keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")` + bytes32 internal constant _SIGNED_COMMIT_DOMAIN_TYPEHASH = + 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f; + + function _signedCommitDomainSeparator(address verifyingContract) internal view returns (bytes32) { + return keccak256( + abi.encode( + _SIGNED_COMMIT_DOMAIN_TYPEHASH, + keccak256("SignedCommitManager"), + keccak256("1"), + block.chainid, + verifyingContract + ) + ); + } + + function _signCommit( + address signedCommitManagerAddr, + uint256 privateKey, + bytes32 moveHash, + bytes32 battleKey, + uint64 turnId + ) internal view returns (bytes memory) { + bytes32 structHash = SignedCommitLib.hashSignedCommit( + SignedCommitLib.SignedCommit({moveHash: moveHash, battleKey: battleKey, turnId: turnId}) + ); + bytes32 digest = keccak256( + abi.encodePacked("\x19\x01", _signedCommitDomainSeparator(signedCommitManagerAddr), structHash) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); + return abi.encodePacked(r, s, v); + } + + function _signDualReveal( + address signedCommitManagerAddr, + uint256 privateKey, + bytes32 battleKey, + uint64 turnId, + bytes32 committerMoveHash, + uint8 revealerMoveIndex, + uint104 revealerSalt, + uint16 revealerExtraData + ) internal view returns (bytes memory) { + bytes32 structHash = SignedCommitLib.hashDualSignedReveal( + SignedCommitLib.DualSignedReveal({ + battleKey: battleKey, + turnId: turnId, + committerMoveHash: committerMoveHash, + revealerMoveIndex: revealerMoveIndex, + revealerSalt: revealerSalt, + revealerExtraData: revealerExtraData + }) + ); + bytes32 digest = keccak256( + abi.encodePacked("\x19\x01", _signedCommitDomainSeparator(signedCommitManagerAddr), structHash) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); + return abi.encodePacked(r, s, v); + } +} diff --git a/test/effects/EffectTest.sol b/test/effects/EffectTest.sol index 95349ff6..86b657b5 100644 --- a/test/effects/EffectTest.sol +++ b/test/effects/EffectTest.sol @@ -146,7 +146,7 @@ contract EffectTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice and Bob both select attacks, both of them are move index 0 (do frostbite damage) @@ -237,11 +237,11 @@ contract EffectTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice switches to mon index 1, Bob induces frostbite - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), 0); // Check that Alice's new mon at index 0 has taken damage assertEq(engine.getMonStateForBattle(battleKey, 0, 1, MonStateIndexName.Hp), -1); @@ -301,7 +301,7 @@ contract EffectTest is Test, BattleHelper { */ bytes32 battleKey = _startBattle(validatorToUse, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 0, 0, 0); assertEq(engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Stamina), -1); assertEq(engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina), 0); @@ -316,7 +316,7 @@ contract EffectTest is Test, BattleHelper { assertEq(engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina), -1); // Alice is asleep, Bob does nothing, Alice switches to mon index 1, should be successful mockOracle.setRNG(1); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(1), 0); assertEq(engine.getActiveMonIndexForBattleState(battleKey)[0], 1); } @@ -392,7 +392,7 @@ contract EffectTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice and Bob both select attacks, both of them are move index 0 (inflict panic) @@ -477,7 +477,7 @@ contract EffectTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice and Bob both select attacks, both of them are move index 0 (apply burn status) @@ -628,7 +628,7 @@ contract EffectTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice and Bob both select attacks, both of them are move index 0 (apply zap status) @@ -640,14 +640,14 @@ contract EffectTest is Test, BattleHelper { assertEq(engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Stamina), -1); // Alice uses Zap, Bob switches to mon index 1 - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, SWITCH_MOVE_INDEX, 0, uint240(1)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, SWITCH_MOVE_INDEX, 0, uint16(1)); // The move should outspeed the swap, so the swap doesn't happen // So Bob's active mon index should still be 0 assertEq(engine.getActiveMonIndexForBattleState(battleKey)[1], 0); // Alice uses slower Zap, Bob switches to mon index 1 - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 1, SWITCH_MOVE_INDEX, 0, uint240(1)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 1, SWITCH_MOVE_INDEX, 0, uint16(1)); // Bob's active mon index should be 1 (swap goes before getting Zapped) assertEq(engine.getActiveMonIndexForBattleState(battleKey)[1], 1); @@ -657,7 +657,7 @@ contract EffectTest is Test, BattleHelper { assertEq(zapEffects.length, 1); // Alice does nothing, Bob attempts to switch to mon index 1, which should succeed because Zap allows switches - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint16(0)); // Check that Bob's active mon index is now 0, and the effect is still there assertEq(engine.getActiveMonIndexForBattleState(battleKey)[1], 0); @@ -665,7 +665,7 @@ contract EffectTest is Test, BattleHelper { assertEq(zapEffectsAfter.length, 1); // Bob switches back to mon index 1, Alice does nothing - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint240(1)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint16(1)); // Bob tries to make a move, Alice does nothing, Zap should skip his turn _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 0, 0, 0); @@ -728,7 +728,7 @@ contract EffectTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses NoDamage, Bob does as well @@ -803,7 +803,7 @@ contract EffectTest is Test, BattleHelper { // First move: both switch in their mons _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Verify Bob's mon has the heal effect applied (from ability on switch in) diff --git a/test/effects/StatBoosts.t.sol b/test/effects/StatBoosts.t.sol index cb276a67..bd0de342 100644 --- a/test/effects/StatBoosts.t.sol +++ b/test/effects/StatBoosts.t.sol @@ -38,11 +38,6 @@ contract StatBoostsTest is Test, BattleHelper { StatBoostsMove statBoostMove; DefaultMatchmaker matchmaker; - // Helper to pack StatBoostsMove extraData: lower 60 bits = playerIndex, next 60 bits = monIndex, next 60 bits = statIndex, upper 60 bits = boostAmount - function _packStatBoost(uint256 playerIndex, uint256 monIndex, uint256 statIndex, int32 boostAmount) internal pure returns (uint240) { - return uint240(playerIndex | (monIndex << 60) | (statIndex << 120) | (uint256(uint32(boostAmount)) << 180)); - } - function setUp() public { typeCalc = new TestTypeCalculator(); mockOracle = new MockRandomnessOracle(); @@ -112,7 +107,7 @@ contract StatBoostsTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // We'll test Attack stat in detail @@ -193,7 +188,7 @@ contract StatBoostsTest is Test, BattleHelper { battleKey, SWITCH_MOVE_INDEX, // Alice switches NO_OP_MOVE_INDEX, // Bob does nothing - uint240(1), // Alice switches to mon 1 + uint16(1), // Alice switches to mon 1 0 // Bob does nothing ); @@ -215,7 +210,7 @@ contract StatBoostsTest is Test, BattleHelper { battleKey, SWITCH_MOVE_INDEX, // Alice switches NO_OP_MOVE_INDEX, // Bob does nothing - uint240(0), // Alice switches back to mon 0 + uint16(0), // Alice switches back to mon 0 0 // Bob does nothing ); @@ -286,7 +281,7 @@ contract StatBoostsTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Test all stats @@ -335,7 +330,7 @@ contract StatBoostsTest is Test, BattleHelper { battleKey, SWITCH_MOVE_INDEX, // Alice switches NO_OP_MOVE_INDEX, // Bob does nothing - uint240(1), // Alice switches to mon 1 + uint16(1), // Alice switches to mon 1 0 // Bob does nothing ); @@ -389,7 +384,7 @@ contract StatBoostsTest is Test, BattleHelper { // Both players select their first mon (index 0) bytes32 battleKey = _startBattle(validatorToUse, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses stat boost move to boost her mon's special atk 50%, Bob does nothing @@ -429,7 +424,7 @@ contract StatBoostsTest is Test, BattleHelper { battleKey, SWITCH_MOVE_INDEX, // Alice switches NO_OP_MOVE_INDEX, // Bob does nothing - uint240(1), // Alice switches to mon 1 + uint16(1), // Alice switches to mon 1 0 // Bob does nothing ); diff --git a/test/mocks/CustomAttack.sol b/test/mocks/CustomAttack.sol index 0f5913a5..9c4a12c0 100644 --- a/test/mocks/CustomAttack.sol +++ b/test/mocks/CustomAttack.sol @@ -55,7 +55,7 @@ contract CustomAttack is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, - uint240 extraData, + uint16 extraData, uint256 rng ) external { _standardAttack.move(engine, battleKey, attackerPlayerIndex, attackerMonIndex, defenderMonIndex, extraData, rng); @@ -73,7 +73,7 @@ contract CustomAttack is IMoveSet { return _standardAttack.moveType(engine, battleKey); } - function isValidTarget(IEngine engine, bytes32 battleKey, uint240 extraData) external view returns (bool) { + function isValidTarget(IEngine engine, bytes32 battleKey, uint16 extraData) external view returns (bool) { return _standardAttack.isValidTarget(engine, battleKey, extraData); } diff --git a/test/mocks/EditEffectAttack.sol b/test/mocks/EditEffectAttack.sol index ebcffea0..670b8601 100644 --- a/test/mocks/EditEffectAttack.sol +++ b/test/mocks/EditEffectAttack.sol @@ -13,11 +13,12 @@ contract EditEffectAttack is IMoveSet { return "Edit Effect Attack"; } - function move(IEngine engine, bytes32, uint256, uint256, uint256, uint240 extraData, uint256) external { - // Unpack extraData: lower 80 bits = targetIndex, next 80 bits = monIndex, upper 80 bits = effectIndex - uint256 targetIndex = uint256(extraData) & ((1 << 80) - 1); - uint256 monIndex = (uint256(extraData) >> 80) & ((1 << 80) - 1); - uint256 effectIndex = (uint256(extraData) >> 160) & ((1 << 80) - 1); + function move(IEngine engine, bytes32, uint256, uint256, uint256, uint16 extraData, uint256) external { + // Unpack extraData (16 bits): bits 0..1 = targetIndex (0=p0, 1=p1, 2=global), + // bits 2..5 = monIndex (max 15), bits 6..15 = effectIndex (max 1023). + uint256 targetIndex = uint256(extraData) & 0x3; + uint256 monIndex = (uint256(extraData) >> 2) & 0xF; + uint256 effectIndex = (uint256(extraData) >> 6) & 0x3FF; engine.editEffect(targetIndex, monIndex, effectIndex, bytes32(uint256(69))); } @@ -33,7 +34,7 @@ contract EditEffectAttack is IMoveSet { return Type.Fire; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/test/mocks/EffectAttack.sol b/test/mocks/EffectAttack.sol index fe05a28d..dcd68cd5 100644 --- a/test/mocks/EffectAttack.sol +++ b/test/mocks/EffectAttack.sol @@ -33,7 +33,7 @@ contract EffectAttack is IMoveSet { return "Effect Attack"; } - function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint240, uint256) external { + function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint16, uint256) external { uint256 targetIndex = (attackerPlayerIndex + 1) % 2; engine.addEffect(targetIndex, defenderMonIndex, EFFECT, bytes32(0)); } @@ -50,7 +50,7 @@ contract EffectAttack is IMoveSet { return TYPE; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/test/mocks/ForceSwitchMove.sol b/test/mocks/ForceSwitchMove.sol index 6b83b8fb..d7966f40 100644 --- a/test/mocks/ForceSwitchMove.sol +++ b/test/mocks/ForceSwitchMove.sol @@ -30,10 +30,10 @@ contract ForceSwitchMove is IMoveSet { return "Force Switch"; } - function move(IEngine engine, bytes32, uint256, uint256, uint256, uint240 extraData, uint256) external { - // Decode data as packed (playerIndex in lower 120 bits, monToSwitchIndex in upper 120 bits) - uint256 playerIndex = uint256(extraData) & ((1 << 120) - 1); - uint256 monToSwitchIndex = uint256(extraData) >> 120; + function move(IEngine engine, bytes32, uint256, uint256, uint256, uint16 extraData, uint256) external { + // Pack: bit 0 = playerIndex (0 or 1), bits 1..15 = monToSwitchIndex + uint256 playerIndex = uint256(extraData) & 0x1; + uint256 monToSwitchIndex = uint256(extraData) >> 1; // Use the new switchActiveMon function engine.switchActiveMon(playerIndex, monToSwitchIndex); @@ -51,7 +51,7 @@ contract ForceSwitchMove is IMoveSet { return TYPE; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/test/mocks/GlobalEffectAttack.sol b/test/mocks/GlobalEffectAttack.sol index f5be9b13..b5bcb375 100644 --- a/test/mocks/GlobalEffectAttack.sol +++ b/test/mocks/GlobalEffectAttack.sol @@ -33,7 +33,7 @@ contract GlobalEffectAttack is IMoveSet { return "Effect Attack"; } - function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256, uint240, uint256) external { + function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256, uint16, uint256) external { engine.addEffect(2, 0, EFFECT, bytes32(attackerPlayerIndex)); } @@ -49,7 +49,7 @@ contract GlobalEffectAttack is IMoveSet { return TYPE; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/test/mocks/InvalidMove.sol b/test/mocks/InvalidMove.sol index 3cee3926..dadb8746 100644 --- a/test/mocks/InvalidMove.sol +++ b/test/mocks/InvalidMove.sol @@ -15,7 +15,7 @@ contract InvalidMove is IMoveSet { return "Effect Attack"; } - function move(IEngine, bytes32, uint256, uint256, uint256, uint240, uint256) external pure { + function move(IEngine, bytes32, uint256, uint256, uint256, uint16, uint256) external pure { // No-op } @@ -31,7 +31,7 @@ contract InvalidMove is IMoveSet { return Type.Fire; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return false; } diff --git a/test/mocks/MockEffectRemover.sol b/test/mocks/MockEffectRemover.sol index df30442f..61802680 100644 --- a/test/mocks/MockEffectRemover.sol +++ b/test/mocks/MockEffectRemover.sol @@ -10,8 +10,7 @@ import {IMoveSet} from "../../src/moves/IMoveSet.sol"; /** * Mock move that removes an effect from a target mon. - * The effect address to remove is passed as extraData (targetArgs). - * Targets the opponent's active mon. + * The effect's slot index is passed as extraData. Targets the opponent's active mon. */ contract MockEffectRemover is IMoveSet { @@ -25,20 +24,23 @@ contract MockEffectRemover is IMoveSet { uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, - uint240 extraData, + uint16 extraData, uint256 ) external { - // extraData contains the address of the effect to remove (packed as uint160) - address effectToRemove = address(uint160(extraData)); + // extraData is the slot index of the effect to remove (from getEffects). + uint256 slotIndex = uint256(extraData); // Target the opponent's active mon uint256 targetPlayerIndex = 1 - attackerPlayerIndex; - // Find and remove the effect (removeEffect in Engine handles calling onRemove with proper params) - (EffectInstance[] memory effects, uint256[] memory indices) = engine.getEffects(battleKey, targetPlayerIndex, defenderMonIndex); - for (uint256 i = 0; i < effects.length; i++) { - if (address(effects[i].effect) == effectToRemove) { - engine.removeEffect(targetPlayerIndex, defenderMonIndex, indices[i]); + // Verify the slot is still occupied at this index before removing + (EffectInstance[] memory effects, uint256[] memory indices) = + engine.getEffects(battleKey, targetPlayerIndex, defenderMonIndex); + for (uint256 i = 0; i < indices.length; i++) { + if (indices[i] == slotIndex) { + if (address(effects[i].effect) != address(0)) { + engine.removeEffect(targetPlayerIndex, defenderMonIndex, slotIndex); + } break; } } @@ -60,7 +62,7 @@ contract MockEffectRemover is IMoveSet { return MoveClass.Other; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/test/mocks/MockKVWriterMove.sol b/test/mocks/MockKVWriterMove.sol index 79640190..22222733 100644 --- a/test/mocks/MockKVWriterMove.sol +++ b/test/mocks/MockKVWriterMove.sol @@ -9,17 +9,18 @@ import {IMoveSet} from "../../src/moves/IMoveSet.sol"; import {MoveMeta} from "../../src/Structs.sol"; /// @notice Test-only move that writes a single, caller-chosen (key, value) pair to globalKV. -/// @dev extraData layout: lower 64 bits = key, upper 176 bits = value. A 0-value input -/// writes 1 by default so tests can quickly assert "something was written" without -/// encoding a non-zero value every time. +/// @dev extraData layout (16 bits total): bits 0..9 = key (≤1023), bits 10..15 = value (≤63). +/// A 0-value input writes 1 by default so tests can quickly assert "something was written" +/// without encoding a non-zero value every time. Tests that need wider key/value ranges +/// should use a different mock. contract MockKVWriterMove is IMoveSet { function name() external pure returns (string memory) { return "MockKVWriter"; } - function move(IEngine engine, bytes32, uint256, uint256, uint256, uint240 extraData, uint256) external { - uint64 key = uint64(extraData); - uint192 value = uint192(uint256(extraData) >> 64); + function move(IEngine engine, bytes32, uint256, uint256, uint256, uint16 extraData, uint256) external { + uint64 key = uint64(extraData) & 0x3FF; // 10 bits + uint192 value = uint192(uint256(extraData) >> 10); // 6 bits if (value == 0) { value = 1; } @@ -42,7 +43,7 @@ contract MockKVWriterMove is IMoveSet { return MoveClass.Self; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/test/mocks/ReduceSpAtkMove.sol b/test/mocks/ReduceSpAtkMove.sol index 375e51dc..b5738980 100644 --- a/test/mocks/ReduceSpAtkMove.sol +++ b/test/mocks/ReduceSpAtkMove.sol @@ -20,7 +20,7 @@ contract ReduceSpAtkMove is IMoveSet { return "Reduce SpAtk"; } - function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint240, uint256) external { + function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint16, uint256) external { // Get the opposing player's index uint256 opposingPlayerIndex = (attackerPlayerIndex + 1) % 2; @@ -40,7 +40,7 @@ contract ReduceSpAtkMove is IMoveSet { return Type.Math; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/test/mocks/SelfSwitchAndDamageMove.sol b/test/mocks/SelfSwitchAndDamageMove.sol index 3fb5b1d1..ac3fff8b 100644 --- a/test/mocks/SelfSwitchAndDamageMove.sol +++ b/test/mocks/SelfSwitchAndDamageMove.sol @@ -21,7 +21,7 @@ contract SelfSwitchAndDamageMove is IMoveSet { return "Self Switch And Damage Move"; } - function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint240 extraData, uint256) external { + function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint16 extraData, uint256) external { uint256 monToSwitchIndex = uint256(extraData); // Deal damage first to opponent @@ -44,7 +44,7 @@ contract SelfSwitchAndDamageMove is IMoveSet { return Type.Fire; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/test/mocks/SkipTurnMove.sol b/test/mocks/SkipTurnMove.sol index be9db1e1..9120f4ae 100644 --- a/test/mocks/SkipTurnMove.sol +++ b/test/mocks/SkipTurnMove.sol @@ -30,7 +30,7 @@ contract SkipTurnMove is IMoveSet { return "Skip Turn"; } - function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint240, uint256) external { + function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint16, uint256) external { uint256 targetIndex = (attackerPlayerIndex + 1) % 2; engine.updateMonState(targetIndex, defenderMonIndex, MonStateIndexName.ShouldSkipTurn, 1); } @@ -47,7 +47,7 @@ contract SkipTurnMove is IMoveSet { return TYPE; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/test/mocks/StatBoostsMove.sol b/test/mocks/StatBoostsMove.sol index 2b869396..4abc7544 100644 --- a/test/mocks/StatBoostsMove.sol +++ b/test/mocks/StatBoostsMove.sol @@ -22,12 +22,12 @@ contract StatBoostsMove is IMoveSet { return ""; } - function move(IEngine engine, bytes32, uint256, uint256, uint256, uint240 extraData, uint256) external { - // Unpack extraData: lower 60 bits = playerIndex, next 60 bits = monIndex, next 60 bits = statIndex, upper 60 bits = boostAmount - uint256 playerIndex = uint256(extraData) & ((1 << 60) - 1); - uint256 monIndex = (uint256(extraData) >> 60) & ((1 << 60) - 1); - uint256 statIndex = (uint256(extraData) >> 120) & ((1 << 60) - 1); - int32 boostAmount = int32(int256((uint256(extraData) >> 180) & ((1 << 60) - 1))); + function move(IEngine engine, bytes32, uint256, uint256, uint256, uint16 extraData, uint256) external { + // Unpack extraData: [boostAmount:8 | statIndex:4 | monIndex:3 | playerIndex:1] + uint256 playerIndex = uint256(extraData) & 0x1; + uint256 monIndex = (uint256(extraData) >> 1) & 0x7; + uint256 statIndex = (uint256(extraData) >> 4) & 0xF; + int32 boostAmount = int32(int8(uint8(uint256(extraData) >> 8))); // For all tests, we'll use Temp stat boosts with Multiply type for positive boosts // and Divide type for negative boosts @@ -59,7 +59,7 @@ contract StatBoostsMove is IMoveSet { return Type.Air; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/test/mocks/TestMoveFactory.sol b/test/mocks/TestMoveFactory.sol index 230d654f..02ab8f1d 100644 --- a/test/mocks/TestMoveFactory.sol +++ b/test/mocks/TestMoveFactory.sol @@ -24,7 +24,7 @@ contract TestMove is IMoveSet { return "Test Move"; } - function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint240, uint256) external { + function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint16, uint256) external { uint256 opponentIndex = (attackerPlayerIndex + 1) % 2; engine.dealDamage(opponentIndex, defenderMonIndex, _damage); } @@ -41,7 +41,7 @@ contract TestMove is IMoveSet { return _moveType; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/test/mons/AuroxTest.sol b/test/mons/AuroxTest.sol index 89914532..bcd566fd 100644 --- a/test/mons/AuroxTest.sol +++ b/test/mons/AuroxTest.sol @@ -87,7 +87,7 @@ contract AuroxTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Bull Rush, Bob does nothing _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, 0, 0); @@ -177,7 +177,7 @@ contract AuroxTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice spends 1 stamina, Bob inflicts frostbite @@ -198,7 +198,7 @@ contract AuroxTest is Test, BattleHelper { ); // Alice swaps to mon index 1, Bob does the 50% attack - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 1, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 1, uint16(1), 0); // Verify that Alice's mon index 1 has taken 50% damage int32 aliceDamage = engine.getMonStateForBattle(battleKey, 0, 1, MonStateIndexName.Hp); @@ -209,7 +209,7 @@ contract AuroxTest is Test, BattleHelper { ); // Alice uses Gilded Recovery targeting self, Bob does nothing - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 2, NO_OP_MOVE_INDEX, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 2, NO_OP_MOVE_INDEX, uint16(1), 0); // Nothing should happen, mon index 0 for Alice should still have -1 staminaDelta, hpDelta for mon index 1 should still be the same assertEq( @@ -224,7 +224,7 @@ contract AuroxTest is Test, BattleHelper { ); // Alice uses Gilded Recovery targeting mon index , Bob does nothing - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 2, NO_OP_MOVE_INDEX, uint240(0), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 2, NO_OP_MOVE_INDEX, uint16(0), 0); // Verify that Alice's mon index 1 is healed by 50% and mon index 0 has staminaDelta of 0, and no longer has frostbite assertEq( @@ -293,7 +293,7 @@ contract AuroxTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Iron Wall, Bob does nothing @@ -326,7 +326,7 @@ contract AuroxTest is Test, BattleHelper { assertEq(aliceDamage, expectedDamagePerHit * 2, "Alice's mon should take reduced damage on second hit"); // Alice switches to mon index 1, Bob does nothing - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(1), 0); // Verify that the Iron Wall effect is now gone after switch out (effects, ) = engine.getEffects(battleKey, 0, 0); @@ -380,7 +380,7 @@ contract AuroxTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Iron Wall, Bob does nothing @@ -443,7 +443,7 @@ contract AuroxTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice does nothing, Bob attacks @@ -518,7 +518,7 @@ contract AuroxTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice does nothing, Bob attacks @@ -613,7 +613,7 @@ contract AuroxTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses attack, Bob does nothing @@ -624,7 +624,7 @@ contract AuroxTest is Test, BattleHelper { assertEq(bobAttackDelta, int32(int8(upOnly.ATTACK_BOOST_PERCENT())) * int32(maxHp) / 100, "Bob's mon should be boosted"); // Alice does nothing, Bob switches to mon index 1 - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint240(1)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint16(1)); // Verify that Bob's mon index 0 has a positive attack delta of upOnly.ATTACK_BOOST_PERCENT() bobAttackDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Attack); @@ -699,7 +699,7 @@ contract AuroxTest is Test, BattleHelper { // Turn 0: Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Turn 1: Alice does nothing, Bob attacks Alice @@ -760,7 +760,7 @@ contract AuroxTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Set rng to be 2 to trigger frostbite diff --git a/test/mons/EkinekiTest.sol b/test/mons/EkinekiTest.sol index 7a4315fd..9bcca4ab 100644 --- a/test/mons/EkinekiTest.sol +++ b/test/mons/EkinekiTest.sol @@ -121,7 +121,7 @@ contract EkinekiTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Bubble Bop, Bob does nothing @@ -147,7 +147,7 @@ contract EkinekiTest is Test, BattleHelper { bytes32 battleKey2 = _startBattle(validator2, engine2, mockOracle, registry2, matchmaker2, address(commitManager2)); _commitRevealExecuteForAliceAndBob( - engine2, commitManager2, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine2, commitManager2, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Bob uses single hit on Alice @@ -194,11 +194,11 @@ contract EkinekiTest is Test, BattleHelper { // Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses SneakAttack targeting Bob's non-active mon (index 1) - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(1), 0); // Verify Bob's mon at index 1 (non-active) took damage int32 bobMon1HpDelta = engine.getMonStateForBattle(battleKey, 1, 1, MonStateIndexName.Hp); @@ -241,17 +241,17 @@ contract EkinekiTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses SneakAttack targeting Bob's non-active mon (index 1) - first use - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(1), 0); int32 bobMon1DamageAfterFirst = engine.getMonStateForBattle(battleKey, 1, 1, MonStateIndexName.Hp); assertTrue(bobMon1DamageAfterFirst < 0, "First sneak attack should deal damage"); // Alice uses SneakAttack again - should do nothing (already used this switch-in) - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(1), 0); int32 bobMon1DamageAfterSecond = engine.getMonStateForBattle(battleKey, 1, 1, MonStateIndexName.Hp); assertEq( @@ -293,21 +293,21 @@ contract EkinekiTest is Test, BattleHelper { // Both players select mon 0 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses SneakAttack - first use works - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(1), 0); int32 damageAfterFirst = engine.getMonStateForBattle(battleKey, 1, 1, MonStateIndexName.Hp); assertTrue(damageAfterFirst < 0, "First sneak attack should deal damage"); // Alice switches to mon 1 (sneak attack effect removed on switch-out) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(1), 0 + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(1), 0 ); // Alice (now mon 1) uses SneakAttack again - should work (reset by switch) - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(1), 0); int32 damageAfterReset = engine.getMonStateForBattle(battleKey, 1, 1, MonStateIndexName.Hp); assertTrue(damageAfterReset < damageAfterFirst, "Sneak attack should work again after switching"); } @@ -362,7 +362,7 @@ contract EkinekiTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Bob uses test attack without 999 (baseline) @@ -457,7 +457,7 @@ contract EkinekiTest is Test, BattleHelper { // Both select mon 0 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Bob attacks Alice's mon 0 (KO), Alice does nothing @@ -473,7 +473,7 @@ contract EkinekiTest is Test, BattleHelper { // Alice forced switch to mon 2 (the one with savior complex) // After KO, playerSwitchForTurnFlag = 0 (Alice must switch, no commit needed) vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, "", uint240(2), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(2), true); engine.resetCallContext(); // Verify that Alice's mon 2 got a sp atk boost (STAGE_1_BOOST = 15% of 100 = 15) int32 spAtkDelta = engine.getMonStateForBattle(battleKey, 0, 2, MonStateIndexName.SpecialAttack); @@ -556,7 +556,7 @@ contract EkinekiTest is Test, BattleHelper { // Both select mon 0 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Bob KOs Alice's mon 0 @@ -564,14 +564,14 @@ contract EkinekiTest is Test, BattleHelper { // Alice forced switch to mon 1 (savior complex triggers with 1 KO) vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, "", uint240(1), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1), true); engine.resetCallContext(); int32 spAtkDeltaFirstSwitch = engine.getMonStateForBattle(battleKey, 0, 1, MonStateIndexName.SpecialAttack); assertEq(spAtkDeltaFirstSwitch, 15, "Should get 15 sp atk boost from 1 KO"); // Alice switches to mon 2, Bob KOs Alice's mon 2 in the same turn (Bob is faster but switch has higher priority) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint240(2), 0 + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint16(2), 0 ); // Verify Alice's mon 2 is KO'd @@ -583,7 +583,7 @@ contract EkinekiTest is Test, BattleHelper { // Alice forced switch back to mon 1 (savior complex should NOT trigger again) vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, "", uint240(1), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1), true); engine.resetCallContext(); int32 spAtkDeltaSecondSwitch = engine.getMonStateForBattle(battleKey, 0, 1, MonStateIndexName.SpecialAttack); // Boost is temp so it was cleared when mon 1 switched out, and savior complex @@ -665,7 +665,7 @@ contract EkinekiTest is Test, BattleHelper { // Alice selects mon 0 (with savior complex) - no KO'd mons _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Verify no boost was applied (0 KOs) @@ -682,7 +682,7 @@ contract EkinekiTest is Test, BattleHelper { // Alice forced switch to mon 1 vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, "", uint240(1), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1), true); engine.resetCallContext(); // Mon 1 has no ability, so no savior complex trigger // But the savior complex on mon 0 should NOT have been consumed (it didn't trigger) @@ -720,7 +720,7 @@ contract EkinekiTest is Test, BattleHelper { _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Overflow, Bob does nothing diff --git a/test/mons/EmbursaTest.sol b/test/mons/EmbursaTest.sol index e80a42df..d5dae67c 100644 --- a/test/mons/EmbursaTest.sol +++ b/test/mons/EmbursaTest.sol @@ -94,12 +94,12 @@ contract EmbursaTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Q5, Bob does nothing _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); // Verify no damage occurred @@ -110,7 +110,7 @@ contract EmbursaTest is Test, BattleHelper { // Wait 4 turns for (uint256 i = 0; i < 4; i++) { _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, NO_OP_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, NO_OP_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); } // Verify no damage occurred @@ -123,7 +123,7 @@ contract EmbursaTest is Test, BattleHelper { // Alice and Bob both do nothing _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, NO_OP_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, NO_OP_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); // Verify damage occurred @@ -226,7 +226,7 @@ contract EmbursaTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, 0, 0); (EffectInstance[] memory effects, ) = engine.getEffects(battleKey, 1, 0); @@ -259,7 +259,7 @@ contract EmbursaTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); (effects, ) = engine.getEffects(battleKey, 1, 0); assertEq(effects.length, 0, "Bob's mon should have no effects"); @@ -276,7 +276,7 @@ contract EmbursaTest is Test, BattleHelper { // Verify Q5 was applied to global effects, verify Alice is KOed battleKey = _startBattle(validatorToUse, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, 0, 0); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 1, 4, 0, 0); @@ -295,7 +295,7 @@ contract EmbursaTest is Test, BattleHelper { // Verify Honey Bribe applied stat boost to Bob's mon, verify Alice is KOed battleKey = _startBattle(validatorToUse, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, 0, 0); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 3, 4, 0, 0); @@ -372,7 +372,7 @@ contract EmbursaTest is Test, BattleHelper { // Both switch in _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Q5, Bob does nothing @@ -381,7 +381,7 @@ contract EmbursaTest is Test, BattleHelper { // Wait 4 turns (Q5 counter ticks from 1 to 5) for (uint256 i = 0; i < 4; i++) { _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, NO_OP_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, NO_OP_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); } @@ -469,7 +469,7 @@ contract EmbursaTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Set RNG so that burn triggers (rng % 3 == 2) @@ -555,7 +555,7 @@ contract EmbursaTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validatorToUse, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Bob uses burn attack on Alice (Bob is faster) diff --git a/test/mons/GhouliathTest.sol b/test/mons/GhouliathTest.sol index 2a12be8d..c94c1ce6 100644 --- a/test/mons/GhouliathTest.sol +++ b/test/mons/GhouliathTest.sol @@ -150,7 +150,7 @@ contract GhouliathTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Bob uses the attack (which KOs) on Alice's mon @@ -170,7 +170,7 @@ contract GhouliathTest is Test, BattleHelper { // Alice swaps in mon index 1 vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); // We wait for the REVIVAL_DELAY - 1 turns to pass for (uint256 i = 0; i < riseFromTheGrave.REVIVAL_DELAY() - 1; i++) { @@ -188,12 +188,12 @@ contract GhouliathTest is Test, BattleHelper { assertEq(damageTaken, -99, "Alice's mon should have 1 HP"); // Alice swaps in mon index 0, Bob does attack again, which KOs Alice's mon - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint240(0), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint16(0), 0); // Verify the mon is not revived after REVIVAL_DELAY turns // (First we swap in mon index 1) vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); for (uint256 i = 0; i < riseFromTheGrave.REVIVAL_DELAY() - 1; i++) { _commitRevealExecuteForAliceAndBob( @@ -256,7 +256,7 @@ contract GhouliathTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Bob uses the attack (which KOs) on Alice's mon @@ -264,14 +264,14 @@ contract GhouliathTest is Test, BattleHelper { // Alice swaps in mon index 1 vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); // Alice KOs Bob's mon _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, 0, 0); // Bob swaps in mon index 1 vm.startPrank(BOB); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); // We wait for the REVIVAL_DELAY turns to pass for (uint256 i = 0; i < riseFromTheGrave.REVIVAL_DELAY() - 1; i++) { @@ -346,7 +346,7 @@ contract GhouliathTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses WitherAway on Bob's mon @@ -448,7 +448,7 @@ contract GhouliathTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Osteoporosis on Bob's mon @@ -500,12 +500,12 @@ contract GhouliathTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice does nothing, Bob switches to mon index 1 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint240(1) + engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint16(1) ); // Alice uses Eternal Grudge on Bob's mon diff --git a/test/mons/GorillaxTest.sol b/test/mons/GorillaxTest.sol index 8373d33d..9093a41f 100644 --- a/test/mons/GorillaxTest.sol +++ b/test/mons/GorillaxTest.sol @@ -97,7 +97,7 @@ contract GorillaxTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice chooses to attack, Bob chooses to do nothing for CHARGE_COUNT rounds @@ -168,12 +168,12 @@ contract GorillaxTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Rock Pull, Bob switches to mon index 1 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, SWITCH_MOVE_INDEX, uint240(0), uint240(1) + engine, commitManager, battleKey, 0, SWITCH_MOVE_INDEX, uint16(0), uint16(1) ); // Assert that Bob's mon index 0 took damage @@ -184,7 +184,7 @@ contract GorillaxTest is Test, BattleHelper { // Alice uses Rock Pull, Bob does not switch _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); // Assert that Alice's mon index 0 took damage diff --git a/test/mons/IblivionTest.sol b/test/mons/IblivionTest.sol index 56fcb7d9..e8bd5659 100644 --- a/test/mons/IblivionTest.sol +++ b/test/mons/IblivionTest.sol @@ -112,7 +112,7 @@ contract IblivionTest is Test, BattleHelper { // Switch in mons _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Check that Baselight level is 2 (1 from switch-in + 1 from round end) @@ -153,7 +153,7 @@ contract IblivionTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Baselight starts at 2 (1 from switch + 1 from round end) @@ -227,7 +227,7 @@ contract IblivionTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice starts with 2 Baselight stacks (1 from switch + 1 from round end) @@ -292,7 +292,7 @@ contract IblivionTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // No Baselight stacks @@ -370,7 +370,7 @@ contract IblivionTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice has 2 stacks (not 3), so normal power (80) @@ -441,7 +441,7 @@ contract IblivionTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Wait for 3 stacks (start at 2, need 1 more round) @@ -500,7 +500,7 @@ contract IblivionTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // At Baselight 2, Loop should give 30% boost @@ -555,7 +555,7 @@ contract IblivionTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Loop first time @@ -611,7 +611,7 @@ contract IblivionTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Wait for 3 stacks (start at 2, need 1 more round) @@ -666,7 +666,7 @@ contract IblivionTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Start at 2 stacks (1 initial + 1 from round end) @@ -714,7 +714,7 @@ contract IblivionTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Loop to get stat boosts @@ -767,7 +767,7 @@ contract IblivionTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Loop @@ -847,7 +847,7 @@ contract IblivionTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Check priority @@ -945,7 +945,7 @@ contract IblivionTest is Test, BattleHelper { // Switch in mons _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Verify Alice's attack starts at base (0 delta) @@ -994,10 +994,20 @@ contract IblivionTest is Test, BattleHelper { } assertTrue(stillHasBurn, "Alice should still have burn effect after Renormalize"); - // Bob uses MockEffectRemover to remove burn from Alice (move index 1) - // Pass burn status address as extraData + // Bob uses MockEffectRemover to remove burn from Alice (move index 1). + // Pass burn status's slot index as extraData (looked up from Alice's effects). + uint16 burnSlot; + { + (EffectInstance[] memory beforeRemove, uint256[] memory beforeIndices) = engine.getEffects(battleKey, 0, 0); + for (uint256 i = 0; i < beforeRemove.length; i++) { + if (address(beforeRemove[i].effect) == address(burnStatus)) { + burnSlot = uint16(beforeIndices[i]); + break; + } + } + } _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 1, 0, uint240(uint160(address(burnStatus))) + engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 1, 0, burnSlot ); // Verify burn effect is removed diff --git a/test/mons/InutiaTest.sol b/test/mons/InutiaTest.sol index 57434918..8c1f37a0 100644 --- a/test/mons/InutiaTest.sol +++ b/test/mons/InutiaTest.sol @@ -112,7 +112,7 @@ contract InutiaTest is Test, BattleHelper { // First move: Alice switches to the mon with Interweaving ability, Bob switches to the other mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(1), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(1), uint16(0) ); // Check that Bob's mon Attack stat has been decreased @@ -124,7 +124,7 @@ contract InutiaTest is Test, BattleHelper { // Alice switches back to the regular mon, Bob does a No-Op _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(0), 0 + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(0), 0 ); // Check that Bob's mon SpecialAttack stat has been decreased @@ -177,7 +177,7 @@ contract InutiaTest is Test, BattleHelper { // Send in mons _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both players select move index 0 @@ -205,7 +205,7 @@ contract InutiaTest is Test, BattleHelper { // Now both players swap to mon index 1 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(1), uint240(1) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(1), uint16(1) ); // The stat boost should carry over @@ -220,7 +220,7 @@ contract InutiaTest is Test, BattleHelper { // Now both players swap back to mon index 0 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both players select move index 0 @@ -326,7 +326,7 @@ contract InutiaTest is Test, BattleHelper { bytes32 battleKey = _startBattle(v, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses CE, Bob does nothing (turn 1) @@ -339,7 +339,7 @@ contract InutiaTest is Test, BattleHelper { // Bob swaps to mon index 1 (turn 3) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint240(1) + engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint16(1) ); // Verify damage dealt to Bob's mon index 1 is 1/16 of max HP (charge 1) @@ -348,7 +348,7 @@ contract InutiaTest is Test, BattleHelper { // Bob swaps to mon index 2 (turn 4) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint240(2) + engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint16(2) ); // Verify damage dealt to Bob's mon index 2 is 1/4 of max HP (charge 2) @@ -357,7 +357,7 @@ contract InutiaTest is Test, BattleHelper { // Bob swaps back to mon index 0 (turn 5) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint240(0) + engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint16(0) ); // Verify damage dealt to Bob's mon index 0 is 1/8 of max HP (charge 3) @@ -366,7 +366,7 @@ contract InutiaTest is Test, BattleHelper { // Bob swaps to mon index 1 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint240(1) + engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint16(1) ); // Verify damage dealt to Bob's mon index 1 is 1/16 of max HP (charge 4) @@ -391,12 +391,12 @@ contract InutiaTest is Test, BattleHelper { // Alice swaps to mon index 1, Bob does nothing _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(1), 0 + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(1), 0 ); // Alice swaps back to mon index 0, Bob does nothing _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(0), 0 + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(0), 0 ); // Verify Alice's mon index 0 is healed by 1/16 of max HP diff --git a/test/mons/MalalienTest.sol b/test/mons/MalalienTest.sol index 63557fbb..569b76d6 100644 --- a/test/mons/MalalienTest.sol +++ b/test/mons/MalalienTest.sol @@ -129,7 +129,7 @@ contract MalalienTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice's mon has ActusReus ability @@ -153,7 +153,7 @@ contract MalalienTest is Test, BattleHelper { // Bob switches to mon index 1 vm.startPrank(BOB); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); // Alice does nothing, Bob attacks and KOs Alice's mon _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 0, 0, 0); @@ -208,11 +208,11 @@ contract MalalienTest is Test, BattleHelper { // Alice and Bob both send in mon index 0 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both players use triple think - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 0, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 0, uint16(0), uint16(0)); // SpecialAttack delta for both is 100 int32 aliceSpAtkBoost = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.SpecialAttack); @@ -223,7 +223,7 @@ contract MalalienTest is Test, BattleHelper { // Alice uses it again, Bob swaps out _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, SWITCH_MOVE_INDEX, uint240(0), uint240(1) + engine, commitManager, battleKey, 0, SWITCH_MOVE_INDEX, uint16(0), uint16(1) ); // Alice should be at 1.75 * 1.75 = 1.225 + 1.75 + 0.0875 = 2.0625, Bob should be at 0 diff --git a/test/mons/PengymTest.sol b/test/mons/PengymTest.sol index 6346bd03..3ff3f6b5 100644 --- a/test/mons/PengymTest.sol +++ b/test/mons/PengymTest.sol @@ -180,14 +180,14 @@ contract PengymTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Check that Alice's mon has the PostWorkout effect (EffectInstance[] memory aliceEffects, ) = engine.getEffects(battleKey, 0, 0); bool hasPostWorkoutEffect = false; for (uint256 i = 0; i < aliceEffects.length; i++) { - if (aliceEffects[i].effect == IEffect(address(postWorkout))) { + if (address(aliceEffects[i].effect) == address(postWorkout)) { hasPostWorkoutEffect = true; break; } @@ -204,7 +204,7 @@ contract PengymTest is Test, BattleHelper { (aliceEffects, ) = engine.getEffects(battleKey, 0, 0); bool hasPanicEffect = false; for (uint256 i = 0; i < aliceEffects.length; i++) { - if (aliceEffects[i].effect == IEffect(address(panicStatus))) { + if (address(aliceEffects[i].effect) == address(panicStatus)) { hasPanicEffect = true; break; } @@ -216,12 +216,12 @@ contract PengymTest is Test, BattleHelper { // Alice switches to her second mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(1), 0 + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(1), 0 ); // Alice switches back to her first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(0), 0 + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(0), 0 ); // Check that Alice's mon no longer has the PanicStatus effect @@ -229,7 +229,7 @@ contract PengymTest is Test, BattleHelper { hasPanicEffect = false; for (uint256 i = 0; i < aliceEffects.length; i++) { - if (aliceEffects[i].effect == IEffect(address(panicStatus))) { + if (address(aliceEffects[i].effect) == address(panicStatus)) { hasPanicEffect = true; break; } @@ -355,14 +355,14 @@ contract PengymTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Check that Alice's mon has the PostWorkout effect (EffectInstance[] memory aliceEffects, ) = engine.getEffects(battleKey, 0, 0); bool hasPostWorkoutEffect = false; for (uint256 i = 0; i < aliceEffects.length; i++) { - if (aliceEffects[i].effect == IEffect(address(postWorkout))) { + if (address(aliceEffects[i].effect) == address(postWorkout)) { hasPostWorkoutEffect = true; break; } @@ -379,7 +379,7 @@ contract PengymTest is Test, BattleHelper { (aliceEffects, ) = engine.getEffects(battleKey, 0, 0); bool hasFrostbiteEffect = false; for (uint256 i = 0; i < aliceEffects.length; i++) { - if (aliceEffects[i].effect == IEffect(address(frostbiteStatus))) { + if (address(aliceEffects[i].effect) == address(frostbiteStatus)) { hasFrostbiteEffect = true; break; } @@ -396,19 +396,19 @@ contract PengymTest is Test, BattleHelper { // Alice switches to her second mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(1), 0 + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(1), 0 ); // Alice switches back to her first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(0), 0 + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(0), 0 ); // Check that Alice's mon no longer has the FrostbiteStatus effect (aliceEffects, ) = engine.getEffects(battleKey, 0, 0); hasFrostbiteEffect = false; for (uint256 i = 0; i < aliceEffects.length; i++) { - if (aliceEffects[i].effect == IEffect(address(frostbiteStatus))) { + if (address(aliceEffects[i].effect) == address(frostbiteStatus)) { hasFrostbiteEffect = true; break; } @@ -472,25 +472,25 @@ contract PengymTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice deals damage to Bob, record the damage dealt _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); int32 deepFreezeDamage = -1 * engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Hp); // Alice inflicts frostbite on Bob, Bob does nothing _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); int32 bobDamageBefore = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Hp); // Alice uses deep freeze, record the damage dealt _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); int32 bobDamageAfter = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Hp); @@ -576,11 +576,11 @@ contract PengymTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice selects pistol squat, Bob selects move index 1 and outspeeds - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 1, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 1, uint16(0), uint16(0)); // Alice should be KO'ed int32 koFlag = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.IsKnockedOut); @@ -588,11 +588,11 @@ contract PengymTest is Test, BattleHelper { // Alice swaps to mon index 1 vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); // Alice selects pistol squat, Bob does nothing _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); // Active mon for Bob should be 1 @@ -601,7 +601,7 @@ contract PengymTest is Test, BattleHelper { // Alice selects pistol squat, Bob does nothing _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); // Active mon for Bob should be 0 @@ -610,7 +610,7 @@ contract PengymTest is Test, BattleHelper { // Alice selects pistol squat, Bob does nothing (and dies) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); bobActiveMonIndex = engine.getActiveMonIndexForBattleState(battleKey)[1]; @@ -618,21 +618,21 @@ contract PengymTest is Test, BattleHelper { // Bob sends in mon index 1 vm.startPrank(BOB); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); // Alice selects pistol squat, Bob does nothing _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); // Now Bob's mon index 1 is KOed // Bob sends in mon index 2 vm.startPrank(BOB); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(2), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(2), true); engine.resetCallContext(); // Alice selects pistol squat, Bob does nothing _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); // Now Bob has mon index 2 (already took damage) and mon index 3 @@ -641,21 +641,21 @@ contract PengymTest is Test, BattleHelper { // Bob switches back to mon index 2, Alice does nothing _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(2) + engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(2) ); // Alice KOs Bob's mon index 2 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); // Bob sends in mon index 3 vm.startPrank(BOB); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, "", uint240(3), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(3), true); engine.resetCallContext(); // Alice tries to force a switch, but active mon should not change _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); bobActiveMonIndex = engine.getActiveMonIndexForBattleState(battleKey)[1]; assertEq(bobActiveMonIndex, 3, "No mons left"); diff --git a/test/mons/SofabbiTest.sol b/test/mons/SofabbiTest.sol index ab64d93d..0221faf7 100644 --- a/test/mons/SofabbiTest.sol +++ b/test/mons/SofabbiTest.sol @@ -108,7 +108,7 @@ contract SofabbiTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Verify that the CarrotHarvest effect was applied to Alice's mon @@ -117,12 +117,12 @@ contract SofabbiTest is Test, BattleHelper { // Now have Alice switch to her second mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(1), 0 + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(1), 0 ); // Now have Alice switch back to her first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(0), 0 + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(0), 0 ); // Verify that the CarrotHarvest effect is still only applied once @@ -188,7 +188,7 @@ contract SofabbiTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Verify that staminaDelta is 1 for both mons @@ -297,28 +297,28 @@ contract SofabbiTest is Test, BattleHelper { // First move: Both players select their first mon (index 0, the Air mon) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Damage is returned in negative, so that's why there are some weird sign cancellations below // Alice uses Guest Feature targeting mon index 1, it should deal 2x damage _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(1), uint240(0) + engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(1), uint16(0) ); int32 bobDmg = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Hp); assertApproxEqRel(-1 * bobDmg, int32(2 * gf.BASE_POWER()), 2e17); // Alice uses Guest Feature targeting mon index 2, it should deal 0 damage _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(2), uint240(0) + engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(2), uint16(0) ); int32 newBobDmg = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Hp); assertEq(newBobDmg, bobDmg, "No damage"); // Alice uses Guest Feature targeting mon index 3, it should deal 1/2 damage _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(3), uint240(0) + engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(3), uint16(0) ); newBobDmg = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Hp); bobDmg = bobDmg - newBobDmg; @@ -377,7 +377,7 @@ contract SofabbiTest is Test, BattleHelper { // Both players send in mon index 0 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses nothing, Bob uses move index 1, puts Alice at 1 HP @@ -454,7 +454,7 @@ contract SofabbiTest is Test, BattleHelper { // Both players send in mon index 0 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Set rng to be 10 diff --git a/test/mons/VolthareTest.sol b/test/mons/VolthareTest.sol index 7991286b..3f222e21 100644 --- a/test/mons/VolthareTest.sol +++ b/test/mons/VolthareTest.sol @@ -124,7 +124,7 @@ contract VolthareTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Verify that Bob's mon took damage from PreemptiveShock @@ -207,11 +207,11 @@ contract VolthareTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses the Overclock move (move index 0), Bob does nothing - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(0), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), 0); // Verify that Overclock is applied (EffectInstance[] memory effects,) = engine.getEffects(battleKey, 2, 0); @@ -222,7 +222,7 @@ contract VolthareTest is Test, BattleHelper { mockOracle.setRNG(2); // Alice uses Mega Star Blast (move index 1), Bob does nothing - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, uint240(0), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, uint16(0), 0); // Verify that Bob's mon is zapped (effects,) = engine.getEffects(battleKey, 1, 0); @@ -237,7 +237,7 @@ contract VolthareTest is Test, BattleHelper { mockOracle.setRNG(msb.BASE_ACCURACY() + 1); // Alice uses Mega Star Blast, Bob does nothing - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, uint240(0), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, uint16(0), 0); // Verify that Bob's mon is not zapped (again) (effects,) = engine.getEffects(battleKey, 1, 0); @@ -303,7 +303,7 @@ contract VolthareTest is Test, BattleHelper { // Both players send in their mons _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Bob applies Overclock (stored with player index 1). Alice does nothing. @@ -320,7 +320,7 @@ contract VolthareTest is Test, BattleHelper { mockOracle.setRNG(msb.BASE_ACCURACY() + 1); // Alice uses MegaStarBlast. Bob does nothing. - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, uint240(0), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, uint16(0), 0); // Overclock should still be present (Alice cannot clear Bob's Overclock) (effects,) = engine.getEffects(battleKey, 2, 0); @@ -396,7 +396,7 @@ contract VolthareTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both players use Dual Shock, Alice should move first and skip their next move diff --git a/test/mons/XmonTest.sol b/test/mons/XmonTest.sol index 10b041b6..8c3d995d 100644 --- a/test/mons/XmonTest.sol +++ b/test/mons/XmonTest.sol @@ -87,7 +87,7 @@ contract XmonTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Contagious Slumber, Bob does nothing @@ -160,7 +160,7 @@ contract XmonTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Set RNG to guarantee stamina steal (>= 50) @@ -219,7 +219,7 @@ contract XmonTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Somniphobia, Bob uses Somniphobia too @@ -337,7 +337,7 @@ contract XmonTest is Test, BattleHelper { // Alice sends in fast mon, Bob sends in slow mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(1) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(1) ); // Verify that Alice has the Dreamcatcher effect @@ -400,7 +400,7 @@ contract XmonTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Turn 1: Alice uses Night Terrors, Bob does nothing @@ -467,7 +467,7 @@ contract XmonTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Turn 1: Alice uses Night Terrors, Bob does nothing @@ -485,7 +485,7 @@ contract XmonTest is Test, BattleHelper { assertTrue(hasNightTerrorsBeforeSwap, "Alice's mon 0 should have Night Terrors effect before swap"); // Turn 2: Alice swaps to mon 1, Bob does nothing - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(1), 0); // Verify Alice's mon 0 no longer has Night Terrors effect (EffectInstance[] memory aliceEffectsAfterSwap, ) = engine.getEffects(battleKey, 0, 0); @@ -555,7 +555,7 @@ contract XmonTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Set RNG to 1 to prevent early waking from sleep (rng % 3 == 0 wakes early) @@ -569,10 +569,10 @@ contract XmonTest is Test, BattleHelper { int32 awakeDamage = -bobHpAfterAwakeDamage; // Turn 2: Alice swaps out to mon 1 to clear Night Terrors, Bob does nothing - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(1), 0); // Turn 3: Alice swaps back to mon 0, Bob does nothing - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(0), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(0), 0); // Turn 4: Alice uses Sleep move on Bob, Bob does nothing _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, 0, 0); diff --git a/test/moves/StandardAttackRngTest.sol b/test/moves/StandardAttackRngTest.sol index d06be33b..836976cc 100644 --- a/test/moves/StandardAttackRngTest.sol +++ b/test/moves/StandardAttackRngTest.sol @@ -93,7 +93,7 @@ contract StandardAttackRngTest is Test, BattleHelper { // Switch in mon 0 on both sides. _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Oracle returns the same rng for both attackers this turn. diff --git a/transpiler/MIGRATION.md b/transpiler/MIGRATION.md deleted file mode 100644 index ee4cf6aa..00000000 --- a/transpiler/MIGRATION.md +++ /dev/null @@ -1,112 +0,0 @@ -# Migration Plan — Extracting `extruder` as a Standalone Library - -## 7. Milestones - -Proposed ordering. Each is a PR-sized chunk. - -### M0 — Repo split (physical) -- [ ] Create `extruder` repo with a copy of `transpiler/`. -- [ ] Strip Chomp-specific `skipFiles`, `skipDirs`, `dependencyOverrides` from the - library's `transpiler-config.json`. Chomp keeps its own config. -- [x] ~~Delete `runtime/battle-harness.ts` from the library. (Move to Chomp.)~~ - Lives at `munch/src/app/ts-output/runtime/battle-harness.ts`. Munch - owns its own `runtime/`; chomp's deploy rsync excludes `runtime/` so - munch-only files aren't clobbered. Chomp's `transpiler/runtime/` no - longer re-exports `BattleHarness`. -- [x] ~~Move Chomp-specific vitest tests out of the transpiler tree.~~ All 30 - Chomp-specific vitest tests + `fixtures/mocks.ts` removed. Extruder - doesn't ship a JS test suite: `package.json`, `package-lock.json`, - `bun.lock`, `vitest.config.ts`, and `node_modules/` are all deleted. - Extruder is a Python tool; regression coverage lives in - `test_transpiler.py`. Consumer-side runtime behavior verification is - the consumer's job, run against their own `ts-output/` with whatever - JS test runner they prefer. -- [ ] Move `runtime/Ownable.ts`, `runtime/EIP712.ts`, - `runtime/EnumerableSetLib.ts` into `examples/runtime-replacements/`. - Also move `runtime/ECDSA.ts` into examples. - Drop their `runtimeReplacements` entries from the library's shipped config. - Chomp copies them into its own repo and re-adds its config entries. -- [ ] Chomp continues to use its vendored `transpiler/` directory — no consumer - switchover yet. - -### M1 — Core decontamination -- [x] ~~Remove `_DEFAULT_INTERFACE_ALIASES` hardcoded map.~~ **Done.** - `DependencyResolver` now reads `interfaceAliases` from config. Chomp's - `transpiler-config.json` re-adds the aliases Chomp needs (IEngine → - Engine, etc.). Transpile output byte-identical. -- [x] ~~Remove `hashBattle`, `hashBattleOffer` from `codegen/expression.py:889`.~~ - Done via `TypeRegistry` return-type lookup instead of a config field — see §2b. -- [x] ~~Rewrite `sol2ts.py` docstring, `package.json` description.~~ **Done.** - Docstring now leads with the extruder framing; `package.json` renamed to - `extruder` at `0.1.0-alpha`. -- [x] ~~Chomp's `transpiler-config.json` adds its interface aliases back in.~~ - **Done.** Included above in the alias removal task. - -### M2 — Clone-and-run hygiene -- [ ] Write `requirements.txt` pinning Python deps (currently managed via `uv`). -- [ ] Add a one-line install check in `extruder.py` that errors helpfully if deps are - missing (e.g. pexpect, pillow, numpy). -- [ ] Confirm `python3 extruder.py --help` works from a fresh clone. - -### M2.5 — Runtime-replacement stub flow (§2f) -- [x] ~~Remove or repurpose the dead `--stub ` flag.~~ **Done.** Flag - and `stubbed_contracts` plumbing removed from `sol2ts.py` (also removed from - `SolidityToTypeScriptTranspiler.__init__`). -- [x] ~~Add `--emit-replacement-stub `.~~ **Done.** - Implemented in `transpiler/codegen/replacement_stub.py` and wired through - `sol2ts.py`. Emits a TypeScript class with mapped method signatures, - `throw new Error('Not implemented')` bodies, default state-variable values, - constants rendered as `static readonly` with literal values when the parser - can extract them, local struct fields rendered as sibling TS interfaces, and - Solidity overloads disambiguated with `_overloadN` suffixes + a TODO comment. - Prints the ready-to-paste `runtimeReplacements` entry (including a - registry-derived `interface` block) to stdout. -- [x] ~~Validate end-to-end~~ **Done.** Regenerated `Ownable.ts` / `ECDSA.ts` / - `EIP712.ts` / `EnumerableSetLib.ts` from `src/lib/*.sol`; each stub - type-checks cleanly against `runtime/base.ts` under `tsc --strict`. Chomp's - normal transpile is byte-identical; full Python test suite stays green. -- [ ] Document the flow in `docs/runtime-replacements.md` with ECDSA as the worked - example. (Follow-up in M3.) - -### M2.75 — `extruder init` bootstrap command (§2g) -- [x] Scan phase (`transpiler/init.py::scan()`) — pure, returns `InitReport`. -- [x] Unit tests covering every verdict path, including false-positive guards - (magic-number `sstore` → OK, not REPLACE; `new bytes(n)` → OK). -- [x] `build_plan` + `apply` split, with `Prompter` behind a mockable seam. -- [x] CLI wiring: `extruder init [--yes] [--stub-output-dir] [--config-path]`. -- [x] Integrated with `--emit-replacement-stub` via an injected `stub_emitter` - callable (keeps the module acyclic). -- [x] Dependency-resolver dry-run phase — `MetadataExtractor` + `DependencyResolver` - walk every constructor; unresolved entries become prompts (or auto-mapped - when a single implementer exists, or punted in `--yes` mode). -- [x] Dog-fooded against Chomp: 112 OK, 8 REPLACE (all legitimate), 0 false - positives. Auto-maps 12 single-impl interfaces, flags `IAbility` + - `IMoveSet` as tag interfaces, resolves 8 of 9 dep-dry-run entries via - aliases, punts `GachaRegistry._MON_REGISTRY` (multi-impl). -- [x] Config merge-conflict prompts — plan consults the existing config and - prompts on conflicting alias/override entries; under `--yes` the - existing value wins silently and the conflict is logged. -- [x] AST-level MAYBE detection — files with modifiers (W001) or - `receive`/`fallback` (W003) surface as MAYBE with specific reasons. - Receive/fallback is detected via a source-text regex because the - parser drops those tokens before the AST. -- [ ] Follow-up: trial-transpile-based MAYBE detection for W002 (try/catch) - and W004 (function pointers). Would require running the generator; - deferred until we see real user demand. - -### M3 — Docs (see §6) -- [x] `README.md` rewritten (~160 lines, down from 370). Leads with the - extruder framing and `extruder init`; full TOC links into `docs/`. -- [x] `docs/quickstart.md`, `docs/init.md`, `docs/configuration.md`, - `docs/runtime-replacements.md`, `docs/runtime.md`, - `docs/semantics.md`, `docs/extending.md` — all written. -- [x] `docs/faq.md` seeded with the common starter-question pointers; - kept deliberately empty until real user questions arrive. - -### M4 — Examples + tests -- [ ] `examples/erc20/` runnable example. -- [ ] Transpiler integration tests against the fixtures (`minimal`, `inheritance`, - `yul`, `mappings`). - ---- - diff --git a/transpiler/README.md b/transpiler/README.md index f19811c4..18aeafc8 100644 --- a/transpiler/README.md +++ b/transpiler/README.md @@ -24,25 +24,48 @@ into TypeScript you can run in Node or the browser. ```bash git clone ~/tools/extruder -cd ~/tools/extruder && pip install -r requirements.txt +cd ~/tools/extruder + +# Optional: install the standalone CLI in editable mode. +python3 -m pip install -e ./transpiler # In your own Foundry project: npm install -D viem vitest ``` +If installed, run: + +```bash +extruder --help +``` + +From a source checkout, you can also run extruder as a Python module from +the directory that contains the `transpiler/` package: + +```bash +cd ~/tools/extruder +python3 -m transpiler --help +``` + +Do not run `transpiler/sol2ts.py` directly; it uses package-relative imports. + ## Quickstart Bootstrap the config and scaffolded runtime-replacement stubs with one command: ```bash -python3 ~/tools/extruder/sol2ts.py init src/ --yes +cd ~/tools/extruder +extruder init /path/to/your/foundry/project/src --yes ``` Then transpile: ```bash -python3 ~/tools/extruder/sol2ts.py src/ -o ts-output -d src --emit-metadata +extruder /path/to/your/foundry/project/src \ + -o /path/to/your/foundry/project/ts-output \ + -d /path/to/your/foundry/project/src \ + --emit-metadata ``` See [`docs/quickstart.md`](docs/quickstart.md) for the full walkthrough. @@ -51,7 +74,7 @@ See [`docs/quickstart.md`](docs/quickstart.md) for the full walkthrough. - **Parse → AST → emit TS.** Not bytecode, not an EVM. - `uint*` / `int*` → `bigint`. `address` / `bytes*` → `string`. Mappings → - `Record`. Structs → interfaces + factory. Enums → `as const` objects. + `Record`. Structs → interfaces + factory. Enums → TypeScript `enum`s. - Contracts become ES classes extending a runtime `Contract` base that carries `_contractAddress`, `_storage`, `_msg`, an event emitter, and a transient-storage reset hook. @@ -75,6 +98,8 @@ extruder init [--yes] [--stub-output-dir DIR] [--config-path PATH] extruder --emit-replacement-stub CONTRACT SOL_FILE [-o OUTPUT] ``` +`python3 -m transpiler ...` supports the same commands from a source checkout. + | Flag | Purpose | |---|---| | `input` *(positional)* | File or directory to transpile. | @@ -82,8 +107,7 @@ extruder --emit-replacement-stub CONTRACT SOL_FILE [-o OUTPUT] | `-d`, `--discover` *(repeatable)* | Root(s) to scan for type discovery. Pass every source root you need cross-file resolution across. | | `--stdout` | Print a single file to stdout instead of writing (debugging). | | `--emit-metadata` | Also emit `factories.ts`. | -| `--metadata-only` | Skip TS generation, only write `factories.ts`. | -| `--overrides` | Path to `transpiler-config.json`. Defaults to the one next to `sol2ts.py`. | +| `--overrides` | Path to `transpiler-config.json`. Defaults to the one bundled with the package. | | `--emit-replacement-stub CONTRACT SOL_FILE` | Emit a TypeScript scaffold for a runtime replacement. Body = `throw new Error('Not implemented')`. See [`docs/runtime-replacements.md`](docs/runtime-replacements.md). | | `init ` | Scan a tree and scaffold a starter `transpiler-config.json` + runtime-replacement stubs. See [`docs/init.md`](docs/init.md). | @@ -103,12 +127,11 @@ extruder --emit-replacement-stub CONTRACT SOL_FILE [-o OUTPUT] reading before shipping. - [Extending](docs/extending.md) — contributor-facing; closing gaps in the transpiler itself. -- [FAQ](docs/faq.md) — currently empty; grows as real questions come in. ## Testing ```bash -python3 transpiler/test_transpiler.py +python3 -m transpiler.test_transpiler ``` Python unit tests cover the lexer, parser, codegen (Yul, type casts, @@ -117,4 +140,4 @@ resolver, and the `init` scan. ## License -AGPL-3.0. \ No newline at end of file +AGPL-3.0. diff --git a/transpiler/__init__.py b/transpiler/__init__.py index fb121c27..42975335 100644 --- a/transpiler/__init__.py +++ b/transpiler/__init__.py @@ -9,7 +9,7 @@ - parser/: AST nodes and parsing (Parser, all AST node types) - types/: Type registry and mappings (TypeRegistry, type conversion utilities) - codegen/: Code generation (TypeScriptCodeGenerator + specialized generators) -- sol2ts.py: Main transpiler entry point +- sol2ts.py: Main transpiler entry point (run with `python3 -m transpiler`) Usage: from transpiler import SolidityToTypeScriptTranspiler diff --git a/transpiler/codegen/abi.py b/transpiler/codegen/abi.py index 8e28593a..e2ce01a3 100644 --- a/transpiler/codegen/abi.py +++ b/transpiler/codegen/abi.py @@ -6,7 +6,10 @@ abi.encodePacked, etc. """ -from typing import List, Optional, Dict, Set +from typing import List, Optional, Dict, Set, TYPE_CHECKING + +if TYPE_CHECKING: + from .type_converter import TypeConverter from ..parser.ast_nodes import ( Expression, @@ -36,6 +39,7 @@ def __init__( known_interfaces: Optional[Set[str]] = None, known_struct_fields: Optional[Dict[str, Dict[str, str]]] = None, method_return_types: Optional[Dict[str, str]] = None, + type_converter: Optional['TypeConverter'] = None, ): """ Initialize the ABI type inferer. @@ -47,6 +51,7 @@ def __init__( known_interfaces: Set of known interface type names known_struct_fields: Maps struct names to their field types method_return_types: Maps method names to their return types + type_converter: Optional shared converter for Solidity→ABI type mapping """ self.var_types = var_types or {} self.known_enums = known_enums or set() @@ -54,6 +59,7 @@ def __init__( self.known_interfaces = known_interfaces or set() self.known_struct_fields = known_struct_fields or {} self.method_return_types = method_return_types or {} + self.type_converter = type_converter def infer_abi_types(self, args: List[Expression]) -> str: """ @@ -202,6 +208,9 @@ def _solidity_type_to_abi(self, type_name: str, is_array: bool = False) -> str: Single source of truth for Solidity→ABI type mapping. """ + if self.type_converter: + return self.type_converter.solidity_type_to_abi_param(type_name, is_array) + array_suffix = '[]' if is_array else '' if type_name in ('address', 'string', 'bool'): return f"{{type: '{type_name}{array_suffix}'}}" @@ -268,6 +277,9 @@ def _infer_single_packed_type(self, arg: Expression) -> str: def _get_packed_type(self, type_name: str, is_array: bool = False) -> str: """Get packed type string for a Solidity type.""" + if self.type_converter: + return self.type_converter.solidity_type_to_abi_type(type_name, is_array) + array_suffix = '[]' if is_array else '' if type_name == 'address': return f'address{array_suffix}' diff --git a/transpiler/codegen/base.py b/transpiler/codegen/base.py index c65011f7..d48f8fbc 100644 --- a/transpiler/codegen/base.py +++ b/transpiler/codegen/base.py @@ -5,21 +5,11 @@ used across all specialized generator classes in the code generation pipeline. """ -from typing import Optional, TYPE_CHECKING +from typing import TYPE_CHECKING if TYPE_CHECKING: from .context import CodeGenerationContext -from ..parser.ast_nodes import ( - Expression, - Identifier, - MemberAccess, - IndexAccess, - FunctionCall, - TypeCast, - TypeName, -) - class BaseGenerator: """ @@ -28,7 +18,6 @@ class BaseGenerator: Provides shared utilities for: - Indentation management - Type name resolution - - Expression type analysis - Value formatting """ @@ -94,202 +83,3 @@ def _to_padded_bytes32(self, val: str) -> str: hex_val = hex(int(val))[2:] return f'"0x{hex_val.zfill(64)}"' - # ========================================================================= - # EXPRESSION ANALYSIS - # ========================================================================= - - def _get_base_var_name(self, expr: Expression) -> Optional[str]: - """Extract the root variable name from an expression. - - For nested expressions like a.b.c or a[x][y], returns the root 'a'. - For `this.X` (state-variable access) returns 'X' — the state variable - is keyed by its own name in ``var_types``, not by `this`. - """ - if isinstance(expr, Identifier): - return None if expr.name == 'this' else expr.name - if isinstance(expr, MemberAccess): - if self._is_this_access(expr): - return expr.member - return self._get_base_var_name(expr.expression) - if isinstance(expr, IndexAccess): - return self._get_base_var_name(expr.base) - return None - - @staticmethod - def _is_this_access(expr: Expression) -> bool: - """True when ``expr`` is ``this.`` (state-variable access).""" - return ( - isinstance(expr, MemberAccess) - and isinstance(expr.expression, Identifier) - and expr.expression.name == 'this' - ) - - def _is_bigint_typed_identifier(self, expr: Expression) -> bool: - """Check if expression is an identifier with uint/int type (bigint in TypeScript).""" - if isinstance(expr, Identifier): - name = expr.name - if name in self._ctx.var_types: - type_name = self._ctx.var_types[name].name or '' - return type_name.startswith('uint') or type_name.startswith('int') - return False - - def _is_already_address_type(self, expr: Expression) -> bool: - """Check if expression is already an address type (doesn't need ._contractAddress). - - Returns True for expressions like msg.sender, tx.origin, etc. that are - already strings representing addresses in the TypeScript runtime. - """ - # Check for msg.sender, msg.origin patterns - if isinstance(expr, MemberAccess): - if isinstance(expr.expression, Identifier): - base_name = expr.expression.name - member = expr.member - # msg.sender is already an address string - if base_name == 'msg' and member == 'sender': - return True - # tx.origin is already an address string - if base_name == 'tx' and member == 'origin': - return True - # Check if this is a struct field that's already an address type - if base_name in self._ctx.var_types: - type_info = self._ctx.var_types[base_name] - if type_info.name and type_info.name in self._ctx.known_struct_fields: - struct_fields = self._ctx.known_struct_fields[type_info.name] - if member in struct_fields: - field_info = struct_fields[member] - field_type = field_info[0] if isinstance(field_info, tuple) else field_info - if field_type == 'address': - return True - # Check if it's a simple identifier with address type - if isinstance(expr, Identifier): - if expr.name in self._ctx.var_types: - type_info = self._ctx.var_types[expr.name] - if type_info.name == 'address': - return True - return False - - def _is_numeric_type_cast(self, expr: Expression) -> bool: - """Check if expression is a numeric type cast (uint160, uint256, etc.). - - Returns True for expressions that cast to integer types and produce bigint values. - This is used to properly handle address(uint160(...)) patterns. - """ - # Check for TypeCast to numeric types - if isinstance(expr, TypeCast): - type_name = expr.type_name.name - if type_name.startswith('uint') or type_name.startswith('int'): - return True - # Check for function call casts like uint160(x) - if isinstance(expr, FunctionCall): - if isinstance(expr.function, Identifier): - func_name = expr.function.name - if func_name.startswith('uint') or func_name.startswith('int'): - return True - return False - - def _resolve_access_type(self, expr: Expression) -> Optional[TypeName]: - """Resolve the TypeName at a given expression point. - - Descends through mapping/array/struct accesses so the returned TypeName - describes the container AT THIS LEVEL. Critical for nested access like - ``this.m[a][b]`` where the inner access needs the INNER mapping's - key_type, not the outer one's. - """ - if isinstance(expr, Identifier): - return None if expr.name == 'this' else self._ctx.var_types.get(expr.name) - if isinstance(expr, MemberAccess): - if self._is_this_access(expr): - return self._ctx.var_types.get(expr.member) - return self._resolve_struct_field_type(expr) - if isinstance(expr, IndexAccess): - container = self._resolve_access_type(expr.base) - return self._step_into_container(container) - return None - - def _resolve_struct_field_type(self, expr: MemberAccess) -> Optional[TypeName]: - """Type of a struct-field access, using ``known_struct_fields``.""" - parent_type = self._resolve_access_type(expr.expression) - if not parent_type or not parent_type.name: - return None - struct_fields = self._ctx.known_struct_fields.get(parent_type.name) - if not struct_fields: - return None - field_info = struct_fields.get(expr.member) - if not field_info: - return None - field_type, field_is_array = ( - field_info if isinstance(field_info, tuple) else (field_info, False) - ) - return self._field_info_to_type_name(field_type, field_is_array) - - @staticmethod - def _step_into_container(container: Optional[TypeName]) -> Optional[TypeName]: - """One indexing step: mapping -> value_type, array -> element type.""" - if container is None: - return None - if container.is_mapping: - return container.value_type - if container.is_array: - return TypeName( - name=container.name, - is_array=False, - is_mapping=False, - key_type=None, - value_type=None, - ) - return None - - @staticmethod - def _field_info_to_type_name(field_type: str, field_is_array: bool) -> Optional[TypeName]: - """Best-effort TypeName for a struct field entry from ``known_struct_fields``. - - The registry stores field types as strings; full AST TypeNames aren't - retained. We reconstruct enough for downstream access-type resolution: - mappings get ``is_mapping=True`` with a numeric key (Solidity mappings - on struct fields always use primitive keys in this codebase) and arrays - get ``is_array=True``. - """ - if not field_type: - return None - if field_type.startswith('mapping'): - return TypeName( - name=field_type, - is_mapping=True, - key_type=TypeName(name='uint256'), - value_type=TypeName(name='uint256'), - ) - return TypeName(name=field_type, is_array=field_is_array) - - def _is_likely_array_access(self, access: IndexAccess) -> bool: - """Determine if this is an array access (needs Number index) vs mapping access. - - Uses type registry for accurate detection instead of name heuristics. - """ - # Get the base variable name to look up its type - base_var_name = self._get_base_var_name(access.base) - - if base_var_name and base_var_name in self._ctx.var_types: - type_info = self._ctx.var_types[base_var_name] - # Check the type - arrays need Number(), mappings don't - if type_info.is_array: - return True - if type_info.is_mapping: - return False - - # For member access (e.g., config.p0States[j]), check if the member type is array - if isinstance(access.base, MemberAccess): - # The member access itself may be accessing an array field in a struct - # Without full struct type info, use the index type as a hint - pass - - # Fallback: check if index is a known integer type variable - if isinstance(access.index, Identifier): - index_name = access.index.name - if index_name in self._ctx.var_types: - index_type = self._ctx.var_types[index_name] - # If index is declared as uint/int, it's likely an array access - if index_type.name and (index_type.name.startswith('uint') or index_type.name.startswith('int')): - return True - - return False - diff --git a/transpiler/codegen/diagnostics.py b/transpiler/codegen/diagnostics.py index 4b06d578..52aa5cc8 100644 --- a/transpiler/codegen/diagnostics.py +++ b/transpiler/codegen/diagnostics.py @@ -11,6 +11,9 @@ from enum import Enum from typing import List, Optional +from ..parser.ast_nodes import FunctionDefinition, ModifierDefinition, SourceUnit +from ..parser.visitor import ASTVisitor + class DiagnosticSeverity(Enum): """Severity levels for transpiler diagnostics.""" @@ -240,3 +243,30 @@ def get_summary(self) -> str: parts = [f'{count} {construct}' for construct, count in sorted(by_construct.items())] return f'Transpiler warnings: {", ".join(parts)}' + + +class AstDiagnosticVisitor(ASTVisitor): + """Collect diagnostics from a parsed AST.""" + + def __init__(self, diagnostics: TranspilerDiagnostics, file_path: str = ''): + self.diagnostics = diagnostics + self.file_path = file_path + + def visit_ModifierDefinition(self, node: ModifierDefinition): + self.diagnostics.warn_modifier_stripped(node.name, file_path=self.file_path) + return self.generic_visit(node) + + def visit_FunctionDefinition(self, node: FunctionDefinition): + for mod_name in node.modifiers: + name = mod_name if isinstance(mod_name, str) else str(mod_name) + self.diagnostics.warn_modifier_stripped(name, file_path=self.file_path) + return self.generic_visit(node) + + +def emit_ast_diagnostics( + ast: SourceUnit, + diagnostics: TranspilerDiagnostics, + file_path: str = '', +) -> None: + """Scan an AST and emit diagnostics for degraded/unsupported constructs.""" + AstDiagnosticVisitor(diagnostics, file_path).visit(ast) diff --git a/transpiler/codegen/expression.py b/transpiler/codegen/expression.py index 35e92a87..636f3240 100644 --- a/transpiler/codegen/expression.py +++ b/transpiler/codegen/expression.py @@ -30,6 +30,7 @@ TupleExpression, ArrayLiteral, TypeCast, + TypeName, ) @@ -79,6 +80,7 @@ def _get_abi_inferer(self) -> 'AbiTypeInferer': known_interfaces=self._ctx.known_interfaces, known_struct_fields=self._ctx.known_struct_fields, method_return_types=self._ctx.current_method_return_types, + type_converter=self._type_converter, ) return self._abi_inferer @@ -546,38 +548,11 @@ def _handle_special_function(self, call: FunctionCall, name: str, args: str) -> def _handle_type_cast_call(self, call: FunctionCall, name: str, args: str) -> Optional[str]: """Handle type cast function calls (uint256(x), address(x), etc.).""" - if name.startswith('uint') or name.startswith('int'): - if call.arguments and isinstance(call.arguments[0], Identifier): + if self._is_primitive_cast_name(name): + if len(call.arguments) != 1: return args - if args.isdigit(): - return f'{args}n' - return self._type_converter._ensure_bigint(args) - elif name == 'address': - if call.arguments: - arg = call.arguments[0] - if isinstance(arg, Literal) and arg.kind in ('number', 'hex'): - return self._to_padded_address(arg.value) - if isinstance(arg, Identifier) and arg.name == 'this': - return 'this._contractAddress' - if self._is_already_address_type(arg): - return self.generate(arg) - if self._is_numeric_type_cast(arg): - inner = self.generate(arg) - return f'`0x${{({inner}).toString(16).padStart(40, "0")}}`' - inner = self.generate(arg) - if inner != 'this' and not inner.startswith('"') and not inner.startswith("'"): - return f'{inner}._contractAddress' - return args - elif name == 'bool': - return args - elif name == 'bytes32': - if call.arguments: - arg = call.arguments[0] - if isinstance(arg, Literal) and arg.kind in ('number', 'hex'): - return self._to_padded_bytes32(arg.value) - return args - elif name.startswith('bytes'): - return args + cast = TypeCast(type_name=TypeName(name=name), expression=call.arguments[0]) + return self._type_converter.generate_type_cast(cast, self.generate) elif name.startswith('I') and len(name) > 1 and name[1].isupper(): # Interface cast return self._handle_interface_cast(call, args) @@ -603,6 +578,15 @@ def _handle_type_cast_call(self, call: FunctionCall, name: str, args: str) -> Op return None + @staticmethod + def _is_primitive_cast_name(name: str) -> bool: + return ( + name in ('address', 'bool', 'bytes', 'bytes32', 'payable', 'string') + or name.startswith('uint') + or name.startswith('int') + or (name.startswith('bytes') and name[5:].isdigit()) + ) + def _handle_interface_cast(self, call: FunctionCall, args: str) -> str: """Handle interface type cast like IEffect(address(x)). @@ -680,7 +664,7 @@ def generate_member_access(self, access: MemberAccess) -> str: # Handle .length if member == 'length': - base_var_name = self._get_base_var_name(access.expression) + base_var_name = self._type_converter.base_var_name(access.expression) if base_var_name and base_var_name in self._ctx.var_types: type_info = self._ctx.var_types[base_var_name] type_name = type_info.name if type_info else '' @@ -711,65 +695,17 @@ def generate_index_access(self, access: IndexAccess) -> str: # `this.m[a][b]` or `config.p0States[j]`, this descends through # mappings, arrays, and struct fields so we always see the type of # the thing actually being indexed. - container = self._resolve_access_type(access.base) - is_array = bool(container and container.is_array) or self._is_likely_array_access(access) - is_numeric_keyed_mapping = bool( - container - and container.is_mapping - and container.key_type - and (container.key_type.name or '').startswith(('uint', 'int')) - ) - + is_array, is_numeric_keyed_mapping = self._type_converter.index_access_kind(access) mapping_access = is_numeric_keyed_mapping needs_conversion = is_array or mapping_access - index = self._convert_index(access, index, needs_conversion, mapping_access) - return f'{base}[{index}]' - - # Index expressions that can be safely wrapped in Number(...) / String(...). - _WRAPPABLE_INDEX = (Identifier, BinaryOperation, UnaryOperation, IndexAccess, MemberAccess) - - def _convert_index( - self, - access: IndexAccess, - index: str, - needs_conversion: bool, - mapping_access: bool, - ) -> str: - """Convert the index expression to match the underlying container's key type. - - Mappings transpile to ``Record``; their keys must preserve full - ``bigint`` precision, so we use ``String(idx)`` — ``Number(idx)`` silently - collapses distinct ``uint64`` values above 2^53 that happen to round to the - same IEEE-754 double, causing collisions. - - Real arrays use ``Number(idx)`` because TS's ``T[]`` index signature expects - a number (and bigint-derived indices are always within the safe integer - range in practice). - """ - wrap = 'String' if mapping_access else 'Number' - - # BigInt(N) where N is a numeric literal -> drop the wrapper entirely. - if index.startswith('BigInt('): - inner = index[7:-1] - if inner.isdigit(): - return inner - - # `5n` -> `5` (numeric literal in bigint form) - if isinstance(access.index, Literal) and index.endswith('n'): - return index[:-1] - - # Wrap wrappable index expressions when the container demands it, or - # always wrap bigint-typed identifiers (they'd otherwise index a Record - # with a `bigint` key, which TS rejects). - should_wrap = ( - (needs_conversion and isinstance(access.index, self._WRAPPABLE_INDEX)) - or (isinstance(access.index, Identifier) and self._is_bigint_typed_identifier(access.index)) + index = self._type_converter.convert_index( + access, + index, + needs_conversion, + mapping_access, ) - if should_wrap and not index.startswith(f'{wrap}('): - return f'{wrap}({index})' - - return index + return f'{base}[{index}]' # ========================================================================= # NEW EXPRESSIONS diff --git a/transpiler/codegen/generator.py b/transpiler/codegen/generator.py index 4ac0d1d3..33d2f497 100644 --- a/transpiler/codegen/generator.py +++ b/transpiler/codegen/generator.py @@ -6,7 +6,7 @@ specialized generators for different AST node types. """ -from typing import Optional, Set, Dict, List +from typing import Optional, Set, Dict from .context import CodeGenerationContext from .type_converter import TypeConverter @@ -27,7 +27,7 @@ class TypeScriptCodeGenerator: This class orchestrates the generation of TypeScript code from Solidity AST by coordinating specialized generators for different concerns: - - TypeConverter: Type conversions + - TypeConverter: Type conversions and type-driven semantic decisions - ExpressionGenerator: Expressions - StatementGenerator: Statements - FunctionGenerator: Functions @@ -152,116 +152,6 @@ def generate(self, ast: SourceUnit) -> str: return '\n'.join(output) - # ========================================================================= - # BACKWARD COMPATIBILITY - # ========================================================================= - - # Expose some context properties for backward compatibility - - @property - def indent_level(self) -> int: - """Get the current indentation level.""" - return self._ctx.indent_level - - @indent_level.setter - def indent_level(self, value: int): - """Set the current indentation level.""" - self._ctx.indent_level = value - - @property - def indent_str(self) -> str: - """Get the indentation string.""" - return self._ctx.indent_str - - def indent(self) -> str: - """Return the current indentation string.""" - return self._ctx.indent() - - @property - def current_state_vars(self) -> Set[str]: - """Get the current contract's state variables.""" - return self._ctx.current_state_vars - - @current_state_vars.setter - def current_state_vars(self, value: Set[str]): - self._ctx.current_state_vars = value - - @property - def current_methods(self) -> Set[str]: - """Get the current contract's methods.""" - return self._ctx.current_methods - - @current_methods.setter - def current_methods(self, value: Set[str]): - self._ctx.current_methods = value - - @property - def var_types(self) -> Dict: - """Get the variable types dictionary.""" - return self._ctx.var_types - - @var_types.setter - def var_types(self, value: Dict): - self._ctx.var_types = value - - @property - def known_structs(self) -> Set[str]: - """Get the set of known struct names.""" - return self._ctx.known_structs - - @property - def known_enums(self) -> Set[str]: - """Get the set of known enum names.""" - return self._ctx.known_enums - - @property - def known_contracts(self) -> Set[str]: - """Get the set of known contract names.""" - return self._ctx.known_contracts - - @property - def known_interfaces(self) -> Set[str]: - """Get the set of known interface names.""" - return self._ctx.known_interfaces - - def get_qualified_name(self, name: str) -> str: - """Get the qualified name for a type.""" - return self._ctx.get_qualified_name(name) - - # Delegate to specialized generators for methods that might be called directly - - def generate_expression(self, expr) -> str: - """Generate TypeScript expression (for backward compatibility).""" - return self._expr_generator.generate(expr) - - def generate_statement(self, stmt) -> str: - """Generate TypeScript statement (for backward compatibility).""" - return self._stmt_generator.generate(stmt) - - def generate_function(self, func) -> str: - """Generate TypeScript function (for backward compatibility).""" - return self._func_generator.generate_function(func) - - def generate_struct(self, struct) -> str: - """Generate TypeScript struct (for backward compatibility).""" - return self._def_generator.generate_struct(struct) - - def generate_enum(self, enum) -> str: - """Generate TypeScript enum (for backward compatibility).""" - return self._def_generator.generate_enum(enum) - - def generate_contract(self, contract) -> str: - """Generate TypeScript contract (for backward compatibility).""" - return self._contract_generator.generate_contract(contract) - - def solidity_type_to_ts(self, type_name) -> str: - """Convert Solidity type to TypeScript (for backward compatibility).""" - return self._type_converter.solidity_type_to_ts(type_name) - - def default_value(self, ts_type: str) -> str: - """Get default value for TypeScript type (for backward compatibility).""" - return self._type_converter.default_value(ts_type) - # ========================================================================= # PRIVATE METHODS # ========================================================================= diff --git a/transpiler/codegen/imports.py b/transpiler/codegen/imports.py index 596ff760..604652ee 100644 --- a/transpiler/codegen/imports.py +++ b/transpiler/codegen/imports.py @@ -59,16 +59,16 @@ def generate(self, contract_name: str = '') -> str: lines.append(f"import {{ {', '.join(runtime_imports)} }} from '{prefix}runtime';") # Base contract imports - lines.extend(self._generate_base_contract_imports(prefix)) + lines.extend(self._generate_base_contract_imports()) # Library imports - lines.extend(self._generate_library_imports(prefix, runtime_imports)) + lines.extend(self._generate_library_imports()) # Contract type imports - lines.extend(self._generate_contract_type_imports(prefix, contract_name)) + lines.extend(self._generate_contract_type_imports(contract_name)) # Inherited struct imports - lines.extend(self._generate_inherited_struct_imports(prefix, contract_name)) + lines.extend(self._generate_inherited_struct_imports(contract_name)) # External struct imports lines.extend(self._generate_external_struct_imports(prefix)) @@ -109,7 +109,7 @@ def _build_runtime_imports(self) -> List[str]: return imports - def _generate_base_contract_imports(self, prefix: str) -> List[str]: + def _generate_base_contract_imports(self) -> List[str]: """Generate import statements for base contracts and their inherited structs.""" lines = [] for base_contract in sorted(self._ctx.base_contracts_needed): @@ -132,11 +132,7 @@ def _generate_base_contract_imports(self, prefix: str) -> List[str]: lines.append(f"import {{ {base_contract} }} from '{import_path}';") return lines - def _generate_library_imports( - self, - prefix: str, - runtime_imports: List[str] - ) -> List[str]: + def _generate_library_imports(self) -> List[str]: """Generate import statements for library contracts.""" lines = [] for library in sorted(self._ctx.libraries_referenced): @@ -148,11 +144,7 @@ def _generate_library_imports( lines.append(f"import {{ {singleton_name} }} from '{import_path}';") return lines - def _generate_contract_type_imports( - self, - prefix: str, - contract_name: str - ) -> List[str]: + def _generate_contract_type_imports(self, contract_name: str) -> List[str]: """Generate import statements for contracts used as types.""" lines = [] for contract in sorted(self._ctx.contracts_referenced): @@ -162,11 +154,7 @@ def _generate_contract_type_imports( lines.append(f"import {{ {contract} }} from '{import_path}';") return lines - def _generate_inherited_struct_imports( - self, - prefix: str, - contract_name: str - ) -> List[str]: + def _generate_inherited_struct_imports(self, contract_name: str) -> List[str]: """Generate import statements for inherited structs.""" lines = [] diff --git a/transpiler/codegen/metadata.py b/transpiler/codegen/metadata.py index 0243bbbf..c7d1b812 100644 --- a/transpiler/codegen/metadata.py +++ b/transpiler/codegen/metadata.py @@ -14,6 +14,7 @@ FunctionDefinition, VariableDeclaration, ) +from ..parser.visitor import ASTVisitor from ..dependency_resolver import DependencyResolver @@ -29,13 +30,14 @@ def __init__(self, name: str, kind: str, file_path: str): self.is_abstract = kind == 'abstract' -class MetadataExtractor: +class MetadataExtractor(ASTVisitor): """Extracts metadata from parsed Solidity ASTs.""" def __init__(self): self.contracts: Dict[str, ContractMetadata] = {} self.interfaces: Set[str] = set() self.libraries: Set[str] = set() + self._file_path = '' def extract_from_ast(self, ast: SourceUnit, file_path: str) -> None: """Extract metadata from a parsed AST. @@ -44,29 +46,34 @@ def extract_from_ast(self, ast: SourceUnit, file_path: str) -> None: ast: The parsed SourceUnit file_path: Relative file path (without .sol extension) """ - for contract in ast.contracts: - metadata = ContractMetadata( - name=contract.name, - kind=contract.kind, - file_path=file_path - ) + previous_file_path = self._file_path + self._file_path = file_path + self.visit(ast) + self._file_path = previous_file_path + + def visit_ContractDefinition(self, contract: ContractDefinition) -> None: + metadata = ContractMetadata( + name=contract.name, + kind=contract.kind, + file_path=self._file_path, + ) - # Track contract type - if contract.kind == 'interface': - self.interfaces.add(contract.name) - elif contract.kind == 'library': - self.libraries.add(contract.name) + # Track contract type + if contract.kind == 'interface': + self.interfaces.add(contract.name) + elif contract.kind == 'library': + self.libraries.add(contract.name) - # Extract base contracts - metadata.base_contracts = list(contract.base_contracts) + # Extract base contracts + metadata.base_contracts = list(contract.base_contracts) - # Extract constructor parameters - if contract.constructor: - metadata.constructor_params = self._extract_params( - contract.constructor - ) + # Extract constructor parameters + if contract.constructor: + metadata.constructor_params = self._extract_params( + contract.constructor + ) - self.contracts[contract.name] = metadata + self.contracts[contract.name] = metadata def _extract_params( self, func: FunctionDefinition diff --git a/transpiler/codegen/statement.py b/transpiler/codegen/statement.py index f20eacf1..e83e8b3a 100644 --- a/transpiler/codegen/statement.py +++ b/transpiler/codegen/statement.py @@ -36,9 +36,7 @@ MemberAccess, Identifier, FunctionCall, - Literal, VariableDeclaration, - TypeName, ) @@ -169,7 +167,7 @@ def _generate_nested_mapping_init(self, access: IndexAccess) -> str: lines = [] # Check if this is actually a mapping (not an array) - base_var_name = self._get_base_var_name(access) + base_var_name = self._type_converter.base_var_name(access) if base_var_name and base_var_name in self._ctx.var_types: type_info = self._ctx.var_types[base_var_name] if type_info and not type_info.is_mapping: @@ -183,43 +181,11 @@ def _generate_nested_mapping_init(self, access: IndexAccess) -> str: if deeper_init: lines.append(deeper_init) - init_value = self._get_mapping_init_value(access) + init_value = self._type_converter.mapping_init_value(access) lines.append(f'{self.indent()}{base_expr} ??= {init_value};') return '\n'.join(lines) - def _get_mapping_init_value(self, access: IndexAccess) -> str: - """Determine the initialization value for a mapping access.""" - base_var_name = self._get_base_var_name(access.base) - if not base_var_name or base_var_name not in self._ctx.var_types: - return '{}' - - type_info = self._ctx.var_types[base_var_name] - if not type_info or not type_info.is_mapping: - return '{}' - - # Navigate through nested mappings to find the value type at this level - depth = 0 - current = access - while isinstance(current.base, IndexAccess): - depth += 1 - current = current.base - - value_type = type_info.value_type - for _ in range(depth): - if value_type and value_type.is_mapping: - value_type = value_type.value_type - else: - break - - if value_type: - if value_type.is_array: - return '[]' - elif value_type.is_mapping: - return '{}' - - return '{}' - # ========================================================================= # VARIABLE DECLARATIONS # ========================================================================= @@ -247,9 +213,18 @@ def generate_variable_declaration_statement(self, stmt: VariableDeclarationState return storage_init init_expr = self._expr.generate(stmt.initial_value) - init_expr = self._add_mapping_default(stmt.initial_value, ts_type, init_expr, decl.type_name) + init_expr = self._type_converter.add_mapping_default( + stmt.initial_value, + ts_type, + init_expr, + decl.type_name, + ) # bytes32 initialized with string literal: convert to hex-padded bytes32 - init_expr = self._convert_bytes32_string_literal(decl.type_name, stmt.initial_value, init_expr) + init_expr = self._type_converter.convert_bytes_string_literal( + decl.type_name, + stmt.initial_value, + init_expr, + ) init = f' = {init_expr}' else: default_val = self._type_converter.default_value(ts_type, decl.type_name) @@ -336,71 +311,6 @@ def _get_storage_init_statement( lines.append(f'{self.indent()}let {decl.name}: {ts_type} = {mapping_expr}[{key_expr}];') return '\n'.join(lines) - def _add_mapping_default( - self, - expr: Expression, - ts_type: str, - generated_expr: str, - solidity_type: Optional[TypeName] = None - ) -> str: - """Add default value for mapping reads to simulate Solidity mapping semantics.""" - if not isinstance(expr, IndexAccess): - return generated_expr - - is_mapping_read = False - - base_var_name = self._get_base_var_name(expr.base) - if base_var_name and base_var_name in self._ctx.var_types: - type_info = self._ctx.var_types[base_var_name] - is_mapping_read = type_info.is_mapping - - if isinstance(expr.base, MemberAccess): - if isinstance(expr.base.expression, Identifier) and expr.base.expression.name == 'this': - member_name = expr.base.member - if member_name in self._ctx.var_types: - type_info = self._ctx.var_types[member_name] - is_mapping_read = type_info.is_mapping - - # Check if the base identifier has a mapping type in var_types - if isinstance(expr.base, Identifier): - name = expr.base.name - if name in self._ctx.var_types: - type_info = self._ctx.var_types[name] - if type_info.is_mapping: - is_mapping_read = True - elif name in self._ctx.current_state_vars: - # State vars that aren't in var_types but are accessed with index - # are likely mappings (conservative: treat as mapping for default values) - is_mapping_read = True - - if not is_mapping_read: - return generated_expr - - default_value = self._type_converter.default_value(ts_type, solidity_type) - if default_value and default_value != 'undefined as any': - return f'({generated_expr} ?? {default_value})' - return generated_expr - - def _convert_bytes32_string_literal(self, type_name, initial_value, init_expr: str) -> str: - """Convert a string literal to hex-padded bytes32 when assigned to a bytes32 variable. - - In Solidity, `bytes32 x = "STATUS_EFFECT"` right-pads the UTF-8 bytes with zeros - to fill 32 bytes. The transpiler must produce the equivalent hex string. - """ - if (type_name and hasattr(type_name, 'name') and type_name.name - and type_name.name.startswith('bytes') - and isinstance(initial_value, Literal) and initial_value.kind == 'string'): - string_val = initial_value.value.strip('"\'') - hex_bytes = string_val.encode('utf-8').hex() - # Determine byte size (bytes32 = 32, bytes16 = 16, etc.) - size_str = type_name.name[5:] - byte_size = int(size_str) if size_str.isdigit() else 32 - hex_bytes = hex_bytes[:byte_size * 2].ljust(byte_size * 2, '0') - return f'"0x{hex_bytes}"' - return init_expr - - # _get_ts_default_value removed — consolidated into type_converter.default_value() - def _get_small_int_conversions_from_decode(self, stmt: VariableDeclarationStatement) -> List[str]: """Get list of variable names that need BigInt conversion from abi.decode. @@ -568,6 +478,9 @@ def generate_return_statement(self, stmt: ReturnStatement) -> str: def generate_delete_statement(self, stmt: DeleteStatement) -> str: """Generate TypeScript code for a delete statement.""" expr = self._expr.generate(stmt.expression) + default_value = self._type_converter.delete_default(stmt.expression) + if default_value is not None: + return f'{self.indent()}{expr} = {default_value};' return f'{self.indent()}delete {expr};' # ========================================================================= diff --git a/transpiler/codegen/type_converter.py b/transpiler/codegen/type_converter.py index b1d66857..ca297340 100644 --- a/transpiler/codegen/type_converter.py +++ b/transpiler/codegen/type_converter.py @@ -1,32 +1,37 @@ -""" -Type conversion utilities for code generation. +"""Type reasoning for code generation. -This module provides the TypeConverter class that handles Solidity to TypeScript -type conversions during code generation, with context-awareness for tracking -imports and handling complex type scenarios. +``TypeConverter`` owns Solidity-to-TypeScript type conversion, default values, +type-cast emission, and the higher-level semantic decisions (expression type +resolution, mapping/index handling, ABI type mapping) that the generators rely +on. It is the single source of truth for type questions during emission. """ -from typing import Optional, Set, Dict, TYPE_CHECKING +from typing import Optional, Tuple, TYPE_CHECKING if TYPE_CHECKING: from .context import CodeGenerationContext from ..type_system import TypeRegistry from .base import BaseGenerator -from ..parser.ast_nodes import TypeName, Expression, Literal, TypeCast, FunctionCall, Identifier +from ..parser.ast_nodes import ( + BinaryOperation, + Expression, + FunctionCall, + Identifier, + IndexAccess, + Literal, + MemberAccess, + TypeCast, + TypeName, + UnaryOperation, +) class TypeConverter(BaseGenerator): - """ - Handles Solidity to TypeScript type conversions. + """Solidity-to-TypeScript type conversion and type-driven semantic decisions.""" - This class provides context-aware type conversion that: - - Converts Solidity types to TypeScript types - - Tracks used types for import generation - - Handles special cases like EnumerableSetLib and contract types - - Provides default values for TypeScript types - - Generates type cast expressions - """ + # Index expressions that can be safely wrapped in Number(...) / String(...). + _WRAPPABLE_INDEX = (Identifier, BinaryOperation, UnaryOperation, IndexAccess, MemberAccess) def __init__( self, @@ -381,6 +386,47 @@ def _ensure_bigint(expr: str) -> str: return expr return f'BigInt({expr})' + def _is_already_address_type(self, expr: Expression) -> bool: + """Check if expression is already an address type.""" + if isinstance(expr, MemberAccess): + if isinstance(expr.expression, Identifier): + base_name = expr.expression.name + member = expr.member + if base_name == 'msg' and member == 'sender': + return True + if base_name == 'tx' and member == 'origin': + return True + if base_name in self._ctx.var_types: + type_info = self._ctx.var_types[base_name] + if type_info.name and type_info.name in self._ctx.known_struct_fields: + struct_fields = self._ctx.known_struct_fields[type_info.name] + if member in struct_fields: + field_info = struct_fields[member] + field_type = field_info[0] if isinstance(field_info, tuple) else field_info + if field_type == 'address': + return True + + if isinstance(expr, Identifier): + if expr.name in self._ctx.var_types: + type_info = self._ctx.var_types[expr.name] + if type_info.name == 'address': + return True + return False + + @staticmethod + def _is_numeric_type_cast(expr: Expression) -> bool: + """Check if expression is a numeric type cast.""" + if isinstance(expr, TypeCast): + type_name = expr.type_name.name + if type_name.startswith('uint') or type_name.startswith('int'): + return True + if isinstance(expr, FunctionCall): + if isinstance(expr.function, Identifier): + func_name = expr.function.name + if func_name.startswith('uint') or func_name.startswith('int'): + return True + return False + def get_mapping_value_type(self, type_name: TypeName) -> Optional[str]: """Get the value type of a mapping, recursively handling nested mappings.""" if not type_name.is_mapping: @@ -405,3 +451,308 @@ def get_array_element_type(self, type_name: TypeName) -> str: value_type=type_name.value_type, ) return self.solidity_type_to_ts(element_type) + + # ========================================================================= + # EXPRESSION ANALYSIS + # ========================================================================= + + def base_var_name(self, expr: Expression) -> Optional[str]: + """Extract the root variable name from an expression. + + For nested expressions like ``a.b.c`` or ``a[x][y]``, returns ``a``. + For ``this.X`` state-variable access, returns ``X``. + """ + if isinstance(expr, Identifier): + return None if expr.name == 'this' else expr.name + if isinstance(expr, MemberAccess): + if self.is_this_access(expr): + return expr.member + return self.base_var_name(expr.expression) + if isinstance(expr, IndexAccess): + return self.base_var_name(expr.base) + return None + + @staticmethod + def is_this_access(expr: Expression) -> bool: + """True when ``expr`` is ``this.``.""" + return ( + isinstance(expr, MemberAccess) + and isinstance(expr.expression, Identifier) + and expr.expression.name == 'this' + ) + + def is_bigint_typed_identifier(self, expr: Expression) -> bool: + """True for identifiers declared as Solidity uint/int types.""" + if isinstance(expr, Identifier): + name = expr.name + if name in self._ctx.var_types: + type_name = self._ctx.var_types[name].name or '' + return type_name.startswith('uint') or type_name.startswith('int') + return False + + def resolve_access_type(self, expr: Expression) -> Optional[TypeName]: + """Resolve the ``TypeName`` at a given expression point.""" + if isinstance(expr, Identifier): + return None if expr.name == 'this' else self._ctx.var_types.get(expr.name) + if isinstance(expr, MemberAccess): + if self.is_this_access(expr): + return self._ctx.var_types.get(expr.member) + return self.resolve_struct_field_type(expr) + if isinstance(expr, IndexAccess): + container = self.resolve_access_type(expr.base) + return self.step_into_container(container) + return None + + def resolve_struct_field_type(self, expr: MemberAccess) -> Optional[TypeName]: + """Type of a struct-field access, using ``known_struct_fields``.""" + parent_type = self.resolve_access_type(expr.expression) + if not parent_type or not parent_type.name: + return None + struct_fields = self._ctx.known_struct_fields.get(parent_type.name) + if not struct_fields: + return None + field_info = struct_fields.get(expr.member) + if not field_info: + return None + field_type, field_is_array = ( + field_info if isinstance(field_info, tuple) else (field_info, False) + ) + return self.field_info_to_type_name(field_type, field_is_array) + + @staticmethod + def step_into_container(container: Optional[TypeName]) -> Optional[TypeName]: + """One indexing step: mapping -> value_type, array -> element type.""" + if container is None: + return None + if container.is_mapping: + return container.value_type + if container.is_array: + return TypeName( + name=container.name, + is_array=False, + is_mapping=False, + key_type=None, + value_type=None, + ) + return None + + @staticmethod + def field_info_to_type_name(field_type: str, field_is_array: bool) -> Optional[TypeName]: + """Best-effort ``TypeName`` for a struct field registry entry.""" + if not field_type: + return None + if field_type.startswith('mapping'): + return TypeName( + name=field_type, + is_mapping=True, + key_type=TypeName(name='uint256'), + value_type=TypeName(name='uint256'), + ) + return TypeName(name=field_type, is_array=field_is_array) + + def is_likely_array_access(self, access: IndexAccess) -> bool: + """Determine if an index access is array-like rather than mapping-like.""" + base_var_name = self.base_var_name(access.base) + + if base_var_name and base_var_name in self._ctx.var_types: + type_info = self._ctx.var_types[base_var_name] + if type_info.is_array: + return True + if type_info.is_mapping: + return False + + if isinstance(access.index, Identifier): + index_name = access.index.name + if index_name in self._ctx.var_types: + index_type = self._ctx.var_types[index_name] + if index_type.name and index_type.name.startswith(('uint', 'int')): + return True + + return False + + def is_mapping_read(self, expr: Expression) -> bool: + """Return True if an index expression reads from a mapping container.""" + if not isinstance(expr, IndexAccess): + return False + + base_var_name = self.base_var_name(expr.base) + if base_var_name and base_var_name in self._ctx.var_types: + type_info = self._ctx.var_types[base_var_name] + if type_info.is_mapping: + return True + + if isinstance(expr.base, MemberAccess): + if isinstance(expr.base.expression, Identifier) and expr.base.expression.name == 'this': + member_name = expr.base.member + if member_name in self._ctx.var_types: + type_info = self._ctx.var_types[member_name] + if type_info.is_mapping: + return True + + if isinstance(expr.base, Identifier): + name = expr.base.name + if name in self._ctx.var_types: + type_info = self._ctx.var_types[name] + if type_info.is_mapping: + return True + if name in self._ctx.current_state_vars: + # Conservative fallback for state vars whose TypeName was not + # threaded into var_types. + return True + + return False + + # ========================================================================= + # DELETE / MAPPING DEFAULTS + # ========================================================================= + + def delete_default(self, expr: Expression) -> Optional[str]: + """Return Solidity's zero/default value for a delete target if known.""" + type_info = self.resolve_access_type(expr) + if not type_info: + return None + ts_type = self.solidity_type_to_ts(type_info) + default_value = self.default_value(ts_type, type_info) + if default_value == 'undefined as any': + return None + return default_value + + def mapping_init_value(self, access: IndexAccess) -> str: + """Determine the initialization value for a mapping access.""" + base_var_name = self.base_var_name(access.base) + if not base_var_name or base_var_name not in self._ctx.var_types: + return '{}' + + type_info = self._ctx.var_types[base_var_name] + if not type_info or not type_info.is_mapping: + return '{}' + + # Navigate through nested mappings to find the value type at this level. + depth = 0 + current = access + while isinstance(current.base, IndexAccess): + depth += 1 + current = current.base + + value_type = type_info.value_type + for _ in range(depth): + if value_type and value_type.is_mapping: + value_type = value_type.value_type + else: + break + + if value_type: + if value_type.is_array: + return '[]' + if value_type.is_mapping: + return '{}' + + return '{}' + + def add_mapping_default( + self, + expr: Expression, + ts_type: str, + generated_expr: str, + solidity_type: Optional[TypeName] = None, + ) -> str: + """Add default value for mapping reads to simulate Solidity semantics.""" + if not self.is_mapping_read(expr): + return generated_expr + + default_value = self.default_value(ts_type, solidity_type) + if default_value and default_value != 'undefined as any': + return f'({generated_expr} ?? {default_value})' + return generated_expr + + @staticmethod + def convert_bytes_string_literal( + type_name: Optional[TypeName], + initial_value: Expression, + init_expr: str, + ) -> str: + """Convert string literals assigned to bytesN into right-padded hex.""" + if not ( + type_name + and getattr(type_name, 'name', '') + and type_name.name.startswith('bytes') + and isinstance(initial_value, Literal) + and initial_value.kind == 'string' + ): + return init_expr + + string_val = initial_value.value.strip('"\'') + hex_bytes = string_val.encode('utf-8').hex() + size_str = type_name.name[5:] + byte_size = int(size_str) if size_str.isdigit() else 32 + hex_bytes = hex_bytes[:byte_size * 2].ljust(byte_size * 2, '0') + return f'"0x{hex_bytes}"' + + # ========================================================================= + # INDEX CONVERSION + # ========================================================================= + + def index_access_kind(self, access: IndexAccess) -> Tuple[bool, bool]: + """Return ``(is_array, is_numeric_keyed_mapping)`` for an index access.""" + container = self.resolve_access_type(access.base) + is_array = bool(container and container.is_array) or self.is_likely_array_access(access) + is_numeric_keyed_mapping = bool( + container + and container.is_mapping + and container.key_type + and (container.key_type.name or '').startswith(('uint', 'int')) + ) + return is_array, is_numeric_keyed_mapping + + def convert_index( + self, + access: IndexAccess, + index: str, + needs_conversion: bool, + mapping_access: bool, + ) -> str: + """Convert an index expression to match the container key type. + + Mappings transpile to ``Record`` and preserve bigint + precision via ``String(idx)``. Arrays use ``Number(idx)`` because TS + array indices are numeric. + """ + wrap = 'String' if mapping_access else 'Number' + + if index.startswith('BigInt('): + inner = index[7:-1] + if inner.isdigit(): + return inner + + if isinstance(access.index, Literal) and index.endswith('n'): + return index[:-1] + + should_wrap = ( + (needs_conversion and isinstance(access.index, self._WRAPPABLE_INDEX)) + or (isinstance(access.index, Identifier) and self.is_bigint_typed_identifier(access.index)) + ) + if should_wrap and not index.startswith(f'{wrap}('): + return f'{wrap}({index})' + + return index + + # ========================================================================= + # ABI TYPE MAPPING + # ========================================================================= + + def solidity_type_to_abi_param(self, type_name: str, is_array: bool = False) -> str: + """Convert a Solidity type name to a viem ABI parameter object string.""" + return f"{{type: '{self.solidity_type_to_abi_type(type_name, is_array)}'}}" + + def solidity_type_to_abi_type(self, type_name: str, is_array: bool = False) -> str: + """Convert a Solidity type name to an ABI type string.""" + array_suffix = '[]' if is_array else '' + if type_name in ('address', 'string', 'bool'): + return f'{type_name}{array_suffix}' + if type_name.startswith(('uint', 'int', 'bytes')): + return f'{type_name}{array_suffix}' + if type_name in self._ctx.known_enums: + return f'uint8{array_suffix}' + if type_name in self._ctx.known_contracts or type_name in self._ctx.known_interfaces: + return f'address{array_suffix}' + return f'uint256{array_suffix}' diff --git a/transpiler/config.py b/transpiler/config.py new file mode 100644 index 00000000..229f9ed5 --- /dev/null +++ b/transpiler/config.py @@ -0,0 +1,186 @@ +"""Shared loader for ``transpiler-config.json``. + +The config file is consumed by code generation, dependency resolution, and +``extruder init``. This module keeps schema parsing and path normalization in +one place while preserving the existing public APIs that accept a config path. +""" + +from dataclasses import dataclass, field +import json +from pathlib import Path +from typing import Dict, List, Optional, Set, Union + + +DependencyOverride = Union[str, List[str]] + + +def normalize_config_path(path: str) -> str: + """Normalize config paths to POSIX-style relative strings.""" + return str(Path(path)).replace('\\', '/') + + +def merge_config_updates( + existing: Optional[dict] = None, + *, + skip_files: Optional[List[str]] = None, + interface_aliases: Optional[Dict[str, Optional[str]]] = None, + dependency_overrides: Optional[Dict[str, Dict[str, DependencyOverride]]] = None, + runtime_replacements: Optional[List[dict]] = None, +) -> dict: + """Merge generated config updates without overwriting user choices. + + Existing aliases, dependency overrides, and replacement sources win on + conflict. New skip paths are unioned and normalized for consistent writes. + """ + existing = existing or {} + merged = dict(existing) + + merged_skip_files = sorted({ + normalize_config_path(str(path)) + for path in existing.get('skipFiles', []) + } | { + normalize_config_path(str(path)) + for path in (skip_files or []) + }) + + merged_aliases = dict(existing.get('interfaceAliases', {})) + for key, value in (interface_aliases or {}).items(): + merged_aliases.setdefault(key, value) + + merged_dep_overrides: Dict[str, Dict[str, DependencyOverride]] = { + key: dict(value) + for key, value in existing.get('dependencyOverrides', {}).items() + } + for contract_name, params in (dependency_overrides or {}).items(): + dest = merged_dep_overrides.setdefault(contract_name, {}) + for param_name, impl in params.items(): + dest.setdefault(param_name, impl) + + merged_replacements = list(existing.get('runtimeReplacements', [])) + existing_sources = { + normalize_config_path(str(entry.get('source') or '')) + for entry in merged_replacements + if isinstance(entry, dict) + } + for replacement in runtime_replacements or []: + source = normalize_config_path(str(replacement.get('source') or '')) + if not source or source in existing_sources: + continue + merged_replacements.append(replacement) + existing_sources.add(source) + + if merged_skip_files: + merged['skipFiles'] = merged_skip_files + if merged_aliases: + merged['interfaceAliases'] = merged_aliases + if merged_dep_overrides: + merged['dependencyOverrides'] = merged_dep_overrides + if merged_replacements: + merged['runtimeReplacements'] = merged_replacements + + return merged + + +@dataclass +class TranspilerConfig: + """Normalized representation of ``transpiler-config.json``.""" + + runtime_replacements: Dict[str, dict] = field(default_factory=dict) + runtime_replacement_classes: Set[str] = field(default_factory=set) + runtime_replacement_mixins: Dict[str, str] = field(default_factory=dict) + runtime_replacement_methods: Dict[str, Set[str]] = field(default_factory=dict) + skip_files: Set[str] = field(default_factory=set) + skip_dirs: Set[str] = field(default_factory=set) + dependency_overrides: Dict[str, Dict[str, DependencyOverride]] = field(default_factory=dict) + interface_aliases: Dict[str, Optional[str]] = field(default_factory=dict) + raw: dict = field(default_factory=dict) + + @classmethod + def default_path(cls) -> Path: + return Path(__file__).parent / 'transpiler-config.json' + + @classmethod + def load( + cls, + path: Optional[str | Path] = None, + *, + warn_missing: bool = False, + label: str = 'transpiler-config.json', + ) -> 'TranspilerConfig': + """Load and normalize a config file. + + Missing files return an empty config. Invalid JSON also returns an empty + config after printing a warning, matching the historical forgiving + behavior. + """ + config_path = Path(path) if path else cls.default_path() + if not config_path.exists(): + if warn_missing: + print(f"Warning: {label} not found at {config_path}") + return cls() + + try: + data = json.loads(config_path.read_text()) + except json.JSONDecodeError as e: + print(f"Warning: Failed to parse {config_path}: {e}") + return cls() + + return cls.from_dict(data) + + @classmethod + def from_dict(cls, data: dict) -> 'TranspilerConfig': + cfg = cls(raw=data) + + for replacement in data.get('runtimeReplacements', []): + source_path = normalize_config_path(replacement.get('source', '')) + if not source_path: + continue + + cfg.runtime_replacements[source_path] = replacement + + for export in replacement.get('exports', []): + cfg.runtime_replacement_classes.add(export) + + interface = replacement.get('interface', {}) + class_name = interface.get('class', '') + mixin_code = interface.get('mixin', '') + if class_name and mixin_code: + cfg.runtime_replacement_mixins[class_name] = mixin_code + + methods = interface.get('methods', []) + if class_name and methods: + cfg.runtime_replacement_methods[class_name] = { + m.get('name', '') for m in methods if m.get('name') + } + + cfg.skip_files = { + normalize_config_path(path) + for path in data.get('skipFiles', []) + } + cfg.skip_dirs = { + normalize_config_path(path).rstrip('/') + for path in data.get('skipDirs', []) + } + + # Consolidated config uses dependencyOverrides; legacy + # dependency-overrides.json used a top-level overrides key. + cfg.dependency_overrides = data.get( + 'dependencyOverrides', + data.get('overrides', {}), + ) + cfg.interface_aliases = data.get('interfaceAliases', {}) + return cfg + + def should_skip_file(self, rel_path: str) -> bool: + return normalize_config_path(rel_path) in self.skip_files + + def should_skip_dir(self, rel_path: str) -> bool: + rel = normalize_config_path(rel_path) + return any(rel == d or rel.startswith(d + '/') for d in self.skip_dirs) + + def runtime_replacement_for(self, rel_path: str) -> Optional[dict]: + rel = normalize_config_path(rel_path) + for source_pattern, replacement in self.runtime_replacements.items(): + if rel == source_pattern or rel.endswith(source_pattern): + return replacement + return None diff --git a/transpiler/dependency_resolver/resolver.py b/transpiler/dependency_resolver/resolver.py index 5a65aaa1..387bf71b 100644 --- a/transpiler/dependency_resolver/resolver.py +++ b/transpiler/dependency_resolver/resolver.py @@ -9,11 +9,10 @@ Unresolved dependencies are tracked and can be exported for user action. """ -import json -from pathlib import Path from typing import Dict, List, Optional, Set, Tuple, Union -from dataclasses import dataclass, field +from dataclasses import dataclass +from ..config import TranspilerConfig from .name_inferrer import NameInferrer @@ -100,15 +99,9 @@ def _load_overrides(self, path: str) -> None: Legacy `dependency-overrides.json` with a top-level `overrides` key is also accepted for backwards compatibility. """ - try: - with open(path, 'r') as f: - data = json.load(f) - self.overrides = data.get('dependencyOverrides', data.get('overrides', {})) - self.interface_aliases = data.get('interfaceAliases', {}) - except FileNotFoundError: - pass # No overrides file is fine - except json.JSONDecodeError as e: - print(f"Warning: Failed to parse {path}: {e}") + config = TranspilerConfig.load(path, warn_missing=False) + self.overrides = config.dependency_overrides + self.interface_aliases = config.interface_aliases def add_known_class(self, class_name: str) -> None: """Add a known concrete class.""" @@ -325,4 +318,3 @@ def export_unresolved(self, output_path: str) -> None: with open(output_path, 'w') as f: json.dump(output, f, indent=2) - diff --git a/transpiler/docs/configuration.md b/transpiler/docs/configuration.md index 37cfd9b4..fe81e711 100644 --- a/transpiler/docs/configuration.md +++ b/transpiler/docs/configuration.md @@ -36,6 +36,10 @@ this Solidity source, emit a stub that re-exports from this runtime module." Use for Solidity whose Yul the generator can't handle, or whose semantics need hand-written TypeScript. +Runtime replacements are explicit and take precedence over `skipFiles` and +`skipDirs`. If a file appears in both places, the transpiler still emits the +runtime re-export and does not parse that Solidity file. + | Field | Purpose | |---|---| | `source` | Path to the `.sol` file, relative to the source root. | @@ -104,8 +108,9 @@ you don't want in your simulator (e.g., legacy migration contracts). ## Field: `skipFiles` Files listed here are skipped at the filesystem level — never parsed, -never transpiled. Nothing from the file is available for cross-file type -discovery. Use for files that can't be parsed, files your simulator +never transpiled, unless the same file has a `runtimeReplacements` entry. +Nothing from the file is available for cross-file type discovery. Use for +files that can't be parsed, files your simulator doesn't need, or files that transpile incorrectly but also aren't worth a runtime replacement. @@ -120,6 +125,15 @@ domain-specific subtrees that don't belong in your simulator (e.g., a ## Precedence rules +File-level transpilation: + +1. `runtimeReplacements[source]` — emits a TypeScript re-export and avoids + parsing the Solidity file. +2. `skipFiles` / `skipDirs` — skips files with no runtime replacement. +3. Normal parse + code generation. + +Dependency resolution: + When multiple sources could decide a dependency: 1. `dependencyOverrides[Contract][param]` — always wins. @@ -131,4 +145,4 @@ When multiple sources could decide a dependency: 5. Unresolved — logged to `unresolved-dependencies.json`. `extruder init` runs exactly this chain dry at bootstrap time and prompts -for anything step 5 would catch. \ No newline at end of file +for anything step 5 would catch. diff --git a/transpiler/docs/extending.md b/transpiler/docs/extending.md index fd44e676..029e2f71 100644 --- a/transpiler/docs/extending.md +++ b/transpiler/docs/extending.md @@ -42,7 +42,7 @@ wire it into codegen, add a test. 1. In `runtime/base.ts`, define `SolidityRevertError extends Error` carrying `reason: string` and `data: string`. 2. In `codegen/expression.py`, change the `require` lowering from - `throw new Error("Require failed")` to `throw new SolidityRevertError()`. + `throw new Error(...)` to `throw new SolidityRevertError()`. Do the same for `RevertStatement` in `codegen/statement.py`, and for `revert CustomError(...)` encode the error selector + args into `data`. 3. Register `SolidityRevertError` in `ImportGenerator`'s runtime imports so diff --git a/transpiler/docs/init.md b/transpiler/docs/init.md index 518dd536..0b36cc9c 100644 --- a/transpiler/docs/init.md +++ b/transpiler/docs/init.md @@ -4,9 +4,10 @@ need, and writes a starter `transpiler-config.json`. ```bash -python3 /path/to/extruder/sol2ts.py init src/ +cd /path/to/extruder +python3 -m transpiler init /path/to/your/foundry/project/src # or non-interactive: -python3 /path/to/extruder/sol2ts.py init src/ --yes +python3 -m transpiler init /path/to/your/foundry/project/src --yes ``` ## What it produces diff --git a/transpiler/docs/quickstart.md b/transpiler/docs/quickstart.md index b4dbcd55..31c34f13 100644 --- a/transpiler/docs/quickstart.md +++ b/transpiler/docs/quickstart.md @@ -1,6 +1,8 @@ # Quickstart -You have a Foundry project with contracts in `src/`. `extruder` can generate files in `ts-output/`, a scaffolded config, and a test file that calls into the transpiled contracts. +You have a Foundry project with contracts in `src/`. `extruder` can generate +TypeScript files in `ts-output/`, plus a scaffolded config and runtime +replacement stubs for Solidity files that need hand-written TypeScript. ## 1. Install @@ -8,9 +10,16 @@ Clone extruder somewhere ```bash git clone ~/tools/extruder -cd ~/tools/extruder && pip install -r requirements.txt +cd ~/tools/extruder +python3 -m pip install -e ./transpiler +extruder --help ``` +If you skip installation, run extruder with `python3 -m transpiler` from the +directory that contains the `transpiler/` package. Running +`transpiler/sol2ts.py` directly will fail because the package uses relative +imports. + Add the JS runtime deps to your own Foundry project: ```bash @@ -21,7 +30,8 @@ npm install -D viem vitest ## 2. Bootstrap your config with `extruder init` ```bash -python3 ~/tools/extruder/sol2ts.py init src/ --yes +cd ~/tools/extruder +extruder init /path/to/your/foundry/project/src --yes ``` This scans `src/` and writes: @@ -41,7 +51,7 @@ Drop `--yes` to run interactively. For each file under `runtime-replacements/`, replace the `throw new Error('Not implemented…')` bodies with real logic. See [`runtime-replacements.md`](runtime-replacements.md) for common patterns and -the ECDSA reference implementation. +the runtime replacement examples. You can defer this step if you want to see the transpiler run end-to-end — the stubs throw loudly at call time, so missing implementations fail fast @@ -50,7 +60,10 @@ rather than silently. ## 4. Transpile ```bash -python3 ~/tools/extruder/sol2ts.py src/ -o ts-output -d src --emit-metadata +extruder /path/to/your/foundry/project/src \ + -o /path/to/your/foundry/project/ts-output \ + -d /path/to/your/foundry/project/src \ + --emit-metadata ``` - `src/` is the input tree. diff --git a/transpiler/docs/runtime-replacements.md b/transpiler/docs/runtime-replacements.md index a410fdc7..636f4626 100644 --- a/transpiler/docs/runtime-replacements.md +++ b/transpiler/docs/runtime-replacements.md @@ -22,18 +22,23 @@ finish the job. ## Anatomy of a replacement -Here's the reference implementation: `runtime/ECDSA.ts`. It stands in for -Solady's `lib/ECDSA.sol`, which uses the `staticcall` precompile for -secp256k1 recovery. +Here's one reference implementation: `runtime/Ownable.ts`. It stands in for +Solady's `lib/Ownable.sol`, whose storage-slot-heavy Yul is better represented +by a small hand-written TypeScript class. ```ts import { Contract } from './base'; -export class ECDSA extends Contract { - static recover(hash: `0x${string}`, signature: `0x${string}`): string { - // ... real viem-backed secp256k1 recovery ... +export abstract class Ownable extends Contract { + private _owner: string = '0x0000000000000000000000000000000000000000'; + + protected _checkOwner(): void { + if (this._msg.sender !== this._owner) throw new Error('Unauthorized'); + } + + owner(): string { + return this._owner; } - // ... other signature-handling methods ... } ``` @@ -44,18 +49,19 @@ Two things to notice: same as transpiled contracts. You can write state if you want; you usually don't. 2. **No explicit registration.** The only thing connecting this file to the - transpiler is the `transpiler-config.json` entry below. The generator - writes `import { ECDSA } from '../runtime-replacements'` into every - generated file that originally imported `ECDSA.sol`. + transpiler is the `transpiler-config.json` entry below. The generator emits + a re-export stub for the replaced Solidity file, so downstream imports keep + working while resolving to your hand-written module. ## Authoring workflow ### 1. Scaffold ```bash -python3 /path/to/extruder/sol2ts.py \ - --emit-replacement-stub MyLib src/lib/MyLib.sol \ - -o runtime-replacements/MyLib.ts +cd /path/to/extruder +python3 -m transpiler \ + --emit-replacement-stub MyLib /path/to/your/foundry/project/src/lib/MyLib.sol \ + -o /path/to/your/foundry/project/runtime-replacements/MyLib.ts ``` This produces a TypeScript class with: @@ -115,8 +121,12 @@ now routes through your replacement. the rest of the contract transpiles fine. If your only concern is access control, fix it in the test harness (spoof `msg.sender`), not via a replacement. -- **Diagnostics (W002 / W003 / W004).** Degraded fidelity, but the - contract still transpiles. Audit case-by-case; a replacement is - overkill for most. +- **Skipped receive/fallback functions (W003).** Degraded fidelity, but the + contract still transpiles. Audit case-by-case; a replacement is overkill for + most. - **You just don't want this file transpiled.** Use `skipFiles` or `skipContracts` in the config; no replacement needed. + +If a source file appears in both `runtimeReplacements` and `skipFiles` or +`skipDirs`, the runtime replacement wins. Remove the replacement entry when +you mean "skip this entirely." diff --git a/transpiler/docs/runtime.md b/transpiler/docs/runtime.md index 6e9471b4..e43b7fc9 100644 --- a/transpiler/docs/runtime.md +++ b/transpiler/docs/runtime.md @@ -208,8 +208,8 @@ Event plumbing: ## Runtime replacements -The runtime ships one reference replacement: `ECDSA.ts`. See -[`runtime-replacements.md`](runtime-replacements.md) for the full authoring -workflow; the short version is that you scaffold via +The runtime ships reference replacements such as `Ownable.ts` and +`EnumerableSetLib.ts`. See [`runtime-replacements.md`](runtime-replacements.md) +for the full authoring workflow; the short version is that you scaffold via `--emit-replacement-stub`, fill in bodies, and register in `transpiler-config.json`. diff --git a/transpiler/docs/semantics.md b/transpiler/docs/semantics.md index c5e30a69..337782cc 100644 --- a/transpiler/docs/semantics.md +++ b/transpiler/docs/semantics.md @@ -9,7 +9,7 @@ extruder is a source-to-source transpiler, not an EVM. Some things are NOT suppo `Storage` class, inheritance as mixins, events). - `uint*` / `int*` → `bigint`. `address` / `bytes*` → `string`. `mapping(K => V)` → `Record` with factory-initialized defaults. - Structs → interfaces + factory. Enums → `as const` objects. + Structs → interfaces + factory. Enums → TypeScript `enum`s. - Contracts become ES classes extending a runtime `Contract` base that carries `_contractAddress`, `_storage`, `_msg`, an event emitter, and a transient-storage reset hook. Inter-contract references are plain object @@ -26,9 +26,8 @@ extruder is a source-to-source transpiler, not an EVM. Some things are NOT suppo - **Modifiers are stripped, not inlined.** Access control and `require` logic inside modifiers disappears. Diagnostic `W001` flags every occurrence so you can hand-audit. -- `try`/`catch` compiles to an empty block (`W002`). `receive`/`fallback` are - skipped (`W003`). Function pointers are coerced to `any` (`W004`). - User-defined operators are unrecognized. +- `try`/`catch` compiles to an empty block. `receive`/`fallback` are skipped + (`W003`). User-defined operators are unrecognized. ## EVM semantics it does not preserve @@ -42,10 +41,10 @@ trust a transpiled contract to behave like its on-chain counterpart: implicit bounds check after every arithmetic op. If you relied on revert-on-overflow as a safety net, add explicit casts or masks. - **`require` and `revert` are plain `throw`.** `require(cond, "msg")` becomes - `if (!cond) throw new Error("Require failed")`; `revert("msg")` and - `revert CustomError(...)` both become `throw new Error("Revert")`. Revert - reasons and custom error data are not propagated, and — per the codegen gap - above — `try/catch` can't recover from them. + `if (!cond) throw new Error("msg")`; bare `require(cond)` throws + `"Require failed"`. `revert("msg")` and `revert CustomError(...)` throw + generic `Error`s rather than Solidity-encoded revert data, and — per the + codegen gap above — `try/catch` can't recover from them. - **Division / modulo by zero** throws `RangeError`, not a Solidity panic. Still halts, different error type. - **Out-of-bounds array access** returns `undefined` rather than reverting. @@ -68,9 +67,11 @@ trust a transpiled contract to behave like its on-chain counterpart: write through automatically; plain TypeScript object references don't. Codegen emits `??=` where it detects the pattern, but hand-check anything with nested-mapping `storage` locals. -- **`delete arr[i]` splices**, not zero-writes. `arr.push()` returns - `undefined`, not the new length. Both differ from Solidity by enough to - bite rarely-tested code paths. +- **`delete` emits a best-effort default assignment** when the target type is + known, so `delete arr[i]` becomes a zero/default write instead of a sparse + JavaScript array hole. Unknown delete targets fall back to JavaScript + `delete`. `arr.push(value)` returns JavaScript's new array length, and + Solidity's storage-reference form `arr.push()` is not modeled. - **Addresses are lowercased strings**, not EIP-55 checksummed. String equality works, but anything that validates EIP-55 needs explicit normalization. @@ -113,13 +114,14 @@ src/*.sol ─► [Type Discovery] ─► [Lex] ─► [Parse] ─► [Codegen] 1. **Type discovery** — scan every `-d` root, build a cross-file registry so codegen can resolve qualified names in O(1). -2. **Lex / Parse** — recursive-descent parser, one AST per file. -3. **Codegen** — `TypeScriptCodeGenerator` orchestrates specialized emitters: - `TypeConverter`, `ExpressionGenerator`, `StatementGenerator`, +2. **Runtime replacement check** — files listed in `transpiler-config.json` + emit a re-export stub instead of parsed/generated output. +3. **Lex / Parse** — recursive-descent parser, one cached AST per generated + file. +4. **Codegen** — `TypeScriptCodeGenerator` orchestrates specialized emitters: + `TypeService` / `TypeConverter`, `ExpressionGenerator`, `StatementGenerator`, `FunctionGenerator`, `DefinitionGenerator`, `ContractGenerator`, `ImportGenerator`, `YulTranspiler`, `AbiTypeInferer`. -4. **Runtime replacement check** — files listed in `transpiler-config.json` - emit a re-export stub instead of transpiled output. 5. **Metadata + factories** (optional) — `MetadataExtractor` records each contract's constructor parameter types, `DependencyResolver` maps interface params to concrete implementations (via config overrides and naming diff --git a/transpiler/init.py b/transpiler/init.py index 7d3126a9..b6df5ba1 100644 --- a/transpiler/init.py +++ b/transpiler/init.py @@ -31,24 +31,19 @@ from .lexer import Lexer from .parser import Parser, SourceUnit +from .parser.visitor import ASTVisitor from .parser.ast_nodes import ( ASTNode, AssemblyBlock, AssemblyStatement, - Block, ContractDefinition, - Expression, - ExpressionStatement, FunctionCall, - FunctionDefinition, Identifier, - IfStatement, MemberAccess, NewExpression, - ReturnStatement, - VariableDeclarationStatement, ) from .codegen.metadata import MetadataExtractor +from .config import TranspilerConfig, merge_config_updates from .dependency_resolver import DependencyResolver from .type_system import TypeRegistry @@ -298,34 +293,35 @@ def _scan_contract_for_maybe(contract: ContractDefinition) -> List[str]: def _walk_for_flags(node: ASTNode, reasons: List[str]) -> None: """Recurse an AST subtree, appending red-flag reasons as we find them.""" - if node is None: - return - - # Assembly block — inspect the raw Yul source for patterns. - if isinstance(node, AssemblyBlock): - _inspect_yul(node.code, reasons) - return - if isinstance(node, AssemblyStatement): - _inspect_yul(node.block.code, reasons) - return - - # Solidity-level function calls. - if isinstance(node, FunctionCall): - _inspect_call(node, reasons) - - # Solidity-level `new Foo(...)` — only flag contract deployment, not - # array allocation (`new bytes(len)`, `new uint[](size)`). - if isinstance(node, NewExpression): + _RedFlagVisitor(reasons).visit(node) + + +class _RedFlagVisitor(ASTVisitor): + """Collect init-time REPLACE red flags from statement/expression trees.""" + + def __init__(self, reasons: List[str]): + self.reasons = reasons + + def visit_AssemblyBlock(self, node: AssemblyBlock): + _inspect_yul(node.code, self.reasons) + + def visit_AssemblyStatement(self, node: AssemblyStatement): + _inspect_yul(node.block.code, self.reasons) + + def visit_FunctionCall(self, node: FunctionCall): + _inspect_call(node, self.reasons) + return self.generic_visit(node) + + def visit_NewExpression(self, node: NewExpression): tn = node.type_name is_array_alloc = getattr(tn, 'is_array', False) or tn.name in ( 'bytes', 'string', ) or tn.name.startswith(('uint', 'int')) if not is_array_alloc: - reasons.append(f'uses `new {tn.name}(...)` to deploy a contract (no bytecode model)') - - # Recurse into children. - for child in _iter_children(node): - _walk_for_flags(child, reasons) + self.reasons.append( + f'uses `new {tn.name}(...)` to deploy a contract (no bytecode model)' + ) + return self.generic_visit(node) def _inspect_call(call: FunctionCall, reasons: List[str]) -> None: @@ -375,19 +371,6 @@ def _inspect_yul(code: str, reasons: List[str]) -> None: ) -def _iter_children(node: ASTNode): - """Yield child AST nodes. Generic — walks any dataclass field that is an - AST node or a list of AST nodes. Keeps the scan insulated from specific - statement/expression shapes.""" - for attr in vars(node).values(): - if isinstance(attr, ASTNode): - yield attr - elif isinstance(attr, list): - for item in attr: - if isinstance(item, ASTNode): - yield item - - def _run_resolver_dry_run( registry: TypeRegistry, existing_config_path: Optional[Path], @@ -721,13 +704,7 @@ def apply( def _load_config(path: Path) -> dict: """Read a `transpiler-config.json` — empty dict if missing or malformed. Warns once on invalid JSON; writers downstream clobber the file.""" - if not path.exists(): - return {} - try: - return json.loads(path.read_text()) - except json.JSONDecodeError: - print(f'Warning: {path} is not valid JSON; will overwrite.') - return {} + return TranspilerConfig.load(path, warn_missing=False).raw def _write_config( @@ -739,44 +716,23 @@ def _write_config( if existing is None: existing = _load_config(path) - # Non-destructive merge: union skipFiles, union interfaceAliases. - # runtimeReplacements entries are added for each replace target. - skip_files = sorted(set(existing.get('skipFiles', [])) | set(plan.skip_files)) - - interface_aliases = dict(existing.get('interfaceAliases', {})) - for k, v in plan.interface_aliases.items(): - interface_aliases.setdefault(k, v) # existing wins on conflict - - runtime_replacements = list(existing.get('runtimeReplacements', [])) - existing_sources = {r.get('source') for r in runtime_replacements} - for rel_path, contract_name in plan.replace_targets: - if rel_path in existing_sources: - continue - runtime_replacements.append({ + runtime_replacements = [ + { 'source': rel_path, 'reason': 'TODO: explain why this Solidity source needs a runtime replacement', 'runtimeModule': '../runtime-replacements', 'exports': [contract_name], - }) + } + for rel_path, contract_name in plan.replace_targets + ] - # Dependency overrides: existing wins on the (contract, param) key pair. - dep_overrides: Dict[str, Dict[str, object]] = { - k: dict(v) for k, v in existing.get('dependencyOverrides', {}).items() - } - for contract_name, params in plan.dependency_overrides.items(): - dest = dep_overrides.setdefault(contract_name, {}) - for param_name, impl in params.items(): - dest.setdefault(param_name, impl) - - merged = dict(existing) - if skip_files: - merged['skipFiles'] = skip_files - if interface_aliases: - merged['interfaceAliases'] = interface_aliases - if dep_overrides: - merged['dependencyOverrides'] = dep_overrides - if runtime_replacements: - merged['runtimeReplacements'] = runtime_replacements + merged = merge_config_updates( + existing, + skip_files=plan.skip_files, + interface_aliases=plan.interface_aliases, + dependency_overrides=plan.dependency_overrides, + runtime_replacements=runtime_replacements, + ) path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(merged, indent=2) + '\n') diff --git a/transpiler/parser/__init__.py b/transpiler/parser/__init__.py index a4300a9a..22ca1e2d 100644 --- a/transpiler/parser/__init__.py +++ b/transpiler/parser/__init__.py @@ -58,6 +58,7 @@ AssemblyStatement, ) from .parser import Parser +from .visitor import ASTVisitor, iter_child_nodes __all__ = [ # Base @@ -113,4 +114,6 @@ 'AssemblyStatement', # Parser 'Parser', + 'ASTVisitor', + 'iter_child_nodes', ] diff --git a/transpiler/parser/visitor.py b/transpiler/parser/visitor.py new file mode 100644 index 00000000..65cdb0fc --- /dev/null +++ b/transpiler/parser/visitor.py @@ -0,0 +1,41 @@ +"""Small generic visitor for Solidity AST nodes.""" + +from dataclasses import fields, is_dataclass +from typing import Iterator + +from .ast_nodes import ASTNode + + +def iter_child_nodes(node: ASTNode) -> Iterator[ASTNode]: + """Yield AST children from dataclass fields, lists, tuples, and dict values.""" + if not is_dataclass(node): + return + + for field in fields(node): + yield from _iter_node_values(getattr(node, field.name)) + + +def _iter_node_values(value) -> Iterator[ASTNode]: + if isinstance(value, ASTNode): + yield value + elif isinstance(value, (list, tuple)): + for item in value: + yield from _iter_node_values(item) + elif isinstance(value, dict): + for item in value.values(): + yield from _iter_node_values(item) + + +class ASTVisitor: + """Pre-order visitor with ``visit_`` dispatch.""" + + def visit(self, node: ASTNode): + if node is None: + return None + method = getattr(self, f'visit_{type(node).__name__}', self.generic_visit) + return method(node) + + def generic_visit(self, node: ASTNode): + for child in iter_child_nodes(node): + self.visit(child) + return None diff --git a/transpiler/pyproject.toml b/transpiler/pyproject.toml new file mode 100644 index 00000000..2659eb48 --- /dev/null +++ b/transpiler/pyproject.toml @@ -0,0 +1,38 @@ +[build-system] +requires = ["setuptools>=69"] +build-backend = "setuptools.build_meta" + +[project] +name = "extruder" +version = "0.1.0" +description = "Source-to-source Solidity to TypeScript transpiler" +readme = "README.md" +requires-python = ">=3.11" +license = { text = "AGPL-3.0" } +authors = [ + { name = "Chomp" } +] + +[project.scripts] +extruder = "transpiler.sol2ts:main" + +[tool.setuptools] +packages = [ + "transpiler", + "transpiler.codegen", + "transpiler.dependency_resolver", + "transpiler.lexer", + "transpiler.parser", + "transpiler.type_system", +] + +[tool.setuptools.package-dir] +transpiler = "." + +[tool.setuptools.package-data] +transpiler = [ + "docs/*.md", + "runtime/*.ts", + "transpiler-config.json", + "tsconfig.json", +] diff --git a/transpiler/sol2ts.py b/transpiler/sol2ts.py index 9fa27d4d..de1f92d8 100644 --- a/transpiler/sol2ts.py +++ b/transpiler/sol2ts.py @@ -15,8 +15,8 @@ - Interface and contract inheritance Usage: - python3 extruder/sol2ts.py src/ - python3 extruder/sol2ts.py --emit-replacement-stub + python3 -m transpiler src/ + python3 -m transpiler --emit-replacement-stub Module layout: - lexer: Tokenization (tokens.py, lexer.py) @@ -26,10 +26,9 @@ - dependency_resolver: Interface → concrete implementation resolution """ -import json import shutil from pathlib import Path -from typing import Optional, List, Dict, Set +from typing import Optional, List, Dict # Import from refactored modules from .lexer import Lexer @@ -37,7 +36,8 @@ from .type_system import TypeRegistry from .codegen import TypeScriptCodeGenerator from .codegen.metadata import MetadataExtractor, FactoryGenerator -from .codegen.diagnostics import TranspilerDiagnostics +from .codegen.diagnostics import TranspilerDiagnostics, emit_ast_diagnostics +from .config import TranspilerConfig, normalize_config_path from .dependency_resolver import DependencyResolver @@ -55,9 +55,11 @@ def __init__( self.source_dir = Path(source_dir) self.output_dir = Path(output_dir) self.parsed_files: Dict[str, SourceUnit] = {} + self._ast_cache: Dict[str, SourceUnit] = {} self.registry = TypeRegistry() self.emit_metadata = emit_metadata self.overrides_path = overrides_path + self._discovery_roots: List[Path] = [] # Metadata extraction for factory generation self.metadata_extractor = MetadataExtractor() if emit_metadata else None @@ -66,93 +68,79 @@ def __init__( self.diagnostics = TranspilerDiagnostics() # Load consolidated transpiler configuration - self.runtime_replacements: Dict[str, dict] = {} - self.runtime_replacement_classes: Set[str] = set() - self.runtime_replacement_mixins: Dict[str, str] = {} - self.runtime_replacement_methods: Dict[str, Set[str]] = {} - self.skip_files: Set[str] = set() - self.skip_dirs: Set[str] = set() - self._load_config() + config_path = self.overrides_path or TranspilerConfig.default_path() + self.config = TranspilerConfig.load(config_path, warn_missing=True) # Run type discovery on specified directories if discovery_dirs: for dir_path in discovery_dirs: - self.registry.discover_from_directory(dir_path) + self._discover_from_directory_cached(dir_path) - def _load_config(self) -> None: - """Load transpiler-config.json (consolidated configuration).""" - config_file = Path(__file__).parent / 'transpiler-config.json' - if not config_file.exists(): - print(f"Warning: transpiler-config.json not found at {config_file}") + def discover_types(self, directory: str, pattern: str = '**/*.sol') -> None: + """Run type discovery on a directory of Solidity files.""" + self._discover_from_directory_cached(directory, pattern) + + def _discover_from_directory_cached(self, directory: str, pattern: str = '**/*.sol') -> None: + """Discover types from Solidity files while reusing parsed ASTs.""" + base_dir = Path(directory) + for sol_file in base_dir.glob(pattern): + try: + rel_path = sol_file.relative_to(base_dir).with_suffix('') + ast = self._parse_file_cached(sol_file) + self.registry.discover_from_ast(ast, str(rel_path)) + except Exception as e: + print(f"Warning: Could not parse {sol_file} for type discovery: {e}") + self._remember_discovery_root(directory) + + def _remember_discovery_root(self, directory: str) -> None: + """Track discovery roots so transpile_file can avoid redundant discovery.""" + try: + root = Path(directory).resolve() + except (OSError, RuntimeError): return + if root not in self._discovery_roots: + self._discovery_roots.append(root) + def _is_covered_by_discovery(self, filepath: str) -> bool: + """Return True if a file lives under a root already type-discovered.""" + if not self._discovery_roots: + return False try: - with open(config_file, 'r') as f: - config = json.load(f) - - # Runtime replacements - for replacement in config.get('runtimeReplacements', []): - source_path = replacement.get('source', '') - if source_path: - self.runtime_replacements[source_path] = replacement - for export in replacement.get('exports', []): - self.runtime_replacement_classes.add(export) - interface = replacement.get('interface', {}) - class_name = interface.get('class', '') - mixin_code = interface.get('mixin', '') - if class_name and mixin_code: - self.runtime_replacement_mixins[class_name] = mixin_code - methods = interface.get('methods', []) - if class_name and methods: - method_names = set(m.get('name', '') for m in methods if m.get('name')) - self.runtime_replacement_methods[class_name] = method_names - - # Skip files and directories - for path in config.get('skipFiles', []): - self.skip_files.add(path) - for path in config.get('skipDirs', []): - self.skip_dirs.add(path) - - except (json.JSONDecodeError, KeyError) as e: - print(f"Warning: Failed to load transpiler-config.json: {e}") + resolved = Path(filepath).resolve() + except (OSError, RuntimeError): + return False + return any(resolved == root or resolved.is_relative_to(root) for root in self._discovery_roots) - def discover_types(self, directory: str, pattern: str = '**/*.sol') -> None: - """Run type discovery on a directory of Solidity files.""" - self.registry.discover_from_directory(directory, pattern) + def _cache_key(self, filepath: str | Path) -> str: + """Stable key for source/AST caches.""" + try: + return str(Path(filepath).resolve()) + except (OSError, RuntimeError): + return str(Path(filepath)) - def transpile_file(self, filepath: str, use_registry: bool = True) -> str: - """Transpile a single Solidity file to TypeScript.""" - with open(filepath, 'r') as f: - source = f.read() + def _parse_file_cached(self, filepath: str | Path) -> SourceUnit: + """Read, lex, and parse a Solidity file once per transpiler instance.""" + cache_key = self._cache_key(filepath) + if cache_key in self._ast_cache: + return self._ast_cache[cache_key] - # Tokenize using the lexer module + source = Path(filepath).read_text() lexer = Lexer(source) tokens = lexer.tokenize() - - # Parse using the parser module parser = Parser(tokens) ast = parser.parse() - self.parsed_files[filepath] = ast - self.registry.discover_from_ast(ast) - - # Extract metadata for factory generation - if self.metadata_extractor: - try: - resolved_filepath = Path(filepath).resolve() - resolved_source_dir = self.source_dir.resolve() - if resolved_filepath.is_relative_to(resolved_source_dir): - rel_path = resolved_filepath.relative_to(resolved_source_dir) - file_path_no_ext = str(rel_path.with_suffix('')) - else: - file_path_no_ext = Path(filepath).stem - self.metadata_extractor.extract_from_ast(ast, file_path_no_ext) - except (ValueError, TypeError, AttributeError): - pass + self._ast_cache[cache_key] = ast + self.parsed_files[str(filepath)] = ast + return ast - # Calculate file depth for imports + def transpile_file(self, filepath: str, use_registry: bool = True) -> str: + """Transpile a single Solidity file to TypeScript.""" + # Calculate file depth for imports before parsing so runtime + # replacements can stand in for files the parser cannot handle. file_depth = 0 current_file_path = '' + rel_path: Optional[Path] = None try: resolved_filepath = Path(filepath).resolve() resolved_source_dir = self.source_dir.resolve() @@ -163,58 +151,55 @@ def transpile_file(self, filepath: str, use_registry: bool = True) -> str: except (ValueError, TypeError, AttributeError): pass - # Emit diagnostics for skipped constructs in the AST - self._emit_ast_diagnostics(ast, filepath) - - # Check for runtime replacement replacement = self._get_runtime_replacement(filepath) if replacement: - self.diagnostics.info_runtime_replacement(filepath, replacement.get('runtime', '')) + runtime_module = replacement.get('runtimeModule', replacement.get('runtime', '')) + self.diagnostics.info_runtime_replacement(filepath, runtime_module) return self._generate_runtime_reexport(replacement, file_depth) + ast = self._parse_file_cached(filepath) + self.parsed_files[filepath] = ast + if not self._is_covered_by_discovery(filepath): + self.registry.discover_from_ast(ast) + + # Extract metadata for factory generation + if self.metadata_extractor: + try: + if rel_path is not None: + file_path_no_ext = str(rel_path.with_suffix('')) + else: + file_path_no_ext = Path(filepath).stem + self.metadata_extractor.extract_from_ast(ast, file_path_no_ext) + except (ValueError, TypeError, AttributeError): + pass + + # Emit diagnostics for skipped constructs in the AST + self._emit_ast_diagnostics(ast, filepath) + # Generate TypeScript using the modular code generator generator = TypeScriptCodeGenerator( self.registry if use_registry else None, file_depth=file_depth, current_file_path=current_file_path, - runtime_replacement_classes=self.runtime_replacement_classes, - runtime_replacement_mixins=self.runtime_replacement_mixins, - runtime_replacement_methods=self.runtime_replacement_methods, + runtime_replacement_classes=self.config.runtime_replacement_classes, + runtime_replacement_mixins=self.config.runtime_replacement_mixins, + runtime_replacement_methods=self.config.runtime_replacement_methods, ) return generator.generate(ast) def _emit_ast_diagnostics(self, ast: SourceUnit, filepath: str) -> None: """Scan the AST and emit diagnostics for skipped/unsupported constructs.""" - for contract in ast.contracts: - # Check for modifiers (they are parsed but not inlined) - for modifier in contract.modifiers: - self.diagnostics.warn_modifier_stripped( - modifier.name, - file_path=filepath, - ) - - # Check for function modifiers referenced on functions - for func in contract.functions: - if func.modifiers: - for mod_name in func.modifiers: - name = mod_name if isinstance(mod_name, str) else str(mod_name) - self.diagnostics.warn_modifier_stripped( - name, - file_path=filepath, - ) + emit_ast_diagnostics(ast, self.diagnostics, filepath) def _get_runtime_replacement(self, filepath: str) -> Optional[dict]: """Check if a file should be replaced with a runtime implementation.""" try: rel_path = Path(filepath).relative_to(self.source_dir) - rel_str = str(rel_path).replace('\\', '/') + rel_str = normalize_config_path(str(rel_path)) except ValueError: - rel_str = str(Path(filepath)).replace('\\', '/') + rel_str = normalize_config_path(str(Path(filepath))) - for source_pattern, replacement in self.runtime_replacements.items(): - if rel_str.endswith(source_pattern) or rel_str == source_pattern: - return replacement - return None + return self.config.runtime_replacement_for(rel_str) def _generate_runtime_reexport(self, replacement: dict, file_depth: int) -> str: """Generate a re-export file for a runtime replacement.""" @@ -244,10 +229,13 @@ def transpile_directory(self, pattern: str = '**/*.sol') -> Dict[str, str]: for sol_file in self.source_dir.glob(pattern): # Check if file or directory should be skipped rel = sol_file.relative_to(self.source_dir) - if str(rel) in self.skip_files: + rel_str = normalize_config_path(str(rel)) + has_replacement = self.config.runtime_replacement_for(rel_str) is not None + if not has_replacement and self.config.should_skip_file(rel_str): continue - if any(str(rel).startswith(d + '/') or str(rel).startswith(d + '\\') for d in self.skip_dirs): + if not has_replacement and self.config.should_skip_dir(rel_str): continue + try: ts_code = self.transpile_file(str(sol_file)) rel_path = sol_file.relative_to(self.source_dir) @@ -387,7 +375,7 @@ def main(): # fails subcommand validation), so we dispatch init manually. if len(sys.argv) > 1 and sys.argv[1] == 'init': init_parser = argparse.ArgumentParser( - prog='extruder init', + prog='python3 -m transpiler init', description=( 'Scan a Solidity source tree and scaffold a starter ' 'transpiler-config.json. Classifies each file ' @@ -413,8 +401,9 @@ def main(): return parser = argparse.ArgumentParser( + prog='python3 -m transpiler', description='extruder — source-to-source Solidity → TypeScript transpiler', - epilog='Subcommands: `extruder init ` to scaffold a config for a new project.', + epilog='Subcommands: `python3 -m transpiler init ` to scaffold a config for a new project.', ) parser.add_argument('input', nargs='?', help='Input Solidity file or directory') parser.add_argument('-o', '--output', default='transpiler/ts-output', help='Output directory (or output file for --emit-replacement-stub)') @@ -423,8 +412,6 @@ def main(): help='Directory to scan for type discovery') parser.add_argument('--emit-metadata', action='store_true', help='Emit dependency manifest and factory functions') - parser.add_argument('--metadata-only', action='store_true', - help='Only emit metadata, skip TypeScript generation') parser.add_argument('--overrides', metavar='FILE', help='Path to transpiler-config.json for manual dependency mappings') parser.add_argument('--emit-replacement-stub', nargs=2, metavar=('CONTRACT', 'SOL_FILE'), @@ -455,7 +442,7 @@ def main(): input_path = Path(args.input) discovery_dirs = args.discover or ([str(input_path)] if input_path.is_dir() else [str(input_path.parent)]) - emit_metadata = args.emit_metadata or args.metadata_only + emit_metadata = args.emit_metadata overrides_path = args.overrides @@ -478,9 +465,7 @@ def main(): ts_code = transpiler.transpile_file(str(input_path)) - if args.metadata_only: - pass # Output metadata only - elif args.stdout: + if args.stdout: print(ts_code) else: output_path = Path(args.output) / input_path.with_suffix('.ts').name @@ -495,11 +480,8 @@ def main(): emit_metadata=emit_metadata, overrides_path=overrides_path, ) - transpiler.discover_types(str(input_path)) - - if not args.metadata_only: - results = transpiler.transpile_directory() - transpiler.write_output(results) + results = transpiler.transpile_directory() + transpiler.write_output(results) else: print(f"Error: {args.input} is not a valid file or directory") raise SystemExit(1) diff --git a/transpiler/test_transpiler.py b/transpiler/test_transpiler.py index 3b492217..95cdde6a 100644 --- a/transpiler/test_transpiler.py +++ b/transpiler/test_transpiler.py @@ -671,6 +671,241 @@ def test_diagnostics_severity_levels(self): self.assertEqual(len(warnings), 1) self.assertEqual(len(infos), 1) + def test_ast_diagnostics_visitor_collects_modifier_warnings(self): + from transpiler.codegen.diagnostics import TranspilerDiagnostics, emit_ast_diagnostics + + source = ''' + contract TestContract { + modifier onlyOwner() { _; } + + function guarded() public onlyOwner { + } + } + ''' + + lexer = Lexer(source) + tokens = lexer.tokenize() + parser = Parser(tokens) + ast = parser.parse() + + diag = TranspilerDiagnostics() + emit_ast_diagnostics(ast, diag, 'TestContract.sol') + + self.assertEqual(len(diag.warnings), 2) + self.assertTrue(all(w.code == 'W001' for w in diag.warnings)) + + +class TestAstVisitor(unittest.TestCase): + """Test generic AST visitor traversal.""" + + def test_visitor_reaches_nested_statements_and_expressions(self): + from transpiler.parser.visitor import ASTVisitor + from transpiler.parser.ast_nodes import BinaryOperation, FunctionCall, Identifier + + source = ''' + contract TestContract { + function run(uint256 x) public { + if (x > 0) { + ping(x + 1); + } + } + } + ''' + + lexer = Lexer(source) + tokens = lexer.tokenize() + parser = Parser(tokens) + ast = parser.parse() + + class RecordingVisitor(ASTVisitor): + def __init__(self): + self.binary_ops = [] + self.calls = [] + + def visit_BinaryOperation(self, node: BinaryOperation): + self.binary_ops.append(node.operator) + return self.generic_visit(node) + + def visit_FunctionCall(self, node: FunctionCall): + if isinstance(node.function, Identifier): + self.calls.append(node.function.name) + return self.generic_visit(node) + + visitor = RecordingVisitor() + visitor.visit(ast) + + self.assertIn('>', visitor.binary_ops) + self.assertIn('+', visitor.binary_ops) + self.assertIn('ping', visitor.calls) + + +class TestTranspilerConfig(unittest.TestCase): + """Test the shared transpiler-config loader.""" + + def test_config_loader_normalizes_runtime_and_dependency_fields(self): + from transpiler.config import TranspilerConfig + + cfg = TranspilerConfig.from_dict({ + 'runtimeReplacements': [{ + 'source': 'lib\\Ownable.sol', + 'exports': ['Ownable'], + 'interface': { + 'class': 'Ownable', + 'methods': [{'name': 'owner'}], + 'mixin': 'mixin code', + }, + }], + 'skipFiles': ['test\\Fixture.sol'], + 'skipDirs': ['script\\deploy'], + 'dependencyOverrides': {'UsesFoo': {'_foo': 'Foo'}}, + 'interfaceAliases': {'IFoo': 'Foo'}, + }) + + self.assertTrue(cfg.runtime_replacement_for('src/lib/Ownable.sol')) + self.assertTrue(cfg.should_skip_file('test/Fixture.sol')) + self.assertTrue(cfg.should_skip_dir('script/deploy/Deploy.sol')) + self.assertEqual(cfg.dependency_overrides['UsesFoo']['_foo'], 'Foo') + self.assertEqual(cfg.interface_aliases['IFoo'], 'Foo') + self.assertIn('Ownable', cfg.runtime_replacement_classes) + self.assertEqual(cfg.runtime_replacement_methods['Ownable'], {'owner'}) + self.assertEqual(cfg.runtime_replacement_mixins['Ownable'], 'mixin code') + + def test_merge_config_preserves_existing_conflicts_and_adds_new_entries(self): + from transpiler.config import merge_config_updates + + merged = merge_config_updates( + { + 'skipFiles': ['legacy\\A.sol'], + 'interfaceAliases': {'IFoo': 'Foo'}, + 'dependencyOverrides': {'UsesFoo': {'_foo': 'OldFoo'}}, + 'runtimeReplacements': [{ + 'source': 'legacy\\Replacement.sol', + 'exports': ['OldReplacement'], + }], + }, + skip_files=['new\\B.sol', 'legacy/A.sol'], + interface_aliases={'IFoo': 'NewFoo', 'IBar': 'Bar'}, + dependency_overrides={ + 'UsesFoo': {'_foo': 'NewFoo', '_bar': 'Bar'}, + 'UsesBaz': {'_baz': ['Baz']}, + }, + runtime_replacements=[ + {'source': 'legacy/Replacement.sol', 'exports': ['Duplicate']}, + {'source': 'new/Replacement.sol', 'exports': ['NewReplacement']}, + ], + ) + + self.assertEqual(merged['skipFiles'], ['legacy/A.sol', 'new/B.sol']) + self.assertEqual(merged['interfaceAliases']['IFoo'], 'Foo') + self.assertEqual(merged['interfaceAliases']['IBar'], 'Bar') + self.assertEqual(merged['dependencyOverrides']['UsesFoo']['_foo'], 'OldFoo') + self.assertEqual(merged['dependencyOverrides']['UsesFoo']['_bar'], 'Bar') + self.assertEqual(merged['dependencyOverrides']['UsesBaz']['_baz'], ['Baz']) + self.assertEqual(len(merged['runtimeReplacements']), 2) + self.assertEqual( + merged['runtimeReplacements'][1]['source'], + 'new/Replacement.sol', + ) + + def test_dependency_resolver_uses_shared_config_loader(self): + import tempfile + from pathlib import Path + from transpiler.dependency_resolver import DependencyResolver + + with tempfile.TemporaryDirectory() as td: + path = Path(td) / 'transpiler-config.json' + path.write_text('''{ + "dependencyOverrides": {"UsesFoo": {"_foo": "Foo"}}, + "interfaceAliases": {"IBar": "Bar"} + }''') + + resolver = DependencyResolver( + overrides_path=str(path), + known_classes={'Foo', 'Bar'}, + ) + + self.assertEqual(resolver.overrides['UsesFoo']['_foo'], 'Foo') + self.assertEqual(resolver.interface_aliases['IBar'], 'Bar') + + def test_transpiler_uses_override_config_for_skip_files(self): + import tempfile + from pathlib import Path + from transpiler.sol2ts import SolidityToTypeScriptTranspiler + + with tempfile.TemporaryDirectory() as td: + tree = Path(td) + (tree / 'A.sol').write_text('contract A { function a() public {} }') + (tree / 'B.sol').write_text('contract B { function b() public {} }') + config_path = tree / 'transpiler-config.json' + config_path.write_text('{"skipFiles": ["B.sol"]}') + + transpiler = SolidityToTypeScriptTranspiler( + source_dir=str(tree), + output_dir=str(tree / 'out'), + discovery_dirs=[str(tree)], + overrides_path=str(config_path), + ) + results = transpiler.transpile_directory() + + self.assertEqual(len(results), 1) + self.assertTrue(any(path.endswith('A.ts') for path in results)) + self.assertFalse(any(path.endswith('B.ts') for path in results)) + + def test_runtime_replacement_wins_over_skip_file_and_avoids_parse(self): + import tempfile + from pathlib import Path + from transpiler.sol2ts import SolidityToTypeScriptTranspiler + + with tempfile.TemporaryDirectory() as td: + tree = Path(td) + (tree / 'Replaced.sol').write_text('contract Replaced {') + config_path = tree / 'transpiler-config.json' + config_path.write_text('''{ + "runtimeReplacements": [{ + "source": "Replaced.sol", + "runtimeModule": "../runtime-replacements", + "exports": ["Replaced"], + "reason": "test replacement" + }], + "skipFiles": ["Replaced.sol"] + }''') + + transpiler = SolidityToTypeScriptTranspiler( + source_dir=str(tree), + output_dir=str(tree / 'out'), + overrides_path=str(config_path), + ) + results = transpiler.transpile_directory() + + self.assertEqual(len(results), 1) + output = next(iter(results.values())) + self.assertIn("export { Replaced } from '../runtime-replacements';", output) + + +class TestPackaging(unittest.TestCase): + """Test standalone package metadata.""" + + def test_pyproject_declares_console_script_and_package_data(self): + import tomllib + from pathlib import Path + + project_root = Path(__file__).parent + pyproject = tomllib.loads((project_root / 'pyproject.toml').read_text()) + + self.assertEqual( + pyproject['project']['scripts']['extruder'], + 'transpiler.sol2ts:main', + ) + + package_data = set(pyproject['tool']['setuptools']['package-data']['transpiler']) + self.assertIn('runtime/*.ts', package_data) + self.assertIn('docs/*.md', package_data) + self.assertIn('transpiler-config.json', package_data) + + self.assertTrue((project_root / 'runtime' / 'index.ts').exists()) + self.assertTrue((project_root / 'docs' / 'quickstart.md').exists()) + self.assertTrue((project_root / 'transpiler-config.json').exists()) + class TestStructDefaultValues(unittest.TestCase): """Test struct default value generation.""" @@ -843,6 +1078,87 @@ def test_address_cast(self): self.assertIn('getAddr', output) +class TestDeleteGeneration(unittest.TestCase): + """Test Solidity delete semantics in generated TypeScript.""" + + def test_delete_array_element_zero_writes(self): + source = ''' + contract TestContract { + uint256[] values; + + function clear(uint256 index) public { + delete values[index]; + } + } + ''' + + lexer = Lexer(source) + tokens = lexer.tokenize() + parser = Parser(tokens) + ast = parser.parse() + + generator = TypeScriptCodeGenerator() + output = generator.generate(ast) + + self.assertIn('this.values[Number(index)] = 0n;', output) + self.assertNotIn('delete this.values', output) + + def test_delete_state_variable_zero_writes(self): + source = ''' + contract TestContract { + bool enabled; + + function clear() public { + delete enabled; + } + } + ''' + + lexer = Lexer(source) + tokens = lexer.tokenize() + parser = Parser(tokens) + ast = parser.parse() + + generator = TypeScriptCodeGenerator() + output = generator.generate(ast) + + self.assertIn('this.enabled = false;', output) + self.assertNotIn('delete this.enabled', output) + + +class TestTranspileDirectoryCaching(unittest.TestCase): + """Test that directory transpilation reuses parsed ASTs after discovery.""" + + def test_directory_transpile_parses_each_file_once(self): + import tempfile + from pathlib import Path + from unittest.mock import patch + from transpiler.sol2ts import SolidityToTypeScriptTranspiler, Parser as Sol2TsParser + + with tempfile.TemporaryDirectory() as td: + tree = Path(td) + (tree / 'A.sol').write_text('contract A { function a() public {} }') + (tree / 'B.sol').write_text('contract B { function b() public {} }') + + parse_count = [0] + original_parse = Sol2TsParser.parse + + def counted_parse(parser_self): + parse_count[0] += 1 + return original_parse(parser_self) + + with patch.object(Sol2TsParser, 'parse', counted_parse): + transpiler = SolidityToTypeScriptTranspiler( + source_dir=str(tree), + output_dir=str(tree / 'out'), + discovery_dirs=[str(tree)], + ) + results = transpiler.transpile_directory() + + self.assertEqual(parse_count[0], 2) + self.assertEqual(len(results), 2) + + # ============================================================================= # extruder init — scan phase # ============================================================================= diff --git a/transpiler/transpiler-config.json b/transpiler/transpiler-config.json index fffc921d..4c1bc4aa 100644 --- a/transpiler/transpiler-config.json +++ b/transpiler/transpiler-config.json @@ -51,7 +51,7 @@ { "name": "at", "visibility": "protected", "params": ["set: AddressSet", "index: bigint"], "returns": "string" } ] } - }, + } ], "dependencyOverrides": { "DefaultRuleset": {