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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 230 additions & 0 deletions PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
# Plan: Re-implement Doubles Battle Support on Current Main

## Design Principles

1. **Work WITH main's architecture** - Keep `getStepsBitmap()`, keep `battleKey` + `p0ActiveMonIndex`/`p1ActiveMonIndex` in effect signatures, keep bitmap-based hook filtering, keep `ValidatorLogic` library
2. **Additive, not destructive** - Singles behavior must remain identical; doubles is a new code path
3. **Minimal interface changes** - Add new functions/getters rather than changing existing ones where possible
4. **Incremental commits** - Each commit should compile and not break existing tests

## Phase 1: Core Data Structures

### 1a. Add GameMode enum to Enums.sol
```solidity
enum GameMode {
Singles, // 0
Doubles // 1
}
```

### 1b. Update Structs.sol

**BattleData** - Add `slotSwitchFlagsAndGameMode` field:
```solidity
struct BattleData {
address p1;
uint64 turnId;
address p0;
uint8 winnerIndex;
uint8 prevPlayerSwitchForTurnFlag;
uint8 playerSwitchForTurnFlag;
uint16 activeMonIndex; // Singles: 8+8 bit packing; Doubles: 4+4+4+4 bit packing
uint8 slotSwitchFlagsAndGameMode; // bits 0-3: slot switch flags, bit 4: gameMode
}
```

**BattleConfig** - Add second move pair for doubles:
```solidity
MoveDecision p0Move; // Slot 0 for p0
MoveDecision p1Move; // Slot 0 for p1
MoveDecision p0Move2; // Slot 1 for p0 (doubles only)
MoveDecision p1Move2; // Slot 1 for p1 (doubles only)
```

**Battle/ProposedBattle** - Add `GameMode gameMode` field

**BattleContext** - Keep existing `p0ActiveMonIndex`/`p1ActiveMonIndex` for singles backward compatibility

**CommitContext** - Add `uint8 slotSwitchFlags` and `GameMode gameMode`

**DamageCalcContext** - No change needed (callers will provide correct mon indices)

**New struct: RevealedMovesPair** - For DoublesCommitManager:
```solidity
struct RevealedMovesPair {
uint8 moveIndex0;
uint240 extraData0;
uint8 moveIndex1;
uint240 extraData1;
bytes32 salt;
}
```

### 1c. Update Constants.sol
Add doubles-specific constants:
```solidity
uint8 constant ACTIVE_MON_INDEX_BITS = 4;
uint8 constant ACTIVE_MON_INDEX_MASK = 0x0F;
uint8 constant SWITCH_FLAG_P0_SLOT0 = 0x01;
uint8 constant SWITCH_FLAG_P0_SLOT1 = 0x02;
uint8 constant SWITCH_FLAG_P1_SLOT0 = 0x04;
uint8 constant SWITCH_FLAG_P1_SLOT1 = 0x08;
uint8 constant SWITCH_FLAGS_MASK = 0x0F;
uint8 constant GAME_MODE_BIT = 0x10;
```

## Phase 2: Engine.sol Changes

### 2a. Doubles active mon index packing helpers
Add alongside existing `_packActiveMonIndices`/`_unpackActiveMonIndex`/`_setActiveMonIndex`:

```solidity
function _unpackActiveMonIndexForSlot(uint16 packed, uint256 playerIndex, uint256 slotIndex) internal pure returns (uint256)
function _setActiveMonIndexForSlot(uint16 packed, uint256 playerIndex, uint256 slotIndex, uint256 monIndex) internal pure returns (uint16)
```

Packing layout for doubles: bits [0-3]=p0slot0, [4-7]=p0slot1, [8-11]=p1slot0, [12-15]=p1slot1

### 2b. Slot switch flag helpers
```solidity
function _getSlotSwitchFlags(BattleData storage battle) internal view returns (uint8)
function _setSlotSwitchFlag(BattleData storage battle, uint256 playerIndex, uint256 slotIndex) internal
function _clearSlotSwitchFlags(BattleData storage battle) internal
function _isDoublesMode(BattleData storage battle) internal view returns (bool)
```

### 2c. New getters (IEngine + Engine)
```solidity
function getGameMode(bytes32 battleKey) external view returns (GameMode)
function getActiveMonIndexForSlot(bytes32 battleKey, uint256 playerIndex, uint256 slotIndex) external view returns (uint256)
function getDamageCalcContextForSlot(bytes32 battleKey, uint256 attackerPlayerIndex, uint256 attackerSlotIndex, uint256 defenderPlayerIndex, uint256 defenderSlotIndex) external view returns (DamageCalcContext memory)
```

### 2d. Doubles move helpers
```solidity
function setMoveForSlot(bytes32 battleKey, uint256 playerIndex, uint256 slotIndex, uint8 moveIndex, bytes32 salt, uint240 extraData) external
function _getMoveDecisionForSlot(BattleConfig storage config, uint256 playerIndex, uint256 slotIndex) internal view returns (MoveDecision memory)
```

### 2e. startBattle updates
- Accept `GameMode` from Battle struct
- When doubles: initialize activeMonIndex with 4-bit packing (p0slot0=0, p0slot1=1, p1slot0=0, p1slot1=1)
- Store gameMode in `slotSwitchFlagsAndGameMode`

