Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
c4c8443
wip
sudo-owen Jul 15, 2026
0c55cb9
engine fix
sudo-owen Jul 15, 2026
b89651d
fix engine test
sudo-owen Jul 15, 2026
30b3e1b
clean up certain moves
sudo-owen Jul 16, 2026
171ab90
wip
sudo-owen Jul 16, 2026
432f875
fix rust flow
sudo-owen Jul 16, 2026
3166763
update some effects and also status effect privilege in engine
sudo-owen Jul 16, 2026
2767055
make stat boosts cheaper
sudo-owen Jul 16, 2026
db520bb
wip
sudo-owen Jul 17, 2026
e6ed65a
cap stat boosts
sudo-owen Jul 20, 2026
53778be
small improvement to onboarding flow
sudo-owen Jul 20, 2026
d4999e8
clean up stray Engine functions
sudo-owen Jul 20, 2026
c0fa04b
wip transpiler fixes
sudo-owen Jul 21, 2026
facd408
optimizing singles/doubles
sudo-owen Jul 21, 2026
2c797f2
gas optimizations
sudo-owen Jul 21, 2026
bae7fb3
migrate moves to optimized access patterns
sudo-owen Jul 21, 2026
259e08a
clean up some moves
sudo-owen Jul 21, 2026
c285129
wip kernel tests
sudo-owen Jul 22, 2026
b460056
wip new sprites
sudo-owen Jul 24, 2026
53b5b50
new attacks
sudo-owen Jul 26, 2026
0e78c39
clean up
sudo-owen Jul 28, 2026
f1129a2
add benchmark flow
sudo-owen Jul 28, 2026
5d2e6ec
fix migration/roll flow
sudo-owen Jul 28, 2026
b9cd027
clean up to focus on rust flow here
sudo-owen Jul 28, 2026
ff2c33e
Merge branch 'main' into feat/engine-fix
sudo-owen Jul 28, 2026
c888d91
transpiler: skip test-only PackedHeaderFrameLib prototype
sudo-owen Jul 28, 2026
1f62572
transpiler: resolve library constants against the class, not the sing…
sudo-owen Jul 28, 2026
1960279
transpiler: fix `.slot` key helper colliding with contract methods
sudo-owen Jul 28, 2026
36172eb
transpiler: tighten _yulSlotKeyOf comments
sudo-owen Jul 28, 2026
390f828
transpiler: simplify library-class import join, tighten comment
sudo-owen Jul 29, 2026
115a384
clean up
sudo-owen Jul 31, 2026
a81a2ea
wip
sudo-owen Jul 31, 2026
dfbb6be
wip ts-specific optimizations
sudo-owen Aug 1, 2026
4e3c289
clean up
sudo-owen Aug 1, 2026
7a84d96
clean up and wip for new mons
sudo-owen Aug 7, 2026
10a0bfd
add mulong / glob
sudo-owen Aug 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
94 changes: 94 additions & 0 deletions .claude/skills/add-game-content/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
name: add-game-content
description: Workflow for adding a new mon, move, ability, or effect to chomp — CSV data, contract patterns, and validation steps. Use when adding/creating a new mon, move, ability, or status/battlefield effect.
---

### Adding a New Mon

1. Add mon stats to `drool/mons.csv` (HP, Attack, Defense, SpAtk, SpDef, Speed, Types)
2. Add 4 moves to `drool/moves.csv` (Name, Mon, Power, Stamina, Accuracy, Priority, Type, Class, etc.)
3. Add ability to `drool/abilities.csv` (Name, Mon, Effect description)
4. Create directory `src/mons/<monname>/` (lowercase, e.g., `src/mons/aurox/`)
5. Implement 4 move contracts as `PascalCase.sol` files (see "Move Implementation Patterns" below)
6. Implement 1 ability contract as `PascalCase.sol` (see "Ability Patterns" below)
7. Run `python processing/validateMoves.py` to validate contracts match CSV data
8. Run `python processing/generateSolidity.py` to regenerate `SetupMons.s.sol`
9. Add tests in `test/mons/<MonName>Test.sol` extending `BattleHelper`

### Move Implementation Patterns

Choose the simplest pattern that fits the move's behavior:

