diff --git a/src/JackpotJunction.sol b/src/JackpotJunction.sol new file mode 100644 index 0000000..7d2c61e --- /dev/null +++ b/src/JackpotJunction.sol @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.13; + +import {ERC1155} from "../lib/openzeppelin-contracts/contracts/token/ERC1155/ERC1155.sol"; +import {ReentrancyGuard} from "../lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol"; + +/// @title Jackpot Junction game contract +/// @author Moonstream Engineering (engineering@moonstream.to) +/// +/// @notice This is the game contract for The Degen Trail: Jackpot Junction, a game in world of The Degen Trail. +contract JackpotJunction is ERC1155, ReentrancyGuard { + // Cumulative mass functions for probability distributions. Total mass for each distribution is 2^20 = 1048576. + uint256[5] public UnmodifiedOutcomesCumulativeMass = [ + 524288, + 524288 + 408934, + 524288 + 408934 + 104857, + 524288 + 408934 + 104857 + 10487, + 524288 + 408934 + 104857 + 10487 + 10 + ]; + uint256[5] public ImprovedOutcomesCumulativeMass = [ + 469283, + 469283 + 408934, + 469283 + 408934 + 154857, + 469283 + 408934 + 154857 + 15487, + 469283 + 408934 + 154857 + 15487 + 15 + ]; + + // How many blocks a player has to act (reroll/accept). + uint256 public BlocksToAct; + + // The block number of the last roll/re-roll by the player. + mapping(address => uint256) public LastRollBlock; + + // Costs (finest denomination of native token on the chain) to roll and reroll. + uint256 public CostToRoll; + uint256 public CostToReroll; + + // Item types: 0 (wagon cover), 1 (wagon body), 2 (wagon wheel), 3 (beast) + // Terrain types: 0 (plain), 1 (forest), 2 (swamp), 3 (water), 4 (mountain), 5 (desert), 6 (ice) + // Encoding of ERC1155 pool IDs: tier*28 + terrainType*4 + itemType + // itemType => terrainType => tier + mapping(uint256 => mapping(uint256 => uint256)) public CurrentTier; + + event TierUnlocked(uint256 indexed itemType, uint256 indexed terrainType, uint256 indexed tier, uint256 poolID); + event Roll(address indexed player); + event Award(address indexed player, uint256 indexed outcome, uint256 value); + + error DeadlineExceeded(); + error WaitForTick(); + error InsufficientValue(); + error InvalidItem(uint256 poolID); + error InsufficientItems(uint256 poolID); + + constructor(uint256 blocksToAct, uint256 costToRoll, uint256 costToReroll) + ERC1155("https://github.com/moonstream-to/degen-trail") + { + BlocksToAct = blocksToAct; + CostToRoll = costToRoll; + CostToReroll = costToReroll; + + for (uint256 i = 0; i < 4; i++) { + for (uint256 j = 0; j < 7; j++) { + emit TierUnlocked(i, j, 0, 4 * j + i); + } + } + } + + function genera(uint256 poolID) public pure returns (uint256 itemType, uint256 terrainType, uint256 tier) { + tier = poolID / 28; + terrainType = (poolID % 28) / 4; + itemType = poolID % 4; + } + + function sampleUnmodifiedOutcomeCumulativeMass(uint256 entropy) public view returns (uint256) { + uint256 sample = entropy << 236 >> 236; + if (sample < UnmodifiedOutcomesCumulativeMass[0]) { + return 0; + } else if (sample < UnmodifiedOutcomesCumulativeMass[1]) { + return 1; + } else if (sample < UnmodifiedOutcomesCumulativeMass[2]) { + return 2; + } else if (sample < UnmodifiedOutcomesCumulativeMass[3]) { + return 3; + } + return 4; + } + + function sampleImprovedOutcomesCumulativeMass(uint256 entropy) public view returns (uint256) { + uint256 sample = entropy << 236 >> 236; + if (sample < ImprovedOutcomesCumulativeMass[0]) { + return 0; + } else if (sample < ImprovedOutcomesCumulativeMass[1]) { + return 1; + } else if (sample < ImprovedOutcomesCumulativeMass[2]) { + return 2; + } else if (sample < ImprovedOutcomesCumulativeMass[3]) { + return 3; + } + return 4; + } + + function roll() external payable { + uint256 requiredFee = CostToRoll; + if (block.number <= LastRollBlock[msg.sender] + BlocksToAct) { + requiredFee = CostToReroll; + } + + if (msg.value < requiredFee) { + revert InsufficientValue(); + } + + LastRollBlock[msg.sender] = block.number; + + emit Roll(msg.sender); + } + + function _entropy(address degenerate) internal virtual view returns (uint256) { + return uint256(blockhash(LastRollBlock[degenerate])); + } + + function outcome(address degenerate, bool bonus) public view returns (uint256, uint256, uint256) { + if (block.number <= LastRollBlock[degenerate]) { + revert WaitForTick(); + } + + if (block.number > LastRollBlock[degenerate] + BlocksToAct) { + revert DeadlineExceeded(); + } + + // entropy layout: + // |- 118 bits -|- 118 bits -|- 20 bits -| + // item type terrain type outcome + uint256 entropy = _entropy(degenerate); + + uint256 _outcome; + if (!bonus) { + _outcome = sampleUnmodifiedOutcomeCumulativeMass(entropy); + } else { + _outcome = sampleImprovedOutcomesCumulativeMass(entropy); + } + + uint256 value; + + if (_outcome == 1) { + uint256 terrainType = (entropy << 118 >> 138) % 7; + uint256 itemType = (entropy >> 138) % 4; + value = 4 * terrainType + itemType; + } else if (_outcome == 2) { + value = CostToRoll + (CostToRoll >> 1); + if (value > address(this).balance >> 6) { + value = address(this).balance >> 6; + } + } else if (_outcome == 3) { + value = address(this).balance >> 6; + } else if (_outcome == 4) { + value = address(this).balance >> 1; + } + + return (entropy, _outcome, value); + } + + function _award(uint256 _outcome, uint256 value) internal { + if (_outcome == 1) { + _mint(msg.sender, value, 1, ""); + } else if (_outcome == 2 || _outcome == 3 || _outcome == 4) { + payable(msg.sender).transfer(value); + } + + emit Award(msg.sender, _outcome, value); + } + + function accept() external nonReentrant returns (uint256, uint256, uint256) { + (uint256 entropy, uint256 _outcome, uint256 value) = outcome(msg.sender, false); + _award(_outcome, value); + return (entropy, _outcome, value); + } + + function acceptWithCards(uint256 wagonCover, uint256 wagonBody, uint256 wheels, uint256 beastTrain) + external + nonReentrant + returns (uint256, uint256, uint256) + { + bool bonus = false; + + uint256 terrainType; + + uint256 currentItemType; + uint256 currentTier; + uint256 currentTerrainType; + + (currentItemType, currentTerrainType, currentTier) = genera(wagonCover); + if (currentItemType != 0) { + revert InvalidItem(wagonCover); + } + if (balanceOf(msg.sender, wagonCover) == 0) { + revert InsufficientItems(wagonCover); + } + if (CurrentTier[currentItemType][currentTerrainType] == currentTier) { + bonus = true; + } + terrainType = currentTerrainType; + + if (bonus) { + (currentItemType, currentTerrainType, currentTier) = genera(wagonBody); + if (currentItemType != 1) { + revert InvalidItem(wagonBody); + } + if (balanceOf(msg.sender, wagonBody) == 0) { + revert InsufficientItems(wagonBody); + } + if (CurrentTier[currentItemType][currentTerrainType] != currentTier || currentTerrainType != terrainType) { + bonus = false; + } + } + + if (bonus) { + (currentItemType, currentTerrainType, currentTier) = genera(wheels); + if (currentItemType != 2) { + revert InvalidItem(wheels); + } + if (balanceOf(msg.sender, wheels) == 0) { + revert InsufficientItems(wheels); + } + if (CurrentTier[currentItemType][currentTerrainType] != currentTier || currentTerrainType != terrainType) { + bonus = false; + } + } + + if (bonus) { + (currentItemType, currentTerrainType, currentTier) = genera(beastTrain); + if (currentItemType != 3) { + revert InvalidItem(beastTrain); + } + if (balanceOf(msg.sender, beastTrain) == 0) { + revert InsufficientItems(beastTrain); + } + if (CurrentTier[currentItemType][currentTerrainType] != currentTier || currentTerrainType != terrainType) { + bonus = false; + } + } + + (uint256 entropy, uint256 _outcome, uint256 value) = outcome(msg.sender, bonus); + _award(_outcome, value); + return (entropy, _outcome, value); + } + + function craft(uint256 poolID, uint256 numOutputs) external nonReentrant returns (uint256 newPoolID) { + if (balanceOf(msg.sender, poolID) < 2 * numOutputs) { + revert InsufficientItems(poolID); + } + + newPoolID = poolID + 28; + + _burn(msg.sender, poolID, 2 * numOutputs); + _mint(msg.sender, newPoolID, numOutputs, ""); + + (uint256 itemType, uint256 terrainType, uint256 tier) = genera(newPoolID); + if (CurrentTier[itemType][terrainType] < tier) { + CurrentTier[itemType][terrainType] = tier; + emit TierUnlocked(itemType, terrainType, tier, newPoolID); + } + } + + function burn(uint256 poolID, uint256 amount) external { + _burn(msg.sender, poolID, amount); + } + + function burnBatch(uint256[] memory poolIDs, uint256[] memory amounts) external { + _burnBatch(msg.sender, poolIDs, amounts); + } +} diff --git a/src/interfaces.sol b/src/interfaces.sol index a2098ce..d15625d 100644 --- a/src/interfaces.sol +++ b/src/interfaces.sol @@ -60,3 +60,99 @@ interface IDegenTrail { error ERC20InvalidSender(address sender); error ERC20InvalidSpender(address spender); } + +/* + * To regenerate this interface, run the following commands from the project root directory: + * $ forge build + * $ jq .abi out/JackpotJunction.sol/JackpotJunction.json | solface -annotations -name IJackpotJunction + */ +// Interface generated by solface: https://github.com/moonstream-to/solface +// solface version: 0.2.3 +// Interface ID: 45a510c1 +interface IJackpotJunction { + // structs + + // events + event ApprovalForAll(address account, address operator, bool approved); + event Award(address player, uint256 outcome, uint256 value); + event Roll(address player); + event TierUnlocked(uint256 itemType, uint256 terrainType, uint256 tier, uint256 poolID); + event TransferBatch(address operator, address from, address to, uint256[] ids, uint256[] values); + event TransferSingle(address operator, address from, address to, uint256 id, uint256 value); + event URI(string value, uint256 id); + + // functions + // Selector: be59cce3 + function BlocksToAct() external view returns (uint256); + // Selector: b870fe80 + function CostToReroll() external view returns (uint256); + // Selector: 50b8aa92 + function CostToRoll() external view returns (uint256); + // Selector: dd88fa7f + function CurrentTier(uint256, uint256) external view returns (uint256); + // Selector: 18ce0a4b + function ImprovedOutcomesCumulativeMass(uint256) external view returns (uint256); + // Selector: 9a0facc2 + function LastRollBlock(address) external view returns (uint256); + // Selector: 418145cf + function UnmodifiedOutcomesCumulativeMass(uint256) external view returns (uint256); + // Selector: 2852b71c + function accept() external returns (uint256, uint256, uint256); + // Selector: 7d2a9f54 + function acceptWithCards(uint256 wagonCover, uint256 wagonBody, uint256 wheels, uint256 beastTrain) + external + returns (uint256, uint256, uint256); + // Selector: 00fdd58e + function balanceOf(address account, uint256 id) external view returns (uint256); + // Selector: 4e1273f4 + function balanceOfBatch(address[] memory accounts, uint256[] memory ids) external view returns (uint256[] memory); + // Selector: b390c0ab + function burn(uint256 poolID, uint256 amount) external; + // Selector: 83ca4b6f + function burnBatch(uint256[] memory poolIDs, uint256[] memory amounts) external; + // Selector: f3917bd2 + function craft(uint256 poolID) external returns (uint256 newPoolID); + // Selector: 3a134f78 + function genera(uint256 poolID) external pure returns (uint256 itemType, uint256 terrainType, uint256 tier); + // Selector: e985e9c5 + function isApprovedForAll(address account, address operator) external view returns (bool); + // Selector: 3a259e6a + function outcome(address degenerate, bool bonus) external view returns (uint256, uint256, uint256); + // Selector: cd5e3c5d + function roll() external; + // Selector: 2eb2c2d6 + function safeBatchTransferFrom( + address from, + address to, + uint256[] memory ids, + uint256[] memory values, + bytes memory data + ) external; + // Selector: f242432a + function safeTransferFrom(address from, address to, uint256 id, uint256 value, bytes memory data) external; + // Selector: ecefbad8 + function sampleImprovedOutcomesCumulativeMass(uint256 entropy) external view returns (uint256); + // Selector: 6c08995d + function sampleUnmodifiedOutcomeCumulativeMass(uint256 entropy) external view returns (uint256); + // Selector: a22cb465 + function setApprovalForAll(address operator, bool approved) external; + // Selector: 01ffc9a7 + function supportsInterface(bytes4 interfaceId) external view returns (bool); + // Selector: 0e89341c + function uri(uint256) external view returns (string memory); + + // errors + error DeadlineExceeded(); + error ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId); + error ERC1155InvalidApprover(address approver); + error ERC1155InvalidArrayLength(uint256 idsLength, uint256 valuesLength); + error ERC1155InvalidOperator(address operator); + error ERC1155InvalidReceiver(address receiver); + error ERC1155InvalidSender(address sender); + error ERC1155MissingApprovalForAll(address operator, address owner); + error InsufficientItems(uint256 poolID); + error InsufficientValue(); + error InvalidItem(uint256 poolID); + error ReentrancyGuardReentrantCall(); + error WaitForTick(); +} diff --git a/test/JackpotJunction.t.sol b/test/JackpotJunction.t.sol new file mode 100644 index 0000000..f05d763 --- /dev/null +++ b/test/JackpotJunction.t.sol @@ -0,0 +1,925 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "../src/JackpotJunction.sol"; + +contract TestableJackpotJunction is JackpotJunction { + uint256 public Entropy; + + constructor(uint256 blocksToAct, uint256 costToRoll, uint256 costToReroll) JackpotJunction(blocksToAct, costToRoll, costToReroll) {} + + function setEntropy(uint256 value) public { + Entropy = value; + } + + function _entropy(address) internal view override returns (uint256) { + return Entropy; + } + + function mint(address to, uint256 poolID, uint256 amount) public { + _mint(to, poolID, amount, ""); + uint256 itemType; + uint256 tier; + uint256 terrainType; + (itemType, terrainType, tier) = genera(poolID); + + if (CurrentTier[itemType][terrainType] < tier) { + CurrentTier[itemType][terrainType] = tier; + } + } +} + +contract JackpotJunctionTest is Test { + event Roll(address indexed player); + + JackpotJunction game; + + uint256 deployerPrivateKey = 0x42; + address deployer = vm.addr(deployerPrivateKey); + + uint256 player1PrivateKey = 0x13371; + address player1 = vm.addr(player1PrivateKey); + + uint256 player2PrivateKey = 0x13372; + address player2 = vm.addr(player2PrivateKey); + + uint256 blocksToAct = 10; + uint256 costToRoll = 1e18; + uint256 costToReroll = 4e17; + + function setUp() public { + game = new JackpotJunction(blocksToAct, costToRoll, costToReroll); + } + + function test_deployment() public { + assertEq(game.BlocksToAct(), blocksToAct); + assertEq(game.CostToRoll(), costToRoll); + assertEq(game.CostToReroll(), costToReroll); + + for (uint256 i = 0; i < 4; i++) { + for (uint256 j = 0; j < 7; j++) { + assertEq(game.CurrentTier(i, j), 0); + } + } + } + + function test_genera() public { + uint256 itemType; + uint256 terrainType; + uint256 tier; + + for (uint256 i = 0; i < 4; i++) { + for (uint256 j = 0; j < 7; j++) { + (itemType, terrainType, tier) = game.genera(4 * j + i); + assertEq(itemType, i); + assertEq(terrainType, j); + assertEq(tier, 0); + } + } + + (itemType, terrainType, tier) = game.genera(95 * 28 + 4 * 3 + 2); + assertEq(itemType, 2); + assertEq(terrainType, 3); + assertEq(tier, 95); + } + + function test_samples() public { + // Unmodified + // uint256[5] public UnmodifiedOutcomesCumulativeMass = [ + // 524288, + // 524288 + 408934, + // 524288 + 408934 + 104857, + // 524288 + 408934 + 104857 + 10487, + // 524288 + 408934 + 104857 + 10487 + 10 + // ]; + assertEq(game.sampleUnmodifiedOutcomeCumulativeMass(0), 0); + assertEq(game.sampleUnmodifiedOutcomeCumulativeMass(524287), 0); + assertEq(game.sampleUnmodifiedOutcomeCumulativeMass(524288), 1); + assertEq(game.sampleUnmodifiedOutcomeCumulativeMass(524288 + 408933), 1); + assertEq(game.sampleUnmodifiedOutcomeCumulativeMass(524288 + 408934), 2); + assertEq(game.sampleUnmodifiedOutcomeCumulativeMass(524288 + 408934 + 104856), 2); + assertEq(game.sampleUnmodifiedOutcomeCumulativeMass(524288 + 408934 + 104857), 3); + assertEq(game.sampleUnmodifiedOutcomeCumulativeMass(524288 + 408934 + 104857 + 10486), 3); + assertEq(game.sampleUnmodifiedOutcomeCumulativeMass(524288 + 408934 + 104857 + 10487), 4); + assertEq(game.sampleUnmodifiedOutcomeCumulativeMass(524288 + 408934 + 104857 + 10487 + 9), 4); + // Overflow + assertEq(game.sampleUnmodifiedOutcomeCumulativeMass(524288 + 408934 + 104857 + 10487 + 10), 0); + + // Improved + // uint256[5] public ImprovedOutcomesCumulativeMass = [ + // 469283, + // 469283 + 408934, + // 469283 + 408934 + 154857, + // 469283 + 408934 + 154857 + 15487, + // 469283 + 408934 + 154857 + 15487 + 15 + // ]; + assertEq(game.sampleImprovedOutcomesCumulativeMass(0), 0); + assertEq(game.sampleImprovedOutcomesCumulativeMass(469282), 0); + assertEq(game.sampleImprovedOutcomesCumulativeMass(469283), 1); + assertEq(game.sampleImprovedOutcomesCumulativeMass(469283 + 408933), 1); + assertEq(game.sampleImprovedOutcomesCumulativeMass(469283 + 408934), 2); + assertEq(game.sampleImprovedOutcomesCumulativeMass(469283 + 408934 + 154856), 2); + assertEq(game.sampleImprovedOutcomesCumulativeMass(469283 + 408934 + 154857), 3); + assertEq(game.sampleImprovedOutcomesCumulativeMass(469283 + 408934 + 154857 + 15486), 3); + assertEq(game.sampleImprovedOutcomesCumulativeMass(469283 + 408934 + 154857 + 15487), 4); + assertEq(game.sampleImprovedOutcomesCumulativeMass(469283 + 408934 + 154857 + 15487 + 14), 4); + // Overflow + assertEq(game.sampleImprovedOutcomesCumulativeMass(469283 + 408934 + 154857 + 15487 + 15), 0); + } + + function test_roll() public { + vm.startPrank(player1); + + vm.deal(player1, costToRoll); + + // Even if the player has enough balance, if they call the roll method without supplying that balance, + // the game reverts. + vm.expectRevert(JackpotJunction.InsufficientValue.selector); + game.roll(); + + vm.expectEmit(); + emit Roll(player1); + game.roll{value: costToRoll}(); + assertEq(player1.balance, 0); + assertEq(address(game).balance, costToRoll); + + vm.roll(block.number + 1); + vm.deal(player1, costToReroll); + game.roll{value: costToReroll}(); + assertEq(player1.balance, 0); + assertEq(address(game).balance, costToRoll + costToReroll); + } + + function test_reroll_cost_not_applied_after_block_deadline() public { + vm.startPrank(player1); + vm.deal(player1, 1000*costToRoll); + vm.deal(address(game), 1000000 ether); + + game.roll{value: costToRoll}(); + + vm.roll(block.number + game.BlocksToAct() + 1); + + vm.expectRevert(JackpotJunction.InsufficientValue.selector); + assertLt(costToReroll, costToRoll); + game.roll{value: costToReroll}(); + + assertEq(player1.balance, 999*costToRoll); + } + + function test_reroll_cost_applied_at_block_deadline() public { + vm.startPrank(player1); + vm.deal(player1, 1000*costToRoll); + vm.deal(address(game), 1000000 ether); + + game.roll{value: costToRoll}(); + + vm.roll(block.number + game.BlocksToAct()); + + game.roll{value: costToReroll}(); + + assertEq(player1.balance, 999*costToRoll - costToReroll); + } + + + function test_outcome_reverts_before_tick() public { + vm.startPrank(player1); + + vm.deal(player1, 1000 * costToRoll); + + game.roll{value: costToRoll}(); + + vm.expectRevert(JackpotJunction.WaitForTick.selector); + game.outcome(player1, false); + } + + function test_outcome_reverts_after_deadline() public { + vm.startPrank(player1); + + vm.deal(player1, 1000 * costToRoll); + + game.roll{value: costToRoll}(); + + vm.roll(block.number + game.BlocksToAct() + 1); + vm.expectRevert(JackpotJunction.DeadlineExceeded.selector); + game.outcome(player1, false); + } + + function test_outcome() public { + vm.startPrank(player1); + + vm.deal(player1, 1000 * costToRoll); + + game.roll{value: costToRoll}(); + + vm.roll(block.number + game.BlocksToAct()); + + uint256 expectedEntropy = uint256(blockhash(block.number - game.BlocksToAct())); + (uint256 entropy,,) = game.outcome(player1, false); + assertEq(entropy, expectedEntropy); + } +} + +contract JackpotJunctionPlayTest is Test { + event Roll(address indexed player); + event TierUnlocked(uint256 indexed itemType, uint256 indexed terrainType, uint256 indexed tier, uint256 poolID); + event Award(address indexed player, uint256 indexed outcome, uint256 value); + + TestableJackpotJunction game; + + uint256 deployerPrivateKey = 0x42; + address deployer = vm.addr(deployerPrivateKey); + + // New player, no items + uint256 player1PrivateKey = 0x13371; + address player1 = vm.addr(player1PrivateKey); + + // Player who has high tier items + uint256 player2PrivateKey = 0x13372; + address player2 = vm.addr(player2PrivateKey); + + uint256 blocksToAct = 10; + uint256 costToRoll = 1e18; + uint256 costToReroll = 4e17; + + function setUp() public { + game = new TestableJackpotJunction(blocksToAct, costToRoll, costToReroll); + + // Mint player2 one of each tier 0 item. + for (uint256 i = 0; i < 4; i++) { + for (uint256 j = 0; j < 7; j++) { + game.mint(player2, 4 * j + i, 1); + } + } + } + + function test_nothing_then_item() public { + uint256 actualEntropy; + uint256 actualOutcome; + uint256 actualValue; + + vm.startPrank(player1); + vm.deal(player1, 1000*costToRoll); + vm.deal(address(game), 1000000 ether); + + vm.expectEmit(); + emit Roll(player1); + game.roll{value: costToRoll}(); + + vm.roll(block.number + 1); + game.setEntropy(0); + (actualEntropy, actualOutcome, actualValue) = game.outcome(player1, false); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 0); + assertEq(actualValue, 0); + + vm.expectEmit(); + emit Roll(player1); + game.roll{value: costToReroll}(); + + vm.roll(block.number + 1); + uint256 itemType = 1; + uint256 terrainType = 2; + game.setEntropy((itemType << 138) + (terrainType << 20) + game.UnmodifiedOutcomesCumulativeMass(0)); + (actualEntropy, actualOutcome, actualValue) = game.outcome(player1, false); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 1); + assertEq(actualValue, 4*terrainType + itemType); + + assertEq(game.balanceOf(player1, 4*terrainType + itemType), 0); + + vm.expectEmit(); + emit Award(player1, 1, 4*terrainType + itemType); + (actualEntropy, actualOutcome, actualValue) = game.accept(); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 1); + assertEq(actualValue, 4*terrainType + itemType); + + assertEq(game.balanceOf(player1, 4*terrainType + itemType), 1); + } + + function test_nothing_then_small_reward() public { + uint256 actualEntropy; + uint256 actualOutcome; + uint256 actualValue; + + uint256 initialGameBalance = 1000000 ether; + + vm.startPrank(player1); + vm.deal(player1, 1000*costToRoll); + vm.deal(address(game), initialGameBalance); + + game.roll{value: costToRoll}(); + + vm.roll(block.number + 1); + game.setEntropy(0); + (actualEntropy, actualOutcome, actualValue) = game.outcome(player1, false); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 0); + assertEq(actualValue, 0); + + game.roll{value: costToReroll}(); + + vm.roll(block.number + 1); + game.setEntropy(game.UnmodifiedOutcomesCumulativeMass(1)); + (actualEntropy, actualOutcome, actualValue) = game.outcome(player1, false); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 2); + assertEq(actualValue, game.CostToRoll() + (game.CostToRoll() >> 1)); + + assertEq(address(game).balance, initialGameBalance + game.CostToRoll() + game.CostToReroll()); + + (actualEntropy, actualOutcome, actualValue) = game.accept(); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 2); + assertEq(actualValue, game.CostToRoll() + (game.CostToRoll() >> 1)); + + assertEq(address(game).balance, initialGameBalance + game.CostToRoll() + game.CostToReroll() - (game.CostToRoll() + (game.CostToRoll() >> 1))); + assertEq(player1.balance, 1000*game.CostToRoll() + (game.CostToRoll() >> 1) - game.CostToReroll()); + } + + function test_nothing_then_medium_reward() public { + uint256 actualEntropy; + uint256 actualOutcome; + uint256 actualValue; + + uint256 initialGameBalance = 1000000 ether; + + vm.startPrank(player1); + vm.deal(player1, 1000*costToRoll); + vm.deal(address(game), initialGameBalance); + + game.roll{value: costToRoll}(); + + vm.roll(block.number + 1); + game.setEntropy(0); + (actualEntropy, actualOutcome, actualValue) = game.outcome(player1, false); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 0); + assertEq(actualValue, 0); + + game.roll{value: costToReroll}(); + + vm.roll(block.number + 1); + game.setEntropy(game.UnmodifiedOutcomesCumulativeMass(2)); + (actualEntropy, actualOutcome, actualValue) = game.outcome(player1, false); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 3); + assertEq(actualValue, (initialGameBalance + game.CostToRoll() + game.CostToReroll()) >> 6); + + assertEq(address(game).balance, initialGameBalance + game.CostToRoll() + game.CostToReroll()); + + (actualEntropy, actualOutcome, actualValue) = game.accept(); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 3); + assertEq(actualValue, (initialGameBalance + game.CostToRoll() + game.CostToReroll()) >> 6); + + assertEq(address(game).balance, initialGameBalance + game.CostToRoll() + game.CostToReroll() - ((initialGameBalance + game.CostToRoll() + game.CostToReroll()) >> 6)); + assertEq(player1.balance, 999*game.CostToRoll()- game.CostToReroll() + actualValue); + } + + function test_nothing_then_large_reward() public { + uint256 actualEntropy; + uint256 actualOutcome; + uint256 actualValue; + + uint256 initialGameBalance = 1000000 ether; + + vm.startPrank(player1); + vm.deal(player1, 1000*costToRoll); + vm.deal(address(game), initialGameBalance); + + game.roll{value: costToRoll}(); + + vm.roll(block.number + 1); + game.setEntropy(0); + (actualEntropy, actualOutcome, actualValue) = game.outcome(player1, false); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 0); + assertEq(actualValue, 0); + + game.roll{value: costToReroll}(); + + vm.roll(block.number + 1); + game.setEntropy(game.UnmodifiedOutcomesCumulativeMass(3)); + (actualEntropy, actualOutcome, actualValue) = game.outcome(player1, false); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 4); + assertEq(actualValue, (initialGameBalance + game.CostToRoll() + game.CostToReroll()) >> 1); + + assertEq(address(game).balance, initialGameBalance + game.CostToRoll() + game.CostToReroll()); + + (actualEntropy, actualOutcome, actualValue) = game.accept(); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 4); + assertEq(actualValue, (initialGameBalance + game.CostToRoll() + game.CostToReroll()) >> 1); + + assertEq(address(game).balance, initialGameBalance + game.CostToRoll() + game.CostToReroll() - ((initialGameBalance + game.CostToRoll() + game.CostToReroll()) >> 1)); + assertEq(player1.balance, 999*game.CostToRoll()- game.CostToReroll() + actualValue); + } + + function test_bonus_nothing_then_item() public { + uint256 actualEntropy; + uint256 actualOutcome; + uint256 actualValue; + + vm.startPrank(player2); + vm.deal(player2, 1000*costToRoll); + vm.deal(address(game), 1000000 ether); + + game.roll{value: costToRoll}(); + + vm.roll(block.number + 1); + game.setEntropy(0); + (actualEntropy, actualOutcome, actualValue) = game.outcome(player2, true); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 0); + assertEq(actualValue, 0); + + game.roll{value: costToReroll}(); + + vm.roll(block.number + 1); + uint256 itemType = 1; + uint256 terrainType = 2; + game.setEntropy((itemType << 138) + (terrainType << 20) + game.ImprovedOutcomesCumulativeMass(0)); + (actualEntropy, actualOutcome, actualValue) = game.outcome(player2, true); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 1); + assertEq(actualValue, 4*terrainType + itemType); + + assertEq(game.balanceOf(player2, 4*terrainType + itemType), 1); + + (actualEntropy, actualOutcome, actualValue) = game.acceptWithCards(0, 1, 2, 3); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 1); + assertEq(actualValue, 4*terrainType + itemType); + + assertEq(game.balanceOf(player2, 4*terrainType + itemType), 2); + } + + function test_bonus_nothing_then_small_reward() public { + uint256 actualEntropy; + uint256 actualOutcome; + uint256 actualValue; + + uint256 initialGameBalance = 1000000 ether; + + vm.startPrank(player2); + vm.deal(player2, 1000*costToRoll); + vm.deal(address(game), initialGameBalance); + + game.roll{value: costToRoll}(); + + vm.roll(block.number + 1); + game.setEntropy(0); + (actualEntropy, actualOutcome, actualValue) = game.outcome(player2, true); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 0); + assertEq(actualValue, 0); + + game.roll{value: costToReroll}(); + + vm.roll(block.number + 1); + game.setEntropy(game.ImprovedOutcomesCumulativeMass(1)); + (actualEntropy, actualOutcome, actualValue) = game.outcome(player2, true); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 2); + assertEq(actualValue, game.CostToRoll() + (game.CostToRoll() >> 1)); + + assertEq(address(game).balance, initialGameBalance + game.CostToRoll() + game.CostToReroll()); + + (actualEntropy, actualOutcome, actualValue) = game.acceptWithCards(0, 1, 2, 3); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 2); + assertEq(actualValue, game.CostToRoll() + (game.CostToRoll() >> 1)); + + assertEq(address(game).balance, initialGameBalance + game.CostToRoll() + game.CostToReroll() - (game.CostToRoll() + (game.CostToRoll() >> 1))); + assertEq(player2.balance, 1000*game.CostToRoll() + (game.CostToRoll() >> 1) - game.CostToReroll()); + } + + function test_bonus_nothing_then_medium_reward() public { + uint256 actualEntropy; + uint256 actualOutcome; + uint256 actualValue; + + uint256 initialGameBalance = 1000000 ether; + + vm.startPrank(player2); + vm.deal(player2, 1000*costToRoll); + vm.deal(address(game), initialGameBalance); + + game.roll{value: costToRoll}(); + + vm.roll(block.number + 1); + game.setEntropy(0); + (actualEntropy, actualOutcome, actualValue) = game.outcome(player2, true); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 0); + assertEq(actualValue, 0); + + game.roll{value: costToReroll}(); + + vm.roll(block.number + 1); + game.setEntropy(game.ImprovedOutcomesCumulativeMass(2)); + (actualEntropy, actualOutcome, actualValue) = game.outcome(player2, true); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 3); + assertEq(actualValue, (initialGameBalance + game.CostToRoll() + game.CostToReroll()) >> 6); + + assertEq(address(game).balance, initialGameBalance + game.CostToRoll() + game.CostToReroll()); + + (actualEntropy, actualOutcome, actualValue) = game.acceptWithCards(0, 1, 2, 3); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 3); + assertEq(actualValue, (initialGameBalance + game.CostToRoll() + game.CostToReroll()) >> 6); + + assertEq(address(game).balance, initialGameBalance + game.CostToRoll() + game.CostToReroll() - ((initialGameBalance + game.CostToRoll() + game.CostToReroll()) >> 6)); + assertEq(player2.balance, 999*game.CostToRoll()- game.CostToReroll() + actualValue); + } + + function test_bonus_nothing_then_large_reward() public { + uint256 actualEntropy; + uint256 actualOutcome; + uint256 actualValue; + + uint256 initialGameBalance = 1000000 ether; + + vm.startPrank(player2); + vm.deal(player2, 1000*costToRoll); + vm.deal(address(game), initialGameBalance); + + game.roll{value: costToRoll}(); + + vm.roll(block.number + 1); + game.setEntropy(0); + (actualEntropy, actualOutcome, actualValue) = game.outcome(player2, true); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 0); + assertEq(actualValue, 0); + + game.roll{value: costToReroll}(); + + vm.roll(block.number + 1); + game.setEntropy(game.ImprovedOutcomesCumulativeMass(3)); + (actualEntropy, actualOutcome, actualValue) = game.outcome(player2, true); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 4); + assertEq(actualValue, (initialGameBalance + game.CostToRoll() + game.CostToReroll()) >> 1); + + assertEq(address(game).balance, initialGameBalance + game.CostToRoll() + game.CostToReroll()); + + (actualEntropy, actualOutcome, actualValue) = game.acceptWithCards(0, 1, 2, 3); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 4); + assertEq(actualValue, (initialGameBalance + game.CostToRoll() + game.CostToReroll()) >> 1); + + assertEq(address(game).balance, initialGameBalance + game.CostToRoll() + game.CostToReroll() - ((initialGameBalance + game.CostToRoll() + game.CostToReroll()) >> 1)); + assertEq(player2.balance, 999*game.CostToRoll()- game.CostToReroll() + actualValue); + } + + function test_bonus_acceptance_fails_with_incorrect_item_types() public { + uint256 initialGameBalance = 1000000 ether; + + vm.startPrank(player2); + vm.deal(player2, 1000*costToRoll); + vm.deal(address(game), initialGameBalance); + + game.roll{value: costToRoll}(); + + vm.roll(block.number + 1); + game.setEntropy(0); + game.roll{value: costToReroll}(); + + vm.roll(block.number + 1); + game.setEntropy(game.ImprovedOutcomesCumulativeMass(3)); + + vm.expectRevert(abi.encodeWithSelector(JackpotJunction.InvalidItem.selector, 1)); + game.acceptWithCards(1, 0, 2, 3); + + vm.expectRevert(abi.encodeWithSelector(JackpotJunction.InvalidItem.selector, 2)); + game.acceptWithCards(0, 2, 1, 3); + + vm.expectRevert(abi.encodeWithSelector(JackpotJunction.InvalidItem.selector, 3)); + game.acceptWithCards(0, 1, 3, 2); + + vm.expectRevert(abi.encodeWithSelector(JackpotJunction.InvalidItem.selector, 2)); + game.acceptWithCards(0, 1, 2, 2); + + assertEq(address(game).balance, initialGameBalance + game.CostToRoll() + game.CostToReroll()); + assertEq(player2.balance, 999*game.CostToRoll() - game.CostToReroll()); + } + + function test_bonus_is_not_applied_if_cover_is_not_max_tier() public { + uint256 actualEntropy; + uint256 actualOutcome; + uint256 actualValue; + + uint256 initialGameBalance = 1000000 ether; + + vm.startPrank(player2); + vm.deal(player2, 1000*costToRoll); + vm.deal(address(game), initialGameBalance); + + game.roll{value: costToRoll}(); + + assertGe(game.UnmodifiedOutcomesCumulativeMass(0) - 1, game.ImprovedOutcomesCumulativeMass(0)); + game.setEntropy(game.UnmodifiedOutcomesCumulativeMass(0) - 1); + game.roll{value: costToReroll}(); + + + vm.roll(block.number + 1); + game.mint(player1, 28, 1); + + assertEq(game.CurrentTier(0, 0), 1); + assertEq(game.CurrentTier(1, 0), 0); + assertEq(game.CurrentTier(2, 0), 0); + assertEq(game.CurrentTier(3, 0), 0); + + (actualEntropy, actualOutcome, actualValue) = game.acceptWithCards(0, 1, 2, 3); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 0); + assertEq(actualValue, 0); + } + + function test_bonus_is_not_applied_if_body_is_not_max_tier() public { + uint256 actualEntropy; + uint256 actualOutcome; + uint256 actualValue; + + uint256 initialGameBalance = 1000000 ether; + + vm.startPrank(player2); + vm.deal(player2, 1000*costToRoll); + vm.deal(address(game), initialGameBalance); + + game.roll{value: costToRoll}(); + + assertGe(game.UnmodifiedOutcomesCumulativeMass(0) - 1, game.ImprovedOutcomesCumulativeMass(0)); + game.setEntropy(game.UnmodifiedOutcomesCumulativeMass(0) - 1); + game.roll{value: costToReroll}(); + + vm.roll(block.number + 1); + game.mint(player1, 28 + 1, 1); + + assertEq(game.CurrentTier(0, 0), 0); + assertEq(game.CurrentTier(1, 0), 1); + assertEq(game.CurrentTier(2, 0), 0); + assertEq(game.CurrentTier(3, 0), 0); + + (actualEntropy, actualOutcome, actualValue) = game.acceptWithCards(0, 1, 2, 3); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 0); + assertEq(actualValue, 0); + } + + function test_bonus_is_not_applied_if_wheel_is_not_max_tier() public { + uint256 actualEntropy; + uint256 actualOutcome; + uint256 actualValue; + + uint256 initialGameBalance = 1000000 ether; + + vm.startPrank(player2); + vm.deal(player2, 1000*costToRoll); + vm.deal(address(game), initialGameBalance); + + game.roll{value: costToRoll}(); + + assertGe(game.UnmodifiedOutcomesCumulativeMass(0) - 1, game.ImprovedOutcomesCumulativeMass(0)); + game.setEntropy(game.UnmodifiedOutcomesCumulativeMass(0) - 1); + game.roll{value: costToReroll}(); + + vm.roll(block.number + 1); + game.mint(player1, 28 + 2, 1); + + assertEq(game.CurrentTier(0, 0), 0); + assertEq(game.CurrentTier(1, 0), 0); + assertEq(game.CurrentTier(2, 0), 1); + assertEq(game.CurrentTier(3, 0), 0); + + (actualEntropy, actualOutcome, actualValue) = game.acceptWithCards(0, 1, 2, 3); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 0); + assertEq(actualValue, 0); + } + + function test_bonus_is_not_applied_if_beast_is_not_max_tier() public { + uint256 actualEntropy; + uint256 actualOutcome; + uint256 actualValue; + + uint256 initialGameBalance = 1000000 ether; + + vm.startPrank(player2); + vm.deal(player2, 1000*costToRoll); + vm.deal(address(game), initialGameBalance); + + game.roll{value: costToRoll}(); + + assertGe(game.UnmodifiedOutcomesCumulativeMass(0) - 1, game.ImprovedOutcomesCumulativeMass(0)); + game.setEntropy(game.UnmodifiedOutcomesCumulativeMass(0) - 1); + game.roll{value: costToReroll}(); + + vm.roll(block.number + 1); + game.mint(player1, 28 + 3, 1); + + assertEq(game.CurrentTier(0, 0), 0); + assertEq(game.CurrentTier(1, 0), 0); + assertEq(game.CurrentTier(2, 0), 0); + assertEq(game.CurrentTier(3, 0), 1); + + (actualEntropy, actualOutcome, actualValue) = game.acceptWithCards(0, 1, 2, 3); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 0); + assertEq(actualValue, 0); + } + + function test_bonus_is_not_applied_if_cover_of_different_terrain_type() public { + uint256 actualEntropy; + uint256 actualOutcome; + uint256 actualValue; + + uint256 initialGameBalance = 1000000 ether; + + vm.startPrank(player2); + vm.deal(player2, 1000*costToRoll); + vm.deal(address(game), initialGameBalance); + + game.roll{value: costToRoll}(); + + assertGe(game.UnmodifiedOutcomesCumulativeMass(0) - 1, game.ImprovedOutcomesCumulativeMass(0)); + game.setEntropy(game.UnmodifiedOutcomesCumulativeMass(0) - 1); + game.roll{value: costToReroll}(); + + vm.roll(block.number + 1); + (actualEntropy, actualOutcome, actualValue) = game.acceptWithCards(4 + 0, 1, 2, 3); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 0); + assertEq(actualValue, 0); + } + + function test_bonus_is_not_applied_if_body_of_different_terrain_type() public { + uint256 actualEntropy; + uint256 actualOutcome; + uint256 actualValue; + + uint256 initialGameBalance = 1000000 ether; + + vm.startPrank(player2); + vm.deal(player2, 1000*costToRoll); + vm.deal(address(game), initialGameBalance); + + game.roll{value: costToRoll}(); + + assertGe(game.UnmodifiedOutcomesCumulativeMass(0) - 1, game.ImprovedOutcomesCumulativeMass(0)); + game.setEntropy(game.UnmodifiedOutcomesCumulativeMass(0) - 1); + game.roll{value: costToReroll}(); + + vm.roll(block.number + 1); + (actualEntropy, actualOutcome, actualValue) = game.acceptWithCards(0, 4 + 1, 2, 3); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 0); + assertEq(actualValue, 0); + } + + function test_bonus_is_not_applied_if_wheel_of_different_terrain_type() public { + uint256 actualEntropy; + uint256 actualOutcome; + uint256 actualValue; + + uint256 initialGameBalance = 1000000 ether; + + vm.startPrank(player2); + vm.deal(player2, 1000*costToRoll); + vm.deal(address(game), initialGameBalance); + + game.roll{value: costToRoll}(); + + assertGe(game.UnmodifiedOutcomesCumulativeMass(0) - 1, game.ImprovedOutcomesCumulativeMass(0)); + game.setEntropy(game.UnmodifiedOutcomesCumulativeMass(0) - 1); + game.roll{value: costToReroll}(); + + vm.roll(block.number + 1); + (actualEntropy, actualOutcome, actualValue) = game.acceptWithCards(0, 1, 4 + 2, 3); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 0); + assertEq(actualValue, 0); + } + + function test_bonus_is_not_applied_if_beast_of_different_terrain_type() public { + uint256 actualEntropy; + uint256 actualOutcome; + uint256 actualValue; + + uint256 initialGameBalance = 1000000 ether; + + vm.startPrank(player2); + vm.deal(player2, 1000*costToRoll); + vm.deal(address(game), initialGameBalance); + + game.roll{value: costToRoll}(); + + assertGe(game.UnmodifiedOutcomesCumulativeMass(0) - 1, game.ImprovedOutcomesCumulativeMass(0)); + game.setEntropy(game.UnmodifiedOutcomesCumulativeMass(0) - 1); + game.roll{value: costToReroll}(); + + vm.roll(block.number + 1); + (actualEntropy, actualOutcome, actualValue) = game.acceptWithCards(0, 1, 2, 4 + 3); + assertEq(actualEntropy, game.Entropy()); + assertEq(actualOutcome, 0); + assertEq(actualValue, 0); + } + + function test_crafting_tier_0_to_tier_1() public { + vm.startPrank(player2); + for (uint256 i = 0; i < 4; i++) { + for (uint256 j = 0; j < 7; j++) { + uint256 inputPoolID = 4 * j + i; + uint256 outputPoolID = 28 + inputPoolID; + game.mint(player2, inputPoolID, 2); + uint256 initialInputBalance = game.balanceOf(player2, inputPoolID); + uint256 initialOutputBalance = game.balanceOf(player2, outputPoolID); + assertEq(game.CurrentTier(i, j), 0); + vm.expectEmit(); + emit TierUnlocked(i, j, 1, outputPoolID); + game.craft(inputPoolID, 1); + uint256 terminalInputBalance = game.balanceOf(player2, inputPoolID); + uint256 terminalOutputBalance = game.balanceOf(player2, outputPoolID); + assertEq(terminalInputBalance, initialInputBalance - 2); + assertEq(terminalOutputBalance, initialOutputBalance + 1); + assertEq(game.CurrentTier(i, j), 1); + } + } + } + + function test_crafting_tier_0_to_tier_1_193284_outputs() public { + vm.startPrank(player2); + for (uint256 i = 0; i < 4; i++) { + for (uint256 j = 0; j < 7; j++) { + uint256 inputPoolID = 4 * j + i; + uint256 outputPoolID = 28 + inputPoolID; + uint256 numOutputs = 193284; + game.mint(player2, inputPoolID, 2 * numOutputs); + uint256 initialInputBalance = game.balanceOf(player2, inputPoolID); + uint256 initialOutputBalance = game.balanceOf(player2, outputPoolID); + assertEq(game.CurrentTier(i, j), 0); + vm.expectEmit(); + emit TierUnlocked(i, j, 1, outputPoolID); + game.craft(inputPoolID, numOutputs); + uint256 terminalInputBalance = game.balanceOf(player2, inputPoolID); + uint256 terminalOutputBalance = game.balanceOf(player2, outputPoolID); + assertEq(terminalInputBalance, initialInputBalance - 2 * numOutputs); + assertEq(terminalOutputBalance, initialOutputBalance + numOutputs); + assertEq(game.CurrentTier(i, j), 1); + } + } + } + + function test_crafting_tier_92384_to_tier_92385() public { + vm.startPrank(player2); + for (uint256 i = 0; i < 4; i++) { + for (uint256 j = 0; j < 7; j++) { + uint256 inputPoolID = 92384 * 28 + 4 * j + i; + uint256 outputPoolID = 28 + inputPoolID; + game.mint(player2, inputPoolID, 2); + uint256 initialInputBalance = game.balanceOf(player2, inputPoolID); + uint256 initialOutputBalance = game.balanceOf(player2, outputPoolID); + assertEq(game.CurrentTier(i, j), 92384); + vm.expectEmit(); + emit TierUnlocked(i, j, 92385, outputPoolID); + game.craft(inputPoolID, 1); + uint256 terminalInputBalance = game.balanceOf(player2, inputPoolID); + uint256 terminalOutputBalance = game.balanceOf(player2, outputPoolID); + assertEq(terminalInputBalance, initialInputBalance - 2); + assertEq(terminalOutputBalance, initialOutputBalance + 1); + assertEq(game.CurrentTier(i, j), 92385); + } + } + } + + function test_crafting_fails_when_insufficient_zero_balance() public { + vm.startPrank(player2); + for (uint256 i = 0; i < 4; i++) { + for (uint256 j = 0; j < 7; j++) { + uint256 inputPoolID = 92384 * 28 + 4 * j + i; + game.burn(inputPoolID, game.balanceOf(player2, inputPoolID)); + vm.expectRevert(abi.encodeWithSelector(JackpotJunction.InsufficientItems.selector, inputPoolID)); + game.craft(inputPoolID, 1); + } + } + } + + function test_crafting_fails_when_insufficient_positive_balance() public { + vm.startPrank(player2); + for (uint256 i = 0; i < 4; i++) { + for (uint256 j = 0; j < 7; j++) { + uint256 inputPoolID = 92384 * 28 + 4 * j + i; + uint256 numOutputs = 19283498; + game.burn(inputPoolID, game.balanceOf(player2, inputPoolID)); + game.mint(player2, inputPoolID, 2*numOutputs - 1); + vm.expectRevert(abi.encodeWithSelector(JackpotJunction.InsufficientItems.selector, inputPoolID)); + game.craft(inputPoolID, numOutputs); + } + } + } +}