### 2f. Doubles execution path
In `execute()` / `_executeInternal()`:
- Check game mode; if doubles, branch to `_executeDoubles()`
- `_executeDoubles()` handles:
- Move order calculation across 4 slots (priority + speed tiebreaker)
- Per-slot move execution via `_handleMoveForSlot()`
- Per-slot switch handling via `_handleSwitchForSlot()`
- Doubles-specific KO/game-over checks
- Slot switch flag management for forced switches

### 2g. switchActiveMonForSlot
New external function for doubles force-switch moves:
```solidity
function switchActiveMonForSlot(uint256 playerIndex, uint256 slotIndex, uint256 monToSwitchIndex) external
```

## Phase 3: AttackCalculator.sol Updates

Add a slot-aware overload of `_calculateDamage`:
```solidity
function _calculateDamageForSlot(
IEngine ENGINE,
ITypeCalculator TYPE_CALCULATOR,
bytes32 battleKey,
uint256 attackerPlayerIndex,
uint256 attackerSlotIndex,
uint256 defenderSlotIndex,
uint32 basePower,
... // same other params
) internal returns (int32, bytes32)
```

This calls `ENGINE.getDamageCalcContextForSlot()` instead of `ENGINE.getDamageCalcContext()`.

Keep existing `_calculateDamage` unchanged for singles backward compatibility.

## Phase 4: Slot-Aware Effects

### 4a. StaminaRegen.sol
Update `onRoundEnd`:
- Check `ENGINE.getGameMode(battleKey)`
- If doubles: iterate both slots for each player using `ENGINE.getActiveMonIndexForSlot()`
- If singles: use existing logic with `p0ActiveMonIndex`/`p1ActiveMonIndex`

### 4b. Overclock.sol
Update `onApply` and `onRemove`:
- Check game mode
- If doubles: apply/remove stat changes for both slots using `ENGINE.getActiveMonIndexForSlot()`
- If singles: use existing `p0ActiveMonIndex`/`p1ActiveMonIndex` logic

**Key insight**: These effects keep all their existing signatures. They just need to internally query extra data from the engine when in doubles mode.

## Phase 5: DoublesCommitManager

New contract `src/commit-manager/DoublesCommitManager.sol`:
- Extends the same pattern as DefaultCommitManager
- Handles committing/revealing 2 moves per turn (one per slot)
- Uses a single hash covering both moves
- Validates both moves are legal for their respective slots
- Prevents both slots from switching to the same mon
- Calls `ENGINE.setMoveForSlot()` for each slot

## Phase 6: IEngine.sol Updates