**1. Pure `StandardAttack`** — constructor-only, no `move()` override. For straightforward damaging moves or simple effect-applying moves. Pass `EFFECT` + `EFFECT_ACCURACY` for probabilistic status application (e.g., 30% chance to burn):

```solidity
contract Blow is StandardAttack {
constructor(IEngine _ENGINE, ITypeCalculator _TYPE_CALCULATOR)
StandardAttack(msg.sender, _ENGINE, _TYPE_CALCULATOR, ATTACK_PARAMS({
BASE_POWER: 70, STAMINA_COST: 2, ACCURACY: DEFAULT_ACCURACY,
PRIORITY: DEFAULT_PRIORITY, MOVE_TYPE: Type.Air,
EFFECT_ACCURACY: 0, MOVE_CLASS: MoveClass.Physical,
CRIT_RATE: DEFAULT_CRIT_RATE, VOLATILITY: DEFAULT_VOL,
NAME: "Blow", EFFECT: IEffect(address(0))
}))
{}
}
```

**2. `StandardAttack` + `move()` override** — for moves with side effects after damage (recoil, self-switch, self-status, multi-hit). Override the full `move(...)` signature and call `_move()`, which returns `(int32 damage, bytes32 eventType)`, then add custom logic:

```solidity
function move(
IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256 attackerMonIndex,
uint256 targetBits, uint256 activesPacked, uint16 extraData, uint256 rng
) public override {
(int32 damage,) = _move(engine, battleKey, attackerPlayerIndex, targetBits, rng);
if (damage > 0) {
engine.dealDamage(attackerPlayerIndex, attackerMonIndex, selfDamage); // recoil
}
}
```

A move's target domain is client-facing metadata in `moves.csv`'s `TargetSpec` column (→ TS `Move.targetSpec`), not a Solidity declaration; `validateMoves.py` checks each move's `targetBits` behavior against it. The player's chosen target arrives as `targetBits` (resolved against `activesPacked` via `TargetLib`); the old `targetSpec()` / `extraDataType()` / `ExtraDataType` selector layers have been removed.

**3. Custom `IMoveSet`** — for complex conditional moves (variable power, healing, stat manipulation, reading opponent state). Implement the `IMoveSet` functions directly (`move` + metadata getters + `getMeta`). Deal damage via `engine.dispatchStandardAttack` / `dispatchCustomAttack` (passing `targetBits`); `AttackCalculator._calculateDamage()` now just delegates to `dispatchCustomAttack`. Store dependencies as `immutable`.

**4. `IMoveSet` + `BasicEffect` hybrid** — for moves that persist as effects across turns (traps, delayed damage, per-turn modifiers). Implement both interfaces in one contract. The `move()` function calls `ENGINE.addEffect(playerIndex, monIndex, IEffect(address(this)), ...)`.

**Shared libraries** — when multiple moves in the same mon share logic, extract into a `library` contract in the same directory (e.g., `NineNineNineLib.sol`, `HeatBeaconLib.sol`).

### Ability Patterns

Each mon has exactly one ability, implemented in its own `PascalCase.sol` file within the mon directory.

**Pure `IAbility`** — for one-time switch-in actions (deal damage, apply a stat boost). Implement `name()` and `activateOnSwitch()`. No persistent state. (e.g., `PreemptiveShock`, `SaviorComplex`)

**`IAbility` + `BasicEffect`** (most common) — for abilities with ongoing lifecycle effects. `activateOnSwitch()` registers `address(this)` as an effect on the mon, then hooks into effect lifecycle via `getStepsBitmap()`. Must override `name()` with `override(IAbility, BasicEffect)`. Uses an idempotency guard to prevent duplicate registration:

```solidity
function activateOnSwitch(bytes32 battleKey, uint256 playerIndex, uint256 monIndex) external {
(EffectInstance[] memory effects,) = ENGINE.getEffects(battleKey, playerIndex, monIndex);
for (uint256 i; i < effects.length; i++) {
if (address(effects[i].effect) == address(this)) return;
}
ENGINE.addEffect(playerIndex, monIndex, IEffect(address(this)), bytes32(0));
}
```

For once-per-battle abilities (e.g., `RiseFromTheGrave`), use a `globalKV` flag instead of the effect-list check.

### Adding a New Effect

