diff --git a/CLAUDE.md b/CLAUDE.md index c8299f56..37ad401e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -257,7 +257,7 @@ How it works: |---|---| | `MonOwnership` | `monsOwned` per-player set; ownership view + bulk-check helpers (`_isOwnerBatch`, `_validateOwnership`). Satisfies the `_isFacetMonOwned` hook on `Facets`. | | `MonRegistry` | Owner-managed mon catalog (`monStats`, `monMoves`, `monAbilities`, `monMetadata`, sequential `monIds` set). Inherits `ITeamRegistry` so its `createMon`/`modifyMon`/batched-getters bind directly. Backs the `_getMonStatsForFacets` hook used by facet delta computation. | -| `PlayerProfile` | Packed `playerData[address]` (one uint256 per player) + the owner-managed `isAssigner` allowlist. Exposes `pointsBalance`, `isWhitelistedOpponent`, `isHardCpu`, the bulk admin flag setters, and `assignPoints` (IGachaPointsAssigner). | +| `PlayerProfile` | Packed `playerData[address]` (one uint256 per player) + the owner-managed `isAssigner` allowlist. Exposes `pointsBalance`, `isWhitelistedOpponent`, the bulk admin flag setters, and `assignPoints` (IGachaPointsAssigner). | | `PackedTeamStore` | Bit-packed team CRUD: `teamGroupsPacked` (4 teams × 64 bits per slot), `teamOrderPacked` (16-slot live bitmap + display order). Constructor takes `MONS_PER_TEAM` / `MOVES_PER_MON` immutables. Subclass hooks: `_packedTeamValidateOwnership`, `_packedTeamIsCpuOpponent`, `_packedTeamGetMonData`. | | `Facets` | 12-facet ±5% stat tradeoff system (see below). Pure helpers, packed per-mon facet data, `assignFacets`. | | `MonExp` | Packed per-mon exp (`packedExpForMon`), level curve, level-up facet draws (`_processLevelUps`), public `getExp` / `getLevel` / `getExpAndLevelsFor*`, assigner-gated `assignExp`. Inherits `Facets` + `PackedTeamStore` because level-ups draw facets and team views need the lane helpers. Subclass hooks: `_assertExpAssigner`, `_monRegistrySize`. | @@ -277,7 +277,7 @@ The leaf adds: the `onBattleEnd` orchestration (streak / quest / points / exp lo playerData[address] (1 slot per player): bit 255 bonusAwarded (first-game-ever bonus claimed) bit 254 isWhitelistedAsOpponent (admin-set; replaces a separate mapping) - bit 253 isHardCpu (only meaningful when bit 254 is set) + bit 253 (reserved; formerly isHardCpu) bits 250-252 streakDay (1..STREAK_FLAT_BONUS_MAX; 0 = no streak yet) bits 224-249 (reserved) bits 192-223 lastQuestCompletedDay (uint32 calendar day) @@ -308,7 +308,7 @@ Both per-mon mappings share the same 16-mon bucketing so `_applyExpAndFacetDraws - Streak flat: `streakDay` (1..5) added inside the parenthetical of both the points and per-mon-exp formulas. Only granted when the battle qualifies as "first of day" (≥24h since the last grant). - Points formula: `(basePts + streakFlat) × gachaMult + firstGameEverBonus`. `gachaMult` is `×QUEST_REWARD_MULT` (= 2) when the active daily quest completes (winner-only, one-shot per day), else 1. `firstGameEverBonus` is `+FIRST_GAME_EVER_BONUS` (= 16), one-shot ever, applied *after* the multiplier. - Per-mon exp formula: `(baseExp + streakFlat) × expMult`, capped at 65535. `baseExp` is `EXP_PER_SURVIVING_MON` (2) for alive slots, `EXP_PER_KOD_MON` (1) for KO'd slots. -- `expMult` stack: `PVP_EXP_MULT` (×2) on any PvP battle **xor** `HARD_CPU_EXP_MULT` (×2) on a battle against the Hard CPU (mutually exclusive — PvP wins the branch), times `QUEST_REWARD_MULT` (×2) on quest completion. Max stack = ×4. +- `expMult` stack: a flat `GAME_EXP_MULT` (×2) on **every** battle (the old PvP/Hard-CPU game-type bonuses were removed — hard-CPU difficulty was client-mutable and so nonsensical as a reward gate), times `QUEST_REWARD_MULT` (×2) on quest completion. Max stack = ×4. Points are **not** multiplied by the game mult — only by quest. - Level-ups (12-tier curve, capped at level 12 to match `TOTAL_FACETS`) trigger one facet draw per level crossed. **Facets.** 12 systematically-derived stat tradeoffs across 4 stat groups (`HP`, `Atk`, `Def`, `Speed`). `_facetDef(facetId)` is pure — no constant table. Magnitudes are **boost-indexed**: the boost stat determines both the boost% and the cost% paid on the nerfed stat. HP/Atk/Def boosts are symmetric (+5% / -5%); speed boosts pay a heavier cost (+5% / -10%) so they can't cheaply break speed ties. The percentages live as `BOOST_PCT_*` / `COST_PCT_*` constants in `Facets.sol`. Unlocks are persistent per-mon; `assignFacets(monIds, facetIds)` is a free bulk re-assign that requires the caller to own every listed mon and the facet to be in the unlocked bitmap (`facetId == 0` clears). `GachaTeamRegistry.getTeams()` folds the active facet's delta into each mon's stats before returning, so by the time the Engine stores teams in `BattleConfig` they already reflect the boost/cost. `validateMon` no longer checks stat equality (the round-trip would always fail with facets applied) — moves and ability membership are still enforced. @@ -320,7 +320,7 @@ Both per-mon mappings share the same 16-mon bucketing so `_applyExpAndFacetDraws **Events.** - `Roll(address indexed player, uint256[] monIds, uint256 pointsSpent)` — fires on both `firstRoll` (spend = 0) and paid `roll`. -- `GachaEvent(bytes32 indexed battleKey, uint256 p0Packed, uint256 p1Packed)` — one per battle, carrying both sides' packed payloads (CPU side is 0). Layout sized for `MONS_PER_TEAM` up to 8: points (bits 0-15), per-mon exp gain (bits 16-79, 8 lanes × 8b), per-mon facets unlocked this battle (bits 80-175, 8 lanes × 12-bit bitmap = 1 bit per facet id), `BONUS_*` flags (bits 176-183: `FIRST_ROLL` | `FIRST_GAME` | `HARD_CPU` | `QUEST`), combined exp multiplier (bits 184-191), outcome (bits 192-199: 0=loss, 1=win, 2=draw), `streakDay` (bits 200-202). Lanes saturate so a future tuning blow-up can't bleed into neighbouring fields. +- `GachaEvent(bytes32 indexed battleKey, uint256 p0Packed, uint256 p1Packed)` — one per battle, carrying both sides' packed payloads (CPU side is 0). Layout sized for `MONS_PER_TEAM` up to 8: points (bits 0-15), per-mon exp gain (bits 16-79, 8 lanes × 8b), per-mon facets unlocked this battle (bits 80-175, 8 lanes × 12-bit bitmap = 1 bit per facet id), `BONUS_*` flags (bits 176-183: `FIRST_ROLL` | `FIRST_GAME` | _bit 2 reserved, formerly `HARD_CPU`_ | `QUEST`), combined exp multiplier (bits 184-191; now always ≥2 from the flat game mult), outcome (bits 192-199: 0=loss, 1=win, 2=draw), `streakDay` (bits 200-202). Lanes saturate so a future tuning blow-up can't bleed into neighbouring fields. ### Storage Architecture diff --git a/processing/generateSetupCPU.py b/processing/generateSetupCPU.py index 2bb30727..a61b4021 100644 --- a/processing/generateSetupCPU.py +++ b/processing/generateSetupCPU.py @@ -33,7 +33,6 @@ def load_cpu_teams(json_path: Path) -> dict[str, Any]: def render_solidity(data: dict[str, Any], mon_names: dict[int, str]) -> str: cpu_players: list[str] = data["cpuPlayers"] - hard_cpus: list[str] = data.get("hardCpus", []) teams: list[list[int]] = data["teams"] lines: list[str] = [] @@ -86,13 +85,6 @@ def render_solidity(data: dict[str, Any], mon_names: dict[int, str]) -> str: lines.append(" }") lines.append(" gachaTeamRegistry.setWhitelistedOpponents(cpuAddresses, empty);") - if hard_cpus: - lines.append("") - lines.append(f" address[] memory hardCpus = new address[]({len(hard_cpus)});") - for i, name in enumerate(hard_cpus): - lines.append(f' hardCpus[{i}] = vm.envAddress("{name}");') - lines.append(" gachaTeamRegistry.setHardCpuOpponents(hardCpus, empty);") - lines.append("") lines.append(" vm.stopBroadcast();") lines.append("") diff --git a/src/Constants.sol b/src/Constants.sol index e49ccae6..2a2d1316 100644 --- a/src/Constants.sol +++ b/src/Constants.sol @@ -86,12 +86,12 @@ uint256 constant GACHA_FIRST_GAME_EVER_BONUS = 16; uint256 constant EXP_PER_SURVIVING_MON = 2; uint256 constant EXP_PER_KOD_MON = 1; -// Always-on exp multipliers (game-type bonuses) -uint256 constant PVP_EXP_MULT = 2; -uint256 constant HARD_CPU_EXP_MULT = 2; +// Flat exp multiplier applied to every game's per-mon exp. Replaces the old +// game-type bonuses (hard-CPU difficulty was client-mutable and PvP/CPU no longer differ). +uint256 constant GAME_EXP_MULT = 2; // First-game-of-the-day flat bonus that ratchets with a daily-login streak. -// Added to base reward *before* multipliers so it rides quest / PvP / hard-CPU mults. +// Added to base reward *before* multipliers so it rides the game + quest mults. // Streak resets to 1 once the gap since the last bonus exceeds STREAK_GRACE_WINDOW. uint256 constant STREAK_FLAT_BONUS_MAX = 5; uint256 constant STREAK_GRACE_WINDOW = 36 hours; diff --git a/src/Structs.sol b/src/Structs.sol index f10d80be..617e9120 100644 --- a/src/Structs.sol +++ b/src/Structs.sol @@ -35,7 +35,6 @@ struct CustomBattleProposal { uint96 p0TeamIndex; uint256[] monIndices; uint8[] facetIds; - bool isHard; // hard-CPU difficulty flag for the phantom config (client-set; drives ×2 exp) ITeamRegistry teamRegistry; IValidator validator; IRandomnessOracle rngOracle; diff --git a/src/cpu/CPU.sol b/src/cpu/CPU.sol index 7d649aa6..7a599fb3 100644 --- a/src/cpu/CPU.sol +++ b/src/cpu/CPU.sol @@ -297,7 +297,7 @@ abstract contract CPU is CPUMoveManager, ICPU, ICPURNG, IMatchmaker { /// to other users' slots. The registry must implement IPhantomTeamRegistry; the relay gate /// enforces that only whitelisted CPUs (i.e. this contract once added) can land the write. function startCustomBattle(CustomBattleProposal calldata p) external returns (bytes32 battleKey) { - IPhantomTeamRegistry(address(p.teamRegistry)).setOpponentTeamFor(p.p0, p.monIndices, p.facetIds, p.isHard); + IPhantomTeamRegistry(address(p.teamRegistry)).setOpponentTeamFor(p.p0, p.monIndices, p.facetIds); uint96 p1TeamIndex = uint96(uint16(uint160(p.p0))); (battleKey,) = ENGINE.computeBattleKey(p.p0, address(this)); diff --git a/src/game-layer/GachaTeamRegistry.sol b/src/game-layer/GachaTeamRegistry.sol index 64de36e1..8e0e9748 100644 --- a/src/game-layer/GachaTeamRegistry.sol +++ b/src/game-layer/GachaTeamRegistry.sol @@ -21,8 +21,7 @@ import { GACHA_FIRST_GAME_EVER_BONUS, EXP_PER_SURVIVING_MON, EXP_PER_KOD_MON, - PVP_EXP_MULT, - HARD_CPU_EXP_MULT, + GAME_EXP_MULT, STREAK_FLAT_BONUS_MAX, STREAK_GRACE_WINDOW, QUEST_REWARD_MULT, @@ -88,7 +87,7 @@ contract GachaTeamRegistry is uint256 internal constant BONUS_FIRST_ROLL = 1 << 0; uint256 internal constant BONUS_FIRST_GAME = 1 << 1; - uint256 internal constant BONUS_HARD_CPU = 1 << 2; + // bit 2 (formerly BONUS_HARD_CPU) is reserved/unused: hard-CPU difficulty was removed. uint256 internal constant BONUS_QUEST = 1 << 3; // ----- Errors ----- @@ -123,12 +122,6 @@ contract GachaTeamRegistry is // resolves both the team's mon ids and its facet config at battle start. uint256 internal constant OPP_FACET_BITS_PER_SLOT = 4; uint256 internal constant OPP_FACET_SLOT_MASK = (1 << OPP_FACET_BITS_PER_SLOT) - 1; - // Hard-CPU difficulty flag inside the per-(user, CPU) phantom config slot - // (opponentTeamFacetsPacked). High bit, far above the facet lanes (<= 8 mons x 4 bits = 32 bits). - // Client-set via setOpponentTeam/For: once the CPU runs off-chain (and the CPU contracts collapse - // to one address) difficulty is a client concept, so the hard-CPU x2 exp keys off this bit rather - // than the opponent's profile IS_HARD_CPU_BIT. Trust-the-client (see PLAN_OFFCHAIN_CPU.md). - uint256 internal constant OPP_HARD_CPU_BIT = 1 << 255; mapping(address opponent => mapping(uint256 phantomKey => uint256 packedFacets)) public opponentTeamFacetsPacked; constructor( @@ -260,11 +253,10 @@ contract GachaTeamRegistry is function setOpponentTeam( address opponent, uint256[] memory monIndices, - uint8[] memory facetIds, - bool isHard + uint8[] memory facetIds ) external { if (!isWhitelistedOpponent(opponent)) revert NotWhitelistedOpponent(); - _setOpponentTeam(opponent, msg.sender, monIndices, facetIds, isHard); + _setOpponentTeam(opponent, msg.sender, monIndices, facetIds); } /// @notice Trusted-relayer entry: a whitelisted CPU writes a user's phantom team @@ -273,19 +265,17 @@ contract GachaTeamRegistry is function setOpponentTeamFor( address user, uint256[] memory monIndices, - uint8[] memory facetIds, - bool isHard + uint8[] memory facetIds ) external override { if (!isWhitelistedOpponent(msg.sender)) revert NotWhitelistedOpponent(); - _setOpponentTeam(msg.sender, user, monIndices, facetIds, isHard); + _setOpponentTeam(msg.sender, user, monIndices, facetIds); } function _setOpponentTeam( address opponent, address user, uint256[] memory monIndices, - uint8[] memory facetIds, - bool isHard + uint8[] memory facetIds ) internal { if (monIndices.length != facetIds.length) revert FacetArgsLengthMismatch(); uint256 phantomKey = uint16(uint160(user)); @@ -298,9 +288,6 @@ contract GachaTeamRegistry is packedFacets |= uint256(facetId) << (i * OPP_FACET_BITS_PER_SLOT); unchecked { ++i; } } - // Difficulty rides in the same slot (high bit), client-set. Whole slot is rewritten each - // call, so the flag must be (re)applied here. - if (isHard) packedFacets |= OPP_HARD_CPU_BIT; opponentTeamFacetsPacked[opponent][phantomKey] = packedFacets; } @@ -534,7 +521,6 @@ contract GachaTeamRegistry is uint256 packed1 = playerData[ctx.p1]; bool isCpu0 = packed0 & IS_CPU_BIT != 0; bool isCpu1 = packed1 & IS_CPU_BIT != 0; - bool isPvP = !(isCpu0 || isCpu1); uint256[2] memory packedEvents; @@ -547,17 +533,8 @@ contract GachaTeamRegistry is uint8 koBitmap = playerIndex == 0 ? ctx.p0KOBitmap : ctx.p1KOBitmap; uint256 basePts = ctx.winner == player ? POINTS_PER_WIN : POINTS_PER_LOSS; uint256 packed = playerIndex == 0 ? packed0 : packed1; - // Hard-CPU x2 exp keys off the opponent's per-(user, CPU) phantom config bit (client-set - // via setOpponentTeam), not the opponent's profile flag: once the CPU runs off-chain and - // the CPU contracts collapse to one address, difficulty is a client concept. The CPU's - // battle team index IS this user's phantom key, so the phantom slot reads back here. - bool oppIsCpu = playerIndex == 0 ? isCpu1 : isCpu0; - address oppAddr = playerIndex == 0 ? ctx.p1 : ctx.p0; - uint256 oppTeamIdx = playerIndex == 0 ? ctx.p1TeamIndex : ctx.p0TeamIndex; - bool oppIsHardCpu = - oppIsCpu && (opponentTeamFacetsPacked[oppAddr][oppTeamIdx] & OPP_HARD_CPU_BIT) != 0; - - uint256 preservedFlags = packed & (BONUS_AWARDED_BIT | IS_CPU_BIT | IS_HARD_CPU_BIT); + + uint256 preservedFlags = packed & (BONUS_AWARDED_BIT | IS_CPU_BIT); uint256 points = packed & POINTS_MASK_128; uint32 lastFirstGameTs = uint32(packed >> LAST_FIRST_GAME_TS_SHIFT); uint32 lastSeenTs = uint32(packed >> LAST_SEEN_TS_SHIFT); @@ -566,16 +543,10 @@ contract GachaTeamRegistry is uint256 bonusFlags; uint256 streakFlat; - uint256 expMult = 1; + // Flat 2x exp on every game (game-type bonuses removed); quest can still stack on top. + uint256 expMult = GAME_EXP_MULT; uint256 gachaMult = 1; - if (isPvP) { - expMult *= PVP_EXP_MULT; - } else if (oppIsHardCpu) { - expMult *= HARD_CPU_EXP_MULT; - bonusFlags |= BONUS_HARD_CPU; - } - // The rolling 24h cooldown (measured from the last bonus-earning game) gates the // streak bonus to once per day. The 36h grace decides ratchet-vs-reset, but is // measured from the last battle of ANY kind (lastSeenTs) rather than the last diff --git a/src/game-layer/IPhantomTeamRegistry.sol b/src/game-layer/IPhantomTeamRegistry.sol index 83901f31..cb80f45b 100644 --- a/src/game-layer/IPhantomTeamRegistry.sol +++ b/src/game-layer/IPhantomTeamRegistry.sol @@ -5,6 +5,6 @@ pragma solidity ^0.8.0; /// caller (a whitelisted CPU). Lets a matchmaker bundle team-config + battle-start /// in one tx while preserving per-user phantom-slot isolation. interface IPhantomTeamRegistry { - function setOpponentTeamFor(address user, uint256[] memory monIndices, uint8[] memory facetIds, bool isHard) + function setOpponentTeamFor(address user, uint256[] memory monIndices, uint8[] memory facetIds) external; } diff --git a/src/game-layer/PlayerProfile.sol b/src/game-layer/PlayerProfile.sol index fc6a7040..49251d38 100644 --- a/src/game-layer/PlayerProfile.sol +++ b/src/game-layer/PlayerProfile.sol @@ -9,7 +9,7 @@ import {IGachaPointsAssigner} from "./IGachaPointsAssigner.sol"; /// `playerData[address]` bit layout: /// bit 255 : bonusAwarded (first-game-ever bonus has been awarded) /// bit 254 : isWhitelistedAsOpponent (CPU flag) -/// bit 253 : isHardCpu (only meaningful when bit 254 is set) +/// bit 253 : (reserved; formerly isHardCpu) /// bits 250-252 : streakDay (1..STREAK_FLAT_BONUS_MAX; 0 = no streak yet) /// bits 224-249 : (reserved) /// bits 192-223 : lastQuestCompletedDay (uint32 calendar day) @@ -24,7 +24,7 @@ abstract contract PlayerProfile is IGachaPointsAssigner, Ownable { uint256 internal constant BONUS_AWARDED_BIT = 1 << 255; uint256 internal constant IS_CPU_BIT = 1 << 254; - uint256 internal constant IS_HARD_CPU_BIT = 1 << 253; + // bit 253 reserved (formerly IS_HARD_CPU_BIT) uint256 internal constant STREAK_DAY_SHIFT = 250; uint256 internal constant STREAK_DAY_MASK = 0x7; uint256 internal constant LAST_FIRST_GAME_TS_SHIFT = 128; @@ -44,10 +44,6 @@ abstract contract PlayerProfile is IGachaPointsAssigner, Ownable { _flipPlayerDataBitBulk(toAllow, toDisallow, IS_CPU_BIT); } - function setHardCpuOpponents(address[] memory toMark, address[] memory toUnmark) external onlyOwner { - _flipPlayerDataBitBulk(toMark, toUnmark, IS_HARD_CPU_BIT); - } - function setAssigners(address[] memory toAdd, address[] memory toRemove) external onlyOwner { for (uint256 i; i < toAdd.length;) { isAssigner[toAdd[i]] = true; @@ -80,10 +76,6 @@ abstract contract PlayerProfile is IGachaPointsAssigner, Ownable { return playerData[addr] & IS_CPU_BIT != 0; } - function isHardCpu(address addr) public view returns (bool) { - return playerData[addr] & IS_HARD_CPU_BIT != 0; - } - // ----- IGachaPointsAssigner ----- function assignPoints(address player, uint256 amount) external override { diff --git a/test/GachaFacetsBattleTest.sol b/test/GachaFacetsBattleTest.sol index 75c9ce04..21de4cec 100644 --- a/test/GachaFacetsBattleTest.sol +++ b/test/GachaFacetsBattleTest.sol @@ -112,7 +112,7 @@ contract GachaFacetsBattleTest is Test { cpuFacets[0] = 1; cpuFacets[1] = 0; vm.prank(ALICE); - registry.setOpponentTeam(CPU, cpuMons, cpuFacets, false); + registry.setOpponentTeam(CPU, cpuMons, cpuFacets); // Both players must authorize the matchmaker with the engine. address[] memory makersToAdd = new address[](1); diff --git a/test/GachaMigrationTest.sol b/test/GachaMigrationTest.sol index c3009e3d..e6d5fa7a 100644 --- a/test/GachaMigrationTest.sol +++ b/test/GachaMigrationTest.sol @@ -84,7 +84,6 @@ contract GachaMigrationTest is Test { address[] memory aliceList = new address[](1); aliceList[0] = ALICE; oldReg.setWhitelistedOpponents(aliceList, new address[](0)); - oldReg.setHardCpuOpponents(aliceList, new address[](0)); // ----- New (destination) registry: points back at oldReg. ----- newReg = new GachaTeamRegistry(MONS_PER_TEAM, MOVES_PER_MON, engine, mockRNG, oldReg); @@ -114,7 +113,6 @@ contract GachaMigrationTest is Test { assertEq(newReg.playerData(ALICE), oldReg.playerData(ALICE), "profile word verbatim"); assertEq(newReg.pointsBalance(ALICE), 1234, "points carried"); assertTrue(newReg.isWhitelistedOpponent(ALICE), "whitelist flag carried"); - assertTrue(newReg.isHardCpu(ALICE), "hard-cpu flag carried"); } function test_migrate_copiesExpAndFacets() public { diff --git a/test/GachaTeamRegistryTest.sol b/test/GachaTeamRegistryTest.sol index cbb0c6ce..5ae0fcf9 100644 --- a/test/GachaTeamRegistryTest.sol +++ b/test/GachaTeamRegistryTest.sol @@ -196,7 +196,7 @@ contract GachaTeamRegistryTest is Test { monIndices[0] = 0; monIndices[1] = 1; vm.expectRevert(GachaTeamRegistry.NotWhitelistedOpponent.selector); - gachaTeamRegistry.setOpponentTeam(CPU, monIndices, _zeroFacets(), false); + gachaTeamRegistry.setOpponentTeam(CPU, monIndices, _zeroFacets()); } // Covers both "phantom team is keyed at uint256(uint160(msg.sender))" and "no ownership check". @@ -208,7 +208,7 @@ contract GachaTeamRegistryTest is Test { uint256[] memory monIndices = new uint256[](MONS_PER_TEAM); monIndices[0] = unownedMonId; // Alice does NOT own this mon. monIndices[1] = 0; - gachaTeamRegistry.setOpponentTeam(CPU, monIndices, _zeroFacets(), false); + gachaTeamRegistry.setOpponentTeam(CPU, monIndices, _zeroFacets()); uint256[] memory readIndices = gachaTeamRegistry.getMonRegistryIndicesForTeam(CPU, uint256(uint16(uint160(ALICE)))); assertEq(readIndices[0], unownedMonId); @@ -223,12 +223,12 @@ contract GachaTeamRegistryTest is Test { uint256[] memory firstIndices = new uint256[](MONS_PER_TEAM); firstIndices[0] = 0; firstIndices[1] = 1; - gachaTeamRegistry.setOpponentTeam(CPU, firstIndices, _zeroFacets(), false); + gachaTeamRegistry.setOpponentTeam(CPU, firstIndices, _zeroFacets()); uint256[] memory secondIndices = new uint256[](MONS_PER_TEAM); secondIndices[0] = 2; secondIndices[1] = 3; - gachaTeamRegistry.setOpponentTeam(CPU, secondIndices, _zeroFacets(), false); + gachaTeamRegistry.setOpponentTeam(CPU, secondIndices, _zeroFacets()); uint256[] memory readIndices = gachaTeamRegistry.getMonRegistryIndicesForTeam(CPU, uint256(uint16(uint160(ALICE)))); assertEq(readIndices[0], 2); @@ -243,7 +243,7 @@ contract GachaTeamRegistryTest is Test { uint256[] memory monIndices = new uint256[](MONS_PER_TEAM); monIndices[0] = 0; monIndices[1] = 0; // duplicate - gachaTeamRegistry.setOpponentTeam(CPU, monIndices, _zeroFacets(), false); + gachaTeamRegistry.setOpponentTeam(CPU, monIndices, _zeroFacets()); uint256[] memory readIndices = gachaTeamRegistry.getMonRegistryIndicesForTeam(CPU, uint256(uint16(uint160(ALICE)))); assertEq(readIndices[0], 0); @@ -258,14 +258,14 @@ contract GachaTeamRegistryTest is Test { uint256[] memory aliceIndices = new uint256[](MONS_PER_TEAM); aliceIndices[0] = 0; aliceIndices[1] = 1; - gachaTeamRegistry.setOpponentTeam(CPU, aliceIndices, _zeroFacets(), false); + gachaTeamRegistry.setOpponentTeam(CPU, aliceIndices, _zeroFacets()); vm.stopPrank(); vm.startPrank(BOB); uint256[] memory bobIndices = new uint256[](MONS_PER_TEAM); bobIndices[0] = 2; bobIndices[1] = 3; - gachaTeamRegistry.setOpponentTeam(CPU, bobIndices, _zeroFacets(), false); + gachaTeamRegistry.setOpponentTeam(CPU, bobIndices, _zeroFacets()); vm.stopPrank(); uint256[] memory aliceTeam = gachaTeamRegistry.getMonRegistryIndicesForTeam(CPU, uint256(uint16(uint160(ALICE)))); @@ -283,7 +283,7 @@ contract GachaTeamRegistryTest is Test { vm.prank(ALICE); vm.expectRevert(Facets.FacetArgsLengthMismatch.selector); - gachaTeamRegistry.setOpponentTeam(CPU, monIndices, facets, false); + gachaTeamRegistry.setOpponentTeam(CPU, monIndices, facets); } function test_setOpponentTeam_revertsOnFacetIdOutOfRange() public { @@ -294,7 +294,7 @@ contract GachaTeamRegistryTest is Test { vm.prank(ALICE); vm.expectRevert(Facets.InvalidFacetId.selector); - gachaTeamRegistry.setOpponentTeam(CPU, monIndices, facets, false); + gachaTeamRegistry.setOpponentTeam(CPU, monIndices, facets); } function test_setOpponentTeam_perUserFacetsAreIsolated() public { @@ -308,9 +308,9 @@ contract GachaTeamRegistryTest is Test { bobFacets[0] = 0; bobFacets[1] = 12; vm.prank(ALICE); - gachaTeamRegistry.setOpponentTeam(CPU, monIndices, aliceFacets, false); + gachaTeamRegistry.setOpponentTeam(CPU, monIndices, aliceFacets); vm.prank(BOB); - gachaTeamRegistry.setOpponentTeam(CPU, monIndices, bobFacets, false); + gachaTeamRegistry.setOpponentTeam(CPU, monIndices, bobFacets); uint8[] memory aliceRead = gachaTeamRegistry.getOpponentTeamFacets(ALICE, CPU); uint8[] memory bobRead = gachaTeamRegistry.getOpponentTeamFacets(BOB, CPU); @@ -330,7 +330,7 @@ contract GachaTeamRegistryTest is Test { facets[1] = 7; vm.prank(ALICE); - gachaTeamRegistry.setOpponentTeam(CPU, monIndices, facets, false); + gachaTeamRegistry.setOpponentTeam(CPU, monIndices, facets); uint256 aliceTeamIdx = _aliceTeamIndex(); uint256 cpuTeamIdx = uint256(uint16(uint160(ALICE))); @@ -353,7 +353,7 @@ contract GachaTeamRegistryTest is Test { vm.prank(ALICE); vm.expectRevert(GachaTeamRegistry.NotWhitelistedOpponent.selector); - gachaTeamRegistry.setOpponentTeamFor(BOB, monIndices, _zeroFacets(), false); + gachaTeamRegistry.setOpponentTeamFor(BOB, monIndices, _zeroFacets()); } function test_setOpponentTeamFor_writesAtUserPhantomKey() public { @@ -364,7 +364,7 @@ contract GachaTeamRegistryTest is Test { monIndices[1] = 0; vm.prank(CPU); - gachaTeamRegistry.setOpponentTeamFor(ALICE, monIndices, _zeroFacets(), false); + gachaTeamRegistry.setOpponentTeamFor(ALICE, monIndices, _zeroFacets()); uint256[] memory readIndices = gachaTeamRegistry.getMonRegistryIndicesForTeam(CPU, uint256(uint16(uint160(ALICE)))); @@ -387,8 +387,8 @@ contract GachaTeamRegistryTest is Test { bobFacets[1] = 12; vm.startPrank(CPU); - gachaTeamRegistry.setOpponentTeamFor(ALICE, aliceIndices, aliceFacets, false); - gachaTeamRegistry.setOpponentTeamFor(BOB, bobIndices, bobFacets, false); + gachaTeamRegistry.setOpponentTeamFor(ALICE, aliceIndices, aliceFacets); + gachaTeamRegistry.setOpponentTeamFor(BOB, bobIndices, bobFacets); vm.stopPrank(); uint256[] memory aliceTeam = @@ -411,7 +411,7 @@ contract GachaTeamRegistryTest is Test { vm.prank(CPU); vm.expectRevert(Facets.FacetArgsLengthMismatch.selector); - gachaTeamRegistry.setOpponentTeamFor(ALICE, monIndices, facets, false); + gachaTeamRegistry.setOpponentTeamFor(ALICE, monIndices, facets); } function test_setOpponentTeamFor_revertsOnFacetIdOutOfRange() public { @@ -422,7 +422,7 @@ contract GachaTeamRegistryTest is Test { vm.prank(CPU); vm.expectRevert(Facets.InvalidFacetId.selector); - gachaTeamRegistry.setOpponentTeamFor(ALICE, monIndices, facets, false); + gachaTeamRegistry.setOpponentTeamFor(ALICE, monIndices, facets); } function test_setOpponentTeam_facetsIgnoredWhenSideNotWhitelisted() public { @@ -452,7 +452,7 @@ contract GachaTeamRegistryTest is Test { facets[1] = 10; // +Speed / -HP at the 10% speed cost. vm.prank(ALICE); - gachaTeamRegistry.setOpponentTeam(CPU, monIndices, facets, false); + gachaTeamRegistry.setOpponentTeam(CPU, monIndices, facets); uint256 aliceTeamIdx = _aliceTeamIndex(); uint256 cpuTeamIdx = uint256(uint16(uint160(ALICE))); @@ -513,7 +513,7 @@ contract GachaTeamRegistryTest is Test { uint256[] memory phantomTeam = new uint256[](MONS_PER_TEAM); phantomTeam[0] = unownedMonId; phantomTeam[1] = 0; - gachaTeamRegistry.setOpponentTeam(CPU, phantomTeam, _zeroFacets(), false); + gachaTeamRegistry.setOpponentTeam(CPU, phantomTeam, _zeroFacets()); vm.stopPrank(); Mon[][] memory teams = new Mon[][](2); @@ -604,7 +604,7 @@ contract GachaTeamRegistryTest is Test { // ===================================================================== // Test: KO'd mons get EXP_PER_KOD_MON, survivors get EXP_PER_SURVIVING_MON. - // (After today's first-game multiplier of 2x, that's 2 and 4.) + // Every game carries the flat GAME_EXP_MULT (2x), so the stored values are 4 and 6. function test_exp_gainsBaseAndDoubleByKOStatus() public { _whitelist(CPU); uint256 teamIdx = _aliceTeamIndex(); @@ -612,41 +612,23 @@ contract GachaTeamRegistryTest is Test { // Slot 0 KO'd, slot 1 alive (KO bitmap = 0b01 → bit 0 set). _runBattleEnd(_ctxAliceVsCpu(ALICE, 0x1, 0x3, uint16(teamIdx))); - // First-ever battle: streak=1, no game-type mult (CPU not hard). - // Slot 0 KO'd: (1 + 1) * 1 = 2. Slot 1 alive: (2 + 1) * 1 = 3. - assertEq(gachaTeamRegistry.getExp(ALICE, ALICE_TEAM_MON_0), 2, "slot 0 (KO'd) exp"); - assertEq(gachaTeamRegistry.getExp(ALICE, ALICE_TEAM_MON_1), 3, "slot 1 (alive) exp"); + // First-ever battle: streak=1, flat game mult 2x. + // Slot 0 KO'd: (1 + 1) * 2 = 4. Slot 1 alive: (2 + 1) * 2 = 6. + assertEq(gachaTeamRegistry.getExp(ALICE, ALICE_TEAM_MON_0), 4, "slot 0 (KO'd) exp"); + assertEq(gachaTeamRegistry.getExp(ALICE, ALICE_TEAM_MON_1), 6, "slot 1 (alive) exp"); } function test_exp_firstGameOfDayMultiplier() public { _whitelist(CPU); uint256 teamIdx = _aliceTeamIndex(); - // First-ever battle: streak=1, alive gain = (2+1)*1 = 3. + // First-ever battle: streak=1, alive gain = (2+1)*2 = 6. _runBattleEnd(_ctxAliceVsCpu(ALICE, 0x0, 0x3, uint16(teamIdx))); - assertEq(gachaTeamRegistry.getExp(ALICE, 0), 3, "first battle: alive + streak 1"); + assertEq(gachaTeamRegistry.getExp(ALICE, 0), 6, "first battle: alive + streak 1"); - // Second battle same day: no streak, gain = (2+0)*1 = 2. + // Second battle same day: no streak, gain = (2+0)*2 = 4. _runBattleEnd(_ctxAliceVsCpu(ALICE, 0x0, 0x3, uint16(teamIdx))); - assertEq(gachaTeamRegistry.getExp(ALICE, 0), 5, "+2 from second battle"); - } - - // The hard-CPU x2 exp multiplier now keys off the per-(user, CPU) phantom config bit (client-set - // via setOpponentTeam), NOT the opponent's profile flag — difficulty is a client concept once the - // CPU runs off-chain. phantomKey == uint16(uint160(ALICE)) == the CPU's p1TeamIndex. - function test_exp_hardCpuMultiplierFromPhantomConfig() public { - _whitelist(CPU); - uint256 teamIdx = _aliceTeamIndex(); - - // ALICE flags this CPU opponent as HARD in its phantom slot (trust-the-client). - uint256[] memory monIndices = new uint256[](MONS_PER_TEAM); - vm.prank(ALICE); - gachaTeamRegistry.setOpponentTeam(CPU, monIndices, _zeroFacets(), true); - - // First-ever battle vs HARD CPU: streak=1, expMult = HARD_CPU_EXP_MULT (x2). - // Alive slot gain = (2 + 1) * 2 = 6 (vs 3 for a non-hard CPU). - _runBattleEnd(_ctxAliceVsCpu(ALICE, 0x0, 0x3, uint16(teamIdx))); - assertEq(gachaTeamRegistry.getExp(ALICE, 0), 6, "hard-CPU x2 exp from phantom config bit"); + assertEq(gachaTeamRegistry.getExp(ALICE, 0), 10, "+4 from second battle"); } function test_exp_pvpAfterCpuSameDay() public { @@ -654,28 +636,28 @@ contract GachaTeamRegistryTest is Test { _bobOwnsTeam(); uint256 aliceTeam = _aliceTeamIndex(); - // First (CPU, not hard) battle: streak=1, gain = (2+1)*1 = 3. + // First (CPU) battle: streak=1, flat 2x mult, gain = (2+1)*2 = 6. _runBattleEnd(_ctxAliceVsCpu(ALICE, 0x0, 0x3, uint16(aliceTeam))); - assertEq(gachaTeamRegistry.getExp(ALICE, 0), 3, "after CPU win: 3"); + assertEq(gachaTeamRegistry.getExp(ALICE, 0), 6, "after CPU win: 6"); - // PvP same day: no streak, expMult = PvP 2x. Gain = (2+0)*2 = 4. + // PvP same day: no streak, same flat 2x mult. Gain = (2+0)*2 = 4. _runBattleEnd(_ctxAliceVsBob(ALICE, 0x0, 0x3, uint16(aliceTeam), 0)); - assertEq(gachaTeamRegistry.getExp(ALICE, 0), 7, "+4 from PvP mult"); + assertEq(gachaTeamRegistry.getExp(ALICE, 0), 10, "+4 from PvP (same flat mult)"); } function test_exp_dailyResetsAtNewDay() public { _whitelist(CPU); uint256 teamIdx = _aliceTeamIndex(); - _runBattleEnd(_ctxAliceVsCpu(ALICE, 0x0, 0x3, uint16(teamIdx))); // (2+1)*1 = 3 - _runBattleEnd(_ctxAliceVsCpu(ALICE, 0x0, 0x3, uint16(teamIdx))); // (2+0)*1 = 2 → 5 + _runBattleEnd(_ctxAliceVsCpu(ALICE, 0x0, 0x3, uint16(teamIdx))); // (2+1)*2 = 6 + _runBattleEnd(_ctxAliceVsCpu(ALICE, 0x0, 0x3, uint16(teamIdx))); // (2+0)*2 = 4 → 10 // Warp 1 day forward. vm.warp(vm.getBlockTimestamp() + 1 days); - // First battle of new day: streak ratchets to 2. Gain = (2+2)*1 = 4. + // First battle of new day: streak ratchets to 2. Gain = (2+2)*2 = 8. _runBattleEnd(_ctxAliceVsCpu(ALICE, 0x0, 0x3, uint16(teamIdx))); - assertEq(gachaTeamRegistry.getExp(ALICE, 0), 9, "+4 from streak day 2"); + assertEq(gachaTeamRegistry.getExp(ALICE, 0), 18, "+8 from streak day 2"); } // Regression: a player who plays slightly more often than once per 24h must still @@ -748,13 +730,14 @@ contract GachaTeamRegistryTest is Test { assertEq(gachaTeamRegistry.getExp(CPU, 1), 0, "CPU side mon 1 exp untouched"); } - function test_exp_pvpDetectionFalseWhenEitherSideWhitelisted() public { + // A CPU game carries the same flat GAME_EXP_MULT (2x) as any other game. + function test_exp_cpuGameGetsFlatMultiplier() public { _whitelist(CPU); uint256 aliceTeam = _aliceTeamIndex(); - // CPU game (not PvP): no PvP mult. Streak=1, gain = (2+1)*1 = 3. + // CPU game: flat 2x mult. Streak=1, gain = (2+1)*2 = 6. _runBattleEnd(_ctxAliceVsCpu(ALICE, 0x0, 0x3, uint16(aliceTeam))); - assertEq(gachaTeamRegistry.getExp(ALICE, 0), 3); + assertEq(gachaTeamRegistry.getExp(ALICE, 0), 6); } // packing_singleBucket: a 2-mon team where both ids are < 16. Verify exp accumulates for both. @@ -763,8 +746,9 @@ contract GachaTeamRegistryTest is Test { uint256 teamIdx = _aliceTeamIndex(); _runBattleEnd(_ctxAliceVsCpu(ALICE, 0x0, 0x3, uint16(teamIdx))); // Both mons < 16 → same bucket (bucket 0). Exp packed in adjacent lanes. - assertEq(gachaTeamRegistry.getExp(ALICE, ALICE_TEAM_MON_0), 3); - assertEq(gachaTeamRegistry.getExp(ALICE, ALICE_TEAM_MON_1), 3); + // First-ever battle: streak=1, flat 2x → (2+1)*2 = 6. + assertEq(gachaTeamRegistry.getExp(ALICE, ALICE_TEAM_MON_0), 6); + assertEq(gachaTeamRegistry.getExp(ALICE, ALICE_TEAM_MON_1), 6); } function test_exp_capsAtMax() public { @@ -1452,14 +1436,14 @@ contract GachaTeamRegistryTest is Test { } function test_quests_bonusStacksWithDailyMultipliers() public { - // First-ever PvP battle that also completes the quest. Streak=1, expMult = PvP 2x * Quest 2x = 4. + // First-ever battle that also completes the quest. Streak=1, expMult = game 2x * Quest 2x = 4. gachaTeamRegistry.addQuest(_simpleTurnsQuest(10)); _bobOwnsTeam(); uint256 aliceTeam = _aliceTeamIndex(); _runBattleEnd(_ctxAliceVsBob(ALICE, 0x0, 0x3, uint16(aliceTeam), 0)); // Surviving mon: (EXP_PER_SURVIVING_MON 2 + streak 1) * mult 4 = 12. - assertEq(gachaTeamRegistry.getExp(ALICE, 0), 12, "PvP * quest stack"); + assertEq(gachaTeamRegistry.getExp(ALICE, 0), 12, "game * quest stack"); } // ===================================================================== @@ -1515,7 +1499,7 @@ contract GachaTeamRegistryTest is Test { // Alice wins (first-ever, streak=1, quest passes, PvP). assertEq(ev.points, _winPts(1, true, true), "points total"); - // Exp mult: PvP 2x * quest 2x = 4. + // Exp mult: flat game 2x * quest 2x = 4. assertEq(ev.multiplier, 4, "exp mult x4"); // Per-mon gain: surviving slots get (baseExp + streak) * mult = (2 + 1) * 4 = 12. assertEq(ev.perMonExp[0], 12, "slot 0 gain"); @@ -1524,7 +1508,7 @@ contract GachaTeamRegistryTest is Test { assertEq(ev.perMonExp[j], 0, "unused lane zero"); assertEq(ev.perMonFacets[j], 0, "unused facet lane zero"); } - // FIRST_ROLL | FIRST_GAME | QUEST. BONUS_HARD_CPU does not fire in PvP. + // FIRST_ROLL | FIRST_GAME | QUEST. (Bit 2, formerly BONUS_HARD_CPU, is retired.) uint256 expectedFlags = (1 << 0) | (1 << 1) | (1 << 3); assertEq(ev.bonusFlags, expectedFlags, "bonus flags"); assertEq(ev.outcome, 1, "win outcome"); @@ -1547,7 +1531,7 @@ contract GachaTeamRegistryTest is Test { DecodedGachaEvent memory secondEv = _expectGachaEvent(ALICE); assertEq(secondEv.outcome, 1, "win outcome"); assertEq(secondEv.bonusFlags, 0, "no bonuses on second battle"); - assertEq(secondEv.multiplier, 1, "no multiplier"); + assertEq(secondEv.multiplier, 2, "flat game exp mult"); assertEq(secondEv.points, _winPts(0, false, false), "POINTS_PER_WIN only"); } @@ -1827,16 +1811,14 @@ contract GachaTeamRegistryTest is Test { } function test_assignPoints_preservesUpperBits() public { - // Mark ALICE as whitelisted CPU (sets IS_CPU_BIT) and hard CPU; both must survive - // the assignPoints write. + // Mark ALICE as whitelisted CPU (sets IS_CPU_BIT); the flag must survive the + // assignPoints write to the low (points) bits. address[] memory toAllow = new address[](1); address[] memory empty = new address[](0); toAllow[0] = ALICE; gachaTeamRegistry.setWhitelistedOpponents(toAllow, empty); - gachaTeamRegistry.setHardCpuOpponents(toAllow, empty); assertTrue(gachaTeamRegistry.isWhitelistedOpponent(ALICE)); - assertTrue(gachaTeamRegistry.isHardCpu(ALICE)); _authorizeAssigner(ASSIGNER); vm.prank(ASSIGNER); @@ -1844,7 +1826,6 @@ contract GachaTeamRegistryTest is Test { assertEq(gachaTeamRegistry.pointsBalance(ALICE), 123); assertTrue(gachaTeamRegistry.isWhitelistedOpponent(ALICE), "IS_CPU_BIT preserved"); - assertTrue(gachaTeamRegistry.isHardCpu(ALICE), "IS_HARD_CPU_BIT preserved"); } function test_assignPoints_revertsOnOverflow() public { diff --git a/test/StartBattleGasTest.t.sol b/test/StartBattleGasTest.t.sol index 0f46048b..a9f89469 100644 --- a/test/StartBattleGasTest.t.sol +++ b/test/StartBattleGasTest.t.sol @@ -123,7 +123,7 @@ contract StartBattleGasTest is Test, GasMeasure { uint8[] memory cpuFacets = new uint8[](MONS_PER_TEAM); for (uint256 i; i < MONS_PER_TEAM; ++i) { cpuMons[i] = mons[i]; cpuFacets[i] = 1; } vm.prank(ALICE); - registry.setOpponentTeam(CPU, cpuMons, cpuFacets, false); + registry.setOpponentTeam(CPU, cpuMons, cpuFacets); } /// @dev propose + accept (unmeasured); caller measures confirmBattle (-> startBattle) alone.