├── .solhintignore ├── bun.lockb ├── .gitmodules ├── src ├── common │ ├── Coord.sol │ ├── Bound.sol │ ├── Bonus.sol │ ├── Constants.sol │ └── Errors.sol ├── libraries │ ├── LibUpdateId.sol │ ├── LibTile.sol │ ├── LibPlayer.sol │ ├── LibTreasury.sol │ ├── LibGame.sol │ ├── LibBonus.sol │ ├── LibPrice.sol │ ├── LibLetters.sol │ ├── LibBoard.sol │ └── LibPoints.sol ├── codegen │ ├── world │ │ ├── IDonateSystem.sol │ │ ├── ISetFeeConfigSystem.sol │ │ ├── IClaimSystem.sol │ │ ├── IDrawSystem.sol │ │ ├── ITransferLettersSystem.sol │ │ ├── ISetDrawLetterOddsSystem.sol │ │ ├── IPlaySystem.sol │ │ ├── IStartSystem.sol │ │ └── IWorld.sol │ ├── common.sol │ ├── index.sol │ └── tables │ │ ├── DrawCount.sol │ │ ├── MerkleRootConfig.sol │ │ ├── Treasury.sol │ │ ├── UpdateId.sol │ │ ├── ClaimRestrictionConfig.sol │ │ ├── Points.sol │ │ ├── Spent.sol │ │ ├── TilePlayer.sol │ │ ├── TileLetter.sol │ │ └── PlayerLetters.sol ├── test │ ├── integration │ │ ├── WorldExists.t.sol │ │ ├── SimpleWord.t.sol │ │ ├── Start.t.sol │ │ ├── Transfer.t.sol │ │ ├── Points.t.sol │ │ └── CrossWordNoEmpty.t.sol │ ├── Words3Test.t.sol │ └── unit │ │ ├── Wrapper.sol │ │ ├── LibLetters.t.sol │ │ ├── LibTreasury.t.sol │ │ ├── LibGame.t.sol │ │ ├── LibBonus.t.sol │ │ ├── LibPrice.t.sol │ │ └── LibPoints.t.sol └── systems │ ├── DonateSystem.sol │ ├── SetFeeConfigSystem.sol │ ├── SetDrawLetterOddsSystem.sol │ ├── TransferLettersSystem.sol │ ├── PlaySystem.sol │ ├── ClaimSystem.sol │ ├── DrawSystem.sol │ └── StartSystem.sol ├── .vscode └── settings.json ├── .gitignore ├── remappings.txt ├── .solhint.json ├── tsconfig.json ├── scripts ├── encodeLetter.ts ├── checkWord.ts ├── generateMerkleTree.ts └── transferLetters.ts ├── worlds.json ├── foundry.toml ├── README.md ├── package.json ├── script └── PostDeploy.s.sol └── mud.config.ts /.solhintignore: -------------------------------------------------------------------------------- 1 | src/codegen 2 | src/test/murky -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smallbraineng/words3-contracts/HEAD/bun.lockb -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/test/murky"] 2 | path = src/test/murky 3 | url = https://github.com/dmfxyz/murky 4 | -------------------------------------------------------------------------------- /src/common/Coord.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | // Coord 5 | struct Coord { 6 | int32 x; 7 | int32 y; 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "solidity.formatter": "forge", 3 | "solidity.enabledAsYouTypeCompilationErrorCheck": true, 4 | "solidity.compileUsingRemoteVersion": "v0.8.24+commit.e11b9ed9" 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | cache/ 3 | node_modules/ 4 | bindings/ 5 | artifacts/ 6 | deployments 7 | deploys 8 | abi/ 9 | types/ 10 | broadcast/ 11 | .env 12 | latticeEnv.sh 13 | foundry.toml 14 | scripts/csv -------------------------------------------------------------------------------- /src/common/Bound.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | // Bounds for a given letter 5 | struct Bound { 6 | uint16 positive; 7 | uint16 negative; 8 | bytes32[] proof; 9 | } 10 | -------------------------------------------------------------------------------- /src/common/Bonus.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | import { BonusType } from "codegen/common.sol"; 5 | 6 | struct Bonus { 7 | uint32 bonusValue; 8 | BonusType bonusType; 9 | } 10 | -------------------------------------------------------------------------------- /src/common/Constants.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | uint32 constant CROSS_WORD_REWARD_FRACTION = 3; 5 | address constant SINGLETON_ADDRESS = address(0); 6 | uint16 constant MAX_WORD_LENGTH = 50; 7 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @openzeppelin/=node_modules/@openzeppelin/ 2 | @latticexyz/=node_modules/@latticexyz/ 3 | @solidstate=node_modules/@solidstate/ 4 | solmate=node_modules/solmate/ 5 | 6 | ds-test/=node_modules/ds-test/src/ 7 | forge-std/=node_modules/forge-std/src/ 8 | common/=src/common 9 | codegen/=src/codegen 10 | systems/=src/systems 11 | libraries/=src/libraries -------------------------------------------------------------------------------- /src/libraries/LibUpdateId.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | import { UpdateId } from "codegen/index.sol"; 5 | 6 | library LibUpdateId { 7 | function getUpdateId() internal returns (uint256) { 8 | uint256 id = UpdateId.get() + 1; 9 | UpdateId.set({ value: id }); 10 | return id; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["solhint:recommended", "mud"], 3 | "plugins": ["mud"], 4 | "rules": { 5 | "compiler-version": ["error", ">=0.8.0"], 6 | "avoid-low-level-calls": "off", 7 | "no-inline-assembly": "off", 8 | "func-visibility": ["warn", { "ignoreConstructors": true }], 9 | "no-empty-blocks": "off", 10 | "no-complex-fallback": "off" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | // Visit https://aka.ms/tsconfig.json for all config options 2 | { 3 | "compilerOptions": { 4 | "target": "ES2020", 5 | "module": "commonjs", 6 | "strict": true, 7 | "resolveJsonModule": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "moduleResolution": "node" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/codegen/world/IDonateSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | /** 7 | * @title IDonateSystem 8 | * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) 9 | * @dev This interface is automatically generated from the corresponding system contract. Do not edit manually. 10 | */ 11 | interface IDonateSystem { 12 | error DonateBeforeStarted(); 13 | 14 | function donate() external payable; 15 | } 16 | -------------------------------------------------------------------------------- /src/codegen/world/ISetFeeConfigSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | /** 7 | * @title ISetFeeConfigSystem 8 | * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) 9 | * @dev This interface is automatically generated from the corresponding system contract. Do not edit manually. 10 | */ 11 | interface ISetFeeConfigSystem { 12 | error NotFeeTaker(); 13 | 14 | function setFeeConfig(uint16 feeBps, address feeTaker) external; 15 | } 16 | -------------------------------------------------------------------------------- /src/codegen/world/IClaimSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | /** 7 | * @title IClaimSystem 8 | * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) 9 | * @dev This interface is automatically generated from the corresponding system contract. Do not edit manually. 10 | */ 11 | interface IClaimSystem { 12 | error NotEnoughPoints(); 13 | error WithinClaimRestrictionPeriod(); 14 | 15 | function claim(uint32 points) external; 16 | } 17 | -------------------------------------------------------------------------------- /src/test/integration/WorldExists.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | /* solhint-disable no-global-import */ 4 | /* solhint-disable func-name-mixedcase */ 5 | 6 | import { Words3Test } from "../Words3Test.t.sol"; 7 | import "forge-std/Test.sol"; 8 | 9 | contract WorldExists is Words3Test { 10 | function test_WorldExists() public { 11 | uint256 codeSize; 12 | address addr = worldAddress; 13 | assembly { 14 | codeSize := extcodesize(addr) 15 | } 16 | assertTrue(codeSize > 0); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/codegen/world/IDrawSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | /** 7 | * @title IDrawSystem 8 | * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) 9 | * @dev This interface is automatically generated from the corresponding system contract. Do not edit manually. 10 | */ 11 | interface IDrawSystem { 12 | error InvalidDrawAddress(); 13 | error NotEnoughValue(); 14 | 15 | function draw(address player) external payable; 16 | 17 | function getDrawPrice() external view returns (uint256); 18 | } 19 | -------------------------------------------------------------------------------- /src/systems/DonateSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | import { System } from "@latticexyz/world/src/System.sol"; 5 | import { LibGame } from "libraries/LibGame.sol"; 6 | import { LibTreasury } from "libraries/LibTreasury.sol"; 7 | 8 | contract DonateSystem is System { 9 | error DonateBeforeStarted(); 10 | 11 | function donate() public payable { 12 | if (!LibGame.canPlay()) { 13 | revert DonateBeforeStarted(); 14 | } 15 | LibTreasury.incrementTreasury({ msgSender: address(0), msgValue: _msgValue() }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/codegen/world/ITransferLettersSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | import { Letter } from "codegen/common.sol"; 7 | 8 | /** 9 | * @title ITransferLettersSystem 10 | * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) 11 | * @dev This interface is automatically generated from the corresponding system contract. Do not edit manually. 12 | */ 13 | interface ITransferLettersSystem { 14 | error TransferMissingLetters(); 15 | 16 | function transfer(Letter[] memory letters, address to) external; 17 | } 18 | -------------------------------------------------------------------------------- /src/codegen/world/ISetDrawLetterOddsSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | /** 7 | * @title ISetDrawLetterOddsSystem 8 | * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) 9 | * @dev This interface is automatically generated from the corresponding system contract. Do not edit manually. 10 | */ 11 | interface ISetDrawLetterOddsSystem { 12 | error AlreadySetOdds(); 13 | error InvalidOddsLength(); 14 | error NonzeroFirstValue(); 15 | 16 | function setDrawLetterOdds(uint8[] memory odds) external; 17 | } 18 | -------------------------------------------------------------------------------- /src/systems/SetFeeConfigSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | import { System } from "@latticexyz/world/src/System.sol"; 5 | import { FeeConfig, FeeConfigData } from "codegen/index.sol"; 6 | 7 | contract SetFeeConfigSystem is System { 8 | error NotFeeTaker(); 9 | 10 | function setFeeConfig(uint16 feeBps, address feeTaker) public { 11 | FeeConfigData memory feeConfig = FeeConfig.get(); 12 | if (_msgSender() != feeConfig.feeTaker) { 13 | revert NotFeeTaker(); 14 | } 15 | FeeConfig.set({ feeBps: feeBps, feeTaker: feeTaker }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/codegen/common.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | enum BonusType { 6 | MULTIPLY_WORD, 7 | MULTIPLY_LETTER 8 | } 9 | 10 | enum Direction { 11 | LEFT_TO_RIGHT, 12 | TOP_TO_BOTTOM 13 | } 14 | 15 | enum Status { 16 | NOT_STARTED, 17 | STARTED 18 | } 19 | 20 | enum Letter { 21 | EMPTY, 22 | A, 23 | B, 24 | C, 25 | D, 26 | E, 27 | F, 28 | G, 29 | H, 30 | I, 31 | J, 32 | K, 33 | L, 34 | M, 35 | N, 36 | O, 37 | P, 38 | Q, 39 | R, 40 | S, 41 | T, 42 | U, 43 | V, 44 | W, 45 | X, 46 | Y, 47 | Z 48 | } 49 | -------------------------------------------------------------------------------- /scripts/encodeLetter.ts: -------------------------------------------------------------------------------- 1 | export const codeToLetter = (code: number): string => { 2 | if (code === 0) return ""; 3 | if (code < 1 || code > 26) { 4 | throw new Error("Code must be between 1 and 26"); 5 | } 6 | return String.fromCharCode(code + 64); 7 | }; 8 | 9 | export const letterToCode = (letter: string): number => { 10 | const code = letter.toUpperCase().charCodeAt(0) - 64; 11 | if (code < 1 || code > 26) { 12 | throw new Error("Letter must be between A and Z"); 13 | } 14 | return code; 15 | }; 16 | 17 | export const wordToCode = (word: string): number[] => { 18 | return word.split("").map((letter) => letterToCode(letter)); 19 | }; 20 | -------------------------------------------------------------------------------- /scripts/checkWord.ts: -------------------------------------------------------------------------------- 1 | import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; 2 | import minimist from "minimist"; 3 | import { readFile } from "fs/promises"; 4 | import { wordToCode } from "./encodeLetter"; 5 | 6 | const main = async () => { 7 | const { tree, word } = minimist(process.argv.slice(2)); 8 | console.log("Reading tree..."); 9 | const treeData = (await readFile(tree)).toString(); 10 | console.log("Loading tree..."); 11 | const t = StandardMerkleTree.load(JSON.parse(treeData).tree); 12 | console.log(`Tree loaded. Generating proof for ${word}`); 13 | console.log(`Proof: ${t.getProof([wordToCode(word)])}`); 14 | }; 15 | 16 | main(); 17 | -------------------------------------------------------------------------------- /src/common/Errors.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | // LibPoints 5 | error NoPointsForEmptyLetter(); 6 | 7 | // LibBoard 8 | error BoundTooLong(); 9 | error EmptyLetterInBounds(); 10 | 11 | // LibPlay 12 | error WordTooLong(); 13 | error InvalidWordStart(); 14 | error InvalidWordEnd(); 15 | error EmptyLetterNotOnExistingLetter(); 16 | error LetterOnExistingLetter(); 17 | error LonelyWord(); 18 | error NoLettersPlayed(); 19 | error WordNotInDictionary(); 20 | error InvalidBoundLength(); 21 | error NonzeroEmptyLetterBound(); 22 | error NonemptyBoundEdges(); 23 | 24 | // LibTreasury 25 | error NoPoints(); 26 | 27 | // LibPrice 28 | error Overflow(); 29 | -------------------------------------------------------------------------------- /src/systems/SetDrawLetterOddsSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | import { System } from "@latticexyz/world/src/System.sol"; 5 | import { DrawLetterOdds } from "codegen/index.sol"; 6 | 7 | contract SetDrawLetterOddsSystem is System { 8 | error AlreadySetOdds(); 9 | error InvalidOddsLength(); 10 | error NonzeroFirstValue(); 11 | 12 | function setDrawLetterOdds(uint8[] memory odds) public { 13 | if (DrawLetterOdds.get().length != 0) { 14 | revert AlreadySetOdds(); 15 | } 16 | if (odds.length != 27) { 17 | revert InvalidOddsLength(); 18 | } 19 | if (odds[0] != 0) { 20 | revert NonzeroFirstValue(); 21 | } 22 | DrawLetterOdds.set({ value: odds }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/codegen/world/IPlaySystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | import { Letter, Direction } from "codegen/common.sol"; 7 | import { Coord } from "common/Coord.sol"; 8 | import { Bound } from "common/Bound.sol"; 9 | 10 | /** 11 | * @title IPlaySystem 12 | * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) 13 | * @dev This interface is automatically generated from the corresponding system contract. Do not edit manually. 14 | */ 15 | interface IPlaySystem { 16 | error CannotPlay(); 17 | error PlayMissingLetters(); 18 | 19 | function play( 20 | Letter[] memory word, 21 | bytes32[] memory proof, 22 | Coord memory coord, 23 | Direction direction, 24 | Bound[] memory bounds 25 | ) external; 26 | } 27 | -------------------------------------------------------------------------------- /src/libraries/LibTile.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | import { Letter } from "codegen/common.sol"; 5 | import { TileLetter, TilePlayer } from "codegen/index.sol"; 6 | import { Coord } from "common/Coord.sol"; 7 | 8 | library LibTile { 9 | function setTile(Coord memory coord, Letter letter, address player) internal { 10 | TileLetter.set({ x: coord.x, y: coord.y, value: letter }); 11 | TilePlayer.set({ x: coord.x, y: coord.y, value: player }); 12 | } 13 | 14 | function getLetter(Coord memory coord) internal view returns (Letter) { 15 | return TileLetter.get({ x: coord.x, y: coord.y }); 16 | } 17 | 18 | function getPlayer(Coord memory coord) internal view returns (address) { 19 | return TilePlayer.get({ x: coord.x, y: coord.y }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/systems/TransferLettersSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | import { System } from "@latticexyz/world/src/System.sol"; 5 | import { Letter } from "codegen/common.sol"; 6 | import { LibLetters } from "libraries/LibLetters.sol"; 7 | 8 | contract TransferLettersSystem is System { 9 | error TransferMissingLetters(); 10 | 11 | function transfer(Letter[] memory letters, address to) public { 12 | address from = _msgSender(); 13 | 14 | if (!LibLetters.hasLetters({ player: from, letters: letters })) { 15 | revert TransferMissingLetters(); 16 | } 17 | 18 | for (uint256 i = 0; i < letters.length; i++) { 19 | LibLetters.removeLetter({ player: from, letter: letters[i] }); 20 | LibLetters.addLetter({ player: to, letter: letters[i] }); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /scripts/generateMerkleTree.ts: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from "fs/promises"; 2 | 3 | import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; 4 | import minimist from "minimist"; 5 | import { wordToCode } from "./encodeLetter"; 6 | 7 | const getAllWords = async (dictionary: string): Promise => { 8 | const words = (await readFile(dictionary)).toString().split("\r\n"); 9 | return words; 10 | }; 11 | 12 | const main = async () => { 13 | const { dictionary, output } = minimist(process.argv.slice(2)); 14 | const words = await getAllWords(dictionary); 15 | const tree = StandardMerkleTree.of( 16 | words.map((word) => [wordToCode(word.toLowerCase())]), 17 | ["uint8[]"], 18 | ); 19 | const outputFilename = `${output}.json`; 20 | await writeFile( 21 | outputFilename, 22 | JSON.stringify({ 23 | root: tree.root, 24 | tree: tree.dump(), 25 | }), 26 | ); 27 | console.log(`Saved to ${outputFilename}`); 28 | }; 29 | 30 | main(); 31 | -------------------------------------------------------------------------------- /src/libraries/LibPlayer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | import { Points } from "codegen/index.sol"; 5 | import { SINGLETON_ADDRESS } from "common/Constants.sol"; 6 | 7 | library LibPlayer { 8 | function incrementPoints(address player, uint32 increment) internal { 9 | uint32 currentPoints = Points.get({ player: player }); 10 | uint32 currentTotalPoints = Points.get({ player: SINGLETON_ADDRESS }); 11 | Points.set({ player: player, value: currentPoints + increment }); 12 | Points.set({ player: SINGLETON_ADDRESS, value: currentTotalPoints + increment }); 13 | } 14 | 15 | function decrementPoints(address player, uint32 decrement) internal { 16 | uint32 currentPoints = Points.get({ player: player }); 17 | uint32 currentTotalPoints = Points.get({ player: SINGLETON_ADDRESS }); 18 | Points.set({ player: player, value: currentPoints - decrement }); 19 | Points.set({ player: SINGLETON_ADDRESS, value: currentTotalPoints - decrement }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /worlds.json: -------------------------------------------------------------------------------- 1 | { 2 | "690": { 3 | "address": "0x1754c1464837e55cb74ad396bc227078bce624bb", 4 | "blockNumber": 1817346 5 | }, 6 | "901": { 7 | "address": "0x1d778f689b42acb8309459ff173d1a9ca0b420b9", 8 | "blockNumber": 12058746 9 | }, 10 | "4242": { 11 | "address": "0x8bf8C6D24c85fC44c7788355e59e256218Ca0Bf4", 12 | "blockNumber": 21816599 13 | }, 14 | "8453": { 15 | "address": "0x227a1513fa19ccded63c0226570d7f950b6349bf", 16 | "blockNumber": 10010490 17 | }, 18 | "17001": { 19 | "address": "0x42eb4c07a60d77ee5de7d044f41d35d5237043c0", 20 | "blockNumber": 6720499 21 | }, 22 | "17069": { 23 | "address": "0x810b70265e8270c6f8454ef368e65a3b9f043040", 24 | "blockNumber": 1738520 25 | }, 26 | "31337": { 27 | "address": "0xcb4eb503f4cae4579a6f0886b499b730ee879c8f" 28 | }, 29 | "84531": { 30 | "address": "0xEF7c64d4773C13158A2419D43b68B6808C00421b", 31 | "blockNumber": 7978538 32 | }, 33 | "84532": { 34 | "address": "0x4dfa52e20db3dc2bd81d400b5b4563ec0a6ccc3d", 35 | "blockNumber": 10277520 36 | } 37 | } -------------------------------------------------------------------------------- /src/codegen/world/IStartSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | import { Letter } from "codegen/common.sol"; 7 | import { PriceConfigData, FeeConfigData } from "codegen/index.sol"; 8 | 9 | /** 10 | * @title IStartSystem 11 | * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) 12 | * @dev This interface is automatically generated from the corresponding system contract. Do not edit manually. 13 | */ 14 | interface IStartSystem { 15 | error GameAlreadyStarted(); 16 | error InitialWordTooLong(); 17 | error PriceWadPriceIncreaseFactorTooSmall(); 18 | 19 | function start( 20 | Letter[] memory initialWord, 21 | uint32[26] memory initialLetterAllocation, 22 | address initialLettersTo, 23 | bytes32 merkleRoot, 24 | uint256 initialPrice, 25 | uint256 claimRestrictionDurationBlocks, 26 | PriceConfigData memory priceConfig, 27 | FeeConfigData memory feeConfig, 28 | uint32 crossWordRewardFraction, 29 | uint16 bonusDistance, 30 | uint8 numDrawLetters 31 | ) external; 32 | } 33 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [fmt] 2 | sort_imports=true 3 | bracket_spacing = true 4 | int_types = "long" 5 | line_length = 120 6 | multiline_func_header = "all" 7 | number_underscore = "thousands" 8 | quote_style = "double" 9 | tab_width = 4 10 | wrap_comments = true 11 | ignore = ["src/codegen/**/*"] 12 | 13 | [profile.default] 14 | solc_version = "0.8.24" 15 | ffi = false 16 | optimizer = true 17 | optimizer_runs = 3000 18 | verbosity = 1 19 | src = "src" 20 | test = "test" 21 | out = "out" 22 | allow_paths = ["./node_modules"] 23 | extra_output_files = [ 24 | "abi", 25 | "evm.bytecode" 26 | ] 27 | fs_permissions = [{ access = "read", path = "./"}] 28 | 29 | [fuzz] 30 | runs = 2056 31 | 32 | [profile.redstone-mainnet] 33 | eth_rpc_url = "https://rpc.redstonechain.com" 34 | 35 | [profile.redstone-garnet] 36 | eth_rpc_url = "https://rpc.garnetchain.com" 37 | 38 | [profile.redstone-holesky-legacy] 39 | eth_rpc_url = "https://rpc.holesky.redstone.xyz" 40 | 41 | [profile.lattice-901] 42 | eth_rpc_url = "https://redstone.linfra.xyz" 43 | 44 | [profile.base-sepolia] 45 | eth_rpc_url = "https://sepolia.base.org" 46 | 47 | [profile.base-mainnet] 48 | eth_rpc_url = "https://base-mainnet.blastapi.io/" 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Words3 3 | 4 | An open source, fully onchain, **word game** built with [MUD](https://mud.dev) 5 | 6 | 7 | 8 | 9 | ## Deployment 10 | 11 | To deploy words3, first clone the repository **including submodules** 12 | 13 | ```bash 14 | git clone --recurse-submodules https://github.com/smallbraingames/words3-contracts 15 | ``` 16 | 17 | Then, install dependencies (we use [pnpm](https://pnpm.io)) 18 | 19 | ```bash 20 | pnpm install 21 | ``` 22 | 23 | Next, cleanup excess files in the murky submodule to prevent compilation errors 24 | 25 | ```bash 26 | pnpm clean-murky 27 | ``` 28 | 29 | Now, deploy contracts locally with 30 | 31 | ``` 32 | pnpm dev 33 | ``` 34 | 35 | 36 | ## Environment Variables 37 | 38 | To run this project, you will need to add the following environment variables to your .env file 39 | 40 | `PRIVATE_KEY` 41 | 42 | You can set this to the default anvil private key with 43 | 44 | ``` 45 | PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 # Anvil 46 | ``` 47 | 48 | ## Running Tests 49 | 50 | To run tests, after following the estup instructions, run the following command 51 | 52 | ``` 53 | pnpm test 54 | ``` 55 | 56 | 57 | ## Authors 58 | 59 | - [@smallbraingames](https://www.github.com/smallbraingames) 60 | 61 | -------------------------------------------------------------------------------- /src/codegen/world/IWorld.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol"; 7 | 8 | import { IClaimSystem } from "./IClaimSystem.sol"; 9 | import { IDonateSystem } from "./IDonateSystem.sol"; 10 | import { IDrawSystem } from "./IDrawSystem.sol"; 11 | import { IPlaySystem } from "./IPlaySystem.sol"; 12 | import { ISetDrawLetterOddsSystem } from "./ISetDrawLetterOddsSystem.sol"; 13 | import { ISetFeeConfigSystem } from "./ISetFeeConfigSystem.sol"; 14 | import { IStartSystem } from "./IStartSystem.sol"; 15 | import { ITransferLettersSystem } from "./ITransferLettersSystem.sol"; 16 | 17 | /** 18 | * @title IWorld 19 | * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) 20 | * @notice This interface integrates all systems and associated function selectors 21 | * that are dynamically registered in the World during deployment. 22 | * @dev This is an autogenerated file; do not edit manually. 23 | */ 24 | interface IWorld is 25 | IBaseWorld, 26 | IClaimSystem, 27 | IDonateSystem, 28 | IDrawSystem, 29 | IPlaySystem, 30 | ISetDrawLetterOddsSystem, 31 | ISetFeeConfigSystem, 32 | IStartSystem, 33 | ITransferLettersSystem 34 | {} 35 | -------------------------------------------------------------------------------- /src/codegen/index.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | import { GameConfig, GameConfigData } from "./tables/GameConfig.sol"; 7 | import { FeeConfig, FeeConfigData } from "./tables/FeeConfig.sol"; 8 | import { ClaimRestrictionConfig } from "./tables/ClaimRestrictionConfig.sol"; 9 | import { MerkleRootConfig } from "./tables/MerkleRootConfig.sol"; 10 | import { PriceConfig, PriceConfigData } from "./tables/PriceConfig.sol"; 11 | import { DrawLetterOdds } from "./tables/DrawLetterOdds.sol"; 12 | import { TileLetter } from "./tables/TileLetter.sol"; 13 | import { TilePlayer } from "./tables/TilePlayer.sol"; 14 | import { PlayerLetters } from "./tables/PlayerLetters.sol"; 15 | import { DrawLastSold, DrawLastSoldData } from "./tables/DrawLastSold.sol"; 16 | import { DrawCount } from "./tables/DrawCount.sol"; 17 | import { Points } from "./tables/Points.sol"; 18 | import { Treasury } from "./tables/Treasury.sol"; 19 | import { Spent } from "./tables/Spent.sol"; 20 | import { UpdateId } from "./tables/UpdateId.sol"; 21 | import { PlayUpdate, PlayUpdateData } from "./tables/PlayUpdate.sol"; 22 | import { PointsUpdate, PointsUpdateData } from "./tables/PointsUpdate.sol"; 23 | import { PointsClaimedUpdate, PointsClaimedUpdateData } from "./tables/PointsClaimedUpdate.sol"; 24 | import { DrawUpdate, DrawUpdateData } from "./tables/DrawUpdate.sol"; 25 | -------------------------------------------------------------------------------- /src/libraries/LibTreasury.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | import { Spent, Treasury } from "codegen/index.sol"; 5 | import { NoPoints } from "common/Errors.sol"; 6 | import { LibPoints } from "libraries/LibPoints.sol"; 7 | 8 | library LibTreasury { 9 | function getClaimAmount(uint32 points) internal view returns (uint256) { 10 | uint256 treasury = Treasury.get(); 11 | if (points == 0) { 12 | revert NoPoints(); 13 | } 14 | uint32 totalPoints = LibPoints.getTotalPoints(); 15 | uint256 claimAmount = (treasury * points) / uint256(totalPoints); 16 | return claimAmount; 17 | } 18 | 19 | function getFeeAmount(uint256 value, uint16 feeBps) internal pure returns (uint256) { 20 | uint256 feeAmount = (value * uint256(feeBps)) / 10_000; 21 | return feeAmount; 22 | } 23 | 24 | function incrementTreasury(address msgSender, uint256 msgValue) internal { 25 | uint256 incrementedTreasury = Treasury.get() + msgValue; 26 | Treasury.set({ value: incrementedTreasury }); 27 | incrementSpent(msgSender, msgValue); 28 | } 29 | 30 | function decrementTreasury(uint256 decrement) internal { 31 | uint256 decrementedTreasury = Treasury.get() - decrement; 32 | Treasury.set({ value: decrementedTreasury }); 33 | } 34 | 35 | function incrementSpent(address msgSender, uint256 msgValue) private { 36 | uint256 incrementedSpent = Spent.get({ player: msgSender }) + msgValue; 37 | Spent.set({ player: msgSender, value: incrementedSpent }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/libraries/LibGame.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | import { Status } from "codegen/common.sol"; 5 | import { 6 | ClaimRestrictionConfig, 7 | DrawLastSold, 8 | FeeConfig, 9 | FeeConfigData, 10 | GameConfig, 11 | MerkleRootConfig, 12 | PriceConfig, 13 | PriceConfigData 14 | } from "codegen/index.sol"; 15 | 16 | library LibGame { 17 | function getGameStatus() internal view returns (Status) { 18 | return GameConfig.getStatus(); 19 | } 20 | 21 | function canPlay() internal view returns (bool) { 22 | return getGameStatus() == Status.STARTED; 23 | } 24 | 25 | function startGame( 26 | bytes32 merkleRoot, 27 | uint256 initialPrice, 28 | uint256 claimRestrictionDurationBlocks, 29 | PriceConfigData memory priceConfig, 30 | FeeConfigData memory feeConfig, 31 | uint32 crossWordRewardFraction, 32 | uint16 bonusDistance, 33 | uint8 numDrawLetters 34 | ) 35 | internal 36 | { 37 | uint256 blockNumber = block.number; 38 | ClaimRestrictionConfig.set({ claimRestrictionBlock: blockNumber + claimRestrictionDurationBlocks }); 39 | GameConfig.set({ 40 | status: Status.STARTED, 41 | crossWordRewardFraction: crossWordRewardFraction, 42 | bonusDistance: bonusDistance, 43 | numDrawLetters: numDrawLetters 44 | }); 45 | MerkleRootConfig.set({ value: merkleRoot }); 46 | DrawLastSold.set({ price: initialPrice, blockNumber: blockNumber }); 47 | PriceConfig.set(priceConfig); 48 | FeeConfig.set(feeConfig); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/Words3Test.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.0; 3 | 4 | import { MudTest } from "@latticexyz/world/test/MudTest.t.sol"; 5 | import { Letter } from "codegen/common.sol"; 6 | import { IWorld } from "codegen/world/IWorld.sol"; 7 | 8 | contract Words3Test is MudTest { 9 | IWorld public world; 10 | address public deployerAddress; 11 | 12 | function setUp() public virtual override { 13 | super.setUp(); 14 | world = IWorld(worldAddress); 15 | deployerAddress = address(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); 16 | } 17 | 18 | function setDefaultLetterOdds() public { 19 | uint8[] memory odds = new uint8[](27); 20 | odds[0] = 0; 21 | odds[uint8(Letter.A)] = 9; 22 | odds[uint8(Letter.B)] = 2; 23 | odds[uint8(Letter.C)] = 2; 24 | odds[uint8(Letter.D)] = 4; 25 | odds[uint8(Letter.E)] = 12; 26 | odds[uint8(Letter.F)] = 2; 27 | odds[uint8(Letter.G)] = 3; 28 | odds[uint8(Letter.H)] = 2; 29 | odds[uint8(Letter.I)] = 9; 30 | odds[uint8(Letter.J)] = 1; 31 | odds[uint8(Letter.K)] = 1; 32 | odds[uint8(Letter.L)] = 4; 33 | odds[uint8(Letter.M)] = 2; 34 | odds[uint8(Letter.N)] = 6; 35 | odds[uint8(Letter.O)] = 8; 36 | odds[uint8(Letter.P)] = 2; 37 | odds[uint8(Letter.Q)] = 1; 38 | odds[uint8(Letter.R)] = 6; 39 | odds[uint8(Letter.S)] = 4; 40 | odds[uint8(Letter.T)] = 6; 41 | odds[uint8(Letter.U)] = 4; 42 | odds[uint8(Letter.V)] = 2; 43 | odds[uint8(Letter.W)] = 2; 44 | odds[uint8(Letter.X)] = 1; 45 | odds[uint8(Letter.Y)] = 2; 46 | odds[uint8(Letter.Z)] = 1; 47 | world.setDrawLetterOdds(odds); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/systems/PlaySystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | import { System } from "@latticexyz/world/src/System.sol"; 5 | import { Direction, Letter } from "codegen/common.sol"; 6 | import { Bound } from "common/Bound.sol"; 7 | import { Coord } from "common/Coord.sol"; 8 | import { LibGame } from "libraries/LibGame.sol"; 9 | 10 | import { LibLetters } from "libraries/LibLetters.sol"; 11 | import { LibPlay } from "libraries/LibPlay.sol"; 12 | 13 | contract PlaySystem is System { 14 | error CannotPlay(); 15 | error PlayMissingLetters(); 16 | 17 | /// @notice Checks if a move is valid and if so, plays a word on the board 18 | /// @param word Letters of the word being played, empty letters mean using existing letters on board 19 | /// @param proof Merkle proof that the word is in the dictionary 20 | /// @param coord Starting coord that the word is being played from 21 | /// @param direction Direction the word is being played (top-down, or left-to-right) 22 | /// @param bounds Bounds of all other words on the cross axis this word makes 23 | function play( 24 | Letter[] memory word, 25 | bytes32[] memory proof, 26 | Coord memory coord, 27 | Direction direction, 28 | Bound[] memory bounds 29 | ) 30 | public 31 | { 32 | address player = _msgSender(); 33 | if (!LibGame.canPlay()) { 34 | revert CannotPlay(); 35 | } 36 | if (!LibLetters.hasLetters({ player: player, letters: word })) { 37 | revert PlayMissingLetters(); 38 | } 39 | LibLetters.useLetters({ player: player, letters: word }); 40 | LibPlay.play({ word: word, proof: proof, coord: coord, direction: direction, bounds: bounds, player: player }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/systems/ClaimSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | import { System } from "@latticexyz/world/src/System.sol"; 5 | import { ClaimRestrictionConfig, FeeConfig, FeeConfigData, Points, PointsClaimedUpdate } from "codegen/index.sol"; 6 | import { LibPlayer } from "libraries/LibPlayer.sol"; 7 | import { LibTreasury } from "libraries/LibTreasury.sol"; 8 | import { LibUpdateId } from "libraries/LibUpdateId.sol"; 9 | 10 | contract ClaimSystem is System { 11 | error NotEnoughPoints(); 12 | error WithinClaimRestrictionPeriod(); 13 | 14 | function claim(uint32 points) public { 15 | uint256 claimRestrictionBlock = ClaimRestrictionConfig.getClaimRestrictionBlock(); 16 | if (block.number <= claimRestrictionBlock) { 17 | revert WithinClaimRestrictionPeriod(); 18 | } 19 | 20 | address player = _msgSender(); 21 | 22 | uint32 playerPoints = Points.get({ player: player }); 23 | if (points > playerPoints) { 24 | revert NotEnoughPoints(); 25 | } 26 | 27 | uint256 claimAmount = LibTreasury.getClaimAmount({ points: points }); 28 | 29 | LibPlayer.decrementPoints({ player: player, decrement: points }); 30 | LibTreasury.decrementTreasury({ decrement: claimAmount }); 31 | 32 | FeeConfigData memory feeConfig = FeeConfig.get(); 33 | 34 | uint256 feeAmount = LibTreasury.getFeeAmount({ value: claimAmount, feeBps: feeConfig.feeBps }); 35 | payable(player).transfer(claimAmount - feeAmount); 36 | payable(feeConfig.feeTaker).transfer(feeAmount); 37 | 38 | PointsClaimedUpdate.set({ 39 | id: LibUpdateId.getUpdateId(), 40 | player: player, 41 | points: points, 42 | value: claimAmount, 43 | timestamp: block.timestamp 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/unit/Wrapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | import { Direction, Letter } from "codegen/common.sol"; 5 | 6 | import { Bonus } from "common/Bonus.sol"; 7 | import { Bound } from "common/Bound.sol"; 8 | import { Coord } from "common/Coord.sol"; 9 | 10 | import { LibBoard } from "libraries/LibBoard.sol"; 11 | import { LibPlay } from "libraries/LibPlay.sol"; 12 | import { LibPoints } from "libraries/LibPoints.sol"; 13 | import { LibPrice } from "libraries/LibPrice.sol"; 14 | 15 | contract Wrapper { 16 | function playCheckCrossWords( 17 | Letter[] memory word, 18 | Coord memory coord, 19 | Direction direction, 20 | Bound[] memory bounds 21 | ) 22 | public 23 | view 24 | returns (address[] memory) 25 | { 26 | return LibPlay.checkCrossWords(word, coord, direction, bounds); 27 | } 28 | 29 | function playCheckWord( 30 | Letter[] memory word, 31 | bytes32[] memory proof, 32 | Coord memory coord, 33 | Direction direction, 34 | Bound[] memory bounds 35 | ) 36 | public 37 | view 38 | { 39 | LibPlay.checkWord(word, proof, coord, direction, bounds); 40 | } 41 | 42 | function boardGetCoordsOutsideBound( 43 | Coord memory coord, 44 | Direction direction, 45 | Bound memory bound 46 | ) 47 | public 48 | pure 49 | returns (Coord memory, Coord memory) 50 | { 51 | return LibBoard.getCoordsOutsideBound(coord, direction, bound); 52 | } 53 | 54 | function pointsGetBonusLetterPoints(Letter letter, Bonus memory bonus) public pure returns (uint32) { 55 | return LibPoints.getBonusLetterPoints(letter, bonus); 56 | } 57 | 58 | function priceToWad(uint256 x) public pure returns (int256) { 59 | return LibPrice.toWad(x); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/unit/LibLetters.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | /* solhint-disable no-global-import */ 4 | /* solhint-disable func-name-mixedcase */ 5 | 6 | import { Words3Test } from "../Words3Test.t.sol"; 7 | import { Letter } from "codegen/common.sol"; 8 | import "forge-std/Test.sol"; 9 | import { LibLetters } from "libraries/LibLetters.sol"; 10 | 11 | contract LibLettersTest is Words3Test { 12 | function test_GetDraw() public { 13 | uint256 random = uint256(keccak256(abi.encodePacked(block.timestamp))); 14 | 15 | uint8[] memory odds = new uint8[](27); 16 | odds[0] = 0; 17 | odds[uint8(Letter.A)] = 1; 18 | odds[uint8(Letter.B)] = 0; 19 | odds[uint8(Letter.C)] = 0; 20 | odds[uint8(Letter.D)] = 0; 21 | odds[uint8(Letter.E)] = 1; 22 | odds[uint8(Letter.F)] = 0; 23 | odds[uint8(Letter.G)] = 0; 24 | 25 | Letter[] memory letters = LibLetters.getDraw(odds, 8, random); 26 | 27 | assertEq(letters.length, 8); 28 | for (uint256 i = 0; i < letters.length; i++) { 29 | assertTrue(letters[i] == Letter.A || letters[i] == Letter.E); 30 | } 31 | } 32 | 33 | function testFuzz_GetDraw(uint256 random) public { 34 | uint8[] memory odds = new uint8[](27); 35 | odds[0] = 0; 36 | odds[uint8(Letter.A)] = 0; 37 | odds[uint8(Letter.B)] = 1; 38 | odds[uint8(Letter.C)] = 0; 39 | odds[uint8(Letter.D)] = 1; 40 | odds[uint8(Letter.E)] = 1; 41 | odds[uint8(Letter.F)] = 0; 42 | odds[uint8(Letter.G)] = 0; 43 | 44 | Letter[] memory letters = LibLetters.getDraw(odds, 8, random); 45 | 46 | assertEq(letters.length, 8); 47 | for (uint256 i = 0; i < letters.length; i++) { 48 | assertTrue(letters[i] == Letter.B || letters[i] == Letter.D || letters[i] == Letter.E); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/libraries/LibBonus.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | import { BonusType } from "codegen/common.sol"; 5 | import { Bonus } from "common/Bonus.sol"; 6 | import { Coord } from "common/Coord.sol"; 7 | 8 | library LibBonus { 9 | function isBonusTile(Coord memory coord, uint16 bonusDistance) internal pure returns (bool) { 10 | int32 x = abs(coord.x); 11 | int32 y = abs(coord.y); 12 | return ((x + y) % int32(uint32(bonusDistance))) == 0; 13 | } 14 | 15 | /// @notice Assumes that isBonusTile is called to check if the tile is a bonus tile first 16 | function getTileBonus(Coord memory coord) internal pure returns (Bonus memory) { 17 | uint256 n = uint256(keccak256(abi.encodePacked(coord.x, coord.y))); 18 | int32 dist = abs(coord.x) + abs(coord.y); 19 | 20 | uint256 bonusTypeThreshold = 6900 + min(uint256(uint32(dist)) * 5, 50_000); 21 | BonusType bonusType = n % 100_000 < bonusTypeThreshold ? BonusType.MULTIPLY_WORD : BonusType.MULTIPLY_LETTER; 22 | 23 | n = n % 100; 24 | uint32 bonusValue = 2; 25 | if (n < 1) { 26 | bonusValue = 5; 27 | } else if (n < 4) { 28 | bonusValue = 4; 29 | } else if (n < 12) { 30 | bonusValue = 3; 31 | } 32 | 33 | return Bonus({ bonusValue: bonusValue, bonusType: bonusType }); 34 | } 35 | 36 | function abs(int32 x) private pure returns (int32) { 37 | if (x < 0) { 38 | return -x; 39 | } 40 | return x; 41 | } 42 | 43 | function max(int32 x, int32 y) private pure returns (int32) { 44 | if (x > y) { 45 | return x; 46 | } 47 | return y; 48 | } 49 | 50 | function min(uint256 x, uint256 y) private pure returns (uint256) { 51 | if (x < y) { 52 | return x; 53 | } 54 | return y; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/systems/DrawSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | import { System } from "@latticexyz/world/src/System.sol"; 5 | 6 | import { Letter } from "codegen/common.sol"; 7 | import { DrawCount, DrawLastSold, DrawLetterOdds, DrawUpdate, GameConfig } from "codegen/index.sol"; 8 | import { SINGLETON_ADDRESS } from "common/Constants.sol"; 9 | import { LibLetters } from "libraries/LibLetters.sol"; 10 | import { LibPrice } from "libraries/LibPrice.sol"; 11 | 12 | import { LibTreasury } from "libraries/LibTreasury.sol"; 13 | import { LibUpdateId } from "libraries/LibUpdateId.sol"; 14 | 15 | contract DrawSystem is System { 16 | error InvalidDrawAddress(); 17 | error NotEnoughValue(); 18 | 19 | function draw(address player) public payable { 20 | if (player == SINGLETON_ADDRESS) { 21 | revert InvalidDrawAddress(); 22 | } 23 | 24 | uint256 value = _msgValue(); 25 | if (value < LibPrice.getDrawPrice()) { 26 | revert NotEnoughValue(); 27 | } 28 | 29 | // Sender might be different than player, track the spend under sender 30 | LibTreasury.incrementTreasury({ msgSender: _msgSender(), msgValue: value }); 31 | 32 | uint32 drawCount = DrawCount.get(); 33 | uint256 random = uint256(keccak256(abi.encodePacked(block.prevrandao, drawCount))); 34 | Letter[] memory drawnLetters = LibLetters.getDraw({ 35 | odds: DrawLetterOdds.get(), 36 | numLetters: GameConfig.getNumDrawLetters(), 37 | random: random 38 | }); 39 | 40 | for (uint256 i = 0; i < drawnLetters.length; i++) { 41 | LibLetters.addLetter({ player: player, letter: drawnLetters[i] }); 42 | } 43 | 44 | DrawLastSold.set({ price: value, blockNumber: block.number }); 45 | DrawCount.set(DrawCount.get() + 1); 46 | DrawUpdate.set({ id: LibUpdateId.getUpdateId(), player: player, value: value, timestamp: block.timestamp }); 47 | } 48 | 49 | function getDrawPrice() public view returns (uint256) { 50 | return LibPrice.getDrawPrice(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/unit/LibTreasury.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | /* solhint-disable no-global-import */ 4 | /* solhint-disable func-name-mixedcase */ 5 | 6 | import { Words3Test } from "../Words3Test.t.sol"; 7 | import { Points, Treasury } from "codegen/index.sol"; 8 | import "forge-std/Test.sol"; 9 | import { LibTreasury } from "libraries/LibTreasury.sol"; 10 | 11 | contract LibTreasuryTest is Words3Test { 12 | function test_GetFeeAmount() public { 13 | assertEq(LibTreasury.getFeeAmount({ value: 100, feeBps: 1000 }), 10); 14 | assertEq(LibTreasury.getFeeAmount({ value: 100, feeBps: 500 }), 5); 15 | assertEq(LibTreasury.getFeeAmount({ value: 100, feeBps: 0 }), 0); 16 | assertEq(LibTreasury.getFeeAmount({ value: 100, feeBps: 10_000 }), 100); 17 | assertEq(LibTreasury.getFeeAmount({ value: 100, feeBps: 9999 }), 99); 18 | assertEq(LibTreasury.getFeeAmount({ value: 100 ether, feeBps: 9999 }), 99.99 ether); 19 | assertEq(LibTreasury.getFeeAmount({ value: 1e6 ether, feeBps: 9999 }), 0.9999e6 ether); 20 | assertEq(LibTreasury.getFeeAmount({ value: 1e6 ether, feeBps: 690 }), 0.069e6 ether); 21 | } 22 | 23 | function testFuzz_GetFeeAmount(uint256 value, uint16 feeBps) public { 24 | value = bound(value, 0, uint256(type(uint128).max)); 25 | feeBps = uint16(bound(feeBps, 0, 10_000)); 26 | uint256 expected = (value * feeBps) / 10_000; 27 | assertEq(LibTreasury.getFeeAmount({ value: value, feeBps: feeBps }), expected); 28 | } 29 | 30 | function testFuzz_GetClaimAmountNeverAboveTreasury(uint32 points, uint256 treasury, uint32 totalPoints) public { 31 | treasury = bound(treasury, 0, uint256(type(uint128).max)); 32 | totalPoints = uint32(bound(totalPoints, 1, type(uint32).max)); 33 | points = uint32(bound(points, 1, totalPoints)); 34 | uint256 expected = (treasury * points) / totalPoints; 35 | vm.startPrank(deployerAddress); 36 | Treasury.set({ value: treasury }); 37 | Points.setValue({ player: address(0), value: totalPoints }); 38 | vm.stopPrank(); 39 | assertEq(LibTreasury.getClaimAmount(points), expected); 40 | assertTrue(expected <= treasury); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contracts", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "scripts": { 7 | "clean-murky": "rm -rf src/test/murky/lib && rm -rf src/test/murky/differential_testing && rm -rf src/test/murky/src/test", 8 | "build": "mud worldgen && mud tablegen && forge build", 9 | "clean": "forge clean && rimraf src/codegen", 10 | "deploy:local": "bun run build && mud deploy", 11 | "deploy:base-testnet": "bun run build && mud deploy --profile=base-sepolia", 12 | "deploy:testnet": "bun run build && mud deploy --profile=redstone-garnet", 13 | "deploy:redstone-mainnet": "bun run build && mud deploy --profile=redstone-mainnet", 14 | "deploy:testnet-legacy": "bun run build && mud deploy --profile=redstone-holesky-legacy", 15 | "deploy:base": "bun run build && mud deploy --profile=base-mainnet", 16 | "dev": "bun mud dev-contracts", 17 | "faucet": "DEBUG=mud:faucet bun faucet-server", 18 | "lint": "forge fmt && bun run solhint", 19 | "solhint": "solhint --config ./.solhint.json 'src/**/*.sol' --fix", 20 | "test": "mud test", 21 | "transfer-letters": "bunx ts-node scripts/transferLetters.ts" 22 | }, 23 | "devDependencies": { 24 | "@latticexyz/cli": "2.0.8", 25 | "@latticexyz/common": "2.0.8", 26 | "@latticexyz/config": "2.0.8", 27 | "@latticexyz/faucet": "2.0.8", 28 | "@latticexyz/schema-type": "2.0.8", 29 | "@latticexyz/store": "2.0.8", 30 | "@latticexyz/world": "2.0.8", 31 | "@latticexyz/world-modules": "2.0.8", 32 | "@openzeppelin/contracts": "5.0.2", 33 | "@solidstate/contracts": "^0.0.52", 34 | "@types/node": "^18.15.11", 35 | "commander": "^12.1.0", 36 | "csv-parse": "^5.5.6", 37 | "ds-test": "https://github.com/dapphub/ds-test.git#c9ce3f25bde29fc5eb9901842bf02850dfd2d084", 38 | "ethers": "^5.7.2", 39 | "forge-std": "https://github.com/foundry-rs/forge-std.git#b4f121555729b3afb3c5ffccb62ff4b6e2818fd3", 40 | "prettier": "3.2.5", 41 | "prettier-plugin-solidity": "1.3.1", 42 | "rimraf": "^3.0.2", 43 | "run-pty": "^4.0.3", 44 | "solhint": "^3.4.1", 45 | "solhint-config-mud": "2.0.8", 46 | "solhint-plugin-mud": "2.0.8", 47 | "solmate": "6.2.0", 48 | "ts-node": "^10.9.1", 49 | "typescript": "5.4.2", 50 | "viem": "^2.11.1" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/unit/LibGame.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | /* solhint-disable no-global-import */ 4 | /* solhint-disable func-name-mixedcase */ 5 | 6 | import { Words3Test } from "../Words3Test.t.sol"; 7 | import { Status } from "codegen/common.sol"; 8 | import { 9 | DrawLastSold, FeeConfigData, GameConfig, MerkleRootConfig, PriceConfig, PriceConfigData 10 | } from "codegen/index.sol"; 11 | import "forge-std/Test.sol"; 12 | import { LibGame } from "libraries/LibGame.sol"; 13 | 14 | contract LibGameTest is Words3Test { 15 | function test_CanPlay() public { 16 | assertFalse(LibGame.canPlay()); 17 | vm.startPrank(deployerAddress); 18 | GameConfig.setStatus(Status.STARTED); 19 | vm.stopPrank(); 20 | assertTrue(LibGame.canPlay()); 21 | } 22 | 23 | function testFuzz_StartGame( 24 | bytes32 merkleRoot, 25 | uint256 initialPrice, 26 | uint256 minPrice, 27 | int256 wadPriceIncreaseFactor, 28 | int256 wadScale, 29 | int256 wadPower, 30 | uint32 crossWordRewardFraction, 31 | uint16 bonusDistance, 32 | uint8 numDrawLetters 33 | ) 34 | public 35 | { 36 | vm.startPrank(deployerAddress); 37 | LibGame.startGame({ 38 | merkleRoot: merkleRoot, 39 | initialPrice: initialPrice, 40 | claimRestrictionDurationBlocks: 0, 41 | priceConfig: PriceConfigData({ 42 | minPrice: minPrice, 43 | wadPriceIncreaseFactor: wadPriceIncreaseFactor, 44 | wadScale: wadScale, 45 | wadPower: wadPower 46 | }), 47 | feeConfig: FeeConfigData({ feeBps: 0, feeTaker: address(0) }), 48 | crossWordRewardFraction: crossWordRewardFraction, 49 | bonusDistance: bonusDistance, 50 | numDrawLetters: numDrawLetters 51 | }); 52 | vm.stopPrank(); 53 | assertEq(merkleRoot, MerkleRootConfig.get()); 54 | assertEq(minPrice, PriceConfig.getMinPrice()); 55 | assertEq(wadScale, PriceConfig.getWadScale()); 56 | assertEq(wadPower, PriceConfig.getWadPower()); 57 | assertEq(initialPrice, DrawLastSold.getPrice()); 58 | assertEq(block.number, DrawLastSold.getBlockNumber()); 59 | assertEq(crossWordRewardFraction, GameConfig.getCrossWordRewardFraction()); 60 | assertEq(bonusDistance, GameConfig.getBonusDistance()); 61 | assertEq(numDrawLetters, GameConfig.getNumDrawLetters()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/libraries/LibPrice.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | import { DrawLastSold, DrawLastSoldData, PriceConfig, PriceConfigData } from "codegen/index.sol"; 5 | import { Overflow } from "common/Errors.sol"; 6 | import { wadDiv, wadMul, wadPow } from "solmate/src/utils/SignedWadMath.sol"; 7 | 8 | library LibPrice { 9 | function getDrawPrice() internal view returns (uint256) { 10 | PriceConfigData memory priceConfig = PriceConfig.get(); 11 | DrawLastSoldData memory drawLastSold = DrawLastSold.get(); 12 | 13 | int256 wadGdaStartPrice = wadMul(toWad(drawLastSold.price), priceConfig.wadPriceIncreaseFactor); 14 | 15 | return getPrice({ 16 | wadStartPrice: wadGdaStartPrice, 17 | wadMinPrice: toWad(priceConfig.minPrice), 18 | wadPower: priceConfig.wadPower, 19 | wadScale: priceConfig.wadScale, 20 | wadPassed: toWad(block.number - drawLastSold.blockNumber) 21 | }); 22 | } 23 | 24 | function getPrice( 25 | int256 wadStartPrice, 26 | int256 wadMinPrice, 27 | int256 wadPower, 28 | int256 wadScale, 29 | int256 wadPassed 30 | ) 31 | internal 32 | pure 33 | returns (uint256) 34 | { 35 | int256 startX = 36 | getInverseF({ y: wadStartPrice, wadPower: wadPower, wadScale: wadScale, wadConstant: wadMinPrice }); 37 | 38 | int256 currentX = startX + wadPassed; 39 | int256 wadPrice = getF({ x: currentX, wadPower: wadPower, wadScale: wadScale, wadConstant: wadMinPrice }); 40 | return uint256(wadPrice / 1e18); 41 | } 42 | 43 | function getF(int256 x, int256 wadPower, int256 wadScale, int256 wadConstant) internal pure returns (int256) { 44 | return wadDiv(wadScale, wadPow(x, wadPower)) + wadConstant; 45 | } 46 | 47 | function getInverseF( 48 | int256 y, 49 | int256 wadPower, 50 | int256 wadScale, 51 | int256 wadConstant 52 | ) 53 | internal 54 | pure 55 | returns (int256) 56 | { 57 | return wadRoot(wadDiv(wadScale, y - wadConstant), wadPower); 58 | } 59 | 60 | function wadRoot(int256 x, int256 root) internal pure returns (int256) { 61 | if (x == 0) return 0; 62 | return wadPow(x, wadDiv(1e18, root)); 63 | } 64 | 65 | function toWad(uint256 x) internal pure returns (int256) { 66 | uint256 wad = x * 1e18; 67 | if (wad > uint256(type(int256).max)) { 68 | revert Overflow(); 69 | } 70 | return int256(wad); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/libraries/LibLetters.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | import { Letter } from "codegen/common.sol"; 5 | import { PlayerLetters } from "codegen/index.sol"; 6 | 7 | library LibLetters { 8 | function getDraw(uint8[] memory odds, uint8 numLetters, uint256 random) internal pure returns (Letter[] memory) { 9 | // This sum is recomputed every function call to support overriding odds in the future 10 | uint256 sumOfOdds = 0; 11 | for (uint8 i = 0; i < odds.length; i++) { 12 | sumOfOdds += odds[i]; 13 | } 14 | 15 | Letter[] memory bag = new Letter[](sumOfOdds); 16 | 17 | uint256 index = 0; 18 | for (uint256 i = 0; i < odds.length; i++) { 19 | Letter letter = Letter(i); 20 | uint8 letterOdds = odds[i]; 21 | 22 | for (uint256 j = 0; j < letterOdds; j++) { 23 | bag[index] = letter; 24 | index++; 25 | } 26 | } 27 | 28 | Letter[] memory letters = new Letter[](numLetters); 29 | for (uint256 i = 0; i < numLetters; i++) { 30 | uint256 randomIndex = uint256(keccak256(abi.encodePacked(random, i))) % sumOfOdds; 31 | letters[i] = bag[randomIndex]; 32 | } 33 | 34 | return letters; 35 | } 36 | 37 | function hasLetters(address player, Letter[] memory letters) internal view returns (bool) { 38 | for (uint256 i = 0; i < letters.length; i++) { 39 | Letter letter = letters[i]; 40 | if (letter == Letter.EMPTY) { 41 | continue; 42 | } 43 | if (PlayerLetters.get({ player: player, letter: letter }) == 0) { 44 | return false; 45 | } 46 | } 47 | return true; 48 | } 49 | 50 | function useLetters(address player, Letter[] memory letters) internal { 51 | for (uint256 i = 0; i < letters.length; i++) { 52 | Letter letter = letters[i]; 53 | if (letter == Letter.EMPTY) { 54 | continue; 55 | } 56 | removeLetter({ player: player, letter: letter }); 57 | } 58 | } 59 | 60 | function addLetter(address player, Letter letter) internal { 61 | uint32 count = PlayerLetters.get({ player: player, letter: letter }); 62 | PlayerLetters.set({ player: player, letter: letter, value: count + 1 }); 63 | } 64 | 65 | function removeLetter(address player, Letter letter) internal { 66 | uint32 count = PlayerLetters.get({ player: player, letter: letter }); 67 | PlayerLetters.set({ player: player, letter: letter, value: count - 1 }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/systems/StartSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | import { System } from "@latticexyz/world/src/System.sol"; 5 | import { Letter, Status } from "codegen/common.sol"; 6 | 7 | import { FeeConfigData, PlayerLetters, PriceConfigData } from "codegen/index.sol"; 8 | import { MAX_WORD_LENGTH } from "common/Constants.sol"; 9 | import { Coord } from "common/Coord.sol"; 10 | import { LibGame } from "libraries/LibGame.sol"; 11 | import { LibTile } from "libraries/LibTile.sol"; 12 | 13 | contract StartSystem is System { 14 | error GameAlreadyStarted(); 15 | error InitialWordTooLong(); 16 | error PriceWadPriceIncreaseFactorTooSmall(); 17 | 18 | function start( 19 | Letter[] memory initialWord, 20 | uint32[26] memory initialLetterAllocation, 21 | address initialLettersTo, 22 | bytes32 merkleRoot, 23 | uint256 initialPrice, 24 | uint256 claimRestrictionDurationBlocks, 25 | PriceConfigData memory priceConfig, 26 | FeeConfigData memory feeConfig, 27 | uint32 crossWordRewardFraction, 28 | uint16 bonusDistance, 29 | uint8 numDrawLetters 30 | ) 31 | public 32 | { 33 | if (LibGame.getGameStatus() != Status.NOT_STARTED) { 34 | revert GameAlreadyStarted(); 35 | } 36 | if (priceConfig.wadPriceIncreaseFactor < 1.01e18) { 37 | revert PriceWadPriceIncreaseFactorTooSmall(); 38 | } 39 | writeInitialWordChecked({ initialWord: initialWord }); 40 | allocateInitialLetters({ initialLetterAllocation: initialLetterAllocation, initialLettersTo: initialLettersTo }); 41 | LibGame.startGame({ 42 | merkleRoot: merkleRoot, 43 | initialPrice: initialPrice, 44 | claimRestrictionDurationBlocks: claimRestrictionDurationBlocks, 45 | priceConfig: priceConfig, 46 | feeConfig: feeConfig, 47 | crossWordRewardFraction: crossWordRewardFraction, 48 | bonusDistance: bonusDistance, 49 | numDrawLetters: numDrawLetters 50 | }); 51 | } 52 | 53 | function writeInitialWordChecked(Letter[] memory initialWord) private { 54 | uint256 wordLength = initialWord.length; 55 | if (wordLength > MAX_WORD_LENGTH) { 56 | revert InitialWordTooLong(); 57 | } 58 | int32 xOffset = int32(uint32(wordLength)) / 2; 59 | for (uint256 i = 0; i < initialWord.length; i++) { 60 | Coord memory coord = Coord({ x: int32(uint32(i)) - xOffset, y: 0 }); 61 | LibTile.setTile({ coord: coord, letter: initialWord[i], player: address(0) }); 62 | } 63 | } 64 | 65 | function allocateInitialLetters(uint32[26] memory initialLetterAllocation, address initialLettersTo) private { 66 | for (uint256 i = 0; i < 26; i++) { 67 | Letter letter = Letter(i + 1); 68 | uint32 count = initialLetterAllocation[i]; 69 | PlayerLetters.set({ player: initialLettersTo, letter: letter, value: count }); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /script/PostDeploy.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | import { Letter } from "codegen/common.sol"; 5 | 6 | import { WorldResourceIdLib } from "@latticexyz/world/src/WorldResourceId.sol"; 7 | import { ROOT_NAMESPACE } from "@latticexyz/world/src/constants.sol"; 8 | import { FeeConfigData, PriceConfigData } from "codegen/index.sol"; 9 | import { IWorld } from "codegen/world/IWorld.sol"; 10 | 11 | import { Script } from "forge-std/Script.sol"; 12 | import { console } from "forge-std/console.sol"; 13 | 14 | contract PostDeploy is Script { 15 | function run(address worldAddress) external { 16 | IWorld world = IWorld(worldAddress); 17 | 18 | // START GAME 19 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 20 | vm.startBroadcast(deployerPrivateKey); 21 | 22 | Letter[] memory words = new Letter[](5); 23 | words[0] = Letter.W; 24 | words[1] = Letter.O; 25 | words[2] = Letter.R; 26 | words[3] = Letter.D; 27 | words[4] = Letter.S; 28 | 29 | uint32[26] memory initialLetterAllocation; 30 | for (uint8 i = 0; i < 26; i++) { 31 | initialLetterAllocation[i] = 350; 32 | } 33 | 34 | world.start({ 35 | initialWord: words, 36 | initialLetterAllocation: initialLetterAllocation, 37 | initialLettersTo: address(0xA9656f80CF8fba7618455e1c904EA30aA6C70F94), 38 | merkleRoot: 0xacd24e8edae5cf4cdbc3ce0c196a670cbea1dbf37576112b0a3defac3318b432, 39 | initialPrice: 0.0015 ether, 40 | claimRestrictionDurationBlocks: 0, 41 | priceConfig: PriceConfigData({ 42 | minPrice: 0.001 ether, 43 | wadPriceIncreaseFactor: 1.115e18, 44 | wadPower: 0.95e18, 45 | wadScale: 1.1715e37 46 | }), 47 | feeConfig: FeeConfigData({ feeBps: 690, feeTaker: address(0x7078272d7AbB477aed006190678054bB654815f4) }), 48 | crossWordRewardFraction: 3, 49 | bonusDistance: 4, 50 | numDrawLetters: 7 51 | }); 52 | 53 | /// SET ODDS 54 | 55 | uint8[] memory odds = new uint8[](27); 56 | odds[0] = 0; 57 | odds[uint8(Letter.A)] = 9; 58 | odds[uint8(Letter.B)] = 2; 59 | odds[uint8(Letter.C)] = 2; 60 | odds[uint8(Letter.D)] = 4; 61 | odds[uint8(Letter.E)] = 12; 62 | odds[uint8(Letter.F)] = 2; 63 | odds[uint8(Letter.G)] = 3; 64 | odds[uint8(Letter.H)] = 2; 65 | odds[uint8(Letter.I)] = 9; 66 | odds[uint8(Letter.J)] = 1; 67 | odds[uint8(Letter.K)] = 1; 68 | odds[uint8(Letter.L)] = 4; 69 | odds[uint8(Letter.M)] = 2; 70 | odds[uint8(Letter.N)] = 6; 71 | odds[uint8(Letter.O)] = 8; 72 | odds[uint8(Letter.P)] = 2; 73 | odds[uint8(Letter.Q)] = 1; 74 | odds[uint8(Letter.R)] = 6; 75 | odds[uint8(Letter.S)] = 4; 76 | odds[uint8(Letter.T)] = 6; 77 | odds[uint8(Letter.U)] = 4; 78 | odds[uint8(Letter.V)] = 2; 79 | odds[uint8(Letter.W)] = 2; 80 | odds[uint8(Letter.X)] = 1; 81 | odds[uint8(Letter.Y)] = 2; 82 | odds[uint8(Letter.Z)] = 1; 83 | 84 | world.setDrawLetterOdds(odds); 85 | 86 | // SET ROOT OWNER 87 | 88 | address latticeOwner = address(0xa726A58346e27616b56C6EF9Fccd1241E5EbFbb1); 89 | world.transferOwnership(WorldResourceIdLib.encodeNamespace(ROOT_NAMESPACE), latticeOwner); 90 | 91 | vm.stopBroadcast(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/libraries/LibBoard.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | import { Direction, Letter } from "codegen/common.sol"; 5 | import { Bound } from "common/Bound.sol"; 6 | import { MAX_WORD_LENGTH } from "common/Constants.sol"; 7 | import { Coord } from "common/Coord.sol"; 8 | import { BoundTooLong, EmptyLetterInBounds } from "common/Errors.sol"; 9 | import { LibTile } from "libraries/LibTile.sol"; 10 | 11 | library LibBoard { 12 | function getRelativeCoord( 13 | Coord memory startCoord, 14 | int32 distance, 15 | Direction direction 16 | ) 17 | internal 18 | pure 19 | returns (Coord memory) 20 | { 21 | if (direction == Direction.LEFT_TO_RIGHT) { 22 | return Coord({ x: startCoord.x + distance, y: startCoord.y }); 23 | } else { 24 | return Coord({ x: startCoord.x, y: startCoord.y + distance }); 25 | } 26 | } 27 | 28 | /// @notice Returns coordinates immediately outside of a bound in the perpendicular direction 29 | function getCoordsOutsideBound( 30 | Coord memory letterCoord, 31 | Direction wordDirection, 32 | Bound memory bound 33 | ) 34 | internal 35 | pure 36 | returns (Coord memory, Coord memory) 37 | { 38 | if (bound.positive > MAX_WORD_LENGTH || bound.negative > MAX_WORD_LENGTH) { 39 | revert BoundTooLong(); 40 | } 41 | 42 | Coord memory start = Coord({ x: letterCoord.x, y: letterCoord.y }); 43 | Coord memory end = Coord({ x: letterCoord.x, y: letterCoord.y }); 44 | 45 | int32 positiveDistance = int32(uint32(bound.positive)) + 1; 46 | int32 negativeDistance = int32(uint32(bound.negative)) + 1; 47 | 48 | if (wordDirection == Direction.LEFT_TO_RIGHT) { 49 | start.y -= negativeDistance; 50 | end.y += positiveDistance; 51 | } else { 52 | start.x -= negativeDistance; 53 | end.x += positiveDistance; 54 | } 55 | 56 | return (start, end); 57 | } 58 | 59 | /// @notice Gets the cross word inside a given boundary 60 | function getCrossWord( 61 | Coord memory letterCoord, 62 | Letter letter, 63 | Direction wordDirection, 64 | Bound memory bound 65 | ) 66 | internal 67 | view 68 | returns (Letter[] memory) 69 | { 70 | uint16 wordLength = bound.positive + bound.negative + 1; 71 | Letter[] memory word = new Letter[](wordLength); 72 | 73 | Direction crossDirection = 74 | wordDirection == Direction.TOP_TO_BOTTOM ? Direction.LEFT_TO_RIGHT : Direction.TOP_TO_BOTTOM; 75 | 76 | Coord memory startCoord = LibBoard.getRelativeCoord({ 77 | startCoord: letterCoord, 78 | distance: -1 * int32(uint32(bound.negative)), 79 | direction: crossDirection 80 | }); 81 | 82 | for (uint16 i = 0; i < wordLength; i++) { 83 | Coord memory coord = LibBoard.getRelativeCoord({ 84 | startCoord: startCoord, 85 | distance: int32(uint32(i)), 86 | direction: crossDirection 87 | }); 88 | word[i] = LibTile.getLetter({ coord: coord }); 89 | 90 | if (i == bound.negative) { 91 | word[i] = letter; 92 | } 93 | 94 | if (word[i] == Letter.EMPTY) { 95 | revert EmptyLetterInBounds(); 96 | } 97 | } 98 | 99 | return word; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/test/unit/LibBonus.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | /* solhint-disable no-global-import */ 4 | /* solhint-disable func-name-mixedcase */ 5 | 6 | import { Words3Test } from "../Words3Test.t.sol"; 7 | import { BonusType } from "codegen/common.sol"; 8 | import { Bonus } from "common/Bonus.sol"; 9 | import { Coord } from "common/Coord.sol"; 10 | import "forge-std/Test.sol"; 11 | import { LibBonus } from "libraries/LibBonus.sol"; 12 | 13 | contract LibBonusTest is Words3Test { 14 | function test_IsBonusTile() public { 15 | int32 bonusDistance = int32(uint32(8)); 16 | // Bonus tiles 17 | Coord memory coord = Coord({ x: 0, y: 0 }); 18 | Coord memory coord2 = Coord({ x: 0, y: bonusDistance }); 19 | Coord memory coord3 = Coord({ x: 0, y: bonusDistance * 2 }); 20 | Coord memory coord4 = Coord({ x: -bonusDistance - 12, y: 12 }); 21 | Coord memory coord5 = Coord({ x: bonusDistance * 3 - 1, y: 1 }); 22 | Coord memory coord6 = Coord({ x: bonusDistance * 3 - 1, y: -1 }); 23 | Coord memory coord7 = Coord({ x: bonusDistance * 9 - 12, y: 12 }); 24 | 25 | // Not bonus tiles 26 | Coord memory coord8 = Coord({ x: 0, y: bonusDistance * 3 + 1 }); 27 | Coord memory coord9 = Coord({ x: 0, y: bonusDistance * 4 + 1 }); 28 | 29 | assertTrue(LibBonus.isBonusTile(coord, 8)); 30 | assertTrue(LibBonus.isBonusTile(coord2, 8)); 31 | assertTrue(LibBonus.isBonusTile(coord3, 8)); 32 | assertTrue(LibBonus.isBonusTile(coord4, 8)); 33 | assertTrue(LibBonus.isBonusTile(coord5, 8)); 34 | assertTrue(LibBonus.isBonusTile(coord6, 8)); 35 | assertTrue(LibBonus.isBonusTile(coord7, 8)); 36 | 37 | assertFalse(LibBonus.isBonusTile(coord8, 8)); 38 | assertFalse(LibBonus.isBonusTile(coord9, 8)); 39 | } 40 | 41 | function testFuzz_IsBonusTile(int32 x, int32 y) public { 42 | uint16 bonusDistance = 3; 43 | vm.assume(x >= -1e9 && x <= 1e9); 44 | vm.assume(y >= -1e9 && y <= 1e9); 45 | Coord memory coord = Coord({ x: x, y: y }); 46 | bool isBonus = LibBonus.isBonusTile(coord, bonusDistance); 47 | if (isBonus) { 48 | int32 absX = x < 0 ? -x : x; 49 | int32 absY = y < 0 ? -y : y; 50 | assertTrue(absX >= 0); 51 | assertTrue(absY >= 0); 52 | assertEq((absX + absY) % int32(uint32(bonusDistance)), 0); 53 | } 54 | } 55 | 56 | function testFuzz_IsBonusTileCross(int32 bonusDistanceMultiple, int32 diff) public { 57 | int32 bonusDistance = int32(uint32(6)); 58 | 59 | vm.assume(bonusDistance > 1); 60 | bonusDistanceMultiple = int32(uint32(bound(uint256(uint32(bonusDistanceMultiple)), 0, 1e3))); 61 | diff = int32(uint32(bound(uint256(uint32(diff)), 0, 1e8))); 62 | vm.assume(bonusDistance * bonusDistanceMultiple > diff); 63 | 64 | // Bonus Tiles 65 | Coord memory coord = Coord({ x: diff, y: bonusDistance * bonusDistanceMultiple - diff }); 66 | Coord memory coord2 = Coord({ x: -diff, y: bonusDistance * bonusDistanceMultiple - diff }); 67 | Coord memory coord3 = Coord({ x: bonusDistance * bonusDistanceMultiple, y: 0 }); 68 | 69 | // Not bonus tiles 70 | Coord memory coord4 = Coord({ x: bonusDistance * bonusDistanceMultiple - diff, y: diff + 1 }); 71 | Coord memory coord5 = Coord({ x: bonusDistance * bonusDistanceMultiple - diff, y: -diff - 1 }); 72 | 73 | assertTrue(LibBonus.isBonusTile(coord, 6)); 74 | assertTrue(LibBonus.isBonusTile(coord2, 6)); 75 | assertTrue(LibBonus.isBonusTile(coord3, 6)); 76 | 77 | assertFalse(LibBonus.isBonusTile(coord4, 6)); 78 | assertFalse(LibBonus.isBonusTile(coord5, 6)); 79 | } 80 | 81 | function testFuzz_GetTileBonusWithinBounds(int32 x, int32 y, uint16 bonusDistance) public { 82 | vm.assume(x >= -1e9 && x <= 1e9); 83 | vm.assume(y >= -1e9 && y <= 1e9); 84 | bonusDistance = uint16(bound(bonusDistance, 1, 1000)); 85 | vm.assume(LibBonus.isBonusTile(Coord({ x: x, y: y }), bonusDistance)); 86 | Bonus memory bonus = LibBonus.getTileBonus(Coord({ x: x, y: y })); 87 | assertTrue(bonus.bonusValue >= 2 && bonus.bonusValue <= 5); 88 | assertTrue(bonus.bonusType == BonusType.MULTIPLY_LETTER || bonus.bonusType == BonusType.MULTIPLY_WORD); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /mud.config.ts: -------------------------------------------------------------------------------- 1 | import { defineWorld } from "@latticexyz/world"; 2 | 3 | export default defineWorld({ 4 | deploy: { 5 | upgradeableWorldImplementation: true, 6 | }, 7 | tables: { 8 | // Config 9 | GameConfig: { 10 | key: [], 11 | schema: { 12 | status: "Status", 13 | crossWordRewardFraction: "uint32", 14 | bonusDistance: "uint16", 15 | numDrawLetters: "uint8", 16 | }, 17 | }, 18 | FeeConfig: { 19 | key: [], 20 | schema: { 21 | feeTaker: "address", 22 | feeBps: "uint16", 23 | }, 24 | }, 25 | ClaimRestrictionConfig: { 26 | key: [], 27 | schema: { 28 | claimRestrictionBlock: "uint256" 29 | } 30 | }, 31 | MerkleRootConfig: { 32 | key: [], 33 | schema: { 34 | value: "bytes32", 35 | }, 36 | }, 37 | PriceConfig: { 38 | key: [], 39 | schema: { 40 | minPrice: "uint256", 41 | wadPriceIncreaseFactor: "int256", 42 | wadPower: "int256", 43 | wadScale: "int256", 44 | }, 45 | }, 46 | DrawLetterOdds: { 47 | key: [], 48 | schema: { 49 | value: "uint8[]" // Letters index the array (A is index 1, B is index 2, etc.) 50 | }, 51 | }, 52 | 53 | // Board 54 | TileLetter: { 55 | key: ["x", "y"], 56 | schema: { x: "int32", y: "int32", value: "Letter" }, 57 | }, 58 | TilePlayer: { 59 | key: ["x", "y"], 60 | schema: { x: "int32", y: "int32", value: "address" } 61 | }, 62 | 63 | // Letters 64 | PlayerLetters: { 65 | key: ["player", "letter"], 66 | schema: { 67 | player: "address", 68 | letter: "Letter", 69 | value: "uint32", 70 | }, 71 | }, 72 | DrawLastSold: { 73 | key: [], 74 | schema: { 75 | price: "uint256", 76 | blockNumber: "uint256", 77 | } 78 | }, 79 | DrawCount: { 80 | key: [], 81 | schema: { 82 | value: "uint32", 83 | }, 84 | }, 85 | 86 | // Points & Treasury 87 | Points: { 88 | key: ["player"], 89 | schema: { 90 | player: "address", 91 | value: "uint32", 92 | }, 93 | }, 94 | Treasury: { 95 | key: [], 96 | schema: { 97 | value: "uint256", 98 | }, 99 | }, 100 | Spent: { 101 | key: ["player"], 102 | schema: { 103 | player: "address", 104 | value: "uint256", 105 | }, 106 | }, 107 | 108 | // Activity (offchain tables used to emit events useful for indexing) 109 | UpdateId: { 110 | key: [], 111 | schema: { 112 | value: "uint256", 113 | }, 114 | }, 115 | PlayUpdate: { 116 | key: ["id"], 117 | schema: { 118 | id: "uint256", 119 | player: "address", 120 | direction: "Direction", 121 | timestamp: "uint256", 122 | x: "int32", 123 | y: "int32", 124 | word: "uint8[]", 125 | filledWord: "uint8[]", 126 | }, 127 | type: "offchainTable" 128 | }, 129 | PointsUpdate: { 130 | key: ["id", "pointsId"], 131 | schema: { 132 | id: "uint256", 133 | player: "address", 134 | pointsId: "int16", 135 | points: "uint32", 136 | }, 137 | type: "offchainTable" 138 | }, 139 | PointsClaimedUpdate: { 140 | key: ["id"], 141 | schema: { 142 | id: "uint256", 143 | player: "address", 144 | points: "uint32", 145 | value: "uint256", 146 | timestamp: "uint256", 147 | }, 148 | type: "offchainTable", 149 | }, 150 | DrawUpdate: { 151 | key: ["id"], 152 | schema: { 153 | id: "uint256", 154 | player: "address", 155 | value: "uint256", 156 | timestamp: "uint256", 157 | }, 158 | type: "offchainTable", 159 | } 160 | }, 161 | enums: { 162 | BonusType: ["MULTIPLY_WORD", "MULTIPLY_LETTER"], 163 | Direction: ["LEFT_TO_RIGHT", "TOP_TO_BOTTOM"], 164 | Status: ["NOT_STARTED", "STARTED"], 165 | Letter: [ 166 | "EMPTY", 167 | "A", 168 | "B", 169 | "C", 170 | "D", 171 | "E", 172 | "F", 173 | "G", 174 | "H", 175 | "I", 176 | "J", 177 | "K", 178 | "L", 179 | "M", 180 | "N", 181 | "O", 182 | "P", 183 | "Q", 184 | "R", 185 | "S", 186 | "T", 187 | "U", 188 | "V", 189 | "W", 190 | "X", 191 | "Y", 192 | "Z", 193 | ], 194 | }, 195 | }); 196 | -------------------------------------------------------------------------------- /src/test/integration/SimpleWord.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | /* solhint-disable no-global-import */ 4 | /* solhint-disable func-name-mixedcase */ 5 | 6 | import { Words3Test } from "../Words3Test.t.sol"; 7 | import { Merkle } from "../murky/src/Merkle.sol"; 8 | import { Direction, Letter } from "codegen/common.sol"; 9 | import { FeeConfigData, PriceConfigData, TileLetter } from "codegen/index.sol"; 10 | import { IWorld } from "codegen/world/IWorld.sol"; 11 | import { Bound } from "common/Bound.sol"; 12 | import { Coord } from "common/Coord.sol"; 13 | import "forge-std/Test.sol"; 14 | 15 | contract SimpleWord is Words3Test { 16 | bytes32[] private words; 17 | Merkle private m; 18 | Letter[] private initialWord; 19 | 20 | function setUp() public override { 21 | super.setUp(); 22 | world = IWorld(worldAddress); 23 | 24 | m = new Merkle(); 25 | Letter[] memory hi = new Letter[](2); 26 | hi[0] = Letter.H; 27 | hi[1] = Letter.I; 28 | Letter[] memory go = new Letter[](2); 29 | go[0] = Letter.G; 30 | go[1] = Letter.O; 31 | words.push(keccak256(bytes.concat(keccak256(abi.encode(hi))))); // hi 32 | words.push(keccak256(bytes.concat(keccak256(abi.encode(go))))); // go 33 | 34 | setDefaultLetterOdds(); 35 | } 36 | 37 | function test_Setup() public { 38 | initialWord = new Letter[](2); 39 | initialWord[0] = Letter.H; 40 | initialWord[1] = Letter.I; 41 | 42 | uint32[26] memory initialLetterAllocation; 43 | world.start({ 44 | initialWord: initialWord, 45 | initialLetterAllocation: initialLetterAllocation, 46 | initialLettersTo: address(0), 47 | merkleRoot: m.getRoot(words), 48 | initialPrice: 0.001 ether, 49 | claimRestrictionDurationBlocks: 0, 50 | priceConfig: PriceConfigData({ 51 | minPrice: 0.001 ether, 52 | wadPriceIncreaseFactor: 1.115e18, 53 | wadPower: 0.9e18, 54 | wadScale: 9.96e36 55 | }), 56 | feeConfig: FeeConfigData({ feeBps: 0, feeTaker: address(0) }), 57 | crossWordRewardFraction: 3, 58 | bonusDistance: 10, 59 | numDrawLetters: 7 60 | }); 61 | assertEq(uint8(TileLetter.get(-1, 0)), uint8(Letter.H)); 62 | assertEq(uint8(TileLetter.get(0, 0)), uint8(Letter.I)); 63 | } 64 | 65 | function test_PlayHi() public { 66 | initialWord = new Letter[](2); 67 | initialWord[0] = Letter.H; 68 | initialWord[1] = Letter.I; 69 | 70 | uint32[26] memory initialLetterAllocation; 71 | world.start({ 72 | initialWord: initialWord, 73 | initialLetterAllocation: initialLetterAllocation, 74 | initialLettersTo: address(0), 75 | merkleRoot: m.getRoot(words), 76 | initialPrice: 0.001 ether, 77 | claimRestrictionDurationBlocks: 0, 78 | priceConfig: PriceConfigData({ 79 | minPrice: 0.001 ether, 80 | wadPriceIncreaseFactor: 1.115e18, 81 | wadPower: 0.9e18, 82 | wadScale: 9.96e36 83 | }), 84 | feeConfig: FeeConfigData({ feeBps: 0, feeTaker: address(0) }), 85 | crossWordRewardFraction: 3, 86 | bonusDistance: 3, 87 | numDrawLetters: 8 88 | }); 89 | 90 | Letter[] memory word = new Letter[](2); 91 | word[0] = Letter.EMPTY; 92 | word[1] = Letter.I; 93 | Bound[] memory bounds = new Bound[](2); 94 | bytes32[] memory proof = m.getProof(words, 0); 95 | 96 | address player = address(0x123); 97 | vm.startPrank(player); 98 | for (uint256 i = 0; i < 50; i++) { 99 | uint256 price = world.getDrawPrice(); 100 | vm.deal(player, price); 101 | vm.roll(block.number + 100); 102 | world.draw{ value: price }(player); 103 | } 104 | world.play(word, proof, Coord({ x: -1, y: 0 }), Direction.TOP_TO_BOTTOM, bounds); 105 | vm.stopPrank(); 106 | } 107 | 108 | function test_MultipleDraws() public { 109 | uint32[26] memory initialLetterAllocation; 110 | world.start({ 111 | initialWord: initialWord, 112 | initialLetterAllocation: initialLetterAllocation, 113 | initialLettersTo: address(0), 114 | merkleRoot: m.getRoot(words), 115 | initialPrice: 0.001 ether, 116 | claimRestrictionDurationBlocks: 0, 117 | priceConfig: PriceConfigData({ 118 | minPrice: 0.001 ether, 119 | wadPriceIncreaseFactor: 1.115e18, 120 | wadPower: 0.95e18, 121 | wadScale: 1.1715e37 122 | }), 123 | feeConfig: FeeConfigData({ feeBps: 0, feeTaker: address(0) }), 124 | crossWordRewardFraction: 3, 125 | bonusDistance: 10, 126 | numDrawLetters: 7 127 | }); 128 | 129 | address player = address(0x123); 130 | vm.deal(player, 50 ether); 131 | vm.startPrank(player); 132 | 133 | world.draw{ value: world.getDrawPrice() }(player); 134 | world.draw{ value: world.getDrawPrice() }(player); 135 | vm.roll(block.number + 5); 136 | world.draw{ value: world.getDrawPrice() }(player); 137 | vm.roll(block.number + 10); 138 | world.draw{ value: world.getDrawPrice() }(player); 139 | world.draw{ value: world.getDrawPrice() }(player); 140 | world.draw{ value: world.getDrawPrice() }(player); 141 | vm.roll(block.number + 10); 142 | world.draw{ value: world.getDrawPrice() }(player); 143 | world.draw{ value: world.getDrawPrice() }(player); 144 | world.draw{ value: world.getDrawPrice() }(player); 145 | world.draw{ value: world.getDrawPrice() }(player); 146 | world.draw{ value: world.getDrawPrice() }(player); 147 | vm.roll(block.number + 3); 148 | world.draw{ value: world.getDrawPrice() }(player); 149 | world.draw{ value: world.getDrawPrice() }(player); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/test/integration/Start.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | /* solhint-disable no-global-import */ 4 | /* solhint-disable func-name-mixedcase */ 5 | 6 | import { Words3Test } from "../Words3Test.t.sol"; 7 | import { Letter } from "codegen/common.sol"; 8 | 9 | import { 10 | DrawLastSold, FeeConfigData, GameConfig, MerkleRootConfig, PriceConfig, PriceConfigData 11 | } from "codegen/index.sol"; 12 | import { Coord } from "common/Coord.sol"; 13 | import "forge-std/Test.sol"; 14 | 15 | import { LibGame } from "libraries/LibGame.sol"; 16 | import { LibTile } from "libraries/LibTile.sol"; 17 | import { StartSystem } from "systems/StartSystem.sol"; 18 | 19 | contract StartTest is Words3Test { 20 | function testFuzz_SystemStartGame( 21 | bytes32 merkleRoot, 22 | uint256 initialPrice, 23 | int256 wadScale, 24 | int256 wadPower, 25 | uint32 crossWordRewardFraction, 26 | uint16 bonusDistance, 27 | uint8 numDrawLetters 28 | ) 29 | public 30 | { 31 | uint32[26] memory initialLetterAllocation; 32 | world.start({ 33 | initialWord: new Letter[](0), 34 | initialLetterAllocation: initialLetterAllocation, 35 | initialLettersTo: address(0), 36 | merkleRoot: merkleRoot, 37 | initialPrice: initialPrice, 38 | claimRestrictionDurationBlocks: 0, 39 | priceConfig: PriceConfigData({ 40 | minPrice: 3, 41 | wadPriceIncreaseFactor: 1.1e18, 42 | wadPower: wadPower, 43 | wadScale: wadScale 44 | }), 45 | feeConfig: FeeConfigData({ feeBps: 0, feeTaker: address(0) }), 46 | crossWordRewardFraction: crossWordRewardFraction, 47 | bonusDistance: bonusDistance, 48 | numDrawLetters: numDrawLetters 49 | }); 50 | assertEq(merkleRoot, MerkleRootConfig.get()); 51 | assertEq(3, PriceConfig.getMinPrice()); 52 | assertEq(wadScale, PriceConfig.getWadScale()); 53 | assertEq(wadPower, PriceConfig.getWadPower()); 54 | assertEq(initialPrice, DrawLastSold.getPrice()); 55 | assertEq(block.number, DrawLastSold.getBlockNumber()); 56 | assertEq(crossWordRewardFraction, GameConfig.getCrossWordRewardFraction()); 57 | assertEq(bonusDistance, GameConfig.getBonusDistance()); 58 | assertEq(numDrawLetters, GameConfig.getNumDrawLetters()); 59 | assertTrue(LibGame.canPlay()); 60 | } 61 | 62 | function testFuzz_InitialWordOffset(uint256 offset) public { 63 | offset = bound(offset, 1, 25); 64 | Letter[] memory initialWord = new Letter[](offset * 2); 65 | for (uint256 i = 0; i < initialWord.length; i++) { 66 | initialWord[i] = Letter.A; 67 | } 68 | uint32[26] memory initialLetterAllocation; 69 | world.start({ 70 | initialWord: initialWord, 71 | initialLetterAllocation: initialLetterAllocation, 72 | initialLettersTo: address(0), 73 | merkleRoot: keccak256("merkleRoot"), 74 | initialPrice: 1, 75 | claimRestrictionDurationBlocks: 0, 76 | priceConfig: PriceConfigData({ 77 | minPrice: 0.001 ether, 78 | wadPriceIncreaseFactor: 1.115e18, 79 | wadPower: 0.9e18, 80 | wadScale: 9.96e36 81 | }), 82 | feeConfig: FeeConfigData({ feeBps: 0, feeTaker: address(0) }), 83 | crossWordRewardFraction: 1, 84 | bonusDistance: 1, 85 | numDrawLetters: 1 86 | }); 87 | for (uint256 i = 0; i < initialWord.length; i++) { 88 | assertEq( 89 | uint8(LibTile.getLetter({ coord: Coord({ x: int32(uint32(i)) - int32(uint32(offset)), y: 0 }) })), 90 | uint8(Letter.A) 91 | ); 92 | } 93 | } 94 | 95 | function test_RevertsWhen_StartTwice() public { 96 | uint32[26] memory initialLetterAllocation; 97 | 98 | world.start({ 99 | initialWord: new Letter[](0), 100 | initialLetterAllocation: initialLetterAllocation, 101 | initialLettersTo: address(0), 102 | merkleRoot: keccak256("merkleRoot"), 103 | initialPrice: 1, 104 | claimRestrictionDurationBlocks: 0, 105 | priceConfig: PriceConfigData({ 106 | minPrice: 0.001 ether, 107 | wadPriceIncreaseFactor: 1.115e18, 108 | wadPower: 0.9e18, 109 | wadScale: 9.96e36 110 | }), 111 | feeConfig: FeeConfigData({ feeBps: 0, feeTaker: address(0) }), 112 | crossWordRewardFraction: 1, 113 | bonusDistance: 1, 114 | numDrawLetters: 1 115 | }); 116 | vm.expectRevert(StartSystem.GameAlreadyStarted.selector); 117 | world.start({ 118 | initialWord: new Letter[](0), 119 | initialLetterAllocation: initialLetterAllocation, 120 | initialLettersTo: address(0), 121 | merkleRoot: keccak256("merkleRoot"), 122 | initialPrice: 1, 123 | claimRestrictionDurationBlocks: 0, 124 | priceConfig: PriceConfigData({ 125 | minPrice: 0.001 ether, 126 | wadPriceIncreaseFactor: 1.115e18, 127 | wadPower: 0.9e18, 128 | wadScale: 9.96e36 129 | }), 130 | feeConfig: FeeConfigData({ feeBps: 0, feeTaker: address(0) }), 131 | crossWordRewardFraction: 1, 132 | bonusDistance: 1, 133 | numDrawLetters: 1 134 | }); 135 | } 136 | 137 | function test_RevertsWhen_InitialWordTooLong() public { 138 | uint32[26] memory initialLetterAllocation; 139 | 140 | vm.expectRevert(StartSystem.InitialWordTooLong.selector); 141 | world.start({ 142 | initialWord: new Letter[](90), 143 | initialLetterAllocation: initialLetterAllocation, 144 | initialLettersTo: address(0), 145 | merkleRoot: keccak256("merkleRoot"), 146 | initialPrice: 1, 147 | claimRestrictionDurationBlocks: 0, 148 | priceConfig: PriceConfigData({ 149 | minPrice: 0.001 ether, 150 | wadPriceIncreaseFactor: 1.115e18, 151 | wadPower: 0.9e18, 152 | wadScale: 9.96e36 153 | }), 154 | feeConfig: FeeConfigData({ feeBps: 0, feeTaker: address(0) }), 155 | crossWordRewardFraction: 1, 156 | bonusDistance: 1, 157 | numDrawLetters: 1 158 | }); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/test/unit/LibPrice.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | /* solhint-disable no-global-import */ 4 | /* solhint-disable func-name-mixedcase */ 5 | 6 | import { Words3Test } from "../Words3Test.t.sol"; 7 | 8 | import { Wrapper } from "./Wrapper.sol"; 9 | import "forge-std/Test.sol"; 10 | import { LibPrice } from "libraries/LibPrice.sol"; 11 | import { toWadUnsafe, wadPow } from "solmate/src/utils/SignedWadMath.sol"; 12 | import { wadMul, wadPow } from "solmate/src/utils/SignedWadMath.sol"; 13 | 14 | contract LibPriceTest is Words3Test { 15 | function test_WadRoot() public { 16 | assertApproxEqRel(uint256(LibPrice.wadRoot(1e18, 2e18)), 1e18, 1); 17 | assertApproxEqRel(uint256(LibPrice.wadRoot(1e18, 3e18)), 1e18, 1); 18 | assertApproxEqRel(uint256(LibPrice.wadRoot(1e18, 4e18)), 1e18, 1); 19 | assertApproxEqRel(uint256(LibPrice.wadRoot(4e18, 2e18)), 2e18, 1); 20 | assertApproxEqRel(uint256(LibPrice.wadRoot(9e18, 2e18)), 3e18, 1); 21 | assertApproxEqRel(uint256(LibPrice.wadRoot(16e18, 4e18)), 2e18, 1); 22 | assertApproxEqRel(uint256(LibPrice.wadRoot(16e18, 4e18)), 2e18, 1); 23 | assertApproxEqRel(uint256(LibPrice.wadRoot(12e18, 3e18)), 2.28942848511e18, 0.00001e18); 24 | } 25 | 26 | function testFuzz_WadRootWhole(uint128 a, uint128 power) public { 27 | a = uint128(bound(a, 1, 50)); 28 | power = uint128(bound(power, 1, 20)); 29 | uint128 b = a ** power; 30 | int256 c = LibPrice.wadRoot(toWadUnsafe(b), toWadUnsafe(power)); 31 | assertApproxEqRel(uint256(c), uint256(toWadUnsafe(uint256(a))), 1e3); 32 | } 33 | 34 | function testFuzz_WadRootFraction(uint128 a, uint32 power) public { 35 | power = uint32(bound(power, 10, 90)); 36 | a = uint128(bound(a, 1, 50)); 37 | int256 wadPower = int256(uint256(power)) * 1e17; 38 | int256 b = wadPow(toWadUnsafe(a), wadPower); 39 | int256 c = LibPrice.wadRoot(b, wadPower); 40 | assertApproxEqRel(uint256(c), uint256(toWadUnsafe(uint256(a))), 1e3); 41 | } 42 | 43 | function test_WadRootFraction() public { 44 | assertApproxEqRel(uint256(LibPrice.wadRoot(4e18, 1.5e18)), 2.51984209979e18, 1e8); 45 | assertApproxEqRel(uint256(LibPrice.wadRoot(12e18, 3.6e18)), 1.99421770808e18, 1e8); 46 | } 47 | 48 | function test_ToWad() public { 49 | assertEq(LibPrice.toWad(1), 1e18); 50 | assertEq(LibPrice.toWad(15), 15e18); 51 | assertEq(LibPrice.toWad(1.5e18), 1.5e36); 52 | assertEq(LibPrice.toWad(1e9 ether), 1e45); 53 | // Reverts on overflow 54 | Wrapper w = new Wrapper(); 55 | uint256 maxUint = 2 ** 256 - 1; 56 | vm.expectRevert(); 57 | w.priceToWad(maxUint / 1e18); 58 | vm.expectRevert(); 59 | w.priceToWad(maxUint); 60 | } 61 | 62 | function testFuzz_ToWad(uint256 x) public { 63 | x = bound(x, 1, uint256(type(int256).max / 1e18)); 64 | int256 wad = LibPrice.toWad(x); 65 | assertEq(uint256(wad), x * 1e18); 66 | } 67 | 68 | function test_GetF() public { 69 | assertEq( 70 | LibPrice.getF({ x: 1.3e18, wadPower: 0.8e18, wadScale: 550e18, wadConstant: 7e18 }), 71 | 452_869_748_949_216_062_373 72 | ); 73 | } 74 | 75 | function test_GetInverseF() public { 76 | assertApproxEqRel( 77 | LibPrice.getInverseF({ 78 | y: 452_869_748_949_216_062_373, 79 | wadPower: 0.8e18, 80 | wadScale: 550e18, 81 | wadConstant: 7e18 82 | }), 83 | 1.3e18, 84 | 5 85 | ); 86 | } 87 | 88 | function testFuzz_GetFMatchesInverse(int256 x, int256 wadPower, int256 wadScale, int256 wadConstant) public { 89 | x = int256(bound(uint256(x), 1e18, 300e18 * 1e18)); 90 | wadPower = int256(bound(uint256(wadPower), 0.1e18, 1e18)); 91 | wadScale = int256(bound(uint256(wadScale), 1e18, 1e45)); 92 | wadConstant = int256(bound(uint256(wadConstant), 0.000000001 ether * 1e18, 1000 ether * 1e18)); 93 | 94 | int256 f = LibPrice.getF({ x: x, wadPower: wadPower, wadScale: wadScale, wadConstant: wadConstant }); 95 | vm.assume(f > (wadConstant + 5e13)); 96 | int256 y = LibPrice.getInverseF({ y: f, wadPower: wadPower, wadScale: wadScale, wadConstant: wadConstant }); 97 | if (y > 100 && x > 100) { 98 | assertApproxEqRel(uint256(y), uint256(x), 1e17); 99 | } else { 100 | assertTrue((y > x ? y - x : x - y) < 10); 101 | } 102 | } 103 | 104 | function testFuzz_GetFMatchesInverseTightlyInReasonableBounds(uint256 x) public { 105 | x = bound(x, 0.01e18, 80e24); 106 | int256 f = LibPrice.getF({ x: int256(x), wadPower: 0.9e18, wadScale: 9.96e36, wadConstant: 0.001 ether * 1e18 }); 107 | int256 y = LibPrice.getInverseF({ y: f, wadPower: 0.9e18, wadScale: 9.96e36, wadConstant: 0.001 ether * 1e18 }); 108 | assertApproxEqRel(uint256(y), x, 1000); 109 | 110 | int256 f2 = 111 | LibPrice.getF({ x: int256(x), wadPower: 0.95e18, wadScale: 1.1715e37, wadConstant: 0.001 ether * 1e18 }); 112 | int256 y2 = 113 | LibPrice.getInverseF({ y: f2, wadPower: 0.95e18, wadScale: 1.1715e37, wadConstant: 0.001 ether * 1e18 }); 114 | assertApproxEqRel(uint256(y2), x, 1000); 115 | 116 | int256 f3 = 117 | LibPrice.getF({ x: int256(x), wadPower: 0.23e18, wadScale: 3.4e36, wadConstant: 0.001 ether * 1e18 }); 118 | int256 y3 = 119 | LibPrice.getInverseF({ y: f3, wadPower: 0.23e18, wadScale: 3.4e36, wadConstant: 0.001 ether * 1e18 }); 120 | assertApproxEqRel(uint256(y3), x, 1000); 121 | } 122 | 123 | function testFuzz_GetPriceNeverBelowMin( 124 | int256 wadStartPrice, 125 | uint256 minPrice, 126 | int256 wadPower, 127 | int256 wadScale, 128 | int256 wadPassed 129 | ) 130 | public 131 | { 132 | minPrice = bound(minPrice, 0.000001 ether, 1e6 ether); 133 | int256 wadMinPrice = LibPrice.toWad(minPrice); 134 | // Make sure start price is always at least 1% higher than the min 135 | wadStartPrice = int256(bound(uint256(wadStartPrice), uint256(wadMul(wadMinPrice, 1.01e18)), 1e50)); 136 | wadPower = int256(bound(uint256(wadPower), 0.7e18, 0.999e18)); 137 | wadScale = int256(bound(uint256(wadScale), 1e18, 1e39)); 138 | wadPassed = int256(bound(uint256(wadPassed), 1e18, 1e26)); 139 | 140 | uint256 price = LibPrice.getPrice({ 141 | wadStartPrice: wadStartPrice, 142 | wadMinPrice: wadMinPrice, 143 | wadPower: wadPower, 144 | wadScale: wadScale, 145 | wadPassed: wadPassed 146 | }); 147 | assertTrue(price >= minPrice); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/codegen/tables/DrawCount.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | // Import store internals 7 | import { IStore } from "@latticexyz/store/src/IStore.sol"; 8 | import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; 9 | import { StoreCore } from "@latticexyz/store/src/StoreCore.sol"; 10 | import { Bytes } from "@latticexyz/store/src/Bytes.sol"; 11 | import { Memory } from "@latticexyz/store/src/Memory.sol"; 12 | import { SliceLib } from "@latticexyz/store/src/Slice.sol"; 13 | import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol"; 14 | import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol"; 15 | import { Schema } from "@latticexyz/store/src/Schema.sol"; 16 | import { EncodedLengths, EncodedLengthsLib } from "@latticexyz/store/src/EncodedLengths.sol"; 17 | import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; 18 | 19 | library DrawCount { 20 | // Hex below is the result of `WorldResourceIdLib.encode({ namespace: "", name: "DrawCount", typeId: RESOURCE_TABLE });` 21 | ResourceId constant _tableId = ResourceId.wrap(0x7462000000000000000000000000000044726177436f756e7400000000000000); 22 | 23 | FieldLayout constant _fieldLayout = 24 | FieldLayout.wrap(0x0004010004000000000000000000000000000000000000000000000000000000); 25 | 26 | // Hex-encoded key schema of () 27 | Schema constant _keySchema = Schema.wrap(0x0000000000000000000000000000000000000000000000000000000000000000); 28 | // Hex-encoded value schema of (uint32) 29 | Schema constant _valueSchema = Schema.wrap(0x0004010003000000000000000000000000000000000000000000000000000000); 30 | 31 | /** 32 | * @notice Get the table's key field names. 33 | * @return keyNames An array of strings with the names of key fields. 34 | */ 35 | function getKeyNames() internal pure returns (string[] memory keyNames) { 36 | keyNames = new string[](0); 37 | } 38 | 39 | /** 40 | * @notice Get the table's value field names. 41 | * @return fieldNames An array of strings with the names of value fields. 42 | */ 43 | function getFieldNames() internal pure returns (string[] memory fieldNames) { 44 | fieldNames = new string[](1); 45 | fieldNames[0] = "value"; 46 | } 47 | 48 | /** 49 | * @notice Register the table with its config. 50 | */ 51 | function register() internal { 52 | StoreSwitch.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); 53 | } 54 | 55 | /** 56 | * @notice Register the table with its config. 57 | */ 58 | function _register() internal { 59 | StoreCore.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); 60 | } 61 | 62 | /** 63 | * @notice Get value. 64 | */ 65 | function getValue() internal view returns (uint32 value) { 66 | bytes32[] memory _keyTuple = new bytes32[](0); 67 | 68 | bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 69 | return (uint32(bytes4(_blob))); 70 | } 71 | 72 | /** 73 | * @notice Get value. 74 | */ 75 | function _getValue() internal view returns (uint32 value) { 76 | bytes32[] memory _keyTuple = new bytes32[](0); 77 | 78 | bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 79 | return (uint32(bytes4(_blob))); 80 | } 81 | 82 | /** 83 | * @notice Get value. 84 | */ 85 | function get() internal view returns (uint32 value) { 86 | bytes32[] memory _keyTuple = new bytes32[](0); 87 | 88 | bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 89 | return (uint32(bytes4(_blob))); 90 | } 91 | 92 | /** 93 | * @notice Get value. 94 | */ 95 | function _get() internal view returns (uint32 value) { 96 | bytes32[] memory _keyTuple = new bytes32[](0); 97 | 98 | bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 99 | return (uint32(bytes4(_blob))); 100 | } 101 | 102 | /** 103 | * @notice Set value. 104 | */ 105 | function setValue(uint32 value) internal { 106 | bytes32[] memory _keyTuple = new bytes32[](0); 107 | 108 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 109 | } 110 | 111 | /** 112 | * @notice Set value. 113 | */ 114 | function _setValue(uint32 value) internal { 115 | bytes32[] memory _keyTuple = new bytes32[](0); 116 | 117 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 118 | } 119 | 120 | /** 121 | * @notice Set value. 122 | */ 123 | function set(uint32 value) internal { 124 | bytes32[] memory _keyTuple = new bytes32[](0); 125 | 126 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 127 | } 128 | 129 | /** 130 | * @notice Set value. 131 | */ 132 | function _set(uint32 value) internal { 133 | bytes32[] memory _keyTuple = new bytes32[](0); 134 | 135 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 136 | } 137 | 138 | /** 139 | * @notice Delete all data for given keys. 140 | */ 141 | function deleteRecord() internal { 142 | bytes32[] memory _keyTuple = new bytes32[](0); 143 | 144 | StoreSwitch.deleteRecord(_tableId, _keyTuple); 145 | } 146 | 147 | /** 148 | * @notice Delete all data for given keys. 149 | */ 150 | function _deleteRecord() internal { 151 | bytes32[] memory _keyTuple = new bytes32[](0); 152 | 153 | StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); 154 | } 155 | 156 | /** 157 | * @notice Tightly pack static (fixed length) data using this table's schema. 158 | * @return The static data, encoded into a sequence of bytes. 159 | */ 160 | function encodeStatic(uint32 value) internal pure returns (bytes memory) { 161 | return abi.encodePacked(value); 162 | } 163 | 164 | /** 165 | * @notice Encode all of a record's fields. 166 | * @return The static (fixed length) data, encoded into a sequence of bytes. 167 | * @return The lengths of the dynamic fields (packed into a single bytes32 value). 168 | * @return The dynamic (variable length) data, encoded into a sequence of bytes. 169 | */ 170 | function encode(uint32 value) internal pure returns (bytes memory, EncodedLengths, bytes memory) { 171 | bytes memory _staticData = encodeStatic(value); 172 | 173 | EncodedLengths _encodedLengths; 174 | bytes memory _dynamicData; 175 | 176 | return (_staticData, _encodedLengths, _dynamicData); 177 | } 178 | 179 | /** 180 | * @notice Encode keys as a bytes32 array using this table's field layout. 181 | */ 182 | function encodeKeyTuple() internal pure returns (bytes32[] memory) { 183 | bytes32[] memory _keyTuple = new bytes32[](0); 184 | 185 | return _keyTuple; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/codegen/tables/MerkleRootConfig.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | // Import store internals 7 | import { IStore } from "@latticexyz/store/src/IStore.sol"; 8 | import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; 9 | import { StoreCore } from "@latticexyz/store/src/StoreCore.sol"; 10 | import { Bytes } from "@latticexyz/store/src/Bytes.sol"; 11 | import { Memory } from "@latticexyz/store/src/Memory.sol"; 12 | import { SliceLib } from "@latticexyz/store/src/Slice.sol"; 13 | import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol"; 14 | import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol"; 15 | import { Schema } from "@latticexyz/store/src/Schema.sol"; 16 | import { EncodedLengths, EncodedLengthsLib } from "@latticexyz/store/src/EncodedLengths.sol"; 17 | import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; 18 | 19 | library MerkleRootConfig { 20 | // Hex below is the result of `WorldResourceIdLib.encode({ namespace: "", name: "MerkleRootConfig", typeId: RESOURCE_TABLE });` 21 | ResourceId constant _tableId = ResourceId.wrap(0x746200000000000000000000000000004d65726b6c65526f6f74436f6e666967); 22 | 23 | FieldLayout constant _fieldLayout = 24 | FieldLayout.wrap(0x0020010020000000000000000000000000000000000000000000000000000000); 25 | 26 | // Hex-encoded key schema of () 27 | Schema constant _keySchema = Schema.wrap(0x0000000000000000000000000000000000000000000000000000000000000000); 28 | // Hex-encoded value schema of (bytes32) 29 | Schema constant _valueSchema = Schema.wrap(0x002001005f000000000000000000000000000000000000000000000000000000); 30 | 31 | /** 32 | * @notice Get the table's key field names. 33 | * @return keyNames An array of strings with the names of key fields. 34 | */ 35 | function getKeyNames() internal pure returns (string[] memory keyNames) { 36 | keyNames = new string[](0); 37 | } 38 | 39 | /** 40 | * @notice Get the table's value field names. 41 | * @return fieldNames An array of strings with the names of value fields. 42 | */ 43 | function getFieldNames() internal pure returns (string[] memory fieldNames) { 44 | fieldNames = new string[](1); 45 | fieldNames[0] = "value"; 46 | } 47 | 48 | /** 49 | * @notice Register the table with its config. 50 | */ 51 | function register() internal { 52 | StoreSwitch.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); 53 | } 54 | 55 | /** 56 | * @notice Register the table with its config. 57 | */ 58 | function _register() internal { 59 | StoreCore.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); 60 | } 61 | 62 | /** 63 | * @notice Get value. 64 | */ 65 | function getValue() internal view returns (bytes32 value) { 66 | bytes32[] memory _keyTuple = new bytes32[](0); 67 | 68 | bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 69 | return (bytes32(_blob)); 70 | } 71 | 72 | /** 73 | * @notice Get value. 74 | */ 75 | function _getValue() internal view returns (bytes32 value) { 76 | bytes32[] memory _keyTuple = new bytes32[](0); 77 | 78 | bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 79 | return (bytes32(_blob)); 80 | } 81 | 82 | /** 83 | * @notice Get value. 84 | */ 85 | function get() internal view returns (bytes32 value) { 86 | bytes32[] memory _keyTuple = new bytes32[](0); 87 | 88 | bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 89 | return (bytes32(_blob)); 90 | } 91 | 92 | /** 93 | * @notice Get value. 94 | */ 95 | function _get() internal view returns (bytes32 value) { 96 | bytes32[] memory _keyTuple = new bytes32[](0); 97 | 98 | bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 99 | return (bytes32(_blob)); 100 | } 101 | 102 | /** 103 | * @notice Set value. 104 | */ 105 | function setValue(bytes32 value) internal { 106 | bytes32[] memory _keyTuple = new bytes32[](0); 107 | 108 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 109 | } 110 | 111 | /** 112 | * @notice Set value. 113 | */ 114 | function _setValue(bytes32 value) internal { 115 | bytes32[] memory _keyTuple = new bytes32[](0); 116 | 117 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 118 | } 119 | 120 | /** 121 | * @notice Set value. 122 | */ 123 | function set(bytes32 value) internal { 124 | bytes32[] memory _keyTuple = new bytes32[](0); 125 | 126 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 127 | } 128 | 129 | /** 130 | * @notice Set value. 131 | */ 132 | function _set(bytes32 value) internal { 133 | bytes32[] memory _keyTuple = new bytes32[](0); 134 | 135 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 136 | } 137 | 138 | /** 139 | * @notice Delete all data for given keys. 140 | */ 141 | function deleteRecord() internal { 142 | bytes32[] memory _keyTuple = new bytes32[](0); 143 | 144 | StoreSwitch.deleteRecord(_tableId, _keyTuple); 145 | } 146 | 147 | /** 148 | * @notice Delete all data for given keys. 149 | */ 150 | function _deleteRecord() internal { 151 | bytes32[] memory _keyTuple = new bytes32[](0); 152 | 153 | StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); 154 | } 155 | 156 | /** 157 | * @notice Tightly pack static (fixed length) data using this table's schema. 158 | * @return The static data, encoded into a sequence of bytes. 159 | */ 160 | function encodeStatic(bytes32 value) internal pure returns (bytes memory) { 161 | return abi.encodePacked(value); 162 | } 163 | 164 | /** 165 | * @notice Encode all of a record's fields. 166 | * @return The static (fixed length) data, encoded into a sequence of bytes. 167 | * @return The lengths of the dynamic fields (packed into a single bytes32 value). 168 | * @return The dynamic (variable length) data, encoded into a sequence of bytes. 169 | */ 170 | function encode(bytes32 value) internal pure returns (bytes memory, EncodedLengths, bytes memory) { 171 | bytes memory _staticData = encodeStatic(value); 172 | 173 | EncodedLengths _encodedLengths; 174 | bytes memory _dynamicData; 175 | 176 | return (_staticData, _encodedLengths, _dynamicData); 177 | } 178 | 179 | /** 180 | * @notice Encode keys as a bytes32 array using this table's field layout. 181 | */ 182 | function encodeKeyTuple() internal pure returns (bytes32[] memory) { 183 | bytes32[] memory _keyTuple = new bytes32[](0); 184 | 185 | return _keyTuple; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/codegen/tables/Treasury.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | // Import store internals 7 | import { IStore } from "@latticexyz/store/src/IStore.sol"; 8 | import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; 9 | import { StoreCore } from "@latticexyz/store/src/StoreCore.sol"; 10 | import { Bytes } from "@latticexyz/store/src/Bytes.sol"; 11 | import { Memory } from "@latticexyz/store/src/Memory.sol"; 12 | import { SliceLib } from "@latticexyz/store/src/Slice.sol"; 13 | import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol"; 14 | import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol"; 15 | import { Schema } from "@latticexyz/store/src/Schema.sol"; 16 | import { EncodedLengths, EncodedLengthsLib } from "@latticexyz/store/src/EncodedLengths.sol"; 17 | import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; 18 | 19 | library Treasury { 20 | // Hex below is the result of `WorldResourceIdLib.encode({ namespace: "", name: "Treasury", typeId: RESOURCE_TABLE });` 21 | ResourceId constant _tableId = ResourceId.wrap(0x7462000000000000000000000000000054726561737572790000000000000000); 22 | 23 | FieldLayout constant _fieldLayout = 24 | FieldLayout.wrap(0x0020010020000000000000000000000000000000000000000000000000000000); 25 | 26 | // Hex-encoded key schema of () 27 | Schema constant _keySchema = Schema.wrap(0x0000000000000000000000000000000000000000000000000000000000000000); 28 | // Hex-encoded value schema of (uint256) 29 | Schema constant _valueSchema = Schema.wrap(0x002001001f000000000000000000000000000000000000000000000000000000); 30 | 31 | /** 32 | * @notice Get the table's key field names. 33 | * @return keyNames An array of strings with the names of key fields. 34 | */ 35 | function getKeyNames() internal pure returns (string[] memory keyNames) { 36 | keyNames = new string[](0); 37 | } 38 | 39 | /** 40 | * @notice Get the table's value field names. 41 | * @return fieldNames An array of strings with the names of value fields. 42 | */ 43 | function getFieldNames() internal pure returns (string[] memory fieldNames) { 44 | fieldNames = new string[](1); 45 | fieldNames[0] = "value"; 46 | } 47 | 48 | /** 49 | * @notice Register the table with its config. 50 | */ 51 | function register() internal { 52 | StoreSwitch.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); 53 | } 54 | 55 | /** 56 | * @notice Register the table with its config. 57 | */ 58 | function _register() internal { 59 | StoreCore.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); 60 | } 61 | 62 | /** 63 | * @notice Get value. 64 | */ 65 | function getValue() internal view returns (uint256 value) { 66 | bytes32[] memory _keyTuple = new bytes32[](0); 67 | 68 | bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 69 | return (uint256(bytes32(_blob))); 70 | } 71 | 72 | /** 73 | * @notice Get value. 74 | */ 75 | function _getValue() internal view returns (uint256 value) { 76 | bytes32[] memory _keyTuple = new bytes32[](0); 77 | 78 | bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 79 | return (uint256(bytes32(_blob))); 80 | } 81 | 82 | /** 83 | * @notice Get value. 84 | */ 85 | function get() internal view returns (uint256 value) { 86 | bytes32[] memory _keyTuple = new bytes32[](0); 87 | 88 | bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 89 | return (uint256(bytes32(_blob))); 90 | } 91 | 92 | /** 93 | * @notice Get value. 94 | */ 95 | function _get() internal view returns (uint256 value) { 96 | bytes32[] memory _keyTuple = new bytes32[](0); 97 | 98 | bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 99 | return (uint256(bytes32(_blob))); 100 | } 101 | 102 | /** 103 | * @notice Set value. 104 | */ 105 | function setValue(uint256 value) internal { 106 | bytes32[] memory _keyTuple = new bytes32[](0); 107 | 108 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 109 | } 110 | 111 | /** 112 | * @notice Set value. 113 | */ 114 | function _setValue(uint256 value) internal { 115 | bytes32[] memory _keyTuple = new bytes32[](0); 116 | 117 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 118 | } 119 | 120 | /** 121 | * @notice Set value. 122 | */ 123 | function set(uint256 value) internal { 124 | bytes32[] memory _keyTuple = new bytes32[](0); 125 | 126 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 127 | } 128 | 129 | /** 130 | * @notice Set value. 131 | */ 132 | function _set(uint256 value) internal { 133 | bytes32[] memory _keyTuple = new bytes32[](0); 134 | 135 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 136 | } 137 | 138 | /** 139 | * @notice Delete all data for given keys. 140 | */ 141 | function deleteRecord() internal { 142 | bytes32[] memory _keyTuple = new bytes32[](0); 143 | 144 | StoreSwitch.deleteRecord(_tableId, _keyTuple); 145 | } 146 | 147 | /** 148 | * @notice Delete all data for given keys. 149 | */ 150 | function _deleteRecord() internal { 151 | bytes32[] memory _keyTuple = new bytes32[](0); 152 | 153 | StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); 154 | } 155 | 156 | /** 157 | * @notice Tightly pack static (fixed length) data using this table's schema. 158 | * @return The static data, encoded into a sequence of bytes. 159 | */ 160 | function encodeStatic(uint256 value) internal pure returns (bytes memory) { 161 | return abi.encodePacked(value); 162 | } 163 | 164 | /** 165 | * @notice Encode all of a record's fields. 166 | * @return The static (fixed length) data, encoded into a sequence of bytes. 167 | * @return The lengths of the dynamic fields (packed into a single bytes32 value). 168 | * @return The dynamic (variable length) data, encoded into a sequence of bytes. 169 | */ 170 | function encode(uint256 value) internal pure returns (bytes memory, EncodedLengths, bytes memory) { 171 | bytes memory _staticData = encodeStatic(value); 172 | 173 | EncodedLengths _encodedLengths; 174 | bytes memory _dynamicData; 175 | 176 | return (_staticData, _encodedLengths, _dynamicData); 177 | } 178 | 179 | /** 180 | * @notice Encode keys as a bytes32 array using this table's field layout. 181 | */ 182 | function encodeKeyTuple() internal pure returns (bytes32[] memory) { 183 | bytes32[] memory _keyTuple = new bytes32[](0); 184 | 185 | return _keyTuple; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/codegen/tables/UpdateId.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | // Import store internals 7 | import { IStore } from "@latticexyz/store/src/IStore.sol"; 8 | import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; 9 | import { StoreCore } from "@latticexyz/store/src/StoreCore.sol"; 10 | import { Bytes } from "@latticexyz/store/src/Bytes.sol"; 11 | import { Memory } from "@latticexyz/store/src/Memory.sol"; 12 | import { SliceLib } from "@latticexyz/store/src/Slice.sol"; 13 | import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol"; 14 | import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol"; 15 | import { Schema } from "@latticexyz/store/src/Schema.sol"; 16 | import { EncodedLengths, EncodedLengthsLib } from "@latticexyz/store/src/EncodedLengths.sol"; 17 | import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; 18 | 19 | library UpdateId { 20 | // Hex below is the result of `WorldResourceIdLib.encode({ namespace: "", name: "UpdateId", typeId: RESOURCE_TABLE });` 21 | ResourceId constant _tableId = ResourceId.wrap(0x7462000000000000000000000000000055706461746549640000000000000000); 22 | 23 | FieldLayout constant _fieldLayout = 24 | FieldLayout.wrap(0x0020010020000000000000000000000000000000000000000000000000000000); 25 | 26 | // Hex-encoded key schema of () 27 | Schema constant _keySchema = Schema.wrap(0x0000000000000000000000000000000000000000000000000000000000000000); 28 | // Hex-encoded value schema of (uint256) 29 | Schema constant _valueSchema = Schema.wrap(0x002001001f000000000000000000000000000000000000000000000000000000); 30 | 31 | /** 32 | * @notice Get the table's key field names. 33 | * @return keyNames An array of strings with the names of key fields. 34 | */ 35 | function getKeyNames() internal pure returns (string[] memory keyNames) { 36 | keyNames = new string[](0); 37 | } 38 | 39 | /** 40 | * @notice Get the table's value field names. 41 | * @return fieldNames An array of strings with the names of value fields. 42 | */ 43 | function getFieldNames() internal pure returns (string[] memory fieldNames) { 44 | fieldNames = new string[](1); 45 | fieldNames[0] = "value"; 46 | } 47 | 48 | /** 49 | * @notice Register the table with its config. 50 | */ 51 | function register() internal { 52 | StoreSwitch.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); 53 | } 54 | 55 | /** 56 | * @notice Register the table with its config. 57 | */ 58 | function _register() internal { 59 | StoreCore.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); 60 | } 61 | 62 | /** 63 | * @notice Get value. 64 | */ 65 | function getValue() internal view returns (uint256 value) { 66 | bytes32[] memory _keyTuple = new bytes32[](0); 67 | 68 | bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 69 | return (uint256(bytes32(_blob))); 70 | } 71 | 72 | /** 73 | * @notice Get value. 74 | */ 75 | function _getValue() internal view returns (uint256 value) { 76 | bytes32[] memory _keyTuple = new bytes32[](0); 77 | 78 | bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 79 | return (uint256(bytes32(_blob))); 80 | } 81 | 82 | /** 83 | * @notice Get value. 84 | */ 85 | function get() internal view returns (uint256 value) { 86 | bytes32[] memory _keyTuple = new bytes32[](0); 87 | 88 | bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 89 | return (uint256(bytes32(_blob))); 90 | } 91 | 92 | /** 93 | * @notice Get value. 94 | */ 95 | function _get() internal view returns (uint256 value) { 96 | bytes32[] memory _keyTuple = new bytes32[](0); 97 | 98 | bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 99 | return (uint256(bytes32(_blob))); 100 | } 101 | 102 | /** 103 | * @notice Set value. 104 | */ 105 | function setValue(uint256 value) internal { 106 | bytes32[] memory _keyTuple = new bytes32[](0); 107 | 108 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 109 | } 110 | 111 | /** 112 | * @notice Set value. 113 | */ 114 | function _setValue(uint256 value) internal { 115 | bytes32[] memory _keyTuple = new bytes32[](0); 116 | 117 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 118 | } 119 | 120 | /** 121 | * @notice Set value. 122 | */ 123 | function set(uint256 value) internal { 124 | bytes32[] memory _keyTuple = new bytes32[](0); 125 | 126 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 127 | } 128 | 129 | /** 130 | * @notice Set value. 131 | */ 132 | function _set(uint256 value) internal { 133 | bytes32[] memory _keyTuple = new bytes32[](0); 134 | 135 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 136 | } 137 | 138 | /** 139 | * @notice Delete all data for given keys. 140 | */ 141 | function deleteRecord() internal { 142 | bytes32[] memory _keyTuple = new bytes32[](0); 143 | 144 | StoreSwitch.deleteRecord(_tableId, _keyTuple); 145 | } 146 | 147 | /** 148 | * @notice Delete all data for given keys. 149 | */ 150 | function _deleteRecord() internal { 151 | bytes32[] memory _keyTuple = new bytes32[](0); 152 | 153 | StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); 154 | } 155 | 156 | /** 157 | * @notice Tightly pack static (fixed length) data using this table's schema. 158 | * @return The static data, encoded into a sequence of bytes. 159 | */ 160 | function encodeStatic(uint256 value) internal pure returns (bytes memory) { 161 | return abi.encodePacked(value); 162 | } 163 | 164 | /** 165 | * @notice Encode all of a record's fields. 166 | * @return The static (fixed length) data, encoded into a sequence of bytes. 167 | * @return The lengths of the dynamic fields (packed into a single bytes32 value). 168 | * @return The dynamic (variable length) data, encoded into a sequence of bytes. 169 | */ 170 | function encode(uint256 value) internal pure returns (bytes memory, EncodedLengths, bytes memory) { 171 | bytes memory _staticData = encodeStatic(value); 172 | 173 | EncodedLengths _encodedLengths; 174 | bytes memory _dynamicData; 175 | 176 | return (_staticData, _encodedLengths, _dynamicData); 177 | } 178 | 179 | /** 180 | * @notice Encode keys as a bytes32 array using this table's field layout. 181 | */ 182 | function encodeKeyTuple() internal pure returns (bytes32[] memory) { 183 | bytes32[] memory _keyTuple = new bytes32[](0); 184 | 185 | return _keyTuple; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/test/integration/Transfer.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | /* solhint-disable no-global-import */ 4 | /* solhint-disable func-name-mixedcase */ 5 | 6 | import { Words3Test } from "../Words3Test.t.sol"; 7 | import { Letter } from "codegen/common.sol"; 8 | import { FeeConfigData, PlayerLetters, PriceConfigData } from "codegen/index.sol"; 9 | import "forge-std/Test.sol"; 10 | import { TransferLettersSystem } from "systems/TransferLettersSystem.sol"; 11 | 12 | contract Transfer is Words3Test { 13 | function setUp() public override { 14 | super.setUp(); 15 | setDefaultLetterOdds(); 16 | } 17 | 18 | function test_Transfer() public { 19 | Letter[] memory initialWord = new Letter[](2); 20 | initialWord[0] = Letter.H; 21 | initialWord[1] = Letter.I; 22 | uint32[26] memory initialLetterAllocation; 23 | world.start({ 24 | initialWord: initialWord, 25 | initialLetterAllocation: initialLetterAllocation, 26 | initialLettersTo: address(0), 27 | merkleRoot: bytes32(0), 28 | initialPrice: 0.001 ether, 29 | claimRestrictionDurationBlocks: 0, 30 | priceConfig: PriceConfigData({ 31 | minPrice: 0.001 ether, 32 | wadPriceIncreaseFactor: 1.115e18, 33 | wadPower: 0.9e18, 34 | wadScale: 9.96e36 35 | }), 36 | feeConfig: FeeConfigData({ feeBps: 0, feeTaker: address(0) }), 37 | crossWordRewardFraction: 3, 38 | bonusDistance: 3, 39 | numDrawLetters: 8 40 | }); 41 | 42 | address player = address(0x123); 43 | address to = address(0x456); 44 | vm.startPrank(player); 45 | for (uint256 i = 0; i < 50; i++) { 46 | uint256 price = world.getDrawPrice(); 47 | vm.deal(player, price); 48 | vm.roll(block.number + 100); 49 | world.draw{ value: price }(player); 50 | } 51 | assertEq(PlayerLetters.get({ player: to, letter: Letter.A }), 0); 52 | Letter[] memory transferLetters = new Letter[](2); 53 | transferLetters[0] = Letter.A; 54 | transferLetters[1] = Letter.B; 55 | world.transfer({ letters: transferLetters, to: to }); 56 | assertEq(PlayerLetters.get({ player: to, letter: Letter.A }), 1); 57 | assertEq(PlayerLetters.get({ player: to, letter: Letter.B }), 1); 58 | vm.stopPrank(); 59 | } 60 | 61 | function test_InitialLetterAllocationAndTransfer() public { 62 | Letter[] memory initialWord = new Letter[](2); 63 | initialWord[0] = Letter.H; 64 | initialWord[1] = Letter.I; 65 | uint32[26] memory initialLetterAllocation; 66 | initialLetterAllocation[0] = 1; // 1 A 67 | initialLetterAllocation[1] = 2; // 2 Bs 68 | initialLetterAllocation[3] = 40; // 40 Ds 69 | initialLetterAllocation[4] = 50; // 50 Es 70 | initialLetterAllocation[5] = 60; // 60 Fs 71 | initialLetterAllocation[25] = 2383; // 2383 Zs 72 | 73 | address initialLettersTo = address(0x123); 74 | 75 | world.start({ 76 | initialWord: initialWord, 77 | initialLetterAllocation: initialLetterAllocation, 78 | initialLettersTo: initialLettersTo, 79 | merkleRoot: bytes32(0), 80 | initialPrice: 0.001 ether, 81 | claimRestrictionDurationBlocks: 0, 82 | priceConfig: PriceConfigData({ 83 | minPrice: 0.001 ether, 84 | wadPriceIncreaseFactor: 1.115e18, 85 | wadPower: 0.9e18, 86 | wadScale: 9.96e36 87 | }), 88 | feeConfig: FeeConfigData({ feeBps: 0, feeTaker: address(0) }), 89 | crossWordRewardFraction: 3, 90 | bonusDistance: 3, 91 | numDrawLetters: 8 92 | }); 93 | 94 | // Check to see if initial letters to has the letters 95 | assertEq(PlayerLetters.get({ player: initialLettersTo, letter: Letter.A }), 1); 96 | assertEq(PlayerLetters.get({ player: initialLettersTo, letter: Letter.B }), 2); 97 | assertEq(PlayerLetters.get({ player: initialLettersTo, letter: Letter.D }), 40); 98 | assertEq(PlayerLetters.get({ player: initialLettersTo, letter: Letter.E }), 50); 99 | assertEq(PlayerLetters.get({ player: initialLettersTo, letter: Letter.F }), 60); 100 | assertEq(PlayerLetters.get({ player: initialLettersTo, letter: Letter.Z }), 2383); 101 | 102 | // Transfer some letters to a new player 103 | address to = address(0x456); 104 | Letter[] memory transferLetters = new Letter[](2); 105 | transferLetters[0] = Letter.A; 106 | transferLetters[1] = Letter.B; 107 | vm.prank(initialLettersTo); 108 | world.transfer({ letters: transferLetters, to: to }); 109 | 110 | // Check to see if the new player has the letters 111 | assertEq(PlayerLetters.get({ player: to, letter: Letter.A }), 1); 112 | assertEq(PlayerLetters.get({ player: to, letter: Letter.B }), 1); 113 | 114 | // Check that the initial allocation player has the correct amount of letters 115 | assertEq(PlayerLetters.get({ player: initialLettersTo, letter: Letter.A }), 0); 116 | assertEq(PlayerLetters.get({ player: initialLettersTo, letter: Letter.B }), 1); 117 | assertEq(PlayerLetters.get({ player: initialLettersTo, letter: Letter.D }), 40); 118 | assertEq(PlayerLetters.get({ player: initialLettersTo, letter: Letter.E }), 50); 119 | assertEq(PlayerLetters.get({ player: initialLettersTo, letter: Letter.F }), 60); 120 | assertEq(PlayerLetters.get({ player: initialLettersTo, letter: Letter.Z }), 2383); 121 | } 122 | 123 | function test_RevertsWhen_NoLetters() public { 124 | Letter[] memory initialWord = new Letter[](2); 125 | initialWord[0] = Letter.H; 126 | initialWord[1] = Letter.I; 127 | uint32[26] memory initialLetterAllocation; 128 | world.start({ 129 | initialWord: initialWord, 130 | initialLetterAllocation: initialLetterAllocation, 131 | initialLettersTo: address(0), 132 | merkleRoot: bytes32(0), 133 | initialPrice: 0.001 ether, 134 | claimRestrictionDurationBlocks: 0, 135 | priceConfig: PriceConfigData({ 136 | minPrice: 0.001 ether, 137 | wadPriceIncreaseFactor: 1.115e18, 138 | wadPower: 0.95e18, 139 | wadScale: 1.1715e37 140 | }), 141 | feeConfig: FeeConfigData({ feeBps: 0, feeTaker: address(0) }), 142 | crossWordRewardFraction: 3, 143 | bonusDistance: 3, 144 | numDrawLetters: 8 145 | }); 146 | 147 | address to = address(0x456); 148 | Letter[] memory transferLetters = new Letter[](2); 149 | transferLetters[0] = Letter.A; 150 | transferLetters[1] = Letter.B; 151 | vm.prank(to); 152 | vm.expectRevert(TransferLettersSystem.TransferMissingLetters.selector); 153 | world.transfer({ letters: transferLetters, to: to }); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/codegen/tables/ClaimRestrictionConfig.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | // Import store internals 7 | import { IStore } from "@latticexyz/store/src/IStore.sol"; 8 | import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; 9 | import { StoreCore } from "@latticexyz/store/src/StoreCore.sol"; 10 | import { Bytes } from "@latticexyz/store/src/Bytes.sol"; 11 | import { Memory } from "@latticexyz/store/src/Memory.sol"; 12 | import { SliceLib } from "@latticexyz/store/src/Slice.sol"; 13 | import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol"; 14 | import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol"; 15 | import { Schema } from "@latticexyz/store/src/Schema.sol"; 16 | import { EncodedLengths, EncodedLengthsLib } from "@latticexyz/store/src/EncodedLengths.sol"; 17 | import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; 18 | 19 | library ClaimRestrictionConfig { 20 | // Hex below is the result of `WorldResourceIdLib.encode({ namespace: "", name: "ClaimRestriction", typeId: RESOURCE_TABLE });` 21 | ResourceId constant _tableId = ResourceId.wrap(0x74620000000000000000000000000000436c61696d5265737472696374696f6e); 22 | 23 | FieldLayout constant _fieldLayout = 24 | FieldLayout.wrap(0x0020010020000000000000000000000000000000000000000000000000000000); 25 | 26 | // Hex-encoded key schema of () 27 | Schema constant _keySchema = Schema.wrap(0x0000000000000000000000000000000000000000000000000000000000000000); 28 | // Hex-encoded value schema of (uint256) 29 | Schema constant _valueSchema = Schema.wrap(0x002001001f000000000000000000000000000000000000000000000000000000); 30 | 31 | /** 32 | * @notice Get the table's key field names. 33 | * @return keyNames An array of strings with the names of key fields. 34 | */ 35 | function getKeyNames() internal pure returns (string[] memory keyNames) { 36 | keyNames = new string[](0); 37 | } 38 | 39 | /** 40 | * @notice Get the table's value field names. 41 | * @return fieldNames An array of strings with the names of value fields. 42 | */ 43 | function getFieldNames() internal pure returns (string[] memory fieldNames) { 44 | fieldNames = new string[](1); 45 | fieldNames[0] = "claimRestrictionBlock"; 46 | } 47 | 48 | /** 49 | * @notice Register the table with its config. 50 | */ 51 | function register() internal { 52 | StoreSwitch.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); 53 | } 54 | 55 | /** 56 | * @notice Register the table with its config. 57 | */ 58 | function _register() internal { 59 | StoreCore.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); 60 | } 61 | 62 | /** 63 | * @notice Get claimRestrictionBlock. 64 | */ 65 | function getClaimRestrictionBlock() internal view returns (uint256 claimRestrictionBlock) { 66 | bytes32[] memory _keyTuple = new bytes32[](0); 67 | 68 | bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 69 | return (uint256(bytes32(_blob))); 70 | } 71 | 72 | /** 73 | * @notice Get claimRestrictionBlock. 74 | */ 75 | function _getClaimRestrictionBlock() internal view returns (uint256 claimRestrictionBlock) { 76 | bytes32[] memory _keyTuple = new bytes32[](0); 77 | 78 | bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 79 | return (uint256(bytes32(_blob))); 80 | } 81 | 82 | /** 83 | * @notice Get claimRestrictionBlock. 84 | */ 85 | function get() internal view returns (uint256 claimRestrictionBlock) { 86 | bytes32[] memory _keyTuple = new bytes32[](0); 87 | 88 | bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 89 | return (uint256(bytes32(_blob))); 90 | } 91 | 92 | /** 93 | * @notice Get claimRestrictionBlock. 94 | */ 95 | function _get() internal view returns (uint256 claimRestrictionBlock) { 96 | bytes32[] memory _keyTuple = new bytes32[](0); 97 | 98 | bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 99 | return (uint256(bytes32(_blob))); 100 | } 101 | 102 | /** 103 | * @notice Set claimRestrictionBlock. 104 | */ 105 | function setClaimRestrictionBlock(uint256 claimRestrictionBlock) internal { 106 | bytes32[] memory _keyTuple = new bytes32[](0); 107 | 108 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((claimRestrictionBlock)), _fieldLayout); 109 | } 110 | 111 | /** 112 | * @notice Set claimRestrictionBlock. 113 | */ 114 | function _setClaimRestrictionBlock(uint256 claimRestrictionBlock) internal { 115 | bytes32[] memory _keyTuple = new bytes32[](0); 116 | 117 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((claimRestrictionBlock)), _fieldLayout); 118 | } 119 | 120 | /** 121 | * @notice Set claimRestrictionBlock. 122 | */ 123 | function set(uint256 claimRestrictionBlock) internal { 124 | bytes32[] memory _keyTuple = new bytes32[](0); 125 | 126 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((claimRestrictionBlock)), _fieldLayout); 127 | } 128 | 129 | /** 130 | * @notice Set claimRestrictionBlock. 131 | */ 132 | function _set(uint256 claimRestrictionBlock) internal { 133 | bytes32[] memory _keyTuple = new bytes32[](0); 134 | 135 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((claimRestrictionBlock)), _fieldLayout); 136 | } 137 | 138 | /** 139 | * @notice Delete all data for given keys. 140 | */ 141 | function deleteRecord() internal { 142 | bytes32[] memory _keyTuple = new bytes32[](0); 143 | 144 | StoreSwitch.deleteRecord(_tableId, _keyTuple); 145 | } 146 | 147 | /** 148 | * @notice Delete all data for given keys. 149 | */ 150 | function _deleteRecord() internal { 151 | bytes32[] memory _keyTuple = new bytes32[](0); 152 | 153 | StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); 154 | } 155 | 156 | /** 157 | * @notice Tightly pack static (fixed length) data using this table's schema. 158 | * @return The static data, encoded into a sequence of bytes. 159 | */ 160 | function encodeStatic(uint256 claimRestrictionBlock) internal pure returns (bytes memory) { 161 | return abi.encodePacked(claimRestrictionBlock); 162 | } 163 | 164 | /** 165 | * @notice Encode all of a record's fields. 166 | * @return The static (fixed length) data, encoded into a sequence of bytes. 167 | * @return The lengths of the dynamic fields (packed into a single bytes32 value). 168 | * @return The dynamic (variable length) data, encoded into a sequence of bytes. 169 | */ 170 | function encode(uint256 claimRestrictionBlock) internal pure returns (bytes memory, EncodedLengths, bytes memory) { 171 | bytes memory _staticData = encodeStatic(claimRestrictionBlock); 172 | 173 | EncodedLengths _encodedLengths; 174 | bytes memory _dynamicData; 175 | 176 | return (_staticData, _encodedLengths, _dynamicData); 177 | } 178 | 179 | /** 180 | * @notice Encode keys as a bytes32 array using this table's field layout. 181 | */ 182 | function encodeKeyTuple() internal pure returns (bytes32[] memory) { 183 | bytes32[] memory _keyTuple = new bytes32[](0); 184 | 185 | return _keyTuple; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/codegen/tables/Points.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | // Import store internals 7 | import { IStore } from "@latticexyz/store/src/IStore.sol"; 8 | import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; 9 | import { StoreCore } from "@latticexyz/store/src/StoreCore.sol"; 10 | import { Bytes } from "@latticexyz/store/src/Bytes.sol"; 11 | import { Memory } from "@latticexyz/store/src/Memory.sol"; 12 | import { SliceLib } from "@latticexyz/store/src/Slice.sol"; 13 | import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol"; 14 | import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol"; 15 | import { Schema } from "@latticexyz/store/src/Schema.sol"; 16 | import { EncodedLengths, EncodedLengthsLib } from "@latticexyz/store/src/EncodedLengths.sol"; 17 | import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; 18 | 19 | library Points { 20 | // Hex below is the result of `WorldResourceIdLib.encode({ namespace: "", name: "Points", typeId: RESOURCE_TABLE });` 21 | ResourceId constant _tableId = ResourceId.wrap(0x74620000000000000000000000000000506f696e747300000000000000000000); 22 | 23 | FieldLayout constant _fieldLayout = 24 | FieldLayout.wrap(0x0004010004000000000000000000000000000000000000000000000000000000); 25 | 26 | // Hex-encoded key schema of (address) 27 | Schema constant _keySchema = Schema.wrap(0x0014010061000000000000000000000000000000000000000000000000000000); 28 | // Hex-encoded value schema of (uint32) 29 | Schema constant _valueSchema = Schema.wrap(0x0004010003000000000000000000000000000000000000000000000000000000); 30 | 31 | /** 32 | * @notice Get the table's key field names. 33 | * @return keyNames An array of strings with the names of key fields. 34 | */ 35 | function getKeyNames() internal pure returns (string[] memory keyNames) { 36 | keyNames = new string[](1); 37 | keyNames[0] = "player"; 38 | } 39 | 40 | /** 41 | * @notice Get the table's value field names. 42 | * @return fieldNames An array of strings with the names of value fields. 43 | */ 44 | function getFieldNames() internal pure returns (string[] memory fieldNames) { 45 | fieldNames = new string[](1); 46 | fieldNames[0] = "value"; 47 | } 48 | 49 | /** 50 | * @notice Register the table with its config. 51 | */ 52 | function register() internal { 53 | StoreSwitch.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); 54 | } 55 | 56 | /** 57 | * @notice Register the table with its config. 58 | */ 59 | function _register() internal { 60 | StoreCore.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); 61 | } 62 | 63 | /** 64 | * @notice Get value. 65 | */ 66 | function getValue(address player) internal view returns (uint32 value) { 67 | bytes32[] memory _keyTuple = new bytes32[](1); 68 | _keyTuple[0] = bytes32(uint256(uint160(player))); 69 | 70 | bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 71 | return (uint32(bytes4(_blob))); 72 | } 73 | 74 | /** 75 | * @notice Get value. 76 | */ 77 | function _getValue(address player) internal view returns (uint32 value) { 78 | bytes32[] memory _keyTuple = new bytes32[](1); 79 | _keyTuple[0] = bytes32(uint256(uint160(player))); 80 | 81 | bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 82 | return (uint32(bytes4(_blob))); 83 | } 84 | 85 | /** 86 | * @notice Get value. 87 | */ 88 | function get(address player) internal view returns (uint32 value) { 89 | bytes32[] memory _keyTuple = new bytes32[](1); 90 | _keyTuple[0] = bytes32(uint256(uint160(player))); 91 | 92 | bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 93 | return (uint32(bytes4(_blob))); 94 | } 95 | 96 | /** 97 | * @notice Get value. 98 | */ 99 | function _get(address player) internal view returns (uint32 value) { 100 | bytes32[] memory _keyTuple = new bytes32[](1); 101 | _keyTuple[0] = bytes32(uint256(uint160(player))); 102 | 103 | bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 104 | return (uint32(bytes4(_blob))); 105 | } 106 | 107 | /** 108 | * @notice Set value. 109 | */ 110 | function setValue(address player, uint32 value) internal { 111 | bytes32[] memory _keyTuple = new bytes32[](1); 112 | _keyTuple[0] = bytes32(uint256(uint160(player))); 113 | 114 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 115 | } 116 | 117 | /** 118 | * @notice Set value. 119 | */ 120 | function _setValue(address player, uint32 value) internal { 121 | bytes32[] memory _keyTuple = new bytes32[](1); 122 | _keyTuple[0] = bytes32(uint256(uint160(player))); 123 | 124 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 125 | } 126 | 127 | /** 128 | * @notice Set value. 129 | */ 130 | function set(address player, uint32 value) internal { 131 | bytes32[] memory _keyTuple = new bytes32[](1); 132 | _keyTuple[0] = bytes32(uint256(uint160(player))); 133 | 134 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 135 | } 136 | 137 | /** 138 | * @notice Set value. 139 | */ 140 | function _set(address player, uint32 value) internal { 141 | bytes32[] memory _keyTuple = new bytes32[](1); 142 | _keyTuple[0] = bytes32(uint256(uint160(player))); 143 | 144 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 145 | } 146 | 147 | /** 148 | * @notice Delete all data for given keys. 149 | */ 150 | function deleteRecord(address player) internal { 151 | bytes32[] memory _keyTuple = new bytes32[](1); 152 | _keyTuple[0] = bytes32(uint256(uint160(player))); 153 | 154 | StoreSwitch.deleteRecord(_tableId, _keyTuple); 155 | } 156 | 157 | /** 158 | * @notice Delete all data for given keys. 159 | */ 160 | function _deleteRecord(address player) internal { 161 | bytes32[] memory _keyTuple = new bytes32[](1); 162 | _keyTuple[0] = bytes32(uint256(uint160(player))); 163 | 164 | StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); 165 | } 166 | 167 | /** 168 | * @notice Tightly pack static (fixed length) data using this table's schema. 169 | * @return The static data, encoded into a sequence of bytes. 170 | */ 171 | function encodeStatic(uint32 value) internal pure returns (bytes memory) { 172 | return abi.encodePacked(value); 173 | } 174 | 175 | /** 176 | * @notice Encode all of a record's fields. 177 | * @return The static (fixed length) data, encoded into a sequence of bytes. 178 | * @return The lengths of the dynamic fields (packed into a single bytes32 value). 179 | * @return The dynamic (variable length) data, encoded into a sequence of bytes. 180 | */ 181 | function encode(uint32 value) internal pure returns (bytes memory, EncodedLengths, bytes memory) { 182 | bytes memory _staticData = encodeStatic(value); 183 | 184 | EncodedLengths _encodedLengths; 185 | bytes memory _dynamicData; 186 | 187 | return (_staticData, _encodedLengths, _dynamicData); 188 | } 189 | 190 | /** 191 | * @notice Encode keys as a bytes32 array using this table's field layout. 192 | */ 193 | function encodeKeyTuple(address player) internal pure returns (bytes32[] memory) { 194 | bytes32[] memory _keyTuple = new bytes32[](1); 195 | _keyTuple[0] = bytes32(uint256(uint160(player))); 196 | 197 | return _keyTuple; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/codegen/tables/Spent.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | // Import store internals 7 | import { IStore } from "@latticexyz/store/src/IStore.sol"; 8 | import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; 9 | import { StoreCore } from "@latticexyz/store/src/StoreCore.sol"; 10 | import { Bytes } from "@latticexyz/store/src/Bytes.sol"; 11 | import { Memory } from "@latticexyz/store/src/Memory.sol"; 12 | import { SliceLib } from "@latticexyz/store/src/Slice.sol"; 13 | import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol"; 14 | import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol"; 15 | import { Schema } from "@latticexyz/store/src/Schema.sol"; 16 | import { EncodedLengths, EncodedLengthsLib } from "@latticexyz/store/src/EncodedLengths.sol"; 17 | import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; 18 | 19 | library Spent { 20 | // Hex below is the result of `WorldResourceIdLib.encode({ namespace: "", name: "Spent", typeId: RESOURCE_TABLE });` 21 | ResourceId constant _tableId = ResourceId.wrap(0x746200000000000000000000000000005370656e740000000000000000000000); 22 | 23 | FieldLayout constant _fieldLayout = 24 | FieldLayout.wrap(0x0020010020000000000000000000000000000000000000000000000000000000); 25 | 26 | // Hex-encoded key schema of (address) 27 | Schema constant _keySchema = Schema.wrap(0x0014010061000000000000000000000000000000000000000000000000000000); 28 | // Hex-encoded value schema of (uint256) 29 | Schema constant _valueSchema = Schema.wrap(0x002001001f000000000000000000000000000000000000000000000000000000); 30 | 31 | /** 32 | * @notice Get the table's key field names. 33 | * @return keyNames An array of strings with the names of key fields. 34 | */ 35 | function getKeyNames() internal pure returns (string[] memory keyNames) { 36 | keyNames = new string[](1); 37 | keyNames[0] = "player"; 38 | } 39 | 40 | /** 41 | * @notice Get the table's value field names. 42 | * @return fieldNames An array of strings with the names of value fields. 43 | */ 44 | function getFieldNames() internal pure returns (string[] memory fieldNames) { 45 | fieldNames = new string[](1); 46 | fieldNames[0] = "value"; 47 | } 48 | 49 | /** 50 | * @notice Register the table with its config. 51 | */ 52 | function register() internal { 53 | StoreSwitch.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); 54 | } 55 | 56 | /** 57 | * @notice Register the table with its config. 58 | */ 59 | function _register() internal { 60 | StoreCore.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); 61 | } 62 | 63 | /** 64 | * @notice Get value. 65 | */ 66 | function getValue(address player) internal view returns (uint256 value) { 67 | bytes32[] memory _keyTuple = new bytes32[](1); 68 | _keyTuple[0] = bytes32(uint256(uint160(player))); 69 | 70 | bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 71 | return (uint256(bytes32(_blob))); 72 | } 73 | 74 | /** 75 | * @notice Get value. 76 | */ 77 | function _getValue(address player) internal view returns (uint256 value) { 78 | bytes32[] memory _keyTuple = new bytes32[](1); 79 | _keyTuple[0] = bytes32(uint256(uint160(player))); 80 | 81 | bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 82 | return (uint256(bytes32(_blob))); 83 | } 84 | 85 | /** 86 | * @notice Get value. 87 | */ 88 | function get(address player) internal view returns (uint256 value) { 89 | bytes32[] memory _keyTuple = new bytes32[](1); 90 | _keyTuple[0] = bytes32(uint256(uint160(player))); 91 | 92 | bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 93 | return (uint256(bytes32(_blob))); 94 | } 95 | 96 | /** 97 | * @notice Get value. 98 | */ 99 | function _get(address player) internal view returns (uint256 value) { 100 | bytes32[] memory _keyTuple = new bytes32[](1); 101 | _keyTuple[0] = bytes32(uint256(uint160(player))); 102 | 103 | bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 104 | return (uint256(bytes32(_blob))); 105 | } 106 | 107 | /** 108 | * @notice Set value. 109 | */ 110 | function setValue(address player, uint256 value) internal { 111 | bytes32[] memory _keyTuple = new bytes32[](1); 112 | _keyTuple[0] = bytes32(uint256(uint160(player))); 113 | 114 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 115 | } 116 | 117 | /** 118 | * @notice Set value. 119 | */ 120 | function _setValue(address player, uint256 value) internal { 121 | bytes32[] memory _keyTuple = new bytes32[](1); 122 | _keyTuple[0] = bytes32(uint256(uint160(player))); 123 | 124 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 125 | } 126 | 127 | /** 128 | * @notice Set value. 129 | */ 130 | function set(address player, uint256 value) internal { 131 | bytes32[] memory _keyTuple = new bytes32[](1); 132 | _keyTuple[0] = bytes32(uint256(uint160(player))); 133 | 134 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 135 | } 136 | 137 | /** 138 | * @notice Set value. 139 | */ 140 | function _set(address player, uint256 value) internal { 141 | bytes32[] memory _keyTuple = new bytes32[](1); 142 | _keyTuple[0] = bytes32(uint256(uint160(player))); 143 | 144 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 145 | } 146 | 147 | /** 148 | * @notice Delete all data for given keys. 149 | */ 150 | function deleteRecord(address player) internal { 151 | bytes32[] memory _keyTuple = new bytes32[](1); 152 | _keyTuple[0] = bytes32(uint256(uint160(player))); 153 | 154 | StoreSwitch.deleteRecord(_tableId, _keyTuple); 155 | } 156 | 157 | /** 158 | * @notice Delete all data for given keys. 159 | */ 160 | function _deleteRecord(address player) internal { 161 | bytes32[] memory _keyTuple = new bytes32[](1); 162 | _keyTuple[0] = bytes32(uint256(uint160(player))); 163 | 164 | StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); 165 | } 166 | 167 | /** 168 | * @notice Tightly pack static (fixed length) data using this table's schema. 169 | * @return The static data, encoded into a sequence of bytes. 170 | */ 171 | function encodeStatic(uint256 value) internal pure returns (bytes memory) { 172 | return abi.encodePacked(value); 173 | } 174 | 175 | /** 176 | * @notice Encode all of a record's fields. 177 | * @return The static (fixed length) data, encoded into a sequence of bytes. 178 | * @return The lengths of the dynamic fields (packed into a single bytes32 value). 179 | * @return The dynamic (variable length) data, encoded into a sequence of bytes. 180 | */ 181 | function encode(uint256 value) internal pure returns (bytes memory, EncodedLengths, bytes memory) { 182 | bytes memory _staticData = encodeStatic(value); 183 | 184 | EncodedLengths _encodedLengths; 185 | bytes memory _dynamicData; 186 | 187 | return (_staticData, _encodedLengths, _dynamicData); 188 | } 189 | 190 | /** 191 | * @notice Encode keys as a bytes32 array using this table's field layout. 192 | */ 193 | function encodeKeyTuple(address player) internal pure returns (bytes32[] memory) { 194 | bytes32[] memory _keyTuple = new bytes32[](1); 195 | _keyTuple[0] = bytes32(uint256(uint160(player))); 196 | 197 | return _keyTuple; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/test/integration/Points.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | /* solhint-disable no-global-import */ 4 | /* solhint-disable func-name-mixedcase */ 5 | 6 | import { Words3Test } from "../Words3Test.t.sol"; 7 | import { Merkle } from "../murky/src/Merkle.sol"; 8 | import { BonusType, Direction, Letter } from "codegen/common.sol"; 9 | import { FeeConfigData, Points, PriceConfigData } from "codegen/index.sol"; 10 | import { IWorld } from "codegen/world/IWorld.sol"; 11 | import { Bonus } from "common/Bonus.sol"; 12 | import { Bound } from "common/Bound.sol"; 13 | import { Coord } from "common/Coord.sol"; 14 | import "forge-std/Test.sol"; 15 | import { LibBonus } from "libraries/LibBonus.sol"; 16 | 17 | contract PointsTest is Words3Test { 18 | bytes32[] public words; 19 | Merkle private m; 20 | 21 | function setUp() public override { 22 | super.setUp(); 23 | world = IWorld(worldAddress); 24 | m = new Merkle(); 25 | Letter[] memory hi = new Letter[](2); 26 | hi[0] = Letter.H; 27 | hi[1] = Letter.I; 28 | Letter[] memory hello = new Letter[](5); 29 | hello[0] = Letter.H; 30 | hello[1] = Letter.E; 31 | hello[2] = Letter.L; 32 | hello[3] = Letter.L; 33 | hello[4] = Letter.O; 34 | Letter[] memory zone = new Letter[](4); 35 | zone[0] = Letter.Z; 36 | zone[1] = Letter.O; 37 | zone[2] = Letter.N; 38 | zone[3] = Letter.E; 39 | Letter[] memory zones = new Letter[](5); 40 | zones[0] = Letter.Z; 41 | zones[1] = Letter.O; 42 | zones[2] = Letter.N; 43 | zones[3] = Letter.E; 44 | zones[4] = Letter.S; 45 | Letter[] memory ollie = new Letter[](5); 46 | ollie[0] = Letter.O; 47 | ollie[1] = Letter.L; 48 | ollie[2] = Letter.L; 49 | ollie[3] = Letter.I; 50 | ollie[4] = Letter.E; 51 | words.push(keccak256(bytes.concat(keccak256(abi.encode(hi))))); // hi 52 | words.push(keccak256(bytes.concat(keccak256(abi.encode(hello))))); // hello 53 | words.push(keccak256(bytes.concat(keccak256(abi.encode(zone))))); // zone 54 | words.push(keccak256(bytes.concat(keccak256(abi.encode(zones))))); // zones 55 | words.push(keccak256(bytes.concat(keccak256(abi.encode(ollie))))); // ollie 56 | 57 | setDefaultLetterOdds(); 58 | } 59 | 60 | function test_CountPoints() public { 61 | // Test works as long as points are not on bonus tiles 62 | address player1 = address(0x12345); 63 | address player2 = address(0x22345); 64 | 65 | Letter[] memory initialWord = new Letter[](5); 66 | initialWord[0] = Letter.H; 67 | initialWord[1] = Letter.E; 68 | initialWord[2] = Letter.L; 69 | initialWord[3] = Letter.L; 70 | initialWord[4] = Letter.O; 71 | 72 | uint32[26] memory initialLetterAllocation; 73 | world.start({ 74 | initialWord: initialWord, 75 | initialLetterAllocation: initialLetterAllocation, 76 | initialLettersTo: address(0), 77 | merkleRoot: m.getRoot(words), 78 | initialPrice: 0.001 ether, 79 | claimRestrictionDurationBlocks: 0, 80 | priceConfig: PriceConfigData({ 81 | minPrice: 0.001 ether, 82 | wadPriceIncreaseFactor: 1.115e18, 83 | wadPower: 0.9e18, 84 | wadScale: 9.96e36 85 | }), 86 | feeConfig: FeeConfigData({ feeBps: 0, feeTaker: address(0) }), 87 | crossWordRewardFraction: 3, 88 | bonusDistance: 10, 89 | numDrawLetters: 7 90 | }); 91 | 92 | Letter[] memory word = new Letter[](4); 93 | word[0] = Letter.Z; 94 | word[1] = Letter.EMPTY; 95 | word[2] = Letter.N; 96 | word[3] = Letter.E; 97 | 98 | Bound[] memory bounds = new Bound[](4); 99 | bytes32[] memory proof = m.getProof(words, 2); 100 | 101 | // Play zone 102 | vm.deal(player1, 50 ether); 103 | vm.startPrank(player1); 104 | for (uint256 i = 0; i < 50; i++) { 105 | vm.roll(block.number + 100); 106 | world.draw{ value: world.getDrawPrice() }(player1); 107 | } 108 | world.play(word, proof, Coord({ x: 2, y: -1 }), Direction.TOP_TO_BOTTOM, bounds); 109 | vm.stopPrank(); 110 | assertEq(Points.get(player1), 13); 111 | 112 | // Play zones 113 | Letter[] memory ext = new Letter[](5); 114 | ext[0] = Letter.EMPTY; 115 | ext[1] = Letter.EMPTY; 116 | ext[2] = Letter.EMPTY; 117 | ext[3] = Letter.EMPTY; 118 | ext[4] = Letter.S; 119 | Bound[] memory extBounds = new Bound[](5); 120 | bytes32[] memory extProof = m.getProof(words, 3); 121 | 122 | vm.deal(player2, 50 ether); 123 | vm.startPrank(player2); 124 | for (uint256 i = 0; i < 50; i++) { 125 | vm.roll(block.number + 100); 126 | world.draw{ value: world.getDrawPrice() }(player2); 127 | } 128 | world.play(ext, extProof, Coord({ x: 2, y: -1 }), Direction.TOP_TO_BOTTOM, extBounds); 129 | vm.stopPrank(); 130 | assertEq(Points.get(player2), 14); 131 | 132 | // We lose 1 because of rounding 133 | assertEq(Points.get(player1), 13 + 4); 134 | } 135 | 136 | function test_Bonus() public { 137 | Letter[] memory initialWord = new Letter[](9); 138 | initialWord[0] = Letter.S; 139 | initialWord[1] = Letter.U; 140 | initialWord[2] = Letter.P; 141 | initialWord[3] = Letter.E; 142 | initialWord[4] = Letter.R; 143 | initialWord[5] = Letter.H; 144 | initialWord[6] = Letter.E; 145 | initialWord[7] = Letter.R; 146 | initialWord[8] = Letter.O; 147 | 148 | uint32[26] memory initialLetterAllocation; 149 | world.start({ 150 | initialWord: initialWord, 151 | initialLetterAllocation: initialLetterAllocation, 152 | initialLettersTo: address(0), 153 | merkleRoot: m.getRoot(words), 154 | initialPrice: 0.001 ether, 155 | claimRestrictionDurationBlocks: 0, 156 | priceConfig: PriceConfigData({ 157 | minPrice: 0.001 ether, 158 | wadPriceIncreaseFactor: 1.115e18, 159 | wadPower: 0.95e18, 160 | wadScale: 1.1715e37 161 | }), 162 | feeConfig: FeeConfigData({ feeBps: 0, feeTaker: address(0) }), 163 | crossWordRewardFraction: 3, 164 | bonusDistance: 5, 165 | numDrawLetters: 7 166 | }); 167 | 168 | Letter[] memory word = new Letter[](5); 169 | word[0] = Letter.EMPTY; 170 | word[1] = Letter.L; 171 | word[2] = Letter.L; 172 | word[3] = Letter.I; 173 | word[4] = Letter.E; 174 | 175 | Bound[] memory bounds = new Bound[](5); 176 | bytes32[] memory proof = m.getProof(words, 4); 177 | 178 | vm.deal(address(this), 50 ether); 179 | for (uint256 i = 0; i < 50; i++) { 180 | vm.roll(block.number + 100); 181 | world.draw{ value: world.getDrawPrice() }(address(this)); 182 | } 183 | world.play(word, proof, Coord({ x: 4, y: 0 }), Direction.TOP_TO_BOTTOM, bounds); 184 | 185 | uint32 truePoints = 5; 186 | Bonus memory bonus = LibBonus.getTileBonus(Coord({ x: 4, y: 1 })); 187 | 188 | if (bonus.bonusType == BonusType.MULTIPLY_WORD) { 189 | truePoints *= bonus.bonusValue; 190 | } else { 191 | truePoints += bonus.bonusValue - 1; 192 | } 193 | assertEq(Points.get(address(this)), truePoints); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/test/unit/LibPoints.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | /* solhint-disable no-global-import */ 4 | /* solhint-disable func-name-mixedcase */ 5 | 6 | import { Words3Test } from "../Words3Test.t.sol"; 7 | import { Wrapper } from "./Wrapper.sol"; 8 | import { BonusType, Direction, Letter } from "codegen/common.sol"; 9 | 10 | import { FeeConfigData, PriceConfigData } from "codegen/index.sol"; 11 | import { Bonus } from "common/Bonus.sol"; 12 | import { Coord } from "common/Coord.sol"; 13 | import "forge-std/Test.sol"; 14 | import { LibBonus } from "libraries/LibPoints.sol"; 15 | import { LibPoints } from "libraries/LibPoints.sol"; 16 | 17 | contract LibPointsTest is Words3Test { 18 | Wrapper private wrapper; 19 | 20 | function setUp() public override { 21 | super.setUp(); 22 | wrapper = new Wrapper(); 23 | } 24 | 25 | function test_GetBaseLetterPoints() public { 26 | assertEq(LibPoints.getBaseLetterPoints(Letter.A), 1); 27 | assertEq(LibPoints.getBaseLetterPoints(Letter.B), 3); 28 | assertEq(LibPoints.getBaseLetterPoints(Letter.C), 3); 29 | assertEq(LibPoints.getBaseLetterPoints(Letter.D), 2); 30 | assertEq(LibPoints.getBaseLetterPoints(Letter.E), 1); 31 | assertEq(LibPoints.getBaseLetterPoints(Letter.X), 8); 32 | assertEq(LibPoints.getBaseLetterPoints(Letter.Y), 4); 33 | assertEq(LibPoints.getBaseLetterPoints(Letter.Z), 10); 34 | } 35 | 36 | function test_GetBonusLetterPoints() public { 37 | Bonus memory bonus = Bonus({ bonusValue: 2, bonusType: BonusType.MULTIPLY_LETTER }); 38 | assertEq(LibPoints.getBonusLetterPoints(Letter.A, bonus), 2); 39 | assertEq(LibPoints.getBonusLetterPoints(Letter.B, bonus), 6); 40 | assertEq(LibPoints.getBonusLetterPoints(Letter.C, bonus), 6); 41 | assertEq(LibPoints.getBonusLetterPoints(Letter.D, bonus), 4); 42 | assertEq(LibPoints.getBonusLetterPoints(Letter.E, bonus), 2); 43 | assertEq(LibPoints.getBonusLetterPoints(Letter.X, bonus), 16); 44 | assertEq(LibPoints.getBonusLetterPoints(Letter.Y, bonus), 8); 45 | assertEq(LibPoints.getBonusLetterPoints(Letter.Z, bonus), 20); 46 | bonus.bonusValue = 3; 47 | assertEq(LibPoints.getBonusLetterPoints(Letter.A, bonus), 3); 48 | assertEq(LibPoints.getBonusLetterPoints(Letter.B, bonus), 9); 49 | assertEq(LibPoints.getBonusLetterPoints(Letter.C, bonus), 9); 50 | assertEq(LibPoints.getBonusLetterPoints(Letter.D, bonus), 6); 51 | assertEq(LibPoints.getBonusLetterPoints(Letter.E, bonus), 3); 52 | assertEq(LibPoints.getBonusLetterPoints(Letter.X, bonus), 24); 53 | assertEq(LibPoints.getBonusLetterPoints(Letter.Y, bonus), 12); 54 | bonus.bonusType = BonusType.MULTIPLY_WORD; 55 | for (uint256 i = 0; i <= 26; i++) { 56 | if (i == 0) { 57 | vm.expectRevert(); 58 | wrapper.pointsGetBonusLetterPoints(Letter(i), bonus); 59 | } else { 60 | assertEq(LibPoints.getBonusLetterPoints(Letter(i), bonus), LibPoints.getBaseLetterPoints(Letter(i))); 61 | } 62 | } 63 | } 64 | 65 | function testFuzz_GetBonusLetterPoints(uint8 multiplier) public { 66 | vm.assume(multiplier > 0); 67 | Bonus memory bonus = Bonus({ bonusValue: uint32(multiplier), bonusType: BonusType.MULTIPLY_LETTER }); 68 | for (uint256 i = 1; i <= 26; i++) { 69 | assertEq( 70 | LibPoints.getBonusLetterPoints(Letter(i), bonus), LibPoints.getBaseLetterPoints(Letter(i)) * multiplier 71 | ); 72 | } 73 | } 74 | 75 | function test_GetWordPoints() public { 76 | Letter[] memory initialWord = new Letter[](1); 77 | initialWord[0] = Letter.A; 78 | uint16 bonusDistance = 8; 79 | uint32[26] memory initialLetterAllocation; 80 | world.start({ 81 | initialWord: initialWord, 82 | initialLetterAllocation: initialLetterAllocation, 83 | initialLettersTo: address(0), 84 | merkleRoot: bytes32(0), 85 | initialPrice: 0.001 ether, 86 | claimRestrictionDurationBlocks: 0, 87 | priceConfig: PriceConfigData({ 88 | minPrice: 0.0001 ether, 89 | wadPriceIncreaseFactor: 1.3e18, 90 | wadPower: 0.2e18, 91 | wadScale: 3000e18 92 | }), 93 | feeConfig: FeeConfigData({ feeBps: 0, feeTaker: address(0) }), 94 | crossWordRewardFraction: 3, 95 | bonusDistance: bonusDistance, 96 | numDrawLetters: 20 97 | }); 98 | 99 | Letter[] memory playWord = new Letter[](bonusDistance); 100 | Letter[] memory filledWord = new Letter[](bonusDistance); 101 | for (uint256 i; i < bonusDistance; i++) { 102 | playWord[i] = Letter.A; 103 | filledWord[i] = Letter.A; 104 | } 105 | playWord[bonusDistance - 1] = Letter.EMPTY; 106 | 107 | Coord memory coord = Coord({ x: 0, y: 0 }); 108 | Direction direction = Direction.LEFT_TO_RIGHT; 109 | uint32 points = LibPoints.getWordPoints(playWord, filledWord, coord, direction); 110 | 111 | uint32 truePoints = bonusDistance; 112 | Bonus memory bonus = LibBonus.getTileBonus(coord); 113 | if (bonus.bonusType == BonusType.MULTIPLY_WORD) { 114 | truePoints *= bonus.bonusValue; 115 | } else { 116 | truePoints += bonus.bonusValue - 1; 117 | } 118 | assertEq(points, truePoints); 119 | } 120 | 121 | /// forge-config: default.fuzz.runs = 300 122 | function testFuzz_GetWordPointsNoBonus(uint8[] memory playWordRaw, bool directionRaw) public { 123 | Letter[] memory initialWord = new Letter[](1); 124 | initialWord[0] = Letter.A; 125 | uint16 bonusDistance = 3; 126 | uint32[26] memory initialLetterAllocation; 127 | world.start({ 128 | initialWord: initialWord, 129 | initialLetterAllocation: initialLetterAllocation, 130 | initialLettersTo: address(0), 131 | merkleRoot: bytes32(0), 132 | initialPrice: 0.001 ether, 133 | claimRestrictionDurationBlocks: 0, 134 | priceConfig: PriceConfigData({ 135 | minPrice: 0.0001 ether, 136 | wadPriceIncreaseFactor: 1.3e18, 137 | wadPower: 0.2e18, 138 | wadScale: 3000e18 139 | }), 140 | feeConfig: FeeConfigData({ feeBps: 0, feeTaker: address(0) }), 141 | crossWordRewardFraction: 3, 142 | bonusDistance: 5, 143 | numDrawLetters: 20 144 | }); 145 | 146 | // If the word does not touch any bonus tiles, points are equal to the base point value 147 | uint256 minLength = playWordRaw.length < bonusDistance - 1 ? playWordRaw.length : bonusDistance - 2; 148 | 149 | Letter[] memory playWord = new Letter[](minLength); 150 | Letter[] memory filledWord = new Letter[](minLength); 151 | for (uint256 i; i < minLength; i++) { 152 | uint8 letter = playWordRaw[i]; 153 | if (letter > 26) { 154 | letter = 26; 155 | } 156 | playWord[i] = Letter(letter); 157 | if (playWordRaw[i] == 0) { 158 | filledWord[i] = Letter(1); 159 | } else { 160 | filledWord[i] = Letter(letter); 161 | } 162 | } 163 | Direction direction = Direction(directionRaw ? 1 : 0); 164 | Coord memory coord = direction == Direction.LEFT_TO_RIGHT ? Coord({ x: 1, y: 0 }) : Coord({ x: 0, y: 1 }); 165 | uint32 points = LibPoints.getWordPoints(playWord, filledWord, coord, direction); 166 | 167 | uint32 basePoints = 0; 168 | for (uint256 i; i < filledWord.length; i++) { 169 | basePoints += LibPoints.getBaseLetterPoints(filledWord[i]); 170 | } 171 | assertEq(points, basePoints); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/codegen/tables/TilePlayer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | // Import store internals 7 | import { IStore } from "@latticexyz/store/src/IStore.sol"; 8 | import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; 9 | import { StoreCore } from "@latticexyz/store/src/StoreCore.sol"; 10 | import { Bytes } from "@latticexyz/store/src/Bytes.sol"; 11 | import { Memory } from "@latticexyz/store/src/Memory.sol"; 12 | import { SliceLib } from "@latticexyz/store/src/Slice.sol"; 13 | import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol"; 14 | import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol"; 15 | import { Schema } from "@latticexyz/store/src/Schema.sol"; 16 | import { EncodedLengths, EncodedLengthsLib } from "@latticexyz/store/src/EncodedLengths.sol"; 17 | import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; 18 | 19 | library TilePlayer { 20 | // Hex below is the result of `WorldResourceIdLib.encode({ namespace: "", name: "TilePlayer", typeId: RESOURCE_TABLE });` 21 | ResourceId constant _tableId = ResourceId.wrap(0x7462000000000000000000000000000054696c65506c61796572000000000000); 22 | 23 | FieldLayout constant _fieldLayout = 24 | FieldLayout.wrap(0x0014010014000000000000000000000000000000000000000000000000000000); 25 | 26 | // Hex-encoded key schema of (int32, int32) 27 | Schema constant _keySchema = Schema.wrap(0x0008020023230000000000000000000000000000000000000000000000000000); 28 | // Hex-encoded value schema of (address) 29 | Schema constant _valueSchema = Schema.wrap(0x0014010061000000000000000000000000000000000000000000000000000000); 30 | 31 | /** 32 | * @notice Get the table's key field names. 33 | * @return keyNames An array of strings with the names of key fields. 34 | */ 35 | function getKeyNames() internal pure returns (string[] memory keyNames) { 36 | keyNames = new string[](2); 37 | keyNames[0] = "x"; 38 | keyNames[1] = "y"; 39 | } 40 | 41 | /** 42 | * @notice Get the table's value field names. 43 | * @return fieldNames An array of strings with the names of value fields. 44 | */ 45 | function getFieldNames() internal pure returns (string[] memory fieldNames) { 46 | fieldNames = new string[](1); 47 | fieldNames[0] = "value"; 48 | } 49 | 50 | /** 51 | * @notice Register the table with its config. 52 | */ 53 | function register() internal { 54 | StoreSwitch.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); 55 | } 56 | 57 | /** 58 | * @notice Register the table with its config. 59 | */ 60 | function _register() internal { 61 | StoreCore.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); 62 | } 63 | 64 | /** 65 | * @notice Get value. 66 | */ 67 | function getValue(int32 x, int32 y) internal view returns (address value) { 68 | bytes32[] memory _keyTuple = new bytes32[](2); 69 | _keyTuple[0] = bytes32(uint256(int256(x))); 70 | _keyTuple[1] = bytes32(uint256(int256(y))); 71 | 72 | bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 73 | return (address(bytes20(_blob))); 74 | } 75 | 76 | /** 77 | * @notice Get value. 78 | */ 79 | function _getValue(int32 x, int32 y) internal view returns (address value) { 80 | bytes32[] memory _keyTuple = new bytes32[](2); 81 | _keyTuple[0] = bytes32(uint256(int256(x))); 82 | _keyTuple[1] = bytes32(uint256(int256(y))); 83 | 84 | bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 85 | return (address(bytes20(_blob))); 86 | } 87 | 88 | /** 89 | * @notice Get value. 90 | */ 91 | function get(int32 x, int32 y) internal view returns (address value) { 92 | bytes32[] memory _keyTuple = new bytes32[](2); 93 | _keyTuple[0] = bytes32(uint256(int256(x))); 94 | _keyTuple[1] = bytes32(uint256(int256(y))); 95 | 96 | bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 97 | return (address(bytes20(_blob))); 98 | } 99 | 100 | /** 101 | * @notice Get value. 102 | */ 103 | function _get(int32 x, int32 y) internal view returns (address value) { 104 | bytes32[] memory _keyTuple = new bytes32[](2); 105 | _keyTuple[0] = bytes32(uint256(int256(x))); 106 | _keyTuple[1] = bytes32(uint256(int256(y))); 107 | 108 | bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 109 | return (address(bytes20(_blob))); 110 | } 111 | 112 | /** 113 | * @notice Set value. 114 | */ 115 | function setValue(int32 x, int32 y, address value) internal { 116 | bytes32[] memory _keyTuple = new bytes32[](2); 117 | _keyTuple[0] = bytes32(uint256(int256(x))); 118 | _keyTuple[1] = bytes32(uint256(int256(y))); 119 | 120 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 121 | } 122 | 123 | /** 124 | * @notice Set value. 125 | */ 126 | function _setValue(int32 x, int32 y, address value) internal { 127 | bytes32[] memory _keyTuple = new bytes32[](2); 128 | _keyTuple[0] = bytes32(uint256(int256(x))); 129 | _keyTuple[1] = bytes32(uint256(int256(y))); 130 | 131 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 132 | } 133 | 134 | /** 135 | * @notice Set value. 136 | */ 137 | function set(int32 x, int32 y, address value) internal { 138 | bytes32[] memory _keyTuple = new bytes32[](2); 139 | _keyTuple[0] = bytes32(uint256(int256(x))); 140 | _keyTuple[1] = bytes32(uint256(int256(y))); 141 | 142 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 143 | } 144 | 145 | /** 146 | * @notice Set value. 147 | */ 148 | function _set(int32 x, int32 y, address value) internal { 149 | bytes32[] memory _keyTuple = new bytes32[](2); 150 | _keyTuple[0] = bytes32(uint256(int256(x))); 151 | _keyTuple[1] = bytes32(uint256(int256(y))); 152 | 153 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 154 | } 155 | 156 | /** 157 | * @notice Delete all data for given keys. 158 | */ 159 | function deleteRecord(int32 x, int32 y) internal { 160 | bytes32[] memory _keyTuple = new bytes32[](2); 161 | _keyTuple[0] = bytes32(uint256(int256(x))); 162 | _keyTuple[1] = bytes32(uint256(int256(y))); 163 | 164 | StoreSwitch.deleteRecord(_tableId, _keyTuple); 165 | } 166 | 167 | /** 168 | * @notice Delete all data for given keys. 169 | */ 170 | function _deleteRecord(int32 x, int32 y) internal { 171 | bytes32[] memory _keyTuple = new bytes32[](2); 172 | _keyTuple[0] = bytes32(uint256(int256(x))); 173 | _keyTuple[1] = bytes32(uint256(int256(y))); 174 | 175 | StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); 176 | } 177 | 178 | /** 179 | * @notice Tightly pack static (fixed length) data using this table's schema. 180 | * @return The static data, encoded into a sequence of bytes. 181 | */ 182 | function encodeStatic(address value) internal pure returns (bytes memory) { 183 | return abi.encodePacked(value); 184 | } 185 | 186 | /** 187 | * @notice Encode all of a record's fields. 188 | * @return The static (fixed length) data, encoded into a sequence of bytes. 189 | * @return The lengths of the dynamic fields (packed into a single bytes32 value). 190 | * @return The dynamic (variable length) data, encoded into a sequence of bytes. 191 | */ 192 | function encode(address value) internal pure returns (bytes memory, EncodedLengths, bytes memory) { 193 | bytes memory _staticData = encodeStatic(value); 194 | 195 | EncodedLengths _encodedLengths; 196 | bytes memory _dynamicData; 197 | 198 | return (_staticData, _encodedLengths, _dynamicData); 199 | } 200 | 201 | /** 202 | * @notice Encode keys as a bytes32 array using this table's field layout. 203 | */ 204 | function encodeKeyTuple(int32 x, int32 y) internal pure returns (bytes32[] memory) { 205 | bytes32[] memory _keyTuple = new bytes32[](2); 206 | _keyTuple[0] = bytes32(uint256(int256(x))); 207 | _keyTuple[1] = bytes32(uint256(int256(y))); 208 | 209 | return _keyTuple; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/codegen/tables/TileLetter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | // Import store internals 7 | import { IStore } from "@latticexyz/store/src/IStore.sol"; 8 | import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; 9 | import { StoreCore } from "@latticexyz/store/src/StoreCore.sol"; 10 | import { Bytes } from "@latticexyz/store/src/Bytes.sol"; 11 | import { Memory } from "@latticexyz/store/src/Memory.sol"; 12 | import { SliceLib } from "@latticexyz/store/src/Slice.sol"; 13 | import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol"; 14 | import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol"; 15 | import { Schema } from "@latticexyz/store/src/Schema.sol"; 16 | import { EncodedLengths, EncodedLengthsLib } from "@latticexyz/store/src/EncodedLengths.sol"; 17 | import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; 18 | 19 | // Import user types 20 | import { Letter } from "./../common.sol"; 21 | 22 | library TileLetter { 23 | // Hex below is the result of `WorldResourceIdLib.encode({ namespace: "", name: "TileLetter", typeId: RESOURCE_TABLE });` 24 | ResourceId constant _tableId = ResourceId.wrap(0x7462000000000000000000000000000054696c654c6574746572000000000000); 25 | 26 | FieldLayout constant _fieldLayout = 27 | FieldLayout.wrap(0x0001010001000000000000000000000000000000000000000000000000000000); 28 | 29 | // Hex-encoded key schema of (int32, int32) 30 | Schema constant _keySchema = Schema.wrap(0x0008020023230000000000000000000000000000000000000000000000000000); 31 | // Hex-encoded value schema of (uint8) 32 | Schema constant _valueSchema = Schema.wrap(0x0001010000000000000000000000000000000000000000000000000000000000); 33 | 34 | /** 35 | * @notice Get the table's key field names. 36 | * @return keyNames An array of strings with the names of key fields. 37 | */ 38 | function getKeyNames() internal pure returns (string[] memory keyNames) { 39 | keyNames = new string[](2); 40 | keyNames[0] = "x"; 41 | keyNames[1] = "y"; 42 | } 43 | 44 | /** 45 | * @notice Get the table's value field names. 46 | * @return fieldNames An array of strings with the names of value fields. 47 | */ 48 | function getFieldNames() internal pure returns (string[] memory fieldNames) { 49 | fieldNames = new string[](1); 50 | fieldNames[0] = "value"; 51 | } 52 | 53 | /** 54 | * @notice Register the table with its config. 55 | */ 56 | function register() internal { 57 | StoreSwitch.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); 58 | } 59 | 60 | /** 61 | * @notice Register the table with its config. 62 | */ 63 | function _register() internal { 64 | StoreCore.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); 65 | } 66 | 67 | /** 68 | * @notice Get value. 69 | */ 70 | function getValue(int32 x, int32 y) internal view returns (Letter value) { 71 | bytes32[] memory _keyTuple = new bytes32[](2); 72 | _keyTuple[0] = bytes32(uint256(int256(x))); 73 | _keyTuple[1] = bytes32(uint256(int256(y))); 74 | 75 | bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 76 | return Letter(uint8(bytes1(_blob))); 77 | } 78 | 79 | /** 80 | * @notice Get value. 81 | */ 82 | function _getValue(int32 x, int32 y) internal view returns (Letter value) { 83 | bytes32[] memory _keyTuple = new bytes32[](2); 84 | _keyTuple[0] = bytes32(uint256(int256(x))); 85 | _keyTuple[1] = bytes32(uint256(int256(y))); 86 | 87 | bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 88 | return Letter(uint8(bytes1(_blob))); 89 | } 90 | 91 | /** 92 | * @notice Get value. 93 | */ 94 | function get(int32 x, int32 y) internal view returns (Letter value) { 95 | bytes32[] memory _keyTuple = new bytes32[](2); 96 | _keyTuple[0] = bytes32(uint256(int256(x))); 97 | _keyTuple[1] = bytes32(uint256(int256(y))); 98 | 99 | bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 100 | return Letter(uint8(bytes1(_blob))); 101 | } 102 | 103 | /** 104 | * @notice Get value. 105 | */ 106 | function _get(int32 x, int32 y) internal view returns (Letter value) { 107 | bytes32[] memory _keyTuple = new bytes32[](2); 108 | _keyTuple[0] = bytes32(uint256(int256(x))); 109 | _keyTuple[1] = bytes32(uint256(int256(y))); 110 | 111 | bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 112 | return Letter(uint8(bytes1(_blob))); 113 | } 114 | 115 | /** 116 | * @notice Set value. 117 | */ 118 | function setValue(int32 x, int32 y, Letter value) internal { 119 | bytes32[] memory _keyTuple = new bytes32[](2); 120 | _keyTuple[0] = bytes32(uint256(int256(x))); 121 | _keyTuple[1] = bytes32(uint256(int256(y))); 122 | 123 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked(uint8(value)), _fieldLayout); 124 | } 125 | 126 | /** 127 | * @notice Set value. 128 | */ 129 | function _setValue(int32 x, int32 y, Letter value) internal { 130 | bytes32[] memory _keyTuple = new bytes32[](2); 131 | _keyTuple[0] = bytes32(uint256(int256(x))); 132 | _keyTuple[1] = bytes32(uint256(int256(y))); 133 | 134 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked(uint8(value)), _fieldLayout); 135 | } 136 | 137 | /** 138 | * @notice Set value. 139 | */ 140 | function set(int32 x, int32 y, Letter value) internal { 141 | bytes32[] memory _keyTuple = new bytes32[](2); 142 | _keyTuple[0] = bytes32(uint256(int256(x))); 143 | _keyTuple[1] = bytes32(uint256(int256(y))); 144 | 145 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked(uint8(value)), _fieldLayout); 146 | } 147 | 148 | /** 149 | * @notice Set value. 150 | */ 151 | function _set(int32 x, int32 y, Letter value) internal { 152 | bytes32[] memory _keyTuple = new bytes32[](2); 153 | _keyTuple[0] = bytes32(uint256(int256(x))); 154 | _keyTuple[1] = bytes32(uint256(int256(y))); 155 | 156 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked(uint8(value)), _fieldLayout); 157 | } 158 | 159 | /** 160 | * @notice Delete all data for given keys. 161 | */ 162 | function deleteRecord(int32 x, int32 y) internal { 163 | bytes32[] memory _keyTuple = new bytes32[](2); 164 | _keyTuple[0] = bytes32(uint256(int256(x))); 165 | _keyTuple[1] = bytes32(uint256(int256(y))); 166 | 167 | StoreSwitch.deleteRecord(_tableId, _keyTuple); 168 | } 169 | 170 | /** 171 | * @notice Delete all data for given keys. 172 | */ 173 | function _deleteRecord(int32 x, int32 y) internal { 174 | bytes32[] memory _keyTuple = new bytes32[](2); 175 | _keyTuple[0] = bytes32(uint256(int256(x))); 176 | _keyTuple[1] = bytes32(uint256(int256(y))); 177 | 178 | StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); 179 | } 180 | 181 | /** 182 | * @notice Tightly pack static (fixed length) data using this table's schema. 183 | * @return The static data, encoded into a sequence of bytes. 184 | */ 185 | function encodeStatic(Letter value) internal pure returns (bytes memory) { 186 | return abi.encodePacked(value); 187 | } 188 | 189 | /** 190 | * @notice Encode all of a record's fields. 191 | * @return The static (fixed length) data, encoded into a sequence of bytes. 192 | * @return The lengths of the dynamic fields (packed into a single bytes32 value). 193 | * @return The dynamic (variable length) data, encoded into a sequence of bytes. 194 | */ 195 | function encode(Letter value) internal pure returns (bytes memory, EncodedLengths, bytes memory) { 196 | bytes memory _staticData = encodeStatic(value); 197 | 198 | EncodedLengths _encodedLengths; 199 | bytes memory _dynamicData; 200 | 201 | return (_staticData, _encodedLengths, _dynamicData); 202 | } 203 | 204 | /** 205 | * @notice Encode keys as a bytes32 array using this table's field layout. 206 | */ 207 | function encodeKeyTuple(int32 x, int32 y) internal pure returns (bytes32[] memory) { 208 | bytes32[] memory _keyTuple = new bytes32[](2); 209 | _keyTuple[0] = bytes32(uint256(int256(x))); 210 | _keyTuple[1] = bytes32(uint256(int256(y))); 211 | 212 | return _keyTuple; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /scripts/transferLetters.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | Chain, 4 | Hex, 5 | PublicClient, 6 | WalletClient, 7 | createPublicClient, 8 | createWalletClient, 9 | getAddress, 10 | http, 11 | } from "viem"; 12 | import IWorldAbi from "../out/IWorld.sol/IWorld.abi.json"; 13 | import worlds from "../worlds.json"; 14 | import { Command } from "commander"; 15 | import { baseSepolia } from "viem/chains"; 16 | import { chainConfig } from "viem/op-stack"; 17 | import { privateKeyToAccount } from "viem/accounts"; 18 | import { parse } from "csv-parse"; 19 | import { readFile } from "fs/promises"; 20 | import { createReadStream } from "fs"; 21 | 22 | const redstone = { 23 | ...chainConfig, 24 | id: 690, 25 | sourceId: 1, 26 | name: "Redstone", 27 | nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, 28 | rpcUrls: { 29 | default: { 30 | http: ["https://rpc.redstonechain.com"], 31 | webSocket: ["wss://rpc.redstonechain.com"], 32 | }, 33 | }, 34 | blockExplorers: { 35 | default: { 36 | name: "Blockscout", 37 | url: "https://explorer.redstone.xyz", 38 | }, 39 | }, 40 | }; 41 | 42 | const SUPPORTED_CHAINS: Chain[] = [redstone, baseSepolia]; 43 | 44 | const getStringLetter = (s: string) => { 45 | if (s.length === 0) { 46 | return 0; 47 | } 48 | return s.charCodeAt(0) - 96; 49 | }; 50 | 51 | const getLetterString = (letter: number) => { 52 | if (letter < 1 || letter > 26) { 53 | throw new Error("[Get Letter String] letter must be between 1 and 26"); 54 | } 55 | 56 | return String.fromCharCode(64 + letter); 57 | }; 58 | 59 | const transferLetters = async ( 60 | letters: number[], 61 | to: Address, 62 | walletClient: WalletClient, 63 | publicClient: PublicClient, 64 | chain: Chain, 65 | worldAddress: Address, 66 | ) => { 67 | if (!walletClient.account) { 68 | throw new Error("[Transfer Letters] Account is not connected"); 69 | } 70 | 71 | console.log( 72 | `[Transfer Letters] transferring letters ${letters.map(getLetterString).join(",")} to ${to} on ${chain.name} chain`, 73 | ); 74 | 75 | const tx = await walletClient.writeContract({ 76 | address: worldAddress, 77 | chain, 78 | abi: IWorldAbi, 79 | functionName: "transfer", 80 | args: [letters, to], 81 | account: walletClient.account!, 82 | }); 83 | 84 | const receipt = await publicClient.waitForTransactionReceipt({ hash: tx }); 85 | 86 | if (receipt.status !== "success") { 87 | console.log("[Transfer Letters] Transaction failed", receipt); 88 | throw new Error(`[Transfer Letters] Transaction failed: ${receipt}`); 89 | } 90 | 91 | console.log( 92 | `[Transfer Letters] Transferped letters ${letters.map(getLetterString).join(",")} to ${to} on ${chain.name} chain (tx hash: ${tx})`, 93 | ); 94 | }; 95 | 96 | type Transfer = { 97 | player: Address; 98 | letters: number[]; 99 | }; 100 | 101 | const processTransfers = async ( 102 | transfers: Transfer[], 103 | walletClient: WalletClient, 104 | publicClient: PublicClient, 105 | chain: Chain, 106 | worldAddress: Address, 107 | ) => { 108 | console.log( 109 | `[Transfer Letters] transferring ${transfers.length} times on ${chain.name} chain`, 110 | ); 111 | for (const transfer of transfers) { 112 | await transferLetters( 113 | transfer.letters, 114 | transfer.player, 115 | walletClient, 116 | publicClient, 117 | chain, 118 | worldAddress, 119 | ); 120 | } 121 | console.log( 122 | `[Transfer Letters] Transferped ${transfers.length} times on ${chain.name} chain`, 123 | ); 124 | }; 125 | 126 | const getPlayerCSVAddresses = async (csv: string): Promise => { 127 | const promise = new Promise((resolve, reject) => { 128 | const addresses: Address[] = []; 129 | createReadStream(csv) 130 | .pipe(parse({ columns: true })) 131 | .on("data", (row) => { 132 | try { 133 | let rowAddress = row.address; 134 | if (!rowAddress.startsWith("0x")) { 135 | rowAddress = `0x${rowAddress}`; 136 | } 137 | addresses.push(getAddress(rowAddress)); 138 | } catch (e) { 139 | console.error( 140 | `[Get Player CSV Addresses] Error parsing row ${row}`, 141 | e, 142 | ); 143 | } 144 | }) 145 | .on("end", () => { 146 | resolve(addresses); 147 | }); 148 | }); 149 | return promise; 150 | }; 151 | 152 | const program = new Command(); 153 | 154 | program 155 | .name("transfer-letters") 156 | .description("Transfer letters to a players") 157 | .argument("", "Chain to transfer letters on") 158 | .argument("", "Wallet private key") 159 | .option("-l, --letters ", "List of letters to transfer") 160 | .option( 161 | "-r, --random ", 162 | "Randomly choose some letters from the letter list instead of transferring all", 163 | ) 164 | .option("--playersCsv ", "CSV file containing player addresses") 165 | .option( 166 | "--player ", 167 | "Player address to transfer to, transfer to one player", 168 | ) 169 | .action( 170 | async ( 171 | chainId: string, 172 | walletPrivateKey: string, 173 | options: { 174 | letters?: string; 175 | random?: string; 176 | playersCsv?: string; 177 | player?: string; 178 | }, 179 | ) => { 180 | // @ts-expect-error 181 | const worldAddressRaw: string | undefined = worlds[chainId]?.address; 182 | if (!worldAddressRaw) { 183 | throw new Error( 184 | `[Transfer Letters] No world address found for chain ${chainId}. Did you run \`mud deploy\`?`, 185 | ); 186 | } 187 | 188 | const worldAddress = getAddress(worldAddressRaw); 189 | 190 | const chain = SUPPORTED_CHAINS.find((c) => c.id.toString() === chainId); 191 | if (!chain) { 192 | throw new Error(`[Transfer Letters] Chain ${chainId} is not supported`); 193 | } 194 | 195 | const letters = 196 | options.letters 197 | ?.split("") 198 | .map((l) => getStringLetter(l.toLowerCase())) ?? 199 | Array.from({ length: 26 }, (_, i) => i + 1); 200 | console.log( 201 | `[Transfer Letters] transferring with letter list ${letters.map(getLetterString).join(",")}`, 202 | ); 203 | 204 | const players: Address[] = []; 205 | if (options.player) { 206 | players.push(getAddress(options.player)); 207 | console.log( 208 | `[Transfer Letters] transferring to player ${options.player}`, 209 | ); 210 | } else if (options.playersCsv) { 211 | const csvPlayers = await getPlayerCSVAddresses(options.playersCsv); 212 | players.push(...csvPlayers); 213 | console.log( 214 | `[Transfer Letters] transferring to ${csvPlayers.length} players from CSV`, 215 | ); 216 | } else { 217 | throw Error( 218 | "[Transfer Letters] No player specified, specify player or playersCsv", 219 | ); 220 | } 221 | 222 | const transfers: Transfer[] = []; 223 | if (options.random) { 224 | const numLetters = parseInt(options.random); 225 | for (const player of players) { 226 | const randomLetters = letters 227 | .sort(() => Math.random() - 0.5) 228 | .slice(0, numLetters); 229 | transfers.push({ player, letters: randomLetters }); 230 | } 231 | console.log( 232 | `[Transfer Letters] transferring ${numLetters} random letter(s) to ${players.length} players`, 233 | ); 234 | } else { 235 | for (const player of players) { 236 | transfers.push({ player, letters }); 237 | } 238 | console.log( 239 | `[Transfer Letters] transferring all letters in list to ${players.length} players`, 240 | ); 241 | } 242 | 243 | const privateKeyAccount = privateKeyToAccount(walletPrivateKey as Hex); 244 | 245 | const walletClient = createWalletClient({ 246 | chain, 247 | account: privateKeyAccount, 248 | transport: http(), 249 | }); 250 | const publicClient = createPublicClient({ chain, transport: http() }); 251 | await processTransfers( 252 | transfers, 253 | walletClient, 254 | publicClient, 255 | chain, 256 | worldAddress, 257 | ); 258 | }, 259 | ); 260 | 261 | program.parse(); 262 | -------------------------------------------------------------------------------- /src/libraries/LibPoints.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | import { BonusType, Direction, Letter } from "codegen/common.sol"; 5 | import { GameConfig, Points, PointsUpdate } from "codegen/index.sol"; 6 | import { Bonus } from "common/Bonus.sol"; 7 | import { Bound } from "common/Bound.sol"; 8 | import { SINGLETON_ADDRESS } from "common/Constants.sol"; 9 | import { Coord } from "common/Coord.sol"; 10 | 11 | import { NoPointsForEmptyLetter } from "common/Errors.sol"; 12 | import { LibBoard } from "libraries/LibBoard.sol"; 13 | import { LibBonus } from "libraries/LibBonus.sol"; 14 | import { LibPlayer } from "libraries/LibPlayer.sol"; 15 | 16 | library LibPoints { 17 | /// @notice Updates the score for a player for the main word and cross words 18 | function setScore( 19 | Letter[] memory playWord, 20 | Letter[] memory filledWord, 21 | Coord memory start, 22 | Direction direction, 23 | Bound[] memory bounds, 24 | address player, 25 | uint256 playUpdateId 26 | ) 27 | internal 28 | returns (uint32) 29 | { 30 | uint32 points = getPoints({ 31 | playWord: playWord, 32 | filledWord: filledWord, 33 | start: start, 34 | direction: direction, 35 | bounds: bounds 36 | }); 37 | LibPlayer.incrementPoints({ player: player, increment: points }); 38 | PointsUpdate.set({ id: playUpdateId, player: player, pointsId: -1, points: points }); 39 | return points; 40 | } 41 | 42 | function getPoints( 43 | Letter[] memory playWord, 44 | Letter[] memory filledWord, 45 | Coord memory start, 46 | Direction direction, 47 | Bound[] memory bounds 48 | ) 49 | internal 50 | view 51 | returns (uint32) 52 | { 53 | uint32 points = getWordPoints({ word: playWord, filledWord: filledWord, start: start, direction: direction }); 54 | 55 | // Count points for cross words (double counts by design) 56 | Direction crossDirection = 57 | direction == Direction.LEFT_TO_RIGHT ? Direction.TOP_TO_BOTTOM : Direction.LEFT_TO_RIGHT; 58 | for (uint256 i; i < playWord.length; i++) { 59 | Letter letter = playWord[i]; 60 | if (letter == Letter.EMPTY) { 61 | continue; 62 | } 63 | uint16 positive = bounds[i].positive; 64 | uint16 negative = bounds[i].negative; 65 | if (positive == 0 && negative == 0) { 66 | continue; 67 | } 68 | 69 | Coord memory letterCoord = 70 | LibBoard.getRelativeCoord({ startCoord: start, distance: int32(uint32(i)), direction: direction }); 71 | 72 | Letter[] memory crossFilledWord = LibBoard.getCrossWord({ 73 | letterCoord: letterCoord, 74 | letter: letter, 75 | wordDirection: direction, 76 | bound: bounds[i] 77 | }); 78 | Letter[] memory crossPlayWord = new Letter[](crossFilledWord.length); 79 | for (uint256 j; j < crossFilledWord.length; j++) { 80 | if (j == negative) { 81 | crossPlayWord[j] = letter; 82 | } else { 83 | crossPlayWord[j] = Letter.EMPTY; 84 | } 85 | } 86 | 87 | Coord memory crossStart = LibBoard.getRelativeCoord({ 88 | startCoord: letterCoord, 89 | distance: -1 * int32(uint32(negative)), 90 | direction: crossDirection 91 | }); 92 | 93 | points += getWordPoints({ 94 | word: crossPlayWord, 95 | filledWord: crossFilledWord, 96 | start: crossStart, 97 | direction: crossDirection 98 | }); 99 | } 100 | return points; 101 | } 102 | 103 | function setBuildsOnWordRewards(uint32 points, address[] memory buildsOnPlayers, uint256 playUpdateId) internal { 104 | if (buildsOnPlayers.length == 0) { 105 | return; 106 | } 107 | 108 | uint32 rewardPoints = points / GameConfig.getCrossWordRewardFraction() / uint32(buildsOnPlayers.length); 109 | 110 | for (uint256 i; i < buildsOnPlayers.length; i++) { 111 | if (buildsOnPlayers[i] != address(0)) { 112 | address player = buildsOnPlayers[i]; 113 | LibPlayer.incrementPoints({ player: player, increment: rewardPoints }); 114 | PointsUpdate.set({ id: playUpdateId, player: player, pointsId: int16(uint16(i)), points: rewardPoints }); 115 | } 116 | } 117 | } 118 | 119 | function getTotalPoints() internal view returns (uint32) { 120 | return Points.get({ player: SINGLETON_ADDRESS }); 121 | } 122 | 123 | function getWordPoints( 124 | Letter[] memory word, 125 | Letter[] memory filledWord, 126 | Coord memory start, 127 | Direction direction 128 | ) 129 | internal 130 | view 131 | returns (uint32) 132 | { 133 | uint32 points = 0; 134 | uint32 multiplier = 1; 135 | 136 | for (uint256 i; i < word.length; i++) { 137 | Coord memory letterCoord = 138 | LibBoard.getRelativeCoord({ startCoord: start, distance: int32(uint32(i)), direction: direction }); 139 | if ( 140 | word[i] != Letter.EMPTY 141 | && LibBonus.isBonusTile({ coord: letterCoord, bonusDistance: GameConfig.getBonusDistance() }) 142 | ) { 143 | Bonus memory bonus = LibBonus.getTileBonus({ coord: letterCoord }); 144 | if (bonus.bonusType == BonusType.MULTIPLY_WORD) { 145 | multiplier *= bonus.bonusValue; 146 | } 147 | 148 | points += getBonusLetterPoints({ letter: word[i], bonus: bonus }); 149 | } else { 150 | points += getBaseLetterPoints({ letter: filledWord[i] }); 151 | } 152 | } 153 | 154 | return points * multiplier; 155 | } 156 | 157 | function getBonusLetterPoints(Letter letter, Bonus memory bonus) internal pure returns (uint32) { 158 | uint32 basePoints = getBaseLetterPoints({ letter: letter }); 159 | if (bonus.bonusType == BonusType.MULTIPLY_LETTER) { 160 | return basePoints * bonus.bonusValue; 161 | } 162 | return basePoints; 163 | } 164 | 165 | function getBaseLetterPoints(Letter letter) internal pure returns (uint32) { 166 | if (letter == Letter.A) { 167 | return 1; 168 | } else if (letter == Letter.B) { 169 | return 3; 170 | } else if (letter == Letter.C) { 171 | return 3; 172 | } else if (letter == Letter.D) { 173 | return 2; 174 | } else if (letter == Letter.E) { 175 | return 1; 176 | } else if (letter == Letter.F) { 177 | return 4; 178 | } else if (letter == Letter.G) { 179 | return 2; 180 | } else if (letter == Letter.H) { 181 | return 4; 182 | } else if (letter == Letter.I) { 183 | return 1; 184 | } else if (letter == Letter.J) { 185 | return 8; 186 | } else if (letter == Letter.K) { 187 | return 5; 188 | } else if (letter == Letter.L) { 189 | return 1; 190 | } else if (letter == Letter.M) { 191 | return 3; 192 | } else if (letter == Letter.N) { 193 | return 1; 194 | } else if (letter == Letter.O) { 195 | return 1; 196 | } else if (letter == Letter.P) { 197 | return 3; 198 | } else if (letter == Letter.Q) { 199 | return 10; 200 | } else if (letter == Letter.R) { 201 | return 1; 202 | } else if (letter == Letter.S) { 203 | return 1; 204 | } else if (letter == Letter.T) { 205 | return 1; 206 | } else if (letter == Letter.U) { 207 | return 1; 208 | } else if (letter == Letter.V) { 209 | return 4; 210 | } else if (letter == Letter.W) { 211 | return 3; 212 | } else if (letter == Letter.X) { 213 | return 8; 214 | } else if (letter == Letter.Y) { 215 | return 4; 216 | } else if (letter == Letter.Z) { 217 | return 10; 218 | } 219 | revert NoPointsForEmptyLetter(); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/codegen/tables/PlayerLetters.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | // Import store internals 7 | import { IStore } from "@latticexyz/store/src/IStore.sol"; 8 | import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; 9 | import { StoreCore } from "@latticexyz/store/src/StoreCore.sol"; 10 | import { Bytes } from "@latticexyz/store/src/Bytes.sol"; 11 | import { Memory } from "@latticexyz/store/src/Memory.sol"; 12 | import { SliceLib } from "@latticexyz/store/src/Slice.sol"; 13 | import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol"; 14 | import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol"; 15 | import { Schema } from "@latticexyz/store/src/Schema.sol"; 16 | import { EncodedLengths, EncodedLengthsLib } from "@latticexyz/store/src/EncodedLengths.sol"; 17 | import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; 18 | 19 | // Import user types 20 | import { Letter } from "./../common.sol"; 21 | 22 | library PlayerLetters { 23 | // Hex below is the result of `WorldResourceIdLib.encode({ namespace: "", name: "PlayerLetters", typeId: RESOURCE_TABLE });` 24 | ResourceId constant _tableId = ResourceId.wrap(0x74620000000000000000000000000000506c617965724c657474657273000000); 25 | 26 | FieldLayout constant _fieldLayout = 27 | FieldLayout.wrap(0x0004010004000000000000000000000000000000000000000000000000000000); 28 | 29 | // Hex-encoded key schema of (address, uint8) 30 | Schema constant _keySchema = Schema.wrap(0x0015020061000000000000000000000000000000000000000000000000000000); 31 | // Hex-encoded value schema of (uint32) 32 | Schema constant _valueSchema = Schema.wrap(0x0004010003000000000000000000000000000000000000000000000000000000); 33 | 34 | /** 35 | * @notice Get the table's key field names. 36 | * @return keyNames An array of strings with the names of key fields. 37 | */ 38 | function getKeyNames() internal pure returns (string[] memory keyNames) { 39 | keyNames = new string[](2); 40 | keyNames[0] = "player"; 41 | keyNames[1] = "letter"; 42 | } 43 | 44 | /** 45 | * @notice Get the table's value field names. 46 | * @return fieldNames An array of strings with the names of value fields. 47 | */ 48 | function getFieldNames() internal pure returns (string[] memory fieldNames) { 49 | fieldNames = new string[](1); 50 | fieldNames[0] = "value"; 51 | } 52 | 53 | /** 54 | * @notice Register the table with its config. 55 | */ 56 | function register() internal { 57 | StoreSwitch.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); 58 | } 59 | 60 | /** 61 | * @notice Register the table with its config. 62 | */ 63 | function _register() internal { 64 | StoreCore.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); 65 | } 66 | 67 | /** 68 | * @notice Get value. 69 | */ 70 | function getValue(address player, Letter letter) internal view returns (uint32 value) { 71 | bytes32[] memory _keyTuple = new bytes32[](2); 72 | _keyTuple[0] = bytes32(uint256(uint160(player))); 73 | _keyTuple[1] = bytes32(uint256(uint8(letter))); 74 | 75 | bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 76 | return (uint32(bytes4(_blob))); 77 | } 78 | 79 | /** 80 | * @notice Get value. 81 | */ 82 | function _getValue(address player, Letter letter) internal view returns (uint32 value) { 83 | bytes32[] memory _keyTuple = new bytes32[](2); 84 | _keyTuple[0] = bytes32(uint256(uint160(player))); 85 | _keyTuple[1] = bytes32(uint256(uint8(letter))); 86 | 87 | bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 88 | return (uint32(bytes4(_blob))); 89 | } 90 | 91 | /** 92 | * @notice Get value. 93 | */ 94 | function get(address player, Letter letter) internal view returns (uint32 value) { 95 | bytes32[] memory _keyTuple = new bytes32[](2); 96 | _keyTuple[0] = bytes32(uint256(uint160(player))); 97 | _keyTuple[1] = bytes32(uint256(uint8(letter))); 98 | 99 | bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 100 | return (uint32(bytes4(_blob))); 101 | } 102 | 103 | /** 104 | * @notice Get value. 105 | */ 106 | function _get(address player, Letter letter) internal view returns (uint32 value) { 107 | bytes32[] memory _keyTuple = new bytes32[](2); 108 | _keyTuple[0] = bytes32(uint256(uint160(player))); 109 | _keyTuple[1] = bytes32(uint256(uint8(letter))); 110 | 111 | bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 112 | return (uint32(bytes4(_blob))); 113 | } 114 | 115 | /** 116 | * @notice Set value. 117 | */ 118 | function setValue(address player, Letter letter, uint32 value) internal { 119 | bytes32[] memory _keyTuple = new bytes32[](2); 120 | _keyTuple[0] = bytes32(uint256(uint160(player))); 121 | _keyTuple[1] = bytes32(uint256(uint8(letter))); 122 | 123 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 124 | } 125 | 126 | /** 127 | * @notice Set value. 128 | */ 129 | function _setValue(address player, Letter letter, uint32 value) internal { 130 | bytes32[] memory _keyTuple = new bytes32[](2); 131 | _keyTuple[0] = bytes32(uint256(uint160(player))); 132 | _keyTuple[1] = bytes32(uint256(uint8(letter))); 133 | 134 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 135 | } 136 | 137 | /** 138 | * @notice Set value. 139 | */ 140 | function set(address player, Letter letter, uint32 value) internal { 141 | bytes32[] memory _keyTuple = new bytes32[](2); 142 | _keyTuple[0] = bytes32(uint256(uint160(player))); 143 | _keyTuple[1] = bytes32(uint256(uint8(letter))); 144 | 145 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 146 | } 147 | 148 | /** 149 | * @notice Set value. 150 | */ 151 | function _set(address player, Letter letter, uint32 value) internal { 152 | bytes32[] memory _keyTuple = new bytes32[](2); 153 | _keyTuple[0] = bytes32(uint256(uint160(player))); 154 | _keyTuple[1] = bytes32(uint256(uint8(letter))); 155 | 156 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); 157 | } 158 | 159 | /** 160 | * @notice Delete all data for given keys. 161 | */ 162 | function deleteRecord(address player, Letter letter) internal { 163 | bytes32[] memory _keyTuple = new bytes32[](2); 164 | _keyTuple[0] = bytes32(uint256(uint160(player))); 165 | _keyTuple[1] = bytes32(uint256(uint8(letter))); 166 | 167 | StoreSwitch.deleteRecord(_tableId, _keyTuple); 168 | } 169 | 170 | /** 171 | * @notice Delete all data for given keys. 172 | */ 173 | function _deleteRecord(address player, Letter letter) internal { 174 | bytes32[] memory _keyTuple = new bytes32[](2); 175 | _keyTuple[0] = bytes32(uint256(uint160(player))); 176 | _keyTuple[1] = bytes32(uint256(uint8(letter))); 177 | 178 | StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); 179 | } 180 | 181 | /** 182 | * @notice Tightly pack static (fixed length) data using this table's schema. 183 | * @return The static data, encoded into a sequence of bytes. 184 | */ 185 | function encodeStatic(uint32 value) internal pure returns (bytes memory) { 186 | return abi.encodePacked(value); 187 | } 188 | 189 | /** 190 | * @notice Encode all of a record's fields. 191 | * @return The static (fixed length) data, encoded into a sequence of bytes. 192 | * @return The lengths of the dynamic fields (packed into a single bytes32 value). 193 | * @return The dynamic (variable length) data, encoded into a sequence of bytes. 194 | */ 195 | function encode(uint32 value) internal pure returns (bytes memory, EncodedLengths, bytes memory) { 196 | bytes memory _staticData = encodeStatic(value); 197 | 198 | EncodedLengths _encodedLengths; 199 | bytes memory _dynamicData; 200 | 201 | return (_staticData, _encodedLengths, _dynamicData); 202 | } 203 | 204 | /** 205 | * @notice Encode keys as a bytes32 array using this table's field layout. 206 | */ 207 | function encodeKeyTuple(address player, Letter letter) internal pure returns (bytes32[] memory) { 208 | bytes32[] memory _keyTuple = new bytes32[](2); 209 | _keyTuple[0] = bytes32(uint256(uint160(player))); 210 | _keyTuple[1] = bytes32(uint256(uint8(letter))); 211 | 212 | return _keyTuple; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/test/integration/CrossWordNoEmpty.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | /* solhint-disable no-global-import */ 4 | /* solhint-disable func-name-mixedcase */ 5 | 6 | import { Words3Test } from "../Words3Test.t.sol"; 7 | import { Merkle } from "../murky/src/Merkle.sol"; 8 | import { Direction, Letter } from "codegen/common.sol"; 9 | import { FeeConfigData, Points, PriceConfigData } from "codegen/index.sol"; 10 | import { Bound } from "common/Bound.sol"; 11 | import { Coord } from "common/Coord.sol"; 12 | import "forge-std/Test.sol"; 13 | 14 | contract CrossWordNoEmpty is Words3Test { 15 | bytes32[] public words; 16 | Merkle private m; 17 | address private player1 = address(0xcafe); 18 | address private player2 = address(0xbabe); 19 | 20 | function setUp() public override { 21 | super.setUp(); 22 | 23 | m = new Merkle(); 24 | 25 | Letter[] memory jobe = new Letter[](4); 26 | jobe[0] = Letter.J; 27 | jobe[1] = Letter.O; 28 | jobe[2] = Letter.B; 29 | jobe[3] = Letter.E; 30 | 31 | Letter[] memory join = new Letter[](4); 32 | join[0] = Letter.J; 33 | join[1] = Letter.O; 34 | join[2] = Letter.I; 35 | join[3] = Letter.N; 36 | 37 | // Spelled wrong for testing purposes 38 | Letter[] memory colaborate = new Letter[](10); 39 | colaborate[0] = Letter.C; 40 | colaborate[1] = Letter.O; 41 | colaborate[2] = Letter.L; 42 | colaborate[3] = Letter.A; 43 | colaborate[4] = Letter.B; 44 | colaborate[5] = Letter.O; 45 | colaborate[6] = Letter.R; 46 | colaborate[7] = Letter.A; 47 | colaborate[8] = Letter.T; 48 | colaborate[9] = Letter.E; 49 | 50 | Letter[] memory ol = new Letter[](2); 51 | ol[0] = Letter.O; 52 | ol[1] = Letter.L; 53 | 54 | Letter[] memory eb = new Letter[](2); 55 | eb[0] = Letter.E; 56 | eb[1] = Letter.B; 57 | 58 | Letter[] memory coins = new Letter[](5); 59 | coins[0] = Letter.C; 60 | coins[1] = Letter.O; 61 | coins[2] = Letter.I; 62 | coins[3] = Letter.N; 63 | coins[4] = Letter.S; 64 | 65 | Letter[] memory joins = new Letter[](5); 66 | joins[0] = Letter.J; 67 | joins[1] = Letter.O; 68 | joins[2] = Letter.I; 69 | joins[3] = Letter.N; 70 | joins[4] = Letter.S; 71 | 72 | words.push(keccak256(bytes.concat(keccak256(abi.encode(jobe))))); // jobe 0 73 | words.push(keccak256(bytes.concat(keccak256(abi.encode(join))))); // join 1 74 | words.push(keccak256(bytes.concat(keccak256(abi.encode(colaborate))))); // colaborate 2 75 | words.push(keccak256(bytes.concat(keccak256(abi.encode(ol))))); // ol 3 76 | words.push(keccak256(bytes.concat(keccak256(abi.encode(eb))))); // eb 4 77 | words.push(keccak256(bytes.concat(keccak256(abi.encode(coins))))); // coins 5 78 | words.push(keccak256(bytes.concat(keccak256(abi.encode(joins))))); // joins 6 79 | 80 | setDefaultLetterOdds(); 81 | 82 | uint32[26] memory initialLetterAllocation; 83 | for (uint8 i = 0; i < 26; i++) { 84 | initialLetterAllocation[i] = 100; 85 | } 86 | 87 | Letter[] memory initialWord = new Letter[](5); 88 | initialWord[0] = Letter.B; 89 | initialWord[1] = Letter.A; 90 | initialWord[2] = Letter.T; 91 | initialWord[3] = Letter.E; 92 | initialWord[4] = Letter.S; 93 | 94 | world.start({ 95 | initialWord: initialWord, 96 | initialLetterAllocation: initialLetterAllocation, 97 | initialLettersTo: player1, 98 | merkleRoot: m.getRoot(words), 99 | initialPrice: 0.001 ether, 100 | claimRestrictionDurationBlocks: 0, 101 | priceConfig: PriceConfigData({ 102 | minPrice: 0.001 ether, 103 | wadPriceIncreaseFactor: 1.115e18, 104 | wadPower: 0.9e18, 105 | wadScale: 9.96e36 106 | }), 107 | feeConfig: FeeConfigData({ feeBps: 0, feeTaker: address(0) }), 108 | crossWordRewardFraction: 3, 109 | bonusDistance: 3, 110 | numDrawLetters: 7 111 | }); 112 | 113 | Letter[] memory lettersToTransfer = new Letter[](26 * 50); 114 | for (uint32 i = 1; i < 27; i++) { 115 | for (uint32 j = 0; j < 50; j++) { 116 | lettersToTransfer[(i - 1) * 50 + j] = Letter(i); 117 | } 118 | } 119 | vm.prank(player1); 120 | world.transfer({ letters: lettersToTransfer, to: player2 }); 121 | 122 | // play jobe 123 | vm.startPrank(player1); 124 | Letter[] memory playJobe = new Letter[](4); 125 | playJobe[0] = Letter.J; 126 | playJobe[1] = Letter.O; 127 | playJobe[2] = Letter.EMPTY; 128 | playJobe[3] = Letter.E; 129 | Bound[] memory jobeBounds = new Bound[](4); 130 | world.play({ 131 | word: playJobe, 132 | proof: m.getProof(words, 0), 133 | coord: Coord({ x: -2, y: -2 }), 134 | direction: Direction.TOP_TO_BOTTOM, 135 | bounds: jobeBounds 136 | }); 137 | vm.stopPrank(); 138 | assertEq(Points.get({ player: player1 }), 28); 139 | assertEq(Points.get({ player: player2 }), 0); 140 | assertEq(Points.get({ player: address(0) }), 28); 141 | 142 | // play join 143 | 144 | vm.startPrank(player2); 145 | Letter[] memory playJoin = new Letter[](4); 146 | playJoin[0] = Letter.EMPTY; 147 | playJoin[1] = Letter.O; 148 | playJoin[2] = Letter.I; 149 | playJoin[3] = Letter.N; 150 | Bound[] memory joinBounds = new Bound[](4); 151 | world.play({ 152 | word: playJoin, 153 | proof: m.getProof(words, 1), 154 | coord: Coord({ x: -2, y: -2 }), 155 | direction: Direction.LEFT_TO_RIGHT, 156 | bounds: joinBounds 157 | }); 158 | vm.stopPrank(); 159 | assertEq(Points.get({ player: player1 }), 32); 160 | assertEq(Points.get({ player: player2 }), 14); 161 | assertEq(Points.get({ player: address(0) }), 46); 162 | 163 | // Play colaborate 164 | vm.startPrank(player1); 165 | Letter[] memory playColaborate = new Letter[](10); 166 | playColaborate[0] = Letter.C; 167 | playColaborate[1] = Letter.EMPTY; 168 | playColaborate[2] = Letter.L; 169 | playColaborate[3] = Letter.EMPTY; 170 | playColaborate[4] = Letter.B; 171 | playColaborate[5] = Letter.O; 172 | playColaborate[6] = Letter.R; 173 | playColaborate[7] = Letter.A; 174 | playColaborate[8] = Letter.T; 175 | playColaborate[9] = Letter.E; 176 | Bound[] memory colaborateBounds = new Bound[](10); 177 | colaborateBounds[2] = Bound({ positive: 0, negative: 1, proof: m.getProof(words, 3) }); 178 | colaborateBounds[4] = Bound({ positive: 0, negative: 1, proof: m.getProof(words, 4) }); 179 | world.play({ 180 | word: playColaborate, 181 | proof: m.getProof(words, 2), 182 | coord: Coord({ x: -1, y: -3 }), 183 | direction: Direction.TOP_TO_BOTTOM, 184 | bounds: colaborateBounds 185 | }); 186 | vm.stopPrank(); 187 | assertEq(Points.get({ player: player1 }), 58); 188 | assertEq(Points.get({ player: player2 }), 16); 189 | assertEq(Points.get({ player: address(0) }), 74); 190 | } 191 | 192 | function test_CrossWordNoEmpty() public { 193 | vm.startPrank(player2); 194 | Letter[] memory playCoins = new Letter[](5); 195 | playCoins[0] = Letter.C; 196 | playCoins[1] = Letter.O; 197 | playCoins[2] = Letter.I; 198 | playCoins[3] = Letter.N; 199 | playCoins[4] = Letter.S; 200 | Bound[] memory coinsBounds = new Bound[](5); 201 | coinsBounds[4] = Bound({ positive: 0, negative: 4, proof: m.getProof(words, 6) }); 202 | world.play({ 203 | word: playCoins, 204 | proof: m.getProof(words, 5), 205 | coord: Coord({ x: 2, y: -6 }), 206 | direction: Direction.TOP_TO_BOTTOM, 207 | bounds: coinsBounds 208 | }); 209 | vm.stopPrank(); 210 | 211 | assertEq(Points.get({ player: player1 }), 58); 212 | assertEq(Points.get({ player: player2 }), 42); 213 | assertEq(Points.get({ player: address(0) }), 100); 214 | } 215 | 216 | function testFuzz_RevertsWhen_CrossWordNoEmptyIncorrectBounds( 217 | uint16 boundsPositive, 218 | uint16 boundsNegative 219 | ) 220 | public 221 | { 222 | vm.assume(boundsPositive != 0 || boundsNegative != 4); 223 | vm.startPrank(player2); 224 | Letter[] memory playCoins = new Letter[](5); 225 | playCoins[0] = Letter.C; 226 | playCoins[1] = Letter.O; 227 | playCoins[2] = Letter.I; 228 | playCoins[3] = Letter.N; 229 | playCoins[4] = Letter.S; 230 | Bound[] memory coinsBounds = new Bound[](5); 231 | coinsBounds[4] = Bound({ positive: boundsPositive, negative: boundsNegative, proof: m.getProof(words, 6) }); 232 | bytes32[] memory coinsProof = m.getProof(words, 5); 233 | vm.expectRevert(); 234 | world.play({ 235 | word: playCoins, 236 | proof: coinsProof, 237 | coord: Coord({ x: 2, y: -6 }), 238 | direction: Direction.TOP_TO_BOTTOM, 239 | bounds: coinsBounds 240 | }); 241 | vm.stopPrank(); 242 | } 243 | } 244 | --------------------------------------------------------------------------------