Effects fall into several categories depending on scope:

- **Status effects** (`src/effects/status/`): Extend the `StatusEffect` marker base. One-status-per-mon is **Engine-native**: each status folds a unique class id into `getStepsBitmap()` bits 10-13 (an internal `STATUS_CLASS` constant; deployable ids 1-14, 15 reserved for test mocks), and the Engine tracks it in a per-mon lane (`BattleConfig.monStatusLanes`, slot 3) — gating adds before any external call, setting the lane on apply, clearing it on removal. Readers use `engine.getMonStatusClass` (identity/existence) and `engine.clearMonStatus(player, mon, expectedClass)` (remove; 0 = any class); reader contracts cache the class id as a constructor `immutable` derived from the injected status's `getStepsBitmap()`. Escalating statuses (Burn) set `HAS_REAPPLY_BIT` (bit 14) and implement `IStatusEffect.onReapply`, which the Engine calls on a same-class re-apply; without the bit a re-apply is a zero-call no-op. Extra apply conditions beyond exclusivity (Sleep's single-sleeper rule) live in a `shouldApply` override (no `ALWAYS_APPLIES_BIT`). Shared across mons — deployed once, injected into moves via constructor parameters. (e.g., `BurnStatus`, `FrostbiteStatus`, `SleepStatus`)
- **Battlefield effects** (`src/effects/battlefield/`): Extend `BasicEffect`, use `targetIndex=2` for global scope. (e.g., `Overclock`)
- **Shared utility effects** (`src/effects/`): Deployed once, used by many contracts. (e.g., `StaminaRegen` for per-turn recovery). NOTE: stat modifiers are **not** an effect — they are inlined Engine functions (`addStatBoost`/`removeStatBoost`/…); see "Stat Boosts" in the root CLAUDE.md.
- **Mon-local effects** (`src/mons/<monname>/`): Abilities or move-effect hybrids that only apply to one mon. These live in the mon's directory, not in `src/effects/`.