Add new function signatures (don't remove existing ones):
- `getGameMode()`
- `getActiveMonIndexForSlot()`
- `getDamageCalcContextForSlot()`
- `setMoveForSlot()`
- `switchActiveMonForSlot()`

## Phase 7: Matchmaker/Battle struct updates

- Add `GameMode gameMode` to `Battle` and `ProposedBattle`
- DefaultMatchmaker passes it through
- Default to `Singles` for backward compatibility

## Phase 8: Test Infrastructure

### 8a. BattleHelper.sol additions
- `_startDoublesBattle()` - starts a doubles mode battle
- `_doublesCommitRevealExecute()` - commit/reveal/execute for doubles (4 moves per turn)

### 8b. Test files
- Doubles validation tests (DoublesValidationTest.sol or inline in EngineTest.sol)
- StaminaRegen doubles test
- Overclock doubles test
- DoublesCommitManager test

## File Change Summary

| File | Change Type | Description |
|------|-------------|-------------|
| `src/Enums.sol` | Modify | Add `GameMode` enum |
| `src/Constants.sol` | Modify | Add doubles packing constants |
| `src/Structs.sol` | Modify | Add fields to BattleData, BattleConfig, Battle, ProposedBattle; add RevealedMovesPair |
| `src/IEngine.sol` | Modify | Add new getter/setter signatures |
| `src/Engine.sol` | Modify | Add doubles execution path, slot helpers, new getters |
| `src/moves/AttackCalculator.sol` | Modify | Add slot-aware damage calc function |
| `src/effects/StaminaRegen.sol` | Modify | Make doubles-aware |
| `src/effects/battlefield/Overclock.sol` | Modify | Make doubles-aware |
| `src/commit-manager/DoublesCommitManager.sol` | New | Doubles commit/reveal manager |
| `src/matchmaker/DefaultMatchmaker.sol` | Modify | Pass through GameMode |
| `test/abstract/BattleHelper.sol` | Modify | Add doubles helpers |
| `test/DoublesTest.sol` (or similar) | New | Doubles test suite |

## Commit Order

1. **Data structures + constants** - Enums, Constants, Structs changes
2. **IEngine interface** - Add new function signatures
3. **Engine core** - Slot packing helpers, getters, startBattle updates
4. **Engine doubles execution** - _executeDoubles and related functions
5. **AttackCalculator** - Slot-aware damage calculation
6. **Effects** - StaminaRegen and Overclock doubles awareness
7. **DoublesCommitManager** - New commit manager
8. **Matchmaker** - GameMode passthrough
9. **Tests** - Test infrastructure and test cases
38 changes: 19 additions & 19 deletions snapshots/EngineGasTest.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
{
"B1_Execute": "941274",
"B1_Setup": "818934",
"B2_Execute": "698961",
"B2_Setup": "280868",
"Battle1_Execute": "505716",
"Battle1_Setup": "794244",
"Battle2_Execute": "398431",
"Battle2_Setup": "235908",
"External_Execute": "502798",
"External_Setup": "784959",
"FirstBattle": "3192359",
"Inline_Execute": "348349",
"Inline_Setup": "224525",
"Intermediary stuff": "46798",
"SecondBattle": "3270181",
"Setup 1": "1675284",
"Setup 2": "298117",
"Setup 3": "340710",
"ThirdBattle": "2580771"
"B1_Execute": "974979",
"B1_Setup": "823882",
"B2_Execute": "726954",
"B2_Setup": "285841",
"Battle1_Execute": "517262",
"Battle1_Setup": "799095",
"Battle2_Execute": "409993",
"Battle2_Setup": "238802",
"External_Execute": "514344",
"External_Setup": "789810",
"FirstBattle": "3308817",
"Inline_Execute": "357126",
"Inline_Setup": "227374",
"Intermediary stuff": "47028",
"SecondBattle": "3407003",
"Setup 1": "1680587",
"Setup 2": "301690",
"Setup 3": "344290",
"ThirdBattle": "2697363"
}
7 changes: 7 additions & 0 deletions snapshots/EngineTest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"FirstBattle": "6745022",
"Intermediary stuff": "47879",
"SecondBattle": "6826261",
"Setup 1": "1906149",
"Setup 2": "363778"
}
28 changes: 14 additions & 14 deletions snapshots/InlineEngineGasTest.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
{
"B1_Execute": "923868",
"B1_Setup": "757634",
"B2_Execute": "663118",
"B2_Setup": "267074",
"Battle1_Execute": "453108",
"Battle1_Setup": "732936",
"Battle2_Execute": "348301",
"Battle2_Setup": "223872",
"FirstBattle": "2907336",
"SecondBattle": "2950149",
"Setup 1": "1612799",
"Setup 2": "322001",
"Setup 3": "318466",
"ThirdBattle": "2295522"
"B1_Execute": "952063",
"B1_Setup": "762525",
"B2_Execute": "685452",
"B2_Setup": "271875",
"Battle1_Execute": "461885",
"Battle1_Setup": "737730",
"Battle2_Execute": "357078",
"Battle2_Setup": "226715",
"FirstBattle": "3005143",
"SecondBattle": "3066262",
"Setup 1": "1618034",
"Setup 2": "325488",
"Setup 3": "321959",
"ThirdBattle": "2393386"
}
6 changes: 3 additions & 3 deletions snapshots/MatchmakerTest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"Accept1": "312196",
"Accept2": "34057",
"Propose1": "197214"
"Accept1": "314219",
"Accept2": "34531",
"Propose1": "199690"
}
23 changes: 23 additions & 0 deletions src/Constants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,29 @@ address constant TOMBSTONE_ADDRESS = address(0xdead);

uint256 constant MAX_BATTLE_DURATION = 1 hours;

// Active mon index packing (uint16):
// Singles: lower 8 bits = p0 active, upper 8 bits = p1 active (backwards compatible)
// Doubles: 4 bits per slot (supports up to 16 mons per team)
// Bits 0-3: p0 slot 0 active mon index
// Bits 4-7: p0 slot 1 active mon index
// Bits 8-11: p1 slot 0 active mon index
// Bits 12-15: p1 slot 1 active mon index
uint8 constant ACTIVE_MON_INDEX_BITS = 4;
uint8 constant ACTIVE_MON_INDEX_MASK = 0x0F; // 4 bits

// Slot switch flags + game mode packing (uint8):
// Bit 0: p0 slot 0 needs switch
// Bit 1: p0 slot 1 needs switch
// Bit 2: p1 slot 0 needs switch
// Bit 3: p1 slot 1 needs switch
// Bit 4: game mode (0 = singles, 1 = doubles)
uint8 constant SWITCH_FLAG_P0_SLOT0 = 0x01;
uint8 constant SWITCH_FLAG_P0_SLOT1 = 0x02;
uint8 constant SWITCH_FLAG_P1_SLOT0 = 0x04;
uint8 constant SWITCH_FLAG_P1_SLOT1 = 0x08;
uint8 constant SWITCH_FLAGS_MASK = 0x0F;
uint8 constant GAME_MODE_BIT = 0x10; // Bit 4: 0 = singles, 1 = doubles

bytes32 constant MOVE_MISS_EVENT_TYPE = sha256(abi.encode("MoveMiss"));
bytes32 constant MOVE_CRIT_EVENT_TYPE = sha256(abi.encode("MoveCrit"));
bytes32 constant MOVE_TYPE_IMMUNITY_EVENT_TYPE = sha256(abi.encode("MoveTypeImmunity"));
Expand Down
Loading
Loading