To implement a new effect:
1. Extend `BasicEffect` (or `StatusEffect` for status conditions)
2. Override the relevant lifecycle hooks (`onRoundEnd`, `onAfterDamage`, `onPreDamage`, …) — each receives `activesPacked` for local slot resolution via `TargetLib`
3. Return a bitmap from `getStepsBitmap()` indicating which hooks to call. Before adding callback getters, opt into a supported fresh context by mirroring the applicable low lifecycle bit into bits 16-31 as described in the root CLAUDE.md. If the effect doesn't override `shouldApply()` (i.e. it relies on `BasicEffect`'s default, which always returns `true`), OR the low bitmap with `ALWAYS_APPLIES_BIT` (`src/Constants.sol`) so the Engine skips the external `shouldApply()` call on `addEffect`. Effects that override `shouldApply()` with real gating logic (e.g. `SleepStatus`'s single-sleeper rule) must NOT set this bit. Statuses additionally carry their class id in bits 10-13 and `HAS_REAPPLY_BIT` (bit 14) iff they implement `onReapply`. `python processing/validateEffectBitmaps.py` checks all of these invariants (bit pairing, class range 1-14, cross-src uniqueness) and is run as part of `deploy.py`.
4. Return `(updatedExtraData, removeAfterRun)` from hooks — use `extraData` (bytes32) to carry state between turns (counters, degrees, flags)
5. Shared effects are injected into moves/abilities via constructor parameters at deploy time — there is no runtime effect registry
73 changes: 73 additions & 0 deletions .claude/skills/deploy-chomp/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
name: deploy-chomp
description: Build and deploy pipeline for chomp — processing scripts, the Solidity-to-TS/Rust transpiler, deployment order, and CI/CD reference. Use when running codegen/validation scripts, transpiling, or deploying to testnet/mainnet.
---

### Processing Scripts (Python 3.11+)

```bash
python processing/validateMoves.py # Validate contracts vs CSV
python processing/validateEffectBitmaps.py # Validate ALWAYS_APPLIES_BIT usage on IEffect contracts
python processing/generateSolidity.py # Generate SetupMons.s.sol
python processing/generateSetupCPU.py # Generate SetupCPU.s.sol from cpu-teams.json
python processing/deploy.py --testnet # Full deployment (forge scripts + codegen)
python processing/deploy.py --mainnet # Production deployment
```

Python dependencies: `numpy`, `pexpect`, `pillow` (managed via `uv`, see `pyproject.toml`)

### Transpiler (Solidity to TypeScript / Rust)

Converts Solidity contracts to TypeScript for local battle simulation:

```bash
# Transpile all contracts
python3 -m transpiler src/ -o transpiler/ts-output -d src --emit-metadata

# Run transpiler unit tests (the TS runtime/integration tests moved to munch with the codegen sync)
python3 transpiler/test_transpiler.py
```

**Rust target (sim performance).** The same front-end also drives a Rust
backend (`codegen_rs/`, native ints instead of bigint — see
`transpiler/codegen_rs/README.md`):

```bash
python3 -m transpiler src/ --target rust # emit transpiler/rs-output (cargo workspace)
cd transpiler/rs-output && cargo build --release # engine + runtime + strategies (+ its bins)
CHOMP_ROOT=$(pwd)/../.. ./target/release/arena --games 6000 # whole-game batches, win-rate table
CHOMP_ROOT=$(pwd)/../.. ./target/release/trace --games 6000 --narrate 0 # trace analysis + greedy counterfactual
```

The full engine, all mons/effects, and the three CPU strategies (hard,
greedy, override) are transpiled/ported. The arena is now a **standalone,
pure-Rust binary** (`chomp-strategies`' `arena`/`trace` bins): it loads the
roster from `drool/*.csv` + `src/mons/*.json` at runtime and runs whole games
natively (~2,000 games/s at 8 threads, deterministic across thread counts).
The stacks are DECOUPLED — the Rust side was ported move-for-move against TS
and then cut loose; it may diverge freely (including its rng stream and CPU
decisions) as the prototyping substrate, and port-backs to the TS game carry
no bit-identicality requirement. Emission is allowlisted in
`transpiler-config-rust.json`. `rs-output/` is regenerated (gitignored);
hand-written crates live in `transpiler/{runtime-rs,strategies-rs}`. The
former bun↔Rust FFI seam (`chomp_run_games`, the `ffi` crate, and
`scripts/batch_benchmark.ts`) was removed once the pure-Rust arena replaced
it; the verification-era machinery (golden-vector suites, replay fixtures,
the drive-mode adapter, lockstep gates) lives in git history if parity ever
needs re-proving. The TS arena was deleted once the pure-Rust one replaced
it; strategies that ship are hand-ported into munch and gated there against
`sim-tests/arena/cpu-reference.json`.

**Known limitation — TODO when a second codebase needs it.** The transpiler hardcodes three TS namespace names (`Enums`, `Structs`, `Constants`) for type-only Solidity files. File-type detection is content-based (a file with only enums maps to the `Enums` namespace regardless of filename), but the *namespace name itself* is fixed, and only structs are tracked per source path. A codebase that splits types across multiple files (e.g. `PoolStructs.sol` + `OrderStructs.sol`, or `Errors.sol`) would produce colliding imports. To generalize: add `enum_paths` / `constant_paths` to `transpiler/type_system/registry.py` mirroring `struct_paths`, derive the namespace name from each source file's basename, and have `imports.py:_generate_module_imports` emit one `import * as <Basename>` per actually-referenced source module instead of three blanket imports.

### Deployment Order

1. `EngineAndPeriphery.s.sol` - Engine, type calculator, `SignedCommitManager`, `GachaTeamRegistry`, `SignedMatchmaker`, CPU host, `SimplePM`, shared effects
2. `SetupMons.s.sol` - All mon contracts (moves, abilities)
3. `SetupCPU.s.sol` - CPU players

### CI/CD

GitHub Actions runs on pull requests (`.github/workflows/main.yml`):
- `forge build`
- `forge test -vvv`
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Compiler files
cache/
out/
cache-fast/
out-fast/

# Ignores development broadcast logs
!/broadcast
Expand Down Expand Up @@ -54,7 +56,12 @@ transpiler/ts-output/
.vscode/
.pytest_cache/
.venv/
.claude/

# Ignore everything in .claude except shared skills
.claude/*
!.claude/skills
!.claude/skills/**

node_modules/

.deploys/
Expand Down
Loading