├── .eslintrc ├── .gitattributes ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── packages ├── client │ ├── .env.example │ ├── .eslintrc │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── public │ │ ├── favicon.svg │ │ ├── og-image.png │ │ └── sounds │ │ │ ├── backgroundMusic.mp3 │ │ │ ├── death.wav │ │ │ ├── direction.wav │ │ │ ├── down.wav │ │ │ ├── eat.wav │ │ │ ├── eatPlayer.wav │ │ │ ├── playerEaten.wav │ │ │ ├── powerPelletEat.wav │ │ │ ├── spawn.wav │ │ │ ├── up.wav │ │ │ ├── wallBounce.wav │ │ │ └── wallHit.wav │ ├── src │ │ ├── App.tsx │ │ ├── Game.tsx │ │ ├── GameUI.tsx │ │ ├── Lines.tsx │ │ ├── common.ts │ │ ├── fonts │ │ │ └── BerkeleyMonoVariable-Regular.woff2 │ │ ├── index.tsx │ │ ├── leaderboard-archive.json │ │ ├── mud │ │ │ └── stash.ts │ │ ├── styles.css │ │ └── utils │ │ │ ├── Countdown.tsx │ │ │ ├── DebugPanel.tsx │ │ │ ├── bigint.ts │ │ │ ├── bigintMinHeap.ts │ │ │ ├── button.tsx │ │ │ ├── chains.ts │ │ │ ├── chakra.tsx │ │ │ ├── debugging.ts │ │ │ ├── game │ │ │ ├── configLib.ts │ │ │ ├── entityLib.ts │ │ │ └── lineLib.ts │ │ │ ├── hooks.ts │ │ │ ├── icons.tsx │ │ │ ├── lineUI.ts │ │ │ ├── music.ts │ │ │ ├── pq96x160.ts │ │ │ ├── spawn.ts │ │ │ ├── sync.ts │ │ │ └── timeLib.ts │ ├── tsconfig.json │ └── vite.config.ts ├── contracts │ ├── .env │ ├── .gitignore │ ├── .prettierrc │ ├── .solhint.json │ ├── foundry.toml │ ├── mud.config.ts │ ├── out │ │ └── IWorld.sol │ │ │ ├── IWorld.abi.json │ │ │ ├── IWorld.abi.json.d.ts │ │ │ └── IWorld.json │ ├── package.json │ ├── remappings.txt │ ├── script │ │ ├── BalanceChanges.s.sol │ │ └── PostDeploy.s.sol │ ├── src │ │ ├── codegen │ │ │ ├── common.sol │ │ │ ├── index.sol │ │ │ ├── tables │ │ │ │ ├── Entity.sol │ │ │ │ ├── GameConfig.sol │ │ │ │ ├── GameState.sol │ │ │ │ ├── Line.sol │ │ │ │ ├── LineOffchain.sol │ │ │ │ ├── Player.sol │ │ │ │ ├── UsernameHash.sol │ │ │ │ └── UsernameOffchain.sol │ │ │ └── world │ │ │ │ ├── IAccessSystem.sol │ │ │ │ ├── IAdminSystem.sol │ │ │ │ ├── IDirectionSystem.sol │ │ │ │ ├── IJumpSystem.sol │ │ │ │ ├── ISpawnSystem.sol │ │ │ │ ├── IUtilitiesSystem.sol │ │ │ │ └── IWorld.sol │ │ ├── systems │ │ │ ├── AccessSystem.sol │ │ │ ├── AdminSystem.sol │ │ │ ├── DirectionSystem.sol │ │ │ ├── JumpSystem.sol │ │ │ ├── SpawnSystem.sol │ │ │ └── UtilitiesSystem.sol │ │ └── utils │ │ │ ├── ConfigLib.sol │ │ │ ├── EntityLib.sol │ │ │ ├── LineLib.sol │ │ │ ├── PriorityQueue96x160Lib.sol │ │ │ └── WadTimeLib.sol │ ├── test │ │ ├── DebugLib.sol │ │ ├── Handler.sol │ │ └── Invariants.t.sol │ ├── tsconfig.json │ ├── worlds.json │ └── worlds.json.d.ts ├── example-bot │ ├── .gitignore │ ├── package.json │ ├── src │ │ └── bot.ts │ └── tsconfig.json └── infra │ ├── .env.example │ ├── .gitignore │ ├── ecosystem.config.cjs │ ├── package.json │ ├── src │ ├── auth.ts │ ├── indexer.sh │ └── poke.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint"], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # suppress diffs for generated files 2 | **/pnpm-lock.yaml linguist-generated=true 3 | **/codegen/**/*.sol linguist-generated=true 4 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | tests: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | 11 | - name: Install Foundry 12 | uses: onbjerg/foundry-toolchain@v1 13 | with: 14 | version: nightly 15 | 16 | - name: Install pnpm 17 | uses: pnpm/action-setup@v4 18 | with: 19 | version: 9.6.0 20 | 21 | - name: Install dependencies 22 | run: pnpm install 23 | 24 | - name: Test contracts 25 | working-directory: packages/contracts 26 | run: pnpm test 27 | env: 28 | FORCE_DETERMINISTIC_TIMESTAMP: true # To ensure reproducible invariant failures. 29 | 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vercel 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 t11s 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 19 | OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RethMatch 2 | 3 | An onchain tournament for bots, inspired by Pac-Man and Agar.io. 4 | 5 | https://github.com/user-attachments/assets/28b4ef2f-8873-4251-8fe7-21390dd14751 6 | 7 | - Website: https://rethmatch.paradigm.xyz 8 | - Get Started: https://hackmd.io/@t11s/rethmatch 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rethmatch", 3 | "private": true, 4 | "scripts": { 5 | "build": "pnpm recursive run build", 6 | "dev:client": "pnpm --filter 'client' run dev", 7 | "deploy:contracts": "pnpm --filter 'contracts' deploy:odyssey", 8 | "mud:up": "pnpm mud set-version --tag main && pnpm install" 9 | }, 10 | "devDependencies": { 11 | "@latticexyz/cli": "2.2.22-8c4b624756007fd02b1c3c3494e34128a5f1c044", 12 | "@types/debug": "4.1.7", 13 | "@typescript-eslint/eslint-plugin": "7.1.1", 14 | "@typescript-eslint/parser": "7.1.1", 15 | "eslint": "8.57.0", 16 | "rimraf": "^3.0.2", 17 | "typescript": "5.4.2" 18 | }, 19 | "engines": { 20 | "node": "^18", 21 | "pnpm": "^8 || ^9" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/client/.env.example: -------------------------------------------------------------------------------- 1 | VITE_CHAIN_ID=911867 2 | VITE_CLERK_PUBLISHABLE_KEY=pk_test_.... -------------------------------------------------------------------------------- /packages/client/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc", "plugin:react/recommended", "plugin:react-hooks/recommended"], 3 | "plugins": ["react", "react-hooks"], 4 | "rules": { 5 | "react/react-in-jsx-scope": "off", 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /packages/client/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | .vercel 5 | .env -------------------------------------------------------------------------------- /packages/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | RethMatch 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /packages/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "type": "module", 7 | "scripts": { 8 | "build": "vite build", 9 | "dev": "vite", 10 | "preview": "vite preview", 11 | "test": "tsc --noEmit" 12 | }, 13 | "dependencies": { 14 | "@chakra-ui/icons": "^2.1.1", 15 | "@chakra-ui/react": "^2.8.2", 16 | "@clerk/clerk-react": "^5.31.8", 17 | "@emotion/react": "^11", 18 | "@emotion/styled": "^11", 19 | "@latticexyz/common": "2.2.22-8c4b624756007fd02b1c3c3494e34128a5f1c044", 20 | "@latticexyz/explorer": "2.2.22-8c4b624756007fd02b1c3c3494e34128a5f1c044", 21 | "@latticexyz/react": "2.2.22-8c4b624756007fd02b1c3c3494e34128a5f1c044", 22 | "@latticexyz/schema-type": "2.2.22-8c4b624756007fd02b1c3c3494e34128a5f1c044", 23 | "@latticexyz/stash": "2.2.22-8c4b624756007fd02b1c3c3494e34128a5f1c044", 24 | "@latticexyz/store-sync": "2.2.22-8c4b624756007fd02b1c3c3494e34128a5f1c044", 25 | "@latticexyz/utils": "2.2.22-8c4b624756007fd02b1c3c3494e34128a5f1c044", 26 | "@latticexyz/world": "2.2.22-8c4b624756007fd02b1c3c3494e34128a5f1c044", 27 | "@tanstack/react-query": "^5.63.0", 28 | "buffer": "^6.0.3", 29 | "contracts": "workspace:*", 30 | "fast-deep-equal": "^3.1.3", 31 | "framer-motion": "^6", 32 | "js-sha3": "^0.9.3", 33 | "react": "^18.2.0", 34 | "react-dom": "^18.2.0", 35 | "rxjs": "7.5.5", 36 | "string-to-unicode-variant": "^1.0.9", 37 | "viem": "^2.29.1", 38 | "wagmi": "^2.15.2" 39 | }, 40 | "devDependencies": { 41 | "@types/react": "18.2.22", 42 | "@types/react-dom": "18.2.7", 43 | "@vitejs/plugin-react": "^4.3.4", 44 | "eslint-plugin-react": "7.31.11", 45 | "eslint-plugin-react-hooks": "4.6.0", 46 | "vite": "^6.0.7", 47 | "vite-plugin-mud": "2.2.22-8c4b624756007fd02b1c3c3494e34128a5f1c044" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/client/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /packages/client/public/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paradigmxyz/rethmatch/60c7b4bdbd54d1757a8ca67adf2d69c6aaaaaf64/packages/client/public/og-image.png -------------------------------------------------------------------------------- /packages/client/public/sounds/backgroundMusic.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paradigmxyz/rethmatch/60c7b4bdbd54d1757a8ca67adf2d69c6aaaaaf64/packages/client/public/sounds/backgroundMusic.mp3 -------------------------------------------------------------------------------- /packages/client/public/sounds/death.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paradigmxyz/rethmatch/60c7b4bdbd54d1757a8ca67adf2d69c6aaaaaf64/packages/client/public/sounds/death.wav -------------------------------------------------------------------------------- /packages/client/public/sounds/direction.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paradigmxyz/rethmatch/60c7b4bdbd54d1757a8ca67adf2d69c6aaaaaf64/packages/client/public/sounds/direction.wav -------------------------------------------------------------------------------- /packages/client/public/sounds/down.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paradigmxyz/rethmatch/60c7b4bdbd54d1757a8ca67adf2d69c6aaaaaf64/packages/client/public/sounds/down.wav -------------------------------------------------------------------------------- /packages/client/public/sounds/eat.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paradigmxyz/rethmatch/60c7b4bdbd54d1757a8ca67adf2d69c6aaaaaf64/packages/client/public/sounds/eat.wav -------------------------------------------------------------------------------- /packages/client/public/sounds/eatPlayer.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paradigmxyz/rethmatch/60c7b4bdbd54d1757a8ca67adf2d69c6aaaaaf64/packages/client/public/sounds/eatPlayer.wav -------------------------------------------------------------------------------- /packages/client/public/sounds/playerEaten.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paradigmxyz/rethmatch/60c7b4bdbd54d1757a8ca67adf2d69c6aaaaaf64/packages/client/public/sounds/playerEaten.wav -------------------------------------------------------------------------------- /packages/client/public/sounds/powerPelletEat.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paradigmxyz/rethmatch/60c7b4bdbd54d1757a8ca67adf2d69c6aaaaaf64/packages/client/public/sounds/powerPelletEat.wav -------------------------------------------------------------------------------- /packages/client/public/sounds/spawn.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paradigmxyz/rethmatch/60c7b4bdbd54d1757a8ca67adf2d69c6aaaaaf64/packages/client/public/sounds/spawn.wav -------------------------------------------------------------------------------- /packages/client/public/sounds/up.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paradigmxyz/rethmatch/60c7b4bdbd54d1757a8ca67adf2d69c6aaaaaf64/packages/client/public/sounds/up.wav -------------------------------------------------------------------------------- /packages/client/public/sounds/wallBounce.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paradigmxyz/rethmatch/60c7b4bdbd54d1757a8ca67adf2d69c6aaaaaf64/packages/client/public/sounds/wallBounce.wav -------------------------------------------------------------------------------- /packages/client/public/sounds/wallHit.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paradigmxyz/rethmatch/60c7b4bdbd54d1757a8ca67adf2d69c6aaaaaf64/packages/client/public/sounds/wallHit.wav -------------------------------------------------------------------------------- /packages/client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { parseSyncStateGivenTables } from "./utils/sync"; 2 | import { Game } from "./Game"; 3 | import { Column, Row } from "./utils/chakra"; 4 | import { Logo } from "./utils/icons"; 5 | import { Text } from "@chakra-ui/react"; 6 | import { useStash } from "@latticexyz/stash/react"; 7 | import { stash } from "./mud/stash"; 8 | import fastDeepEqual from "fast-deep-equal"; 9 | 10 | export const App = () => { 11 | const { syncProgress, data } = useStash(stash, parseSyncStateGivenTables, { 12 | isEqual: fastDeepEqual, // TODO: Can maybe speed up by just looking at syncProgress? 13 | }); 14 | 15 | const lastSyncedTime = performance.now(); 16 | 17 | if (syncProgress.step === "live") 18 | console.log("[!] Caught up to live at:", Number(lastSyncedTime.toFixed(1))); 19 | 20 | return ( 21 | 29 | {!data ? ( 30 | 31 | 32 | 33 | Syncing 34 | . 35 | . 36 | .{" "} 37 | 38 | {syncProgress.message === "Got snapshot" || 39 | syncProgress.message === "Failed to get snapshot" 40 | ? "0.0" 41 | : syncProgress.percentage.toFixed(1)} 42 | 43 | % 44 | 45 | 46 | ) : ( 47 | 57 | )} 58 | 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /packages/client/src/Game.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | import { GameConfig } from "./utils/game/configLib"; 4 | import { DEBUG_ITER, DEBUG_LINE, DEBUG_PERF } from "./utils/debugging"; 5 | import { useInterval } from "./utils/hooks"; 6 | import { GameUI } from "./GameUI"; 7 | import { Column } from "./utils/chakra"; 8 | import { Text } from "@chakra-ui/react"; 9 | import { Logo } from "./utils/icons"; 10 | import { LiveState, forwardStateTo } from "./utils/sync"; 11 | import { useAccount } from "wagmi"; 12 | import { toEntityId } from "./utils/game/entityLib"; 13 | 14 | export function Game({ 15 | syncedState, 16 | gameConfig, 17 | }: { 18 | syncedState: LiveState; 19 | gameConfig: GameConfig; 20 | }) { 21 | if (DEBUG_LINE != null && syncedState.lines.length > 1) { 22 | if (Number.isNaN(DEBUG_LINE)) alert("DEBUG_LINE is NaN"); 23 | syncedState.lines = [syncedState.lines[DEBUG_LINE]]; 24 | syncedState.lineStates = [syncedState.lineStates[DEBUG_LINE]]; 25 | } 26 | 27 | const { address: userAddress } = useAccount(); 28 | 29 | const [liveState, setLiveState] = useState(syncedState); 30 | 31 | useInterval( 32 | () => { 33 | setLiveState((prevState) => { 34 | if (DEBUG_PERF === 2) console.time("Interval"); 35 | 36 | const isNewSyncedState = prevState.lastSyncedTime != syncedState.lastSyncedTime; 37 | 38 | let newState = isNewSyncedState ? structuredClone(syncedState) : prevState; 39 | 40 | if (isNewSyncedState) { 41 | for (let i = 0; i < newState.lines.length; i++) { 42 | // Re-use current state for the line if the last touched time for the 43 | // line hasn't changed since last sync. Avoids needless re-processing. 44 | if (newState.lineStates[i].lastTouchedTime == prevState.lineStates[i].lastTouchedTime) { 45 | newState.lines[i] = structuredClone(prevState.lines[i]); 46 | newState.lineStates[i] = structuredClone(prevState.lineStates[i]); 47 | } else { 48 | console.log("Re-syncing line #", i); 49 | } 50 | } 51 | } 52 | 53 | newState = forwardStateTo( 54 | newState, 55 | gameConfig, 56 | !!userAddress && !isNewSyncedState && newState.lastProcessedTime > 0n, 57 | userAddress ? toEntityId(BigInt(userAddress)) : null, 58 | { 59 | stopAtIteration: DEBUG_ITER, // Will likely be null, if so goes to 99999999999999. 60 | stopAtTimestampWad: null, // Will use timeWad() if null. 61 | } 62 | ); 63 | 64 | if (DEBUG_PERF === 2) console.timeEnd("Interval"); 65 | 66 | return newState; 67 | }); 68 | }, 69 | DEBUG_PERF != 1 && DEBUG_ITER == null ? 15 : null // For debugging. 70 | ); 71 | 72 | return liveState.lastProcessedTime > 0n ? ( 73 | 74 | ) : ( 75 | 76 | 77 | 78 | Catching up 79 | . 80 | . 81 | . 82 | 83 | 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /packages/client/src/common.ts: -------------------------------------------------------------------------------- 1 | import { createClient, fallback, http, webSocket } from "viem"; 2 | import { createConfig, Config } from "wagmi"; 3 | import { QueryClient } from "@tanstack/react-query"; 4 | import { ODYSSEY_CHAIN } from "./utils/chains"; 5 | 6 | export const CHAIN_ID = import.meta.env.CHAIN_ID!; 7 | export const WORLD_ADDRESS = import.meta.env.WORLD_ADDRESS!; 8 | export const START_BLOCK = BigInt(import.meta.env.START_BLOCK ?? 0n); 9 | export const CLERK_PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY; 10 | 11 | if (!CHAIN_ID || !WORLD_ADDRESS || !START_BLOCK) 12 | throw new Error("Core environment variables are not set!"); 13 | 14 | export const WAGMI_CONFIG: Config = createConfig({ 15 | chains: [ODYSSEY_CHAIN], 16 | client: ({ chain }) => 17 | createClient({ 18 | chain, 19 | transport: fallback([webSocket(), http()]), 20 | pollingInterval: 100, 21 | cacheTime: 100, 22 | }), 23 | }); 24 | 25 | export const QUERY_CLIENT = new QueryClient(); 26 | -------------------------------------------------------------------------------- /packages/client/src/fonts/BerkeleyMonoVariable-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paradigmxyz/rethmatch/60c7b4bdbd54d1757a8ca67adf2d69c6aaaaaf64/packages/client/src/fonts/BerkeleyMonoVariable-Regular.woff2 -------------------------------------------------------------------------------- /packages/client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from "react-dom/client"; 2 | 3 | import { ChakraProvider } from "@chakra-ui/react"; 4 | 5 | import { App } from "./App"; 6 | 7 | import "./styles.css"; 8 | 9 | import { WagmiProvider } from "wagmi"; 10 | import { QueryClientProvider } from "@tanstack/react-query"; 11 | import { ClerkProvider } from "@clerk/clerk-react"; 12 | 13 | import { createSyncAdapter } from "@latticexyz/store-sync/internal"; 14 | import { SyncProvider } from "@latticexyz/store-sync/react"; 15 | import { 16 | CHAIN_ID, 17 | CLERK_PUBLISHABLE_KEY, 18 | QUERY_CLIENT, 19 | START_BLOCK, 20 | WAGMI_CONFIG, 21 | WORLD_ADDRESS, 22 | } from "./common"; 23 | import { stash } from "./mud/stash"; 24 | 25 | createRoot(document.getElementById("react-root")!).render( 26 | 27 | 28 | 29 | 30 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | -------------------------------------------------------------------------------- /packages/client/src/leaderboard-archive.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "username": "transmissions11", 4 | "highScores": [ 5 | "1474323623539854735964", 6 | "1790583633525363154956", 7 | "1838756877534204222620", 8 | "2339361733026030422058", 9 | "3182314184301991148480", 10 | "1934206671270218215098", 11 | "2261187687509761885636", 12 | "5717889737642058148944", 13 | "6675958749239671218827", 14 | "4039405712451464315525" 15 | ] 16 | }, 17 | { 18 | "username": "tome11i", 19 | "highScores": ["512000000000000000000", "512000000000000000000", "88390439494926001496"] 20 | }, 21 | { 22 | "username": "0xhank", 23 | "highScores": [ 24 | "60016904942573711724", 25 | "200046324038811164511", 26 | "98173591330469942963", 27 | "347176699657089379257", 28 | "206812761915263740069", 29 | "185718114041461068494", 30 | "228544651580368852317", 31 | "3183998987948954765991", 32 | "627475750359808051987", 33 | "3771101106107257043918" 34 | ] 35 | }, 36 | { 37 | "username": "0xnimara", 38 | "highScores": [ 39 | "41521503854729486775", 40 | "147192339956816129954", 41 | "1394623716779797557689", 42 | "2892865027786743823892", 43 | "1067195166047427240836", 44 | "1093256678798898482579" 45 | ] 46 | }, 47 | { 48 | "username": "azacharyf", 49 | "highScores": [ 50 | "2105494687711481098685", 51 | "2246386352357798574907", 52 | "2295074179140997197699", 53 | "2257002481636218782006", 54 | "2952962984695782335398", 55 | "3893605519339914393678", 56 | "2479336526667712093202", 57 | "2671108662784668847189", 58 | "2515758980412581652214", 59 | "3837315579310808307097" 60 | ] 61 | }, 62 | 63 | { 64 | "username": "ncah", 65 | "highScores": ["226751275747999485328"] 66 | }, 67 | { 68 | "username": "0xoptimus", 69 | "highScores": ["101842780323044220542"] 70 | }, 71 | { 72 | "username": "rjected", 73 | "highScores": [ 74 | "494092226785658235282", 75 | "547924992835203217243", 76 | "508585288222973957851", 77 | "640540337058458587002", 78 | "670936891577425900920", 79 | "579810130182832507235", 80 | "807477442410767587575", 81 | "1202401639582050100463", 82 | "1472282531085974458380", 83 | "904531803076050415042" 84 | ] 85 | }, 86 | { 87 | "username": "hibillh", 88 | "highScores": [ 89 | "16434124512936730434886", 90 | "19527582047289877935715", 91 | "20474681813440450164916", 92 | "20799760193623978806266", 93 | "25855275799711920452984", 94 | "26161255605423629498549", 95 | "22606710447967273301266", 96 | "22301387872466145157538", 97 | "27693868555813433768235", 98 | "37777213452627853225780" 99 | ] 100 | }, 101 | { 102 | "username": "nubit_org", 103 | "highScores": [ 104 | "5085495076670436181900", 105 | "5273504315960755761956", 106 | "5205219060949721974824", 107 | "6121499868564962703447", 108 | "6834765468730517948299", 109 | "8604769024587500676676", 110 | "6483510655312907737769", 111 | "8424979708694720239904", 112 | "6921790690760363885262", 113 | "8612298932061778274993" 114 | ] 115 | }, 116 | { 117 | "username": "plotchy", 118 | "highScores": [ 119 | "1063774364635810840832", 120 | "1271294008817918844612", 121 | "1118829202892893210138", 122 | "1348747247626941529843", 123 | "1334700936318341326252", 124 | "1595670790306196343540", 125 | "2494810889879353346552", 126 | "1683150544348849749079", 127 | "3567392884139599523791", 128 | "1635957089440137974427" 129 | ] 130 | }, 131 | { 132 | "username": "exwhyyy", 133 | "highScores": [ 134 | "1834259514856611955813", 135 | "2188191144245427367617", 136 | "1961628329436950669520", 137 | "2826705491950290841612", 138 | "2464754723115432954953", 139 | "2341856186893553154028", 140 | "2156431171519793366888", 141 | "2853790400916528266129", 142 | "3326436592848686330692", 143 | "3588839500311721662448" 144 | ] 145 | }, 146 | { 147 | "username": "aalimsahin", 148 | "highScores": [ 149 | "515360784853065043808", 150 | "571246874253611860798", 151 | "554073134332106626884", 152 | "907748928993371995774", 153 | "1082936188189831392101", 154 | "824023967241678156236", 155 | "1614963718416558024378", 156 | "1528817890747145874658", 157 | "1822351282923715223540", 158 | "1092510394026337115606" 159 | ] 160 | } 161 | ] 162 | -------------------------------------------------------------------------------- /packages/client/src/mud/stash.ts: -------------------------------------------------------------------------------- 1 | import { createStash } from "@latticexyz/stash/internal"; 2 | 3 | import raw_config from "contracts/mud.config"; 4 | // For some reason when importing config in Node.js, instead of just 5 | // being the object, it's wrapped in an object with a `default` property. 6 | const config = "default" in raw_config ? (raw_config.default as typeof raw_config) : raw_config; 7 | 8 | export const stash: ReturnType> = createStash(config); 9 | -------------------------------------------------------------------------------- /packages/client/src/styles.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | height: 100%; 5 | overflow: hidden; 6 | } 7 | 8 | body { 9 | background-color: black !important; 10 | } 11 | 12 | @font-face { 13 | font-family: "BerkeleyMono"; 14 | src: url("./fonts/BerkeleyMonoVariable-Regular.woff2") format("woff2-variations"); 15 | font-display: swap; 16 | font-weight: 100; 17 | } 18 | 19 | .lineContainer { 20 | position: relative; 21 | } 22 | 23 | .lineContainer::before { 24 | content: ""; 25 | position: absolute; 26 | top: calc(50% - var(--line-thickness) / 2); 27 | height: var(--line-thickness); 28 | width: 100%; 29 | background-color: #4d4d4d; 30 | } 31 | 32 | .line { 33 | width: 100%; 34 | display: flex; 35 | align-items: center; 36 | } 37 | 38 | .entity { 39 | border-radius: 50%; 40 | position: absolute; 41 | } 42 | 43 | .wall { 44 | border-radius: 0%; 45 | } 46 | 47 | @keyframes blink { 48 | 0% { 49 | opacity: 0; 50 | } 51 | 50% { 52 | opacity: 1; 53 | } 54 | 100% { 55 | opacity: 0; 56 | } 57 | } 58 | 59 | .dot-1 { 60 | animation: blink 0.5s infinite 0s; 61 | } 62 | .dot-2 { 63 | animation: blink 0.5s infinite 0.165s; 64 | } 65 | .dot-3 { 66 | animation: blink 0.5s infinite 0.33s; 67 | } 68 | 69 | @keyframes sideLoadingAnimation { 70 | 0% { 71 | width: 0; 72 | } 73 | 100% { 74 | width: 100%; 75 | } 76 | } 77 | 78 | @keyframes glow00bcff { 79 | 0%, 80 | 100% { 81 | box-shadow: 82 | 0 0 10px #00bcff, 83 | 0 0 30px #00bcff; 84 | } 85 | 50% { 86 | box-shadow: 87 | 0 0 0px #00bcff, 88 | 0 0 0px #00bcff; 89 | } 90 | } 91 | 92 | @keyframes glowffc000 { 93 | 0%, 94 | 100% { 95 | box-shadow: 96 | 0 0 10px #ffc000, 97 | 0 0 30px #ffc000; 98 | } 99 | 50% { 100 | box-shadow: 101 | 0 0 0px #ffc000, 102 | 0 0 0px #ffc000; 103 | } 104 | } 105 | 106 | @keyframes glowff5700 { 107 | 0%, 108 | 100% { 109 | box-shadow: 110 | 0 0 10px #ff5700, 111 | 0 0 30px #ff5700; 112 | } 113 | 50% { 114 | box-shadow: 115 | 0 0 0px #ff5700, 116 | 0 0 0px #ff5700; 117 | } 118 | } 119 | 120 | /* @keyframes glowff00ff { 121 | 0%, 122 | 100% { 123 | box-shadow: 124 | 0 0 10px #ff00ff, 125 | 0 0 30px #ff00ff; 126 | } 127 | 50% { 128 | box-shadow: 129 | 0 0 0px #ff00ff, 130 | 0 0 0px #ff00ff; 131 | } 132 | } */ 133 | 134 | @keyframes glow00e893 { 135 | 0%, 136 | 100% { 137 | box-shadow: 138 | 0 0 10px #00e893, 139 | 0 0 30px #00e893; 140 | } 141 | 50% { 142 | box-shadow: 143 | 0 0 0px #00e893, 144 | 0 0 0px #00e893; 145 | } 146 | } 147 | 148 | @keyframes blinkMild { 149 | 0% { 150 | opacity: 0.4; 151 | } 152 | 100% { 153 | opacity: 1; 154 | } 155 | } 156 | 157 | .chakra-alert { 158 | /* background-color: #ff0420 !important; */ 159 | background-color: #ff5700 !important; 160 | } 161 | 162 | .chakra-alert__desc { 163 | padding-right: 25px; 164 | } 165 | 166 | .disableScrollBar { 167 | scrollbar-width: none; /* Firefox */ 168 | -ms-overflow-style: none; /* Internet Explorer 10+ */ 169 | } 170 | 171 | .disableScrollBar::-webkit-scrollbar { 172 | display: none; /* WebKit */ 173 | } 174 | 175 | .fadeBottom { 176 | -webkit-mask-image: linear-gradient(to bottom, black 95%, transparent 100%); 177 | mask-image: linear-gradient(to bottom, black 95%, transparent 100%); 178 | } 179 | -------------------------------------------------------------------------------- /packages/client/src/utils/Countdown.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | export function Countdown({ targetDate }: { targetDate: string }) { 4 | const [timeLeft, setTimeLeft] = useState(""); 5 | 6 | useEffect(() => { 7 | const calculateTimeLeft = () => { 8 | const CONTEST_END_DATE = new Date(targetDate); 9 | 10 | const difference = CONTEST_END_DATE.getTime() - new Date().getTime(); 11 | 12 | if (difference > 0) { 13 | const days = Math.floor(difference / (1000 * 60 * 60 * 24)); 14 | const hours = Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); 15 | const minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)); 16 | const seconds = Math.floor((difference % (1000 * 60)) / 1000); 17 | 18 | setTimeLeft(`${days}d ${hours}h ${minutes}m ${seconds}s`); 19 | } 20 | }; 21 | 22 | calculateTimeLeft(); 23 | const timer = setInterval(calculateTimeLeft, 1000); 24 | 25 | return () => clearInterval(timer); 26 | }, [targetDate]); 27 | 28 | return timeLeft; 29 | } 30 | -------------------------------------------------------------------------------- /packages/client/src/utils/DebugPanel.tsx: -------------------------------------------------------------------------------- 1 | import { DEBUG_ITER, findOrThrow, mapEntityToEmoji } from "./debugging"; 2 | import { LineState, GameConfig } from "./game/configLib"; 3 | import { Entity, computeCollisionTime } from "./game/entityLib"; 4 | import { timeWad } from "./timeLib"; 5 | import { GameButton } from "./button"; // Import GameButton 6 | 7 | export function DebugPanel({ 8 | lastProcessedTime, 9 | line, 10 | lineState, 11 | gameConfig, 12 | }: { 13 | lastProcessedTime: bigint; 14 | line: Entity[]; 15 | lineState: LineState; 16 | gameConfig: GameConfig; 17 | }) { 18 | return ( 19 |
20 |

21 | Last proc'd time: {lastProcessedTime.fromWad().toFixed(2)} 22 | Current time: {timeWad().fromWad().toFixed(2)} 23 |
24 |
25 | Collision queue{" "} 26 | { 29 | const urlParams = new URLSearchParams(window.location.search); 30 | urlParams.set("debug_iter", (DEBUG_ITER! - 1).toString()); 31 | 32 | window.history.replaceState( 33 | null, 34 | "", 35 | `${window.location.pathname}?${urlParams.toString()}` 36 | ); 37 | 38 | window.location.replace(window.location.href); 39 | }} 40 | > 41 | Previous iteration 42 | 43 | { 46 | const urlParams = new URLSearchParams(window.location.search); 47 | urlParams.set("debug_iter", (DEBUG_ITER! + 1).toString()); 48 | 49 | window.history.replaceState( 50 | null, 51 | "", 52 | `${window.location.pathname}?${urlParams.toString()}` 53 | ); 54 | 55 | window.location.replace(window.location.href); 56 | }} 57 | > 58 | Next iteration 59 | 60 |

61 | {lineState.collisionQueue.map((entry, j) => { 62 | const rightEntity = line.find((e) => e.entityId === entry.value); 63 | 64 | const leftEntity = rightEntity ? findOrThrow(line, rightEntity.leftNeighbor) : null; 65 | const colTime = 66 | leftEntity && rightEntity 67 | ? computeCollisionTime(leftEntity, rightEntity, gameConfig.velocityCoefficient) 68 | : null; 69 | 70 | return ( 71 |
72 | Time until: {(entry.priority - lastProcessedTime).fromWad().toFixed(2)} | 73 | Entities:{" "} 74 | 75 | {colTime != entry.priority ? "" : mapEntityToEmoji(leftEntity!.entityId)} 76 | {" ← "} 77 | {mapEntityToEmoji(entry.value)} 78 | 79 |
80 | ); 81 | })} 82 |
83 |
84 | Entities: 85 | {line.map((e) => { 86 | return ( 87 |
88 | E: {mapEntityToEmoji(e.entityId)} — LEFT NEIGHBOR: {mapEntityToEmoji(e.leftNeighbor)} — 89 | RIGHT NEIGHBOR: {mapEntityToEmoji(e.rightNeighbor)} 90 |
91 | ); 92 | })} 93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /packages/client/src/utils/bigint.ts: -------------------------------------------------------------------------------- 1 | export const WAD = 10n ** 18n; 2 | export const UINT144_MAX = 2n ** 144n - 1n; 3 | export const UINT152_MAX = 2n ** 152n - 1n; 4 | export const UINT256_MAX = 2n ** 256n - 1n; 5 | 6 | declare global { 7 | interface BigInt { 8 | toJSON(): string; 9 | fromWad(): number; 10 | toNumber(): number; 11 | abs(): bigint; 12 | sqrtWad(): bigint; 13 | sqrt(): bigint; 14 | lnWad(): bigint; 15 | log10Wad(): bigint; 16 | mulWad(y: bigint): bigint; 17 | divWad(y: bigint): bigint; 18 | max(y: bigint): bigint; 19 | min(y: bigint): bigint; 20 | } 21 | } 22 | 23 | BigInt.prototype.toJSON = function (): string { 24 | return this.toString(); 25 | }; 26 | 27 | BigInt.prototype.fromWad = function (): number { 28 | // Note: this has precision issues, but 29 | // seems mostly fine in practice. Could 30 | // employ a more complex algo but don't 31 | // want this to become a bottleneck. 32 | return this.toNumber() / 1e18; 33 | }; 34 | 35 | BigInt.prototype.toNumber = function (): number { 36 | return Number(this); 37 | }; 38 | 39 | BigInt.prototype.abs = function (): bigint { 40 | return this.valueOf() < 0n ? -this.valueOf() : this.valueOf(); 41 | }; 42 | 43 | // port of solady sqrtWad 44 | BigInt.prototype.sqrtWad = function (): bigint { 45 | let z = 10n ** 9n; 46 | let value = this.valueOf(); 47 | if (value <= UINT256_MAX / (10n ** 36n - 1n)) { 48 | value *= WAD; 49 | z = 1n; 50 | } 51 | z *= value.sqrt(); 52 | return z; 53 | }; 54 | 55 | // via https://www.npmjs.com/package/bigint-isqrt 56 | BigInt.prototype.sqrt = function (): bigint { 57 | let value = this.valueOf(); 58 | 59 | if (value < 2n) { 60 | return value; 61 | } 62 | 63 | if (value < 16n) { 64 | return BigInt(Math.sqrt(value.toNumber()) | 0); 65 | } 66 | 67 | let x0, x1; 68 | if (value < 1n << 52n) { 69 | x1 = BigInt(Math.sqrt(value.toNumber()) | 0) - 3n; 70 | } else { 71 | let vlen = value.toString().length; 72 | if (!(vlen & 1)) { 73 | x1 = 10n ** BigInt(vlen / 2); 74 | } else { 75 | x1 = 4n * 10n ** BigInt((vlen / 2) | 0); 76 | } 77 | } 78 | 79 | do { 80 | x0 = x1; 81 | x1 = (value / x0 + x0) >> 1n; 82 | } while (x0 !== x1 && x0 !== x1 - 1n); 83 | 84 | return x0; 85 | }; 86 | 87 | // via https://github.com/vectorized/solady/blob/main/src/utils/FixedPointMathLib.sol 88 | BigInt.prototype.lnWad = function (): bigint { 89 | let x = this.valueOf(); 90 | 91 | if (x <= 0n) throw new Error("LN_WAD_UNDEFINED"); 92 | 93 | let r: bigint = 0n; 94 | 95 | // We want to convert `x` from `10**18` fixed point to `2**96` fixed point. 96 | // We do this by multiplying by `2**96 / 10**18`. But since 97 | // `ln(x * C) = ln(x) + ln(C)`, we can simply do nothing here 98 | // and add `ln(2**96 / 10**18)` at the end. 99 | 100 | // Compute `k = log2(x) - 96`, `r = 159 - k = 255 - log2(x) = 255 ^ log2(x)`. 101 | r = (0xffffffffffffffffffffffffffffffffn < x ? 1n : 0n) << 7n; 102 | r = r | ((0xffffffffffffffffn < x >> r ? 1n : 0n) << 6n); 103 | r = r | ((0xffffffffn < x >> r ? 1n : 0n) << 5n); 104 | r = r | ((0xffffn < x >> r ? 1n : 0n) << 4n); 105 | r = r | ((0xffn < x >> r ? 1n : 0n) << 3n); 106 | 107 | r = 108 | r ^ 109 | ((0xf8f9f9faf9fdfafbf9fdfcfdfafbfcfef9fafdfafcfcfbfefafafcfbffffffffn >> 110 | (8n * (31n - (0x1fn & (0x8421084210842108cc6318c6db6d54ben >> (x >> r)))))) & 111 | 0xffn); 112 | 113 | // Reduce range of x to (1, 2) * 2**96 114 | // ln(2^k * x) = k * ln(2) + ln(x) 115 | x = (x << r) >> 159n; 116 | 117 | // Evaluate using a (8, 8)-term rational approximation. 118 | // `p` is made monic, we will multiply by a scale factor later. 119 | let p = 120 | (((43456485725739037958740375743393n + 121 | (((24828157081833163892658089445524n + 122 | (((3273285459638523848632254066296n + x) * x) >> 96n)) * 123 | x) >> 124 | 96n)) * 125 | x) >> 126 | 96n) - 127 | 11111509109440967052023855526967n; 128 | p = ((p * x) >> 96n) - 45023709667254063763336534515857n; 129 | p = ((p * x) >> 96n) - 14706773417378608786704636184526n; 130 | p = p * x - (795164235651350426258249787498n << 96n); 131 | // We leave `p` in `2**192` basis so we don't need to scale it back up for the division. 132 | 133 | // `q` is monic by convention. 134 | let q = 5573035233440673466300451813936n + x; 135 | q = 71694874799317883764090561454958n + ((x * q) >> 96n); 136 | q = 283447036172924575727196451306956n + ((x * q) >> 96n); 137 | q = 401686690394027663651624208769553n + ((x * q) >> 96n); 138 | q = 204048457590392012362485061816622n + ((x * q) >> 96n); 139 | q = 31853899698501571402653359427138n + ((x * q) >> 96n); 140 | q = 909429971244387300277376558375n + ((x * q) >> 96n); 141 | 142 | // `p / q` is in the range `(0, 0.125) * 2**96`. 143 | 144 | // Finalization, we need to: 145 | // - Multiply by the scale factor `s = 5.549…`. 146 | // - Add `ln(2**96 / 10**18)`. 147 | // - Add `k * ln(2)`. 148 | // - Multiply by `10**18 / 2**96 = 5**18 >> 78`. 149 | 150 | // The q polynomial is known not to have zeros in the domain. 151 | // No scaling required because p is already `2**96` too large. 152 | p = p / q; 153 | // Multiply by the scaling factor: `s * 5**18 * 2**96`, base is now `5**18 * 2**192`. 154 | p = 1677202110996718588342820967067443963516166n * p; 155 | // Add `ln(2) * k * 5**18 * 2**192`. 156 | p = 16597577552685614221487285958193947469193820559219878177908093499208371n * (159n - r) + p; 157 | // Add `ln(2**96 / 10**18) * 5**18 * 2**192`. 158 | p = 600920179829731861736702779321621459595472258049074101567377883020018308n + p; 159 | // Base conversion: mul `2**18 / 2**192`. 160 | r = p >> 174n; 161 | 162 | return r; 163 | }; 164 | 165 | BigInt.prototype.log10Wad = function (): bigint { 166 | // change of base formula, log10(x) = ln(x) / ln(10) 167 | return (this.valueOf().lnWad() * WAD) / 2302585092994045683n; // divWad inlined. 168 | }; 169 | 170 | BigInt.prototype.mulWad = function (y: bigint): bigint { 171 | return (this.valueOf() * y) / WAD; 172 | }; 173 | 174 | BigInt.prototype.divWad = function (y: bigint): bigint { 175 | return (this.valueOf() * WAD) / y; 176 | }; 177 | 178 | BigInt.prototype.max = function (y: bigint): bigint { 179 | return this.valueOf() > y ? this.valueOf() : y; 180 | }; 181 | 182 | BigInt.prototype.min = function (y: bigint): bigint { 183 | return this.valueOf() < y ? this.valueOf() : y; 184 | }; 185 | -------------------------------------------------------------------------------- /packages/client/src/utils/bigintMinHeap.ts: -------------------------------------------------------------------------------- 1 | // Readonly so no one accidentally mutates the underlying array 2 | // instead of using the specialized heap functions defined below. 3 | export type BigintMinHeap = readonly bigint[]; 4 | 5 | export function sum(mh: readonly bigint[]): bigint { 6 | return mh.reduce((acc, val) => acc + val, 0n); 7 | } 8 | 9 | export function enqueue(mh: BigintMinHeap, element: bigint, maxLength: number) { 10 | if (maxLength <= 0) throw new Error("maxLength must be greater than zero"); 11 | 12 | if (mh.length < maxLength) { 13 | (mh as bigint[]).push(element); 14 | siftUp(mh, mh.length - 1); 15 | return; 16 | } 17 | 18 | if (element <= mh[0]) return; 19 | 20 | (mh as bigint[])[0] = element; 21 | siftDown(mh, 0); 22 | } 23 | 24 | function siftUp(heap: BigintMinHeap, index: number): void { 25 | while (index > 0) { 26 | const parentIndex = Math.floor((index - 1) / 2); 27 | if (heap[index] >= heap[parentIndex]) break; 28 | [(heap as bigint[])[index], (heap as bigint[])[parentIndex]] = [heap[parentIndex], heap[index]]; 29 | index = parentIndex; 30 | } 31 | } 32 | 33 | function siftDown(heap: BigintMinHeap, index: number): void { 34 | const length = heap.length; 35 | while (true) { 36 | let smallest = index; 37 | const left = 2 * index + 1; 38 | const right = 2 * index + 2; 39 | 40 | if (left < length && heap[left] < heap[smallest]) { 41 | smallest = left; 42 | } 43 | 44 | if (right < length && heap[right] < heap[smallest]) { 45 | smallest = right; 46 | } 47 | 48 | if (smallest === index) break; 49 | 50 | [(heap as bigint[])[index], (heap as bigint[])[smallest]] = [heap[smallest], heap[index]]; 51 | index = smallest; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/client/src/utils/button.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { Button, ButtonProps, Box } from "@chakra-ui/react"; 3 | 4 | export function GameButton({ 5 | children, 6 | isLoading, 7 | ...rest 8 | }: ButtonProps & { children: ReactNode; isLoading?: boolean }) { 9 | return ( 10 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /packages/client/src/utils/chains.ts: -------------------------------------------------------------------------------- 1 | import { defineChain } from "viem"; 2 | 3 | export const ODYSSEY_CHAIN = defineChain({ 4 | id: 911867, 5 | name: "Odyssey", 6 | nativeCurrency: { 7 | decimals: 18, 8 | name: "Ether", 9 | symbol: "ETH", 10 | }, 11 | rpcUrls: { 12 | default: { 13 | http: ["https://odyssey.ithaca.xyz"], 14 | webSocket: ["wss://odyssey.ithaca.xyz"], 15 | }, 16 | }, 17 | indexerUrl: "https://rethmatch-indexer.paradigm.xyz", 18 | }); 19 | -------------------------------------------------------------------------------- /packages/client/src/utils/chakra.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { Flex, FlexProps } from "@chakra-ui/react"; 3 | 4 | /* Typings */ 5 | export type MainAxisAlignmentStrings = 6 | | "space-between" 7 | | "space-around" 8 | | "flex-start" 9 | | "center" 10 | | "flex-end"; 11 | 12 | export type MainAxisAlignment = 13 | | MainAxisAlignmentStrings 14 | | { md: MainAxisAlignmentStrings; base: MainAxisAlignmentStrings }; 15 | 16 | export type CrossAxisAlignmentStrings = "flex-start" | "center" | "flex-end" | "stretch"; 17 | 18 | export type CrossAxisAlignment = 19 | | CrossAxisAlignmentStrings 20 | | { 21 | md: CrossAxisAlignmentStrings; 22 | base: CrossAxisAlignmentStrings; 23 | }; 24 | 25 | export class PixelMeasurement { 26 | size: number; 27 | 28 | constructor(num: number) { 29 | this.size = num; 30 | } 31 | 32 | asPxString(): string { 33 | return this.size + "px"; 34 | } 35 | 36 | toString(): string { 37 | return this.asPxString(); 38 | } 39 | 40 | asNumber(): number { 41 | return this.size; 42 | } 43 | } 44 | 45 | export class PercentageSize { 46 | percent: number; 47 | 48 | constructor(num: number) { 49 | if (num > 1) { 50 | throw new Error("Cannot have a percentage higher than 1!"); 51 | } 52 | 53 | this.percent = num; 54 | } 55 | } 56 | 57 | export class PercentOnDesktopPixelOnMobileSize { 58 | percent: number; 59 | pixel: number; 60 | 61 | constructor({ percentageSize, pixelSize }: { percentageSize: number; pixelSize: number }) { 62 | if (percentageSize > 1) { 63 | throw new Error("Cannot have a percentage higher than 1!"); 64 | } 65 | 66 | this.percent = percentageSize; 67 | this.pixel = pixelSize; 68 | } 69 | } 70 | 71 | export class PixelSize { 72 | pixel: number; 73 | 74 | constructor(num: number) { 75 | this.pixel = num; 76 | } 77 | } 78 | 79 | export class ResponsivePixelSize { 80 | desktop: number; 81 | mobile: number; 82 | 83 | constructor({ desktop, mobile }: { desktop: number; mobile: number }) { 84 | this.mobile = mobile; 85 | this.desktop = desktop; 86 | } 87 | } 88 | 89 | /************************************** 90 | * 91 | * 92 | * Components 93 | * - Center.tsx 94 | * - Column.tsx 95 | * - Row.tsx 96 | * - RowOnDesktopColumnOnMobile.tsx 97 | * - RowOrColumn.tsx 98 | * 99 | *************************************** 100 | */ 101 | 102 | /** 103 | * Center.tsx 104 | * 105 | * Creates a Flex where `justifyContent === 'center'` and `alignItems === 'center'` 106 | * If `expand === true` it will set the height and width of the Flex to 100%. 107 | * Passes all extra props to the Flex. 108 | */ 109 | 110 | export type CenterProps = { 111 | children: React.ReactNode; 112 | expand?: boolean; 113 | } & FlexProps; 114 | 115 | export const Center = ({ children, expand, ...others }: CenterProps) => { 116 | if (expand) { 117 | others.height = "100%"; 118 | others.width = "100%"; 119 | } 120 | 121 | return ( 122 | 123 | {children} 124 | 125 | ); 126 | }; 127 | 128 | /** 129 | * Column.tsx 130 | * 131 | * Creates a Flex with a column direction 132 | * and sets the `justifyContent` to the `mainAxisAlignment` 133 | * and the `alignItems` to the `crossAxisAlignment`. 134 | * If `expand === true` it will set the height and width of the Flex to 100%. 135 | * Passes all extra props to the Flex. 136 | */ 137 | 138 | export type ColumnProps = { 139 | mainAxisAlignment: MainAxisAlignment; 140 | crossAxisAlignment: CrossAxisAlignment; 141 | children: React.ReactNode; 142 | expand?: boolean; 143 | } & FlexProps; 144 | 145 | export const Column = ({ 146 | mainAxisAlignment, 147 | crossAxisAlignment, 148 | children, 149 | expand, 150 | ...others 151 | }: ColumnProps) => { 152 | if (expand) { 153 | others.height = "100%"; 154 | others.width = "100%"; 155 | } 156 | 157 | return ( 158 | 164 | {children} 165 | 166 | ); 167 | }; 168 | 169 | /** 170 | * Row.tsx 171 | * 172 | * Creates a Flex with a row direction 173 | * and sets the `justifyContent` to the `mainAxisAlignment` 174 | * and the `alignItems` to the `crossAxisAlignment`. 175 | * If `expand === true` it will set the height and width of the Flex to 100%. 176 | * Passes all extra props to the Flex. 177 | */ 178 | 179 | export type RowProps = { 180 | mainAxisAlignment: MainAxisAlignment; 181 | crossAxisAlignment: CrossAxisAlignment; 182 | children: React.ReactNode; 183 | expand?: boolean; 184 | } & FlexProps; 185 | 186 | export const Row = ({ 187 | mainAxisAlignment, 188 | crossAxisAlignment, 189 | children, 190 | expand, 191 | ...others 192 | }: RowProps) => { 193 | if (expand) { 194 | others.height = "100%"; 195 | others.width = "100%"; 196 | } 197 | 198 | return ( 199 | 205 | {children} 206 | 207 | ); 208 | }; 209 | 210 | /** 211 | * RowOnDesktopColumnOnMobile.tsx 212 | * 213 | * Creates a Flex with a row direction on desktop and a column direction on mobile. 214 | * and sets the `justifyContent` to the `mainAxisAlignment` 215 | * and the `alignItems` to the `crossAxisAlignment`. 216 | * If `expand === true` it will set the height and width of the Flex to 100%. 217 | * Passes all extra props to the Flex. 218 | */ 219 | export const RowOnDesktopColumnOnMobile = ({ 220 | mainAxisAlignment, 221 | crossAxisAlignment, 222 | children, 223 | expand, 224 | ...others 225 | }: RowProps) => { 226 | if (expand) { 227 | others.height = "100%"; 228 | others.width = "100%"; 229 | } 230 | 231 | return ( 232 | 238 | {children} 239 | 240 | ); 241 | }; 242 | 243 | /** 244 | * RowOrColumn.tsx 245 | * 246 | * Creates a Flex which will be a row if `isRow` is true 247 | * and sets the `justifyContent` to the `mainAxisAlignment` 248 | * and the `alignItems` to the `crossAxisAlignment`. 249 | * If `expand === true` it will set the height and width of the Flex to 100%. 250 | * Passes all extra props to the Flex. 251 | */ 252 | export const RowOrColumn = ({ 253 | mainAxisAlignment, 254 | crossAxisAlignment, 255 | children, 256 | expand, 257 | isRow, 258 | ...others 259 | }: RowProps & { isRow: boolean }) => { 260 | if (expand) { 261 | others.height = "100%"; 262 | others.width = "100%"; 263 | } 264 | 265 | return ( 266 | 272 | {children} 273 | 274 | ); 275 | }; 276 | 277 | /************************************** 278 | * 279 | * 280 | * Hooks 281 | * - useWindowSize.ts 282 | * - useLockedViewHeight.ts 283 | * - useIsMobile.ts 284 | * - useSpacedLayout.ts 285 | * 286 | *************************************** 287 | */ 288 | 289 | /** 290 | * useWindowSize.ts 291 | * 292 | * Gets the height and width of the current window. 293 | */ 294 | export const useWindowSize = () => { 295 | const [windowSize, setWindowSize] = useState({ 296 | width: window.innerWidth, 297 | height: window.innerHeight, 298 | }); 299 | 300 | useEffect(() => { 301 | // Handler to call on window resize 302 | function handleResize() { 303 | // Set window width/height to state 304 | setWindowSize({ 305 | width: window.innerWidth, 306 | height: window.innerHeight, 307 | }); 308 | } 309 | 310 | // Add event listener 311 | window.addEventListener("resize", handleResize); 312 | 313 | // Call handler right away so state gets updated with initial window size 314 | handleResize(); 315 | 316 | // Remove event listener on cleanup 317 | return () => window.removeEventListener("resize", handleResize); 318 | }, []); // Empty array ensures that effect is only run on mount 319 | 320 | return windowSize; 321 | }; 322 | 323 | /** 324 | * useLockedViewHeight.ts 325 | * 326 | * Returns the pixel count of the height of the window, 327 | * but will not return a value lower or higher than the minimum/maximum passed. 328 | */ 329 | export function useLockedViewHeight({ 330 | min = -1, 331 | max = Number.MAX_SAFE_INTEGER, 332 | }: { 333 | min?: number; 334 | max?: number; 335 | }) { 336 | const { height } = useWindowSize(); 337 | 338 | if (height <= min) { 339 | return { 340 | windowHeight: new PixelMeasurement(min), 341 | isLocked: true, 342 | }; 343 | } else if (height >= max) { 344 | return { 345 | windowHeight: new PixelMeasurement(max), 346 | isLocked: true, 347 | }; 348 | } else { 349 | return { 350 | windowHeight: new PixelMeasurement(height), 351 | isLocked: false, 352 | }; 353 | } 354 | } 355 | 356 | /** 357 | * useIsMobile.ts 358 | * 359 | * Returns whether the width of the window makes it likely a mobile device. 360 | * */ 361 | export function useIsMobile() { 362 | const { width } = useWindowSize(); 363 | 364 | return width < 768; 365 | } 366 | 367 | /** 368 | * useSpacedLayout.ts 369 | * 370 | * Takes the height of the parent, the desired spacing between children, 371 | * and the desired percentage sizes of the children (relative to their parent minus the spacing desired and the size of fixed sized children) 372 | * or the size of the child in pixels 373 | * and returns the pixel size of each child 374 | * that makes that child conform to the desired percentage. 375 | */ 376 | export function useSpacedLayout({ 377 | parentHeight, 378 | spacing, 379 | childSizes, 380 | }: { 381 | parentHeight: number; 382 | spacing: number; 383 | childSizes: ( 384 | | PercentageSize 385 | | PercentOnDesktopPixelOnMobileSize 386 | | PixelSize 387 | | ResponsivePixelSize 388 | )[]; 389 | }) { 390 | const isMobile = useIsMobile(); 391 | 392 | let parentMinusSpacingAndFixedChildSizes = 393 | parentHeight - 394 | spacing * (childSizes.length - 1) - 395 | childSizes.reduce((past, value) => { 396 | if ( 397 | value instanceof PixelSize || 398 | (value instanceof PercentOnDesktopPixelOnMobileSize && isMobile) 399 | ) { 400 | return past + value.pixel; 401 | } else if (value instanceof ResponsivePixelSize) { 402 | return past + (isMobile ? value.mobile : value.desktop); 403 | } else { 404 | return past; 405 | } 406 | }, 0); 407 | 408 | let spacedChildren: PixelMeasurement[] = []; 409 | 410 | for (const size of childSizes) { 411 | if ( 412 | size instanceof PercentageSize || 413 | (size instanceof PercentOnDesktopPixelOnMobileSize && !isMobile) 414 | ) { 415 | spacedChildren.push( 416 | new PixelMeasurement(size.percent * parentMinusSpacingAndFixedChildSizes) 417 | ); 418 | } else if (size instanceof PercentOnDesktopPixelOnMobileSize && isMobile) { 419 | spacedChildren.push(new PixelMeasurement(size.pixel)); 420 | } else if (size instanceof ResponsivePixelSize) { 421 | spacedChildren.push(new PixelMeasurement(isMobile ? size.mobile : size.desktop)); 422 | } else { 423 | spacedChildren.push(new PixelMeasurement(size.pixel)); 424 | } 425 | } 426 | 427 | return { 428 | parentHeight: new PixelMeasurement(parentHeight), 429 | spacing: new PixelMeasurement(spacing), 430 | childSizes: spacedChildren, 431 | }; 432 | } 433 | -------------------------------------------------------------------------------- /packages/client/src/utils/debugging.ts: -------------------------------------------------------------------------------- 1 | import { Entity, isLeftmostEntity, isRightmostEntity } from "./game/entityLib"; 2 | 3 | export const [DEBUG_ITER, DEBUG_LINE, DEBUG_PERF, DEBUG_GAS, DEBUG_EMOJI, DEBUG_VERBOSE] = [ 4 | getDebugParam("debug_iter"), 5 | getDebugParam("debug_line"), 6 | getDebugParam("debug_perf"), 7 | getDebugParam("debug_gas"), 8 | getDebugParam("debug_emoji"), 9 | getDebugParam("debug_verbose"), 10 | ]; // For debugging. 11 | 12 | if (DEBUG_ITER != null && DEBUG_LINE == null) 13 | alert("DEBUG_LINE should almost certainly be set if using DEBUG_ITER"); 14 | 15 | export function getDebugParam(param: string): number | null { 16 | const value = 17 | typeof window !== "undefined" ? new URLSearchParams(window.location.search).get(param) : null; 18 | return value !== null ? Number(value) : null; 19 | } 20 | 21 | const colorfulEmojis = [ 22 | "🕋", // Kaaba (Black) 23 | "🐸", // Frog (Green) 24 | "🍅", // Tomato (Red) 25 | "🍊", // Tangerine (Orange) 26 | "🍋", // Lemon (Yellow) 27 | "🍇", // Grapes (Purple) 28 | "🌸", // Cherry Blossom (Pink) 29 | "🌻", // Sunflower (Yellow) 30 | "🌼", // Blossom (Light Yellow) 31 | "🌿", // Herb (Green) 32 | "🔥", // Fire (Red/Orange) 33 | "💧", // Droplet (Blue) 34 | "🌍", // Globe Showing Europe-Africa (Green/Blue) 35 | "🌙", // Crescent Moon (Yellow) 36 | "⭐", // Star (Yellow) 37 | "🍁", // Maple Leaf (Red) 38 | "🍀", // Four Leaf Clover (Green) 39 | "🌈", // Rainbow 40 | "🌊", // Water Wave (Blue) 41 | "🌌", // Milky Way (Space Colors) 42 | "🎈", // Balloon (Red) 43 | "💎", // Gem Stone (Blue) 44 | "🍑", // Peach (Orange) 45 | "🍒", // Cherries (Red) 46 | "🍓", // Strawberry (Red) 47 | "🌹", // Rose (Red) 48 | "🥑", // Avocado (Green) 49 | "🥥", // Coconut (Brown) 50 | "🫐", // Blueberries (Blue) 51 | "🌺", // Hibiscus (Red) 52 | "🥕", // Carrot (Orange) 53 | "🌽", // Corn (Yellow) 54 | "🍆", // Eggplant (Purple) 55 | "🌶️", // Hot Pepper (Red) 56 | "🥒", // Cucumber (Green) 57 | "🍄", // Mushroom (Red/White) 58 | "🌰", // Chestnut (Brown) 59 | "🍯", // Honey Pot (Yellow) 60 | "🦋", // Butterfly (Blue) 61 | "🐠", // Tropical Fish (Orange/Blue) 62 | "🦜", // Parrot (Red/Green/Yellow) 63 | "🐙", // Octopus (Purple) 64 | "🦚", // Peacock (Green/Blue) 65 | "🌖", // Waning Gibbous Moon (Yellow) 66 | "❄️", // Snowflake (Blue/White) 67 | "🔮", // Crystal Ball (Purple) 68 | "🎃", // Jack-o-lantern (Orange) 69 | "🌟", // Glowing Star (Yellow) 70 | "🌠", // Shooting Star (Yellow) 71 | "🌋", // Volcano (Red/Orange) 72 | "🏜️", // Desert (Yellow/Brown) 73 | "🏝️", // Desert Island (Green/Blue) 74 | "🌅", // Sunrise (Yellow/Blue) 75 | "🌄", // Mountain at Sunrise (Orange/Blue) 76 | "🏞️", // National Park (Green) 77 | "🌐", // Globe with Meridians (Blue/Green) 78 | "🧊", // Ice (Light Blue/White) 79 | "🛸", // Flying Saucer (Grey) 80 | "🎍", // Pine Decoration (Green) 81 | "🎋", // Tanabata Tree (Green) 82 | "🧨", // Firecracker (Red) 83 | "🎏", // Carp Streamer (Red/Blue) 84 | "🏮", // Red Paper Lantern 85 | "🎴", // Flower Playing Cards (Red/Blue), 86 | "🥮", // Moon Cake (Yellow) 87 | "🥭", // Mango (Yellow/Orange) 88 | "🍍", // Pineapple (Yellow) 89 | "🥖", // Baguette Bread (Brown) 90 | "🥨", // Pretzel (Brown) 91 | "🍩", // Doughnut (Brown/Pink) 92 | "🍪", // Cookie (Brown) 93 | "⛩️", // Shinto Shrine (Red) 94 | "🚌", // Bus (Yellow) 95 | "🛶", // Canoe (Brown) 96 | "🛎️", // Bellhop Bell (Gold) 97 | "🍟", // French Fries (Yellow) 98 | "🥣", // Bowl with Spoon (White) 99 | "🧁", // Cupcake (Pink/White) 100 | "🍭", // Lollipop (Rainbow) 101 | "🍬", // Candy (Colorful) 102 | "🦖", // T-Rex (Green) 103 | "🍫", // Chocolate Bar (Brown) 104 | "🦄", // Unicorn (White/Pink) 105 | "🐲", // Dragon Face (Green) 106 | "🎳", // Bowling (White/Red) 107 | "🗽", // Statue of Liberty (Green) 108 | "🎟️", // Admission Tickets (Red) 109 | "🎬", // Clapper Board (Black/White) 110 | "🎨", // Artist Palette (Colorful) 111 | "🧶", // Yarn (Blue) 112 | "🧵", // Thread (Red) 113 | "🪡", // Sewing Needle (Silver) 114 | "🧩", // Puzzle Piece (Blue) 115 | "🎯", // Bullseye (Red/White) 116 | "🎱", // Pool 8 Ball (Black/White) 117 | "🚧", // Construction (Yellow/Black) 118 | "⚓", // Anchor (Black) 119 | "⛵", // Sailboat (White) 120 | "📟", // Pager (Grey) 121 | "📚", // Books (Colorful) 122 | "🎙️", // Studio Microphone (Grey) 123 | "💽", // Computer Disk (Black) 124 | "🎽", // Running Shirt (Blue) 125 | ]; 126 | 127 | export function mapEntityToEmoji(entity: bigint) { 128 | if (entity == 0n) return "N/A"; 129 | 130 | if (isLeftmostEntity(entity)) return "[LEFT_BOUNDARY]"; 131 | if (isRightmostEntity(entity)) return "[RIGHT_BOUNDARY]"; 132 | 133 | return colorfulEmojis[Number(entity % BigInt(colorfulEmojis.length))]; 134 | } 135 | 136 | export function findOrThrow(line: Entity[], entityId: bigint): Entity { 137 | const entity = line.find((e) => e.entityId === entityId); 138 | if (!entity) { 139 | console.trace("findOrThrow: Entity not found!"); 140 | throw new Error("ENTITY_NOT_FOUND"); 141 | } 142 | return entity; 143 | } 144 | -------------------------------------------------------------------------------- /packages/client/src/utils/game/configLib.ts: -------------------------------------------------------------------------------- 1 | import { BigintMinHeap } from "../bigintMinHeap"; 2 | import { PQ96x160 } from "../pq96x160"; 3 | 4 | export interface GameConfig { 5 | lineJumpDecayFactor: bigint; 6 | 7 | velocityCoefficient: bigint; 8 | 9 | minFoodMass: bigint; 10 | maxFoodMass: bigint; 11 | wallMass: bigint; 12 | playerStartingMass: bigint; 13 | 14 | lineWidth: bigint; 15 | consumableSpawnGap: bigint; 16 | 17 | powerPelletEffectTime: bigint; 18 | powerPelletSpawnOdds: number; 19 | 20 | highScoreTopK: number; 21 | } 22 | 23 | export interface GameState { 24 | numLines: number; 25 | ///////////////////////////////////// 26 | highScores: Map; 27 | usernames: Map; 28 | } 29 | 30 | export interface LineState { 31 | lastTouchedTime: bigint; 32 | 33 | collisionQueue: PQ96x160; 34 | } 35 | 36 | export function mapMassToDiameter(mass: bigint): bigint { 37 | return mass.sqrtWad(); 38 | } 39 | 40 | export function mapMassToVelocity(mass: bigint, globalVelCoeff: bigint): bigint { 41 | // 1000000001000000000 = 1.000000001e18, here to avoid negative and 0 outputs. 42 | return globalVelCoeff.divWad((mass + 1000000001000000000n).log10Wad()); 43 | } 44 | 45 | export function computeMassAfterJumpingLine(mass: bigint, lineJumpDecayFactor: bigint): bigint { 46 | return mass.mulWad(lineJumpDecayFactor); 47 | } 48 | -------------------------------------------------------------------------------- /packages/client/src/utils/game/entityLib.ts: -------------------------------------------------------------------------------- 1 | import { UINT144_MAX, UINT152_MAX } from "../bigint"; 2 | import { enqueue } from "../bigintMinHeap"; 3 | import { GameConfig, GameState, mapMassToDiameter, mapMassToVelocity } from "./configLib"; 4 | 5 | import { stopBackgroundMusic } from "../music"; 6 | 7 | export enum EntityType { 8 | DEAD = 0, 9 | ALIVE, 10 | FOOD, 11 | WALL, 12 | POWER_PELLET, 13 | } 14 | 15 | export type Entity = { 16 | entityId: bigint; 17 | ///////////////////////////////////////////////////////////////////// 18 | etype: EntityType; 19 | ///////////////////////////////////////////////////////////////////// 20 | mass: bigint; 21 | ///////////////////////////////////////////////////////////////////// 22 | velMultiplier: bigint; 23 | ///////////////////////////////////////////////////////////////////// 24 | lineId: number; 25 | lastX: bigint; 26 | lastTouchedTime: bigint; 27 | leftNeighbor: bigint; 28 | rightNeighbor: bigint; 29 | ///////////////////////////////////////////////////////////////////// 30 | lastConsumedPowerPelletTime: bigint; 31 | consumedMass: bigint; 32 | }; 33 | 34 | /*////////////////////////////////////////////////////////////// 35 | UP-TO-DATE ENTITY PROPERTY DATA 36 | //////////////////////////////////////////////////////////////*/ 37 | 38 | export function computeX(entity: Entity, wadTime: bigint, globalVelCoeff: bigint): bigint { 39 | return ( 40 | entity.lastX + computeVelocity(entity, globalVelCoeff).mulWad(wadTime - entity.lastTouchedTime) 41 | ); 42 | } 43 | 44 | export function computeCollisionTime( 45 | leftEntity: Entity, 46 | rightEntity: Entity, 47 | globalVelCoeff: bigint 48 | ) { 49 | const [leftEntityRightEdge, rightEntityLeftEdge, leftVelocity, rightVelocity] = [ 50 | leftEntity.lastX + computeDiameter(leftEntity), 51 | rightEntity.lastX, 52 | computeVelocity(leftEntity, globalVelCoeff), 53 | computeVelocity(rightEntity, globalVelCoeff), 54 | ]; 55 | 56 | // If leftVelocity > rightVelocity -> relativeVelocity > 0 -> will collide, since: 57 | // - the left entity is going right faster than the right entity can run away 58 | // - or the right entity is going left faster than the left entity can run away 59 | // - or the left entity is going right and the right entity is going to the left. 60 | // 61 | // If leftVelocity == rightVelocity -> relativeVelocity == 0 -> won't collide, since: 62 | // - the left entity and the right entity are both going at the exact same speed. 63 | // 64 | // If leftVelocity < rightVelocity -> relativeVelocity < 0 -> won't collide, since: 65 | // - the right entity is going right faster than the left entity can catch up 66 | // - or the left entity is going left faster than the right entity can catch up 67 | // - or the left entity is going left while the right entity is going to the right. 68 | const relativeVelocity = leftVelocity - rightVelocity; 69 | 70 | // Bail early as no need to compute a time when we know they won't collide. 71 | if (relativeVelocity <= 0n) return 0n; // Caller will ignore times in the past. 72 | 73 | // prettier-ignore 74 | return ((rightEntityLeftEdge - leftEntityRightEdge) 75 | + (leftEntity.lastTouchedTime.mulWad(leftVelocity)) 76 | - (rightEntity.lastTouchedTime.mulWad(rightVelocity))) 77 | .divWad(relativeVelocity); 78 | } 79 | 80 | export function isPoweredUp(entity: Entity, wadTime: bigint, powerPelletEffectTime: bigint) { 81 | return wadTime - entity.lastConsumedPowerPelletTime <= powerPelletEffectTime; 82 | } 83 | 84 | export function computeDiameter(entity: Entity) { 85 | return mapMassToDiameter(entity.mass); 86 | } 87 | 88 | export function computeVelocity(entity: Entity, globalVelCoeff: bigint) { 89 | return mapMassToVelocity(entity.mass, globalVelCoeff).mulWad(entity.velMultiplier); 90 | } 91 | 92 | /*////////////////////////////////////////////////////////////// 93 | ENTITY OPERATIONS 94 | //////////////////////////////////////////////////////////////*/ 95 | 96 | export function onConsumeEntity( 97 | line: Entity[], 98 | gameState: GameState, 99 | gameConfig: GameConfig, 100 | consumerEntity: Entity, 101 | consumedEntity: Entity, 102 | wadTime: bigint, 103 | playSfx: boolean, 104 | playerIdForSfx: bigint | null 105 | ) { 106 | // If the killed entity was a player (vs food, etc): 107 | if (consumedEntity.etype === EntityType.ALIVE) { 108 | // Enqueue (push only if >min) the consumed entity's consumed mass into its top-k scores. 109 | const highScores = gameState.highScores.get(consumedEntity.entityId) ?? ([] as bigint[]); 110 | enqueue(highScores, consumedEntity.consumedMass, gameConfig.highScoreTopK); 111 | gameState.highScores.set(consumedEntity.entityId, highScores); 112 | 113 | // NOTE: Spurious because we're about to remove the entity from state anyway, but whatever: 114 | consumedEntity.consumedMass = 0n; // Reset the consumed player's consumed mass. 115 | 116 | // SFX LOGIC — NOT PRESENT ON-CHAIN 117 | { 118 | if ( 119 | playSfx && 120 | consumerEntity.etype == EntityType.WALL && 121 | consumedEntity.entityId == playerIdForSfx 122 | ) 123 | new Audio("sounds/wallHit.wav").play(); 124 | 125 | if ( 126 | playSfx && 127 | consumerEntity.etype == EntityType.ALIVE && 128 | consumedEntity.entityId == playerIdForSfx 129 | ) 130 | new Audio("sounds/playerEaten.wav").play(); 131 | 132 | if (playSfx && consumedEntity.entityId == playerIdForSfx) { 133 | new Audio("sounds/death.wav").play(); 134 | stopBackgroundMusic(); 135 | } 136 | 137 | if (playSfx && consumerEntity.entityId == playerIdForSfx) 138 | new Audio("sounds/eatPlayer.wav").play(); 139 | } 140 | } else if ( 141 | // If the consumed entity was a power pellet and the consumer is a player: 142 | consumedEntity.etype === EntityType.POWER_PELLET && 143 | consumerEntity.etype === EntityType.ALIVE 144 | ) { 145 | consumerEntity.lastConsumedPowerPelletTime = wadTime; 146 | 147 | // SFX LOGIC — NOT PRESENT ON-CHAIN 148 | { 149 | if (playSfx && consumerEntity.entityId == playerIdForSfx) 150 | new Audio("sounds/powerPelletEat.wav").play(); 151 | } 152 | } 153 | // SFX LOGIC — NOT PRESENT ON-CHAIN: 154 | else { 155 | if ( 156 | playSfx && 157 | consumerEntity.entityId == playerIdForSfx && 158 | consumedEntity.etype == EntityType.FOOD 159 | ) 160 | new Audio("sounds/eat.wav").play(); 161 | } 162 | 163 | // Remove the entity from state. 164 | line.splice( 165 | line.findIndex((e) => e.entityId == consumedEntity.entityId), 166 | 1 167 | ); 168 | } 169 | 170 | /*////////////////////////////////////////////////////////////// 171 | ENTITY ID HELPERS 172 | //////////////////////////////////////////////////////////////*/ 173 | 174 | export function toEntityId(seed: bigint): bigint { 175 | // Add 1 to avoid returning 0, so we're sure 176 | // 0 means "not set" in the context of the game. 177 | // We also avoid returning ids over type(uint144).max, 178 | // so we can reserve that id range for boundary entities. 179 | return (seed % UINT144_MAX) + 1n; 180 | } 181 | 182 | export function leftmostEntityId(line: number) { 183 | return UINT144_MAX + 1n + BigInt(line); // + 1 to avoid overlap with non-boundary ids. 184 | } 185 | 186 | export function rightmostEntityId(line: number) { 187 | return UINT152_MAX + BigInt(line); 188 | } 189 | 190 | export function isRightmostEntity(entityId: bigint) { 191 | return entityId >= UINT152_MAX; 192 | } 193 | 194 | export function isLeftmostEntity(entity: bigint): boolean { 195 | return isBoundaryEntity(entity) && entity < UINT152_MAX; 196 | } 197 | 198 | export function isBoundaryEntity(entityId: bigint) { 199 | return entityId > UINT144_MAX; 200 | } 201 | -------------------------------------------------------------------------------- /packages/client/src/utils/hooks.ts: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from "react"; 2 | 3 | // Note: a delay of null will still run the callback once more immediately! 4 | export function useInterval(callback: () => void, delay: number | null) { 5 | const intervalRef = useRef(null); 6 | const savedCallback = useRef(callback); 7 | 8 | useEffect(() => { 9 | savedCallback.current = callback; 10 | }, [callback]); 11 | 12 | useEffect(() => { 13 | const tick = () => savedCallback.current(); 14 | tick(); // Run the callback once immediately. 15 | if (typeof delay === "number") { 16 | intervalRef.current = window.setInterval(tick, delay); 17 | return () => { 18 | if (intervalRef.current !== null) { 19 | window.clearInterval(intervalRef.current); 20 | } 21 | }; 22 | } 23 | }, [delay]); 24 | 25 | return intervalRef; 26 | } 27 | -------------------------------------------------------------------------------- /packages/client/src/utils/lineUI.ts: -------------------------------------------------------------------------------- 1 | // Efficiently calculates if a line is visible or partially visible 2 | // within the lines container. Uses totalHeight to avoid touching dom. 3 | export function calculateLineVisibility( 4 | totalHeightExclusive: number, 5 | totalHeightInclusive: number, 6 | containerHeight: number, 7 | scrollTop: number 8 | ) { 9 | const isVisible = 10 | totalHeightExclusive < scrollTop + containerHeight && totalHeightInclusive > scrollTop; 11 | 12 | const isOnlyPartiallyVisible = 13 | (totalHeightExclusive < scrollTop && totalHeightInclusive > scrollTop) || 14 | (totalHeightExclusive < scrollTop + containerHeight && 15 | totalHeightInclusive > scrollTop + containerHeight); 16 | 17 | return { isVisible, isOnlyPartiallyVisible }; 18 | } 19 | 20 | // NOTE: Ensure there is a corresponding CSS anim 21 | // class for every color you pass to this function! 22 | export function colorToGlowClass(color: string) { 23 | return `glow${color.toLowerCase().slice(1)}`; 24 | } 25 | 26 | export function centerLineInViewport(lineId: number) { 27 | setTimeout(() => { 28 | document.getElementById(`line-${lineId}`)?.scrollIntoView({ 29 | behavior: "smooth", 30 | block: "center", 31 | }); 32 | }, 1); 33 | } 34 | -------------------------------------------------------------------------------- /packages/client/src/utils/music.ts: -------------------------------------------------------------------------------- 1 | const BACKGROUND_MUSIC_VOLUME = 0.25; 2 | 3 | export function startBackgroundMusic(restart: boolean = false) { 4 | setTimeout(() => { 5 | const bgMusicElement = document.getElementById("backgroundMusic") as HTMLAudioElement; 6 | 7 | if (bgMusicElement.volume != BACKGROUND_MUSIC_VOLUME) 8 | bgMusicElement.volume = BACKGROUND_MUSIC_VOLUME; 9 | 10 | if (restart) bgMusicElement.currentTime = 0; 11 | 12 | if (restart || bgMusicElement.paused) { 13 | bgMusicElement.play(); 14 | console.log("Started background music!", { 15 | restart, 16 | paused: bgMusicElement.paused, 17 | src: bgMusicElement.src, 18 | }); 19 | } else { 20 | console.log("Ignored request to start background music!", { 21 | restart, 22 | paused: bgMusicElement.paused, 23 | src: bgMusicElement.src, 24 | }); 25 | } 26 | }, 1); 27 | } 28 | 29 | export function stopBackgroundMusic() { 30 | setTimeout(() => { 31 | const bgMusicElement = document.getElementById("backgroundMusic") as HTMLAudioElement; 32 | bgMusicElement.pause(); 33 | }, 1); 34 | } 35 | -------------------------------------------------------------------------------- /packages/client/src/utils/pq96x160.ts: -------------------------------------------------------------------------------- 1 | export function heapifyPacked(raw: readonly bigint[]): PQ96x160 { 2 | return raw.map(unpack); 3 | } 4 | 5 | export function unpack(packed: bigint): PQ96x160Entry { 6 | return { 7 | priority: packed >> 160n, 8 | value: packed & 0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffffn, 9 | }; 10 | } 11 | 12 | export type PQ96x160Entry = { 13 | priority: bigint; 14 | value: bigint; 15 | }; 16 | 17 | // Readonly so no one accidentally mutates the underlying array 18 | // instead of using the specialized heap functions defined below. 19 | export type PQ96x160 = readonly PQ96x160Entry[]; 20 | 21 | export function peek(pq: PQ96x160): PQ96x160Entry | undefined { 22 | return pq[0]; 23 | } 24 | 25 | export function push(pq: PQ96x160, element: PQ96x160Entry): boolean { 26 | _sortNodeUp(pq as PQ96x160Entry[], (pq as PQ96x160Entry[]).push(element) - 1); 27 | return true; 28 | } 29 | 30 | export function pop(pq: PQ96x160): PQ96x160Entry | undefined { 31 | const last = (pq as PQ96x160Entry[]).pop(); 32 | if (pq.length > 0 && last !== undefined) { 33 | return replace(pq, last); 34 | } 35 | return last; 36 | } 37 | 38 | export function replace(pq: PQ96x160, element: PQ96x160Entry): PQ96x160Entry { 39 | const peek = pq[0]; 40 | (pq as PQ96x160Entry[])[0] = element; 41 | _sortNodeDown(pq, 0); 42 | return peek; 43 | } 44 | 45 | // Internal functions: 46 | 47 | function _sortNodeDown(pq: PQ96x160, i: number): void { 48 | let moveIt = i < pq.length - 1; 49 | const self = pq[i]; 50 | 51 | const getPotentialParent = (best: number, j: number) => { 52 | if (pq.length > j && pq[j].priority - pq[best].priority < 0n) { 53 | best = j; 54 | } 55 | return best; 56 | }; 57 | 58 | while (moveIt) { 59 | const childrenIdx = _getChildrenIndexOf(i); 60 | const bestChildIndex = childrenIdx.reduce(getPotentialParent, childrenIdx[0]); 61 | const bestChild = pq[bestChildIndex]; 62 | if (typeof bestChild !== "undefined" && self.priority - bestChild.priority > 0n) { 63 | _moveNode(pq, i, bestChildIndex); 64 | i = bestChildIndex; 65 | } else { 66 | moveIt = false; 67 | } 68 | } 69 | } 70 | 71 | function _sortNodeUp(pq: PQ96x160, i: number): void { 72 | let moveIt = i > 0; 73 | while (moveIt) { 74 | const pi = _getParentIndexOf(i); 75 | if (pi >= 0 && pq[pi].priority - pq[i].priority > 0n) { 76 | _moveNode(pq, i, pi); 77 | i = pi; 78 | } else { 79 | moveIt = false; 80 | } 81 | } 82 | } 83 | 84 | function _moveNode(pq: PQ96x160, j: number, k: number): void { 85 | [(pq as PQ96x160Entry[])[j], (pq as PQ96x160Entry[])[k]] = [pq[k], pq[j]]; 86 | } 87 | 88 | function _getChildrenIndexOf(idx: number): Array { 89 | return [idx * 2 + 1, idx * 2 + 2]; 90 | } 91 | 92 | function _getParentIndexOf(idx: number): number { 93 | if (idx <= 0) { 94 | return -1; 95 | } 96 | const whichChildren = idx % 2 ? 1 : 2; 97 | return Math.floor((idx - whichChildren) / 2); 98 | } 99 | -------------------------------------------------------------------------------- /packages/client/src/utils/spawn.ts: -------------------------------------------------------------------------------- 1 | import { mapEntityToEmoji } from "./debugging"; 2 | import { GameConfig } from "./game/configLib"; 3 | import { 4 | Entity, 5 | EntityType, 6 | isRightmostEntity, 7 | computeX, 8 | isLeftmostEntity, 9 | computeDiameter, 10 | } from "./game/entityLib"; 11 | 12 | export function findBestRightSpawnNeighbor( 13 | line: Entity[], 14 | expectedInclusionBlockTimestampWad: bigint, 15 | gameConfig: GameConfig 16 | ) { 17 | let largestGap = 0n; 18 | let spawnRightNeighbor: Entity | null = null; 19 | 20 | // prettier-ignore 21 | line.sort((a, b) => { 22 | // Sometimes we'll end in cases where the rightmost or leftmost entity ids are not 23 | // last/first respectively, so we use a big number to ensure they are always last/first. 24 | const aX = isLeftmostEntity(a.entityId) ? -999999999999999999999999999999999999999999999n : 25 | isRightmostEntity(a.entityId) ? 999999999999999999999999999999999999999999999n : 26 | computeX(a, expectedInclusionBlockTimestampWad, gameConfig.velocityCoefficient); 27 | const bX = isLeftmostEntity(b.entityId) ? -999999999999999999999999999999999999999999999n : 28 | isRightmostEntity(b.entityId) ? 999999999999999999999999999999999999999999999n : 29 | computeX(b, expectedInclusionBlockTimestampWad, gameConfig.velocityCoefficient); 30 | return Number(aX - bX); 31 | }); 32 | 33 | console.log( 34 | "Line to spawn on:", 35 | line.map((e) => mapEntityToEmoji(e.entityId) + " " + EntityType[e.etype]) 36 | ); 37 | 38 | for (let i = line.length - 1; i >= 0; i--) { 39 | const rightEntity = line[i]; 40 | 41 | // We only care about walls and rightmost entities, as they never disappear from the line. 42 | if (rightEntity.etype !== EntityType.WALL && !isRightmostEntity(rightEntity.entityId)) continue; 43 | 44 | const rightLeftEdge = computeX( 45 | rightEntity, 46 | expectedInclusionBlockTimestampWad, 47 | gameConfig.velocityCoefficient 48 | ); 49 | 50 | console.log( 51 | "Left edge of", 52 | mapEntityToEmoji(rightEntity.entityId), 53 | rightLeftEdge.fromWad().toFixed(1) 54 | ); 55 | 56 | const closestLeftHostile = 57 | line 58 | .slice(0, i) 59 | .reverse() 60 | .find((e) => e.etype === EntityType.ALIVE || e.etype === EntityType.WALL) ?? 61 | line.find((e) => isLeftmostEntity(e.entityId)); 62 | 63 | console.log( 64 | "Search set for closest left hostile:", 65 | line 66 | .slice(0, i) 67 | .reverse() 68 | .map((e) => mapEntityToEmoji(e.entityId) + " " + EntityType[e.etype]) 69 | ); 70 | 71 | if (!closestLeftHostile) throw new Error("No closest left hostile found... somehow?"); // Just in case. 72 | 73 | console.log( 74 | "Closest left hostile of", 75 | mapEntityToEmoji(rightEntity.entityId), 76 | "is", 77 | mapEntityToEmoji(closestLeftHostile?.entityId) 78 | ); 79 | 80 | const leftRightEdge = 81 | computeX( 82 | closestLeftHostile, 83 | expectedInclusionBlockTimestampWad, 84 | gameConfig.velocityCoefficient 85 | ) + computeDiameter(closestLeftHostile); 86 | 87 | console.log( 88 | "Left hostile right edge:", 89 | mapEntityToEmoji(closestLeftHostile.entityId), 90 | leftRightEdge.fromWad().toFixed(1) 91 | ); 92 | 93 | const gap = rightLeftEdge - leftRightEdge; 94 | 95 | console.log("Gap:", gap.fromWad().toFixed(1)); 96 | 97 | if (gap > largestGap) { 98 | largestGap = gap; 99 | spawnRightNeighbor = rightEntity; 100 | } 101 | } 102 | 103 | return spawnRightNeighbor; 104 | } 105 | -------------------------------------------------------------------------------- /packages/client/src/utils/sync.ts: -------------------------------------------------------------------------------- 1 | import { BigintMinHeap } from "./bigintMinHeap"; 2 | import { GameState, GameConfig, LineState } from "./game/configLib"; 3 | import { Entity } from "./game/entityLib"; 4 | import { processCollisions } from "./game/lineLib"; 5 | import { heapifyPacked } from "./pq96x160"; 6 | import { timeWad } from "./timeLib"; 7 | import { getRecord, getRecords, State } from "@latticexyz/stash/internal"; 8 | import { 9 | initialProgress, 10 | SyncProgress as SyncProgressTable, 11 | } from "@latticexyz/store-sync/internal"; 12 | 13 | import raw_config from "contracts/mud.config"; 14 | // For some reason when importing config in Node.js, instead of just 15 | // being the object, it's wrapped in an object with a `default` property. 16 | const config = "default" in raw_config ? (raw_config.default as typeof raw_config) : raw_config; 17 | 18 | // Must be idempotent / deterministic with respect to the state. 19 | export function parseSyncStateGivenTables(state: State) { 20 | const syncProgress: { 21 | step: string; 22 | message: string; 23 | percentage: number; 24 | latestBlockNumber: bigint; 25 | lastBlockNumberProcessed: bigint; 26 | } = getRecord({ 27 | state, 28 | table: SyncProgressTable, 29 | key: {}, 30 | defaultValue: initialProgress, 31 | }); 32 | 33 | if (syncProgress.step !== "live") 34 | return { 35 | syncProgress, 36 | data: undefined, 37 | }; 38 | 39 | const [rawGameState, gameConfig] = [ 40 | getRecord({ state, table: config.tables.GameState, key: {} }) as GameState, 41 | getRecord({ state, table: config.tables.GameConfig, key: {} }) as GameConfig, 42 | ]; 43 | 44 | const highScores = new Map(); // Will be put into GameState. 45 | Object.values(getRecords({ state, table: config.tables.Player })).forEach((record) => { 46 | if (record.highScores.length !== 0) highScores.set(record.entityId, record.highScores); 47 | }); 48 | const usernames = new Map(); // Will be put into GameState. 49 | Object.values(getRecords({ state, table: config.tables.UsernameOffchain })).forEach((record) => { 50 | usernames.set(record.entityId, record.username); 51 | }); 52 | 53 | // Many tables share the same key schema, so we'll merge them by entityId for simplicity. 54 | const flatEntities: Entity[] = Object.values( 55 | getRecords({ state, table: config.tables.Entity }) 56 | ).map((record) => { 57 | const entityId = record.entityId; 58 | return { 59 | entityId, 60 | etype: record.etype, 61 | mass: record.mass, 62 | velMultiplier: record.velMultiplier, 63 | lineId: record.lineId, 64 | lastX: record.lastX, 65 | lastTouchedTime: record.lastTouchedTime, 66 | leftNeighbor: record.leftNeighbor, 67 | rightNeighbor: record.rightNeighbor, 68 | // This is not actually in this table, but it's simpler to just put it in here: 69 | lastConsumedPowerPelletTime: 70 | getRecord({ state, table: config.tables.Player, key: { entityId } }) 71 | ?.lastConsumedPowerPelletTime ?? 0n, 72 | consumedMass: 73 | getRecord({ state, table: config.tables.Player, key: { entityId } })?.consumedMass ?? 0n, 74 | }; 75 | }); 76 | // Reshape flat entities into a multi-dimensional array, where each sub-array is a lineId. 77 | const lines = flatEntities.reduce((acc, record) => { 78 | if (!acc[record.lineId]) acc[record.lineId] = []; 79 | acc[record.lineId].push(record); 80 | return acc; 81 | }, [] as Entity[][]); 82 | const lineStates = Object.values(getRecords({ state, table: config.tables.Line })) 83 | .sort((a, b) => a.lineId - b.lineId) // This is crucial for the order of lines to be consistent! 84 | .map((q) => ({ 85 | ...q, 86 | collisionQueue: heapifyPacked(q.collisionQueue), 87 | // This is not actually in this table, but it's simpler to just put it in here: 88 | lastTouchedTime: 89 | getRecord({ state, table: config.tables.LineOffchain, key: { lineId: q.lineId } }) 90 | ?.lastTouchedTime ?? 0n, 91 | })) as LineState[]; 92 | 93 | return { 94 | syncProgress, 95 | data: { 96 | gameState: { 97 | ...rawGameState, 98 | highScores, // Add highScores to GameState. 99 | usernames, // Add usernames to GameState. 100 | }, 101 | gameConfig, 102 | lines, 103 | lineStates, 104 | }, 105 | }; 106 | } 107 | 108 | export type LiveState = { 109 | lastSyncedTime: number; 110 | lastProcessedTime: bigint; 111 | lines: Entity[][]; 112 | lineStates: LineState[]; 113 | gameState: GameState; 114 | }; 115 | 116 | export function forwardStateTo( 117 | prevState: LiveState, 118 | gameConfig: GameConfig, 119 | playSfx: boolean, 120 | playerIdForSfx: bigint | null, 121 | options: { stopAtIteration: number | null; stopAtTimestampWad: bigint | null } 122 | ): LiveState { 123 | const { stopAtIteration, stopAtTimestampWad } = options; 124 | 125 | let newState = structuredClone(prevState); 126 | 127 | let lastCollisionTime = -1n; // For debugging. 128 | for (let i = 0; i < newState.lines.length; i++) { 129 | lastCollisionTime = processCollisions( 130 | newState.lines[i], 131 | newState.gameState, 132 | gameConfig, 133 | newState.lineStates[i], 134 | playSfx, 135 | playerIdForSfx, 136 | { 137 | stopAtIteration, 138 | stopAtTimestampWad, 139 | } 140 | ); 141 | } 142 | 143 | return { 144 | ...newState, 145 | lastProcessedTime: 146 | stopAtIteration == null ? stopAtTimestampWad ?? timeWad() : lastCollisionTime, 147 | }; 148 | } 149 | -------------------------------------------------------------------------------- /packages/client/src/utils/timeLib.ts: -------------------------------------------------------------------------------- 1 | import { WAD } from "./bigint"; 2 | 3 | export function chainTime(): number { 4 | return Date.now(); 5 | } 6 | 7 | // seconds since epoch * 1e18 8 | export function timeWad(): bigint { 9 | return msToWad(chainTime()); 10 | } 11 | 12 | export function msToWad(ms: number): bigint { 13 | return (BigInt(ms) * WAD) / 1000n; 14 | } 15 | 16 | export function formatOffset(offset: number): string { 17 | return (offset >= 0 ? "+" : "") + offset.toFixed(2) + "ms"; 18 | } 19 | -------------------------------------------------------------------------------- /packages/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["vite/client", "vite-plugin-mud/env"], 5 | "target": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "jsx": "react-jsx", 8 | "jsxImportSource": "react" 9 | }, 10 | "include": ["src"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import { mud } from "vite-plugin-mud"; 4 | 5 | export default defineConfig({ 6 | plugins: [react(), mud({ worldsFile: "../contracts/worlds.json" })], 7 | server: { 8 | port: 3000, 9 | }, 10 | build: { 11 | target: "es2022", 12 | minify: true, 13 | sourcemap: true, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /packages/contracts/.env: -------------------------------------------------------------------------------- 1 | # This .env file is for demonstration purposes only. 2 | # 3 | # This should usually be excluded via .gitignore and the env vars attached to 4 | # your deployment enviroment, but we're including this here for ease of local 5 | # development. Please do not commit changes to this file! 6 | # 7 | # Enable debug logs for MUD CLI 8 | DEBUG=mud:* 9 | # 10 | # Anvil default private key: 11 | PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 12 | -------------------------------------------------------------------------------- /packages/contracts/.gitignore: -------------------------------------------------------------------------------- 1 | out/* 2 | !out/IWorld.sol/ 3 | cache/ 4 | node_modules/ 5 | bindings/ 6 | artifacts/ 7 | broadcast/ 8 | .mud/ 9 | 10 | # Ignore MUD deploy artifacts 11 | deploys/**/*.json 12 | 13 | -------------------------------------------------------------------------------- /packages/contracts/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "printWidth": 100, 4 | 5 | "overrides": [ 6 | { 7 | "files": "*.sol", 8 | "options": { 9 | "tabWidth": 4, 10 | "printWidth": 125 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/contracts/.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 | -------------------------------------------------------------------------------- /packages/contracts/foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | solc = "0.8.25" 3 | allow_internal_expect_revert = true 4 | script_execution_protection = false 5 | optimizer_runs = 1000000 6 | bytecode_hash = "none" 7 | show_progress = true 8 | allow_paths = [ 9 | # pnpm symlinks to the project root's node_modules 10 | "../../node_modules", 11 | # template uses linked mud packages from within the mud monorepo 12 | "../../../../packages", 13 | # projects created from this template and using linked mud packages 14 | "../../../mud/packages", 15 | ] 16 | extra_output_files = [ 17 | "abi", 18 | "evm.bytecode" 19 | ] 20 | fs_permissions = [{ access = "read", path = "./"}] 21 | 22 | [fuzz] 23 | runs = 1000 24 | depth = 250 25 | 26 | [invariant] 27 | shrink_run_limit = 4294967295 28 | 29 | [profile.odyssey] 30 | eth_rpc_url = "https://odyssey.ithaca.xyz" 31 | -------------------------------------------------------------------------------- /packages/contracts/mud.config.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { defineWorld } from "@latticexyz/world"; 3 | 4 | export default defineWorld({ 5 | enums: { 6 | EntityType: ["DEAD", "ALIVE", "FOOD", "WALL", "POWER_PELLET"], 7 | }, 8 | tables: { 9 | GameConfig: { 10 | key: [], // Singleton. 11 | schema: { 12 | // 64 bits because the decay factor will 13 | // be <1 (1e18), and 2**64 - 1 is ~1.8e19. 14 | lineJumpDecayFactor: "uint64", 15 | 16 | velocityCoefficient: "uint96", 17 | 18 | minFoodMass: "uint96", 19 | maxFoodMass: "uint96", 20 | wallMass: "uint96", 21 | playerStartingMass: "uint96", 22 | 23 | lineWidth: "uint128", 24 | consumableSpawnGap: "uint128", 25 | 26 | powerPelletEffectTime: "uint96", 27 | powerPelletSpawnOdds: "uint32", 28 | 29 | highScoreTopK: "uint8", 30 | 31 | accessSigner: "address", 32 | }, 33 | }, 34 | 35 | GameState: { 36 | key: [], // Singleton. 37 | schema: { 38 | numLines: "uint32", 39 | }, 40 | }, 41 | 42 | Line: { 43 | key: ["lineId"], 44 | schema: { 45 | lineId: "uint32", 46 | 47 | collisionQueue: "uint256[]", // will be managed via PriorityQueue96x160Lib 48 | }, 49 | }, 50 | 51 | Player: { 52 | key: ["entityId"], 53 | schema: { 54 | entityId: "uint160", 55 | 56 | consumedMass: "uint128", 57 | // We overload the purpose of this field to both manage access and to prevent spamming. 58 | lastJumpBlockNumber: "uint32", // Spawning counts as a jump. 59 | lastConsumedPowerPelletTime: "uint96", 60 | highScores: "uint256[]", // will be managed via solady/MinHeapLib 61 | }, 62 | }, 63 | 64 | Entity: { 65 | key: ["entityId"], 66 | schema: { 67 | entityId: "uint160", 68 | 69 | etype: "EntityType", 70 | 71 | mass: "uint128", 72 | 73 | velMultiplier: "int128", 74 | 75 | lineId: "uint32", 76 | // Note: lastX is the x position of the *left edge* of the entity. 77 | lastX: "uint128", // Add diameter (computed via mass) for right edge. 78 | lastTouchedTime: "uint96", 79 | leftNeighbor: "uint160", 80 | rightNeighbor: "uint160", 81 | }, 82 | }, 83 | 84 | UsernameHash: { 85 | key: ["usernameHash"], 86 | schema: { 87 | usernameHash: "bytes32", 88 | 89 | taken: "bool", // To prevent registering multiple addresses under the same username. 90 | }, 91 | }, 92 | 93 | // Offchain tables are just for users/clients, 94 | // just emits an event the MUD indexer tracks. 95 | LineOffchain: { 96 | key: ["lineId"], 97 | schema: { 98 | lineId: "uint32", 99 | 100 | lastTouchedTime: "uint96", 101 | }, 102 | type: "offchainTable", 103 | }, 104 | UsernameOffchain: { 105 | key: ["entityId"], 106 | schema: { 107 | entityId: "uint160", 108 | 109 | username: "string", 110 | }, 111 | type: "offchainTable", 112 | }, 113 | }, 114 | systems: { 115 | AdminSystem: { 116 | openAccess: false, 117 | }, 118 | }, 119 | }); 120 | -------------------------------------------------------------------------------- /packages/contracts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contracts", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "mud build", 8 | "clean": "forge clean && rimraf src/codegen", 9 | "deploy:local": "mud deploy", 10 | "deploy:odyssey": "mud deploy --profile=odyssey", 11 | "dev": "pnpm mud dev-contracts", 12 | "lint": "pnpm run prettier && pnpm run solhint", 13 | "prettier": "prettier --write 'src/**/*.sol'", 14 | "solhint": "solhint --config ./.solhint.json 'src/**/*.sol' --fix", 15 | "test": "tsc --noEmit && mud test" 16 | }, 17 | "dependencies": { 18 | "@latticexyz/cli": "2.2.22-8c4b624756007fd02b1c3c3494e34128a5f1c044", 19 | "@latticexyz/schema-type": "2.2.22-8c4b624756007fd02b1c3c3494e34128a5f1c044", 20 | "@latticexyz/store": "2.2.22-8c4b624756007fd02b1c3c3494e34128a5f1c044", 21 | "@latticexyz/world": "2.2.22-8c4b624756007fd02b1c3c3494e34128a5f1c044", 22 | "@latticexyz/world-modules": "2.2.22-8c4b624756007fd02b1c3c3494e34128a5f1c044" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "^18.15.11", 26 | "ds-test": "https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0", 27 | "forge-std": "https://github.com/foundry-rs/forge-std.git#4d63c978718517fa02d4e330fbe7372dbb06c2f1", 28 | "prettier": "3.2.5", 29 | "prettier-plugin-solidity": "1.3.1", 30 | "solady": "^0.0.232", 31 | "solhint": "^3.3.7", 32 | "solhint-config-mud": "2.2.22-8c4b624756007fd02b1c3c3494e34128a5f1c044", 33 | "solhint-plugin-mud": "2.2.22-8c4b624756007fd02b1c3c3494e34128a5f1c044" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/contracts/remappings.txt: -------------------------------------------------------------------------------- 1 | ds-test/=node_modules/ds-test/src/ 2 | forge-std/=node_modules/forge-std/src/ 3 | @latticexyz/=node_modules/@latticexyz/ 4 | solady/=node_modules/solady/src/ -------------------------------------------------------------------------------- /packages/contracts/script/BalanceChanges.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.21; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {StoreSwitch} from "@latticexyz/store/src/StoreSwitch.sol"; 6 | 7 | import {ConfigLib} from "../src/utils/ConfigLib.sol"; 8 | 9 | import "../src/codegen/index.sol"; 10 | 11 | contract BalanceChanges is Script { 12 | function run(address worldAddress) external { 13 | // Set PRIVATE_KEY environment variable in .env file. 14 | vm.startBroadcast(vm.envUint("PRIVATE_KEY")); 15 | 16 | StoreSwitch.setStoreAddress(worldAddress); // Must be called to use stores directly in scripts. 17 | 18 | uint96 newMaxFoodMass = 256e18 * 3; 19 | uint96 newMinFoodMass = 32e18 * 4; 20 | uint64 newLineJumpDecayFactor = 0.75e18; 21 | 22 | require( 23 | GameConfig.getConsumableSpawnGap() > 3 * ConfigLib.mapMassToDiameter(newMaxFoodMass), 24 | "FOOD_SPAWN_GAP_TOO_LOW" 25 | ); // Must be at least 2x for power pellets. 26 | 27 | GameConfig.setMinFoodMass(newMinFoodMass); 28 | GameConfig.setMaxFoodMass(newMaxFoodMass); 29 | GameConfig.setLineJumpDecayFactor(newLineJumpDecayFactor); 30 | 31 | vm.stopBroadcast(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/contracts/script/PostDeploy.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.21; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {StoreSwitch} from "@latticexyz/store/src/StoreSwitch.sol"; 6 | 7 | import {IWorld} from "../src/codegen/world/IWorld.sol"; 8 | 9 | import {EntityLib} from "../src/utils/EntityLib.sol"; 10 | import {ConfigLib} from "../src/utils/ConfigLib.sol"; 11 | 12 | import "../src/codegen/index.sol"; 13 | 14 | contract PostDeploy is Script { 15 | function run(address worldAddress) external { 16 | // Set PRIVATE_KEY environment variable in .env file. 17 | vm.startBroadcast(vm.envUint("PRIVATE_KEY")); 18 | 19 | StoreSwitch.setStoreAddress(worldAddress); // Must be called to use stores directly in scripts. 20 | 21 | uint64 lineJumpDecayFactor = 0.9e18; // 1e18 = no decay. 22 | 23 | uint96 velocityCoefficient = 315e18; 24 | 25 | uint96 minFoodMass = 32e18 * 1; 26 | uint96 maxFoodMass = 256e18 * 1; 27 | uint96 wallMass = 9e18; 28 | uint96 playerStartingMass = 1600e18; 29 | 30 | uint128 lineWidth = 1000e18; 31 | uint128 consumableSpawnGap = lineWidth / 5; 32 | require(consumableSpawnGap > 3 * ConfigLib.mapMassToDiameter(maxFoodMass), "FOOD_SPAWN_GAP_TOO_LOW"); // Must be at least 2x for power pellets. 33 | 34 | uint96 powerPelletEffectTime = 10e18; // 10 seconds. 35 | uint32 powerPelletSpawnOdds = 50; // 1 in 50 consumables. 36 | 37 | uint8 highScoreTopK = 10; 38 | 39 | address accessSigner = 0x518df905D5E7E7C74B41f178fB078ea028A79cC3; 40 | 41 | GameConfig.set( 42 | GameConfigData({ 43 | lineJumpDecayFactor: lineJumpDecayFactor, 44 | ///////////////////////////////////////// 45 | velocityCoefficient: velocityCoefficient, 46 | ///////////////////////////////////////// 47 | minFoodMass: minFoodMass, 48 | maxFoodMass: maxFoodMass, 49 | wallMass: wallMass, 50 | playerStartingMass: playerStartingMass, 51 | ///////////////////////////////////////// 52 | lineWidth: lineWidth, 53 | consumableSpawnGap: consumableSpawnGap, 54 | ///////////////////////////////////////// 55 | powerPelletEffectTime: powerPelletEffectTime, 56 | powerPelletSpawnOdds: powerPelletSpawnOdds, 57 | //////////////////////////////////////// 58 | highScoreTopK: highScoreTopK, 59 | //////////////////////////////////////// 60 | accessSigner: accessSigner 61 | }) 62 | ); 63 | 64 | // Needed to ensure reproducible invariant failures. 65 | if (vm.envOr("FORCE_DETERMINISTIC_TIMESTAMP", false)) { 66 | vm.warp(9999999999); 67 | vm.rpc("evm_setNextBlockTimestamp", '["9999999999"]'); 68 | } 69 | 70 | IWorld(worldAddress).addLines(8); 71 | 72 | vm.stopBroadcast(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/contracts/src/codegen/common.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | enum EntityType { 7 | DEAD, 8 | ALIVE, 9 | FOOD, 10 | WALL, 11 | POWER_PELLET 12 | } 13 | -------------------------------------------------------------------------------- /packages/contracts/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 { GameState } from "./tables/GameState.sol"; 8 | import { Line } from "./tables/Line.sol"; 9 | import { Player, PlayerData } from "./tables/Player.sol"; 10 | import { Entity, EntityData } from "./tables/Entity.sol"; 11 | import { UsernameHash } from "./tables/UsernameHash.sol"; 12 | import { LineOffchain } from "./tables/LineOffchain.sol"; 13 | import { UsernameOffchain } from "./tables/UsernameOffchain.sol"; 14 | -------------------------------------------------------------------------------- /packages/contracts/src/codegen/tables/GameState.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 GameState { 20 | // Hex below is the result of `WorldResourceIdLib.encode({ namespace: "", name: "GameState", typeId: RESOURCE_TABLE });` 21 | ResourceId constant _tableId = ResourceId.wrap(0x7462000000000000000000000000000047616d65537461746500000000000000); 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] = "numLines"; 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 numLines. 64 | */ 65 | function getNumLines() internal view returns (uint32 numLines) { 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 numLines. 74 | */ 75 | function _getNumLines() internal view returns (uint32 numLines) { 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 numLines. 84 | */ 85 | function get() internal view returns (uint32 numLines) { 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 numLines. 94 | */ 95 | function _get() internal view returns (uint32 numLines) { 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 numLines. 104 | */ 105 | function setNumLines(uint32 numLines) internal { 106 | bytes32[] memory _keyTuple = new bytes32[](0); 107 | 108 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((numLines)), _fieldLayout); 109 | } 110 | 111 | /** 112 | * @notice Set numLines. 113 | */ 114 | function _setNumLines(uint32 numLines) internal { 115 | bytes32[] memory _keyTuple = new bytes32[](0); 116 | 117 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((numLines)), _fieldLayout); 118 | } 119 | 120 | /** 121 | * @notice Set numLines. 122 | */ 123 | function set(uint32 numLines) internal { 124 | bytes32[] memory _keyTuple = new bytes32[](0); 125 | 126 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((numLines)), _fieldLayout); 127 | } 128 | 129 | /** 130 | * @notice Set numLines. 131 | */ 132 | function _set(uint32 numLines) internal { 133 | bytes32[] memory _keyTuple = new bytes32[](0); 134 | 135 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((numLines)), _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 numLines) internal pure returns (bytes memory) { 161 | return abi.encodePacked(numLines); 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 numLines) internal pure returns (bytes memory, EncodedLengths, bytes memory) { 171 | bytes memory _staticData = encodeStatic(numLines); 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 | -------------------------------------------------------------------------------- /packages/contracts/src/codegen/tables/LineOffchain.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 LineOffchain { 20 | // Hex below is the result of `WorldResourceIdLib.encode({ namespace: "", name: "LineOffchain", typeId: RESOURCE_OFFCHAIN_TABLE });` 21 | ResourceId constant _tableId = ResourceId.wrap(0x6f7400000000000000000000000000004c696e654f6666636861696e00000000); 22 | 23 | FieldLayout constant _fieldLayout = 24 | FieldLayout.wrap(0x000c01000c000000000000000000000000000000000000000000000000000000); 25 | 26 | // Hex-encoded key schema of (uint32) 27 | Schema constant _keySchema = Schema.wrap(0x0004010003000000000000000000000000000000000000000000000000000000); 28 | // Hex-encoded value schema of (uint96) 29 | Schema constant _valueSchema = Schema.wrap(0x000c01000b000000000000000000000000000000000000000000000000000000); 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] = "lineId"; 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] = "lastTouchedTime"; 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 Set lastTouchedTime. 65 | */ 66 | function setLastTouchedTime(uint32 lineId, uint96 lastTouchedTime) internal { 67 | bytes32[] memory _keyTuple = new bytes32[](1); 68 | _keyTuple[0] = bytes32(uint256(lineId)); 69 | 70 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((lastTouchedTime)), _fieldLayout); 71 | } 72 | 73 | /** 74 | * @notice Set lastTouchedTime. 75 | */ 76 | function _setLastTouchedTime(uint32 lineId, uint96 lastTouchedTime) internal { 77 | bytes32[] memory _keyTuple = new bytes32[](1); 78 | _keyTuple[0] = bytes32(uint256(lineId)); 79 | 80 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((lastTouchedTime)), _fieldLayout); 81 | } 82 | 83 | /** 84 | * @notice Set the full data using individual values. 85 | */ 86 | function set(uint32 lineId, uint96 lastTouchedTime) internal { 87 | bytes memory _staticData = encodeStatic(lastTouchedTime); 88 | 89 | EncodedLengths _encodedLengths; 90 | bytes memory _dynamicData; 91 | 92 | bytes32[] memory _keyTuple = new bytes32[](1); 93 | _keyTuple[0] = bytes32(uint256(lineId)); 94 | 95 | StoreSwitch.setRecord(_tableId, _keyTuple, _staticData, _encodedLengths, _dynamicData); 96 | } 97 | 98 | /** 99 | * @notice Set the full data using individual values. 100 | */ 101 | function _set(uint32 lineId, uint96 lastTouchedTime) internal { 102 | bytes memory _staticData = encodeStatic(lastTouchedTime); 103 | 104 | EncodedLengths _encodedLengths; 105 | bytes memory _dynamicData; 106 | 107 | bytes32[] memory _keyTuple = new bytes32[](1); 108 | _keyTuple[0] = bytes32(uint256(lineId)); 109 | 110 | StoreCore.setRecord(_tableId, _keyTuple, _staticData, _encodedLengths, _dynamicData, _fieldLayout); 111 | } 112 | 113 | /** 114 | * @notice Decode the tightly packed blob of static data using this table's field layout. 115 | */ 116 | function decodeStatic(bytes memory _blob) internal pure returns (uint96 lastTouchedTime) { 117 | lastTouchedTime = (uint96(Bytes.getBytes12(_blob, 0))); 118 | } 119 | 120 | /** 121 | * @notice Decode the tightly packed blobs using this table's field layout. 122 | * @param _staticData Tightly packed static fields. 123 | * 124 | * 125 | */ 126 | function decode( 127 | bytes memory _staticData, 128 | EncodedLengths, 129 | bytes memory 130 | ) internal pure returns (uint96 lastTouchedTime) { 131 | (lastTouchedTime) = decodeStatic(_staticData); 132 | } 133 | 134 | /** 135 | * @notice Delete all data for given keys. 136 | */ 137 | function deleteRecord(uint32 lineId) internal { 138 | bytes32[] memory _keyTuple = new bytes32[](1); 139 | _keyTuple[0] = bytes32(uint256(lineId)); 140 | 141 | StoreSwitch.deleteRecord(_tableId, _keyTuple); 142 | } 143 | 144 | /** 145 | * @notice Delete all data for given keys. 146 | */ 147 | function _deleteRecord(uint32 lineId) internal { 148 | bytes32[] memory _keyTuple = new bytes32[](1); 149 | _keyTuple[0] = bytes32(uint256(lineId)); 150 | 151 | StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); 152 | } 153 | 154 | /** 155 | * @notice Tightly pack static (fixed length) data using this table's schema. 156 | * @return The static data, encoded into a sequence of bytes. 157 | */ 158 | function encodeStatic(uint96 lastTouchedTime) internal pure returns (bytes memory) { 159 | return abi.encodePacked(lastTouchedTime); 160 | } 161 | 162 | /** 163 | * @notice Encode all of a record's fields. 164 | * @return The static (fixed length) data, encoded into a sequence of bytes. 165 | * @return The lengths of the dynamic fields (packed into a single bytes32 value). 166 | * @return The dynamic (variable length) data, encoded into a sequence of bytes. 167 | */ 168 | function encode(uint96 lastTouchedTime) internal pure returns (bytes memory, EncodedLengths, bytes memory) { 169 | bytes memory _staticData = encodeStatic(lastTouchedTime); 170 | 171 | EncodedLengths _encodedLengths; 172 | bytes memory _dynamicData; 173 | 174 | return (_staticData, _encodedLengths, _dynamicData); 175 | } 176 | 177 | /** 178 | * @notice Encode keys as a bytes32 array using this table's field layout. 179 | */ 180 | function encodeKeyTuple(uint32 lineId) internal pure returns (bytes32[] memory) { 181 | bytes32[] memory _keyTuple = new bytes32[](1); 182 | _keyTuple[0] = bytes32(uint256(lineId)); 183 | 184 | return _keyTuple; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /packages/contracts/src/codegen/tables/UsernameHash.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 UsernameHash { 20 | // Hex below is the result of `WorldResourceIdLib.encode({ namespace: "", name: "UsernameHash", typeId: RESOURCE_TABLE });` 21 | ResourceId constant _tableId = ResourceId.wrap(0x74620000000000000000000000000000557365726e616d654861736800000000); 22 | 23 | FieldLayout constant _fieldLayout = 24 | FieldLayout.wrap(0x0001010001000000000000000000000000000000000000000000000000000000); 25 | 26 | // Hex-encoded key schema of (bytes32) 27 | Schema constant _keySchema = Schema.wrap(0x002001005f000000000000000000000000000000000000000000000000000000); 28 | // Hex-encoded value schema of (bool) 29 | Schema constant _valueSchema = Schema.wrap(0x0001010060000000000000000000000000000000000000000000000000000000); 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] = "usernameHash"; 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] = "taken"; 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 taken. 65 | */ 66 | function getTaken(bytes32 usernameHash) internal view returns (bool taken) { 67 | bytes32[] memory _keyTuple = new bytes32[](1); 68 | _keyTuple[0] = usernameHash; 69 | 70 | bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 71 | return (_toBool(uint8(bytes1(_blob)))); 72 | } 73 | 74 | /** 75 | * @notice Get taken. 76 | */ 77 | function _getTaken(bytes32 usernameHash) internal view returns (bool taken) { 78 | bytes32[] memory _keyTuple = new bytes32[](1); 79 | _keyTuple[0] = usernameHash; 80 | 81 | bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 82 | return (_toBool(uint8(bytes1(_blob)))); 83 | } 84 | 85 | /** 86 | * @notice Get taken. 87 | */ 88 | function get(bytes32 usernameHash) internal view returns (bool taken) { 89 | bytes32[] memory _keyTuple = new bytes32[](1); 90 | _keyTuple[0] = usernameHash; 91 | 92 | bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 93 | return (_toBool(uint8(bytes1(_blob)))); 94 | } 95 | 96 | /** 97 | * @notice Get taken. 98 | */ 99 | function _get(bytes32 usernameHash) internal view returns (bool taken) { 100 | bytes32[] memory _keyTuple = new bytes32[](1); 101 | _keyTuple[0] = usernameHash; 102 | 103 | bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); 104 | return (_toBool(uint8(bytes1(_blob)))); 105 | } 106 | 107 | /** 108 | * @notice Set taken. 109 | */ 110 | function setTaken(bytes32 usernameHash, bool taken) internal { 111 | bytes32[] memory _keyTuple = new bytes32[](1); 112 | _keyTuple[0] = usernameHash; 113 | 114 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((taken)), _fieldLayout); 115 | } 116 | 117 | /** 118 | * @notice Set taken. 119 | */ 120 | function _setTaken(bytes32 usernameHash, bool taken) internal { 121 | bytes32[] memory _keyTuple = new bytes32[](1); 122 | _keyTuple[0] = usernameHash; 123 | 124 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((taken)), _fieldLayout); 125 | } 126 | 127 | /** 128 | * @notice Set taken. 129 | */ 130 | function set(bytes32 usernameHash, bool taken) internal { 131 | bytes32[] memory _keyTuple = new bytes32[](1); 132 | _keyTuple[0] = usernameHash; 133 | 134 | StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((taken)), _fieldLayout); 135 | } 136 | 137 | /** 138 | * @notice Set taken. 139 | */ 140 | function _set(bytes32 usernameHash, bool taken) internal { 141 | bytes32[] memory _keyTuple = new bytes32[](1); 142 | _keyTuple[0] = usernameHash; 143 | 144 | StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((taken)), _fieldLayout); 145 | } 146 | 147 | /** 148 | * @notice Delete all data for given keys. 149 | */ 150 | function deleteRecord(bytes32 usernameHash) internal { 151 | bytes32[] memory _keyTuple = new bytes32[](1); 152 | _keyTuple[0] = usernameHash; 153 | 154 | StoreSwitch.deleteRecord(_tableId, _keyTuple); 155 | } 156 | 157 | /** 158 | * @notice Delete all data for given keys. 159 | */ 160 | function _deleteRecord(bytes32 usernameHash) internal { 161 | bytes32[] memory _keyTuple = new bytes32[](1); 162 | _keyTuple[0] = usernameHash; 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(bool taken) internal pure returns (bytes memory) { 172 | return abi.encodePacked(taken); 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(bool taken) internal pure returns (bytes memory, EncodedLengths, bytes memory) { 182 | bytes memory _staticData = encodeStatic(taken); 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(bytes32 usernameHash) internal pure returns (bytes32[] memory) { 194 | bytes32[] memory _keyTuple = new bytes32[](1); 195 | _keyTuple[0] = usernameHash; 196 | 197 | return _keyTuple; 198 | } 199 | } 200 | 201 | /** 202 | * @notice Cast a value to a bool. 203 | * @dev Boolean values are encoded as uint8 (1 = true, 0 = false), but Solidity doesn't allow casting between uint8 and bool. 204 | * @param value The uint8 value to convert. 205 | * @return result The boolean value. 206 | */ 207 | function _toBool(uint8 value) pure returns (bool result) { 208 | assembly { 209 | result := value 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /packages/contracts/src/codegen/tables/UsernameOffchain.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 UsernameOffchain { 20 | // Hex below is the result of `WorldResourceIdLib.encode({ namespace: "", name: "UsernameOffchain", typeId: RESOURCE_OFFCHAIN_TABLE });` 21 | ResourceId constant _tableId = ResourceId.wrap(0x6f740000000000000000000000000000557365726e616d654f6666636861696e); 22 | 23 | FieldLayout constant _fieldLayout = 24 | FieldLayout.wrap(0x0000000100000000000000000000000000000000000000000000000000000000); 25 | 26 | // Hex-encoded key schema of (uint160) 27 | Schema constant _keySchema = Schema.wrap(0x0014010013000000000000000000000000000000000000000000000000000000); 28 | // Hex-encoded value schema of (string) 29 | Schema constant _valueSchema = Schema.wrap(0x00000001c5000000000000000000000000000000000000000000000000000000); 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] = "entityId"; 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] = "username"; 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 Set the full data using individual values. 65 | */ 66 | function set(uint160 entityId, string memory username) internal { 67 | bytes memory _staticData; 68 | EncodedLengths _encodedLengths = encodeLengths(username); 69 | bytes memory _dynamicData = encodeDynamic(username); 70 | 71 | bytes32[] memory _keyTuple = new bytes32[](1); 72 | _keyTuple[0] = bytes32(uint256(entityId)); 73 | 74 | StoreSwitch.setRecord(_tableId, _keyTuple, _staticData, _encodedLengths, _dynamicData); 75 | } 76 | 77 | /** 78 | * @notice Set the full data using individual values. 79 | */ 80 | function _set(uint160 entityId, string memory username) internal { 81 | bytes memory _staticData; 82 | EncodedLengths _encodedLengths = encodeLengths(username); 83 | bytes memory _dynamicData = encodeDynamic(username); 84 | 85 | bytes32[] memory _keyTuple = new bytes32[](1); 86 | _keyTuple[0] = bytes32(uint256(entityId)); 87 | 88 | StoreCore.setRecord(_tableId, _keyTuple, _staticData, _encodedLengths, _dynamicData, _fieldLayout); 89 | } 90 | 91 | /** 92 | * @notice Decode the tightly packed blob of dynamic data using the encoded lengths. 93 | */ 94 | function decodeDynamic( 95 | EncodedLengths _encodedLengths, 96 | bytes memory _blob 97 | ) internal pure returns (string memory username) { 98 | uint256 _start; 99 | uint256 _end; 100 | unchecked { 101 | _end = _encodedLengths.atIndex(0); 102 | } 103 | username = (string(SliceLib.getSubslice(_blob, _start, _end).toBytes())); 104 | } 105 | 106 | /** 107 | * @notice Decode the tightly packed blobs using this table's field layout. 108 | * 109 | * @param _encodedLengths Encoded lengths of dynamic fields. 110 | * @param _dynamicData Tightly packed dynamic fields. 111 | */ 112 | function decode( 113 | bytes memory, 114 | EncodedLengths _encodedLengths, 115 | bytes memory _dynamicData 116 | ) internal pure returns (string memory username) { 117 | (username) = decodeDynamic(_encodedLengths, _dynamicData); 118 | } 119 | 120 | /** 121 | * @notice Delete all data for given keys. 122 | */ 123 | function deleteRecord(uint160 entityId) internal { 124 | bytes32[] memory _keyTuple = new bytes32[](1); 125 | _keyTuple[0] = bytes32(uint256(entityId)); 126 | 127 | StoreSwitch.deleteRecord(_tableId, _keyTuple); 128 | } 129 | 130 | /** 131 | * @notice Delete all data for given keys. 132 | */ 133 | function _deleteRecord(uint160 entityId) internal { 134 | bytes32[] memory _keyTuple = new bytes32[](1); 135 | _keyTuple[0] = bytes32(uint256(entityId)); 136 | 137 | StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); 138 | } 139 | 140 | /** 141 | * @notice Tightly pack dynamic data lengths using this table's schema. 142 | * @return _encodedLengths The lengths of the dynamic fields (packed into a single bytes32 value). 143 | */ 144 | function encodeLengths(string memory username) internal pure returns (EncodedLengths _encodedLengths) { 145 | // Lengths are effectively checked during copy by 2**40 bytes exceeding gas limits 146 | unchecked { 147 | _encodedLengths = EncodedLengthsLib.pack(bytes(username).length); 148 | } 149 | } 150 | 151 | /** 152 | * @notice Tightly pack dynamic (variable length) data using this table's schema. 153 | * @return The dynamic data, encoded into a sequence of bytes. 154 | */ 155 | function encodeDynamic(string memory username) internal pure returns (bytes memory) { 156 | return abi.encodePacked(bytes((username))); 157 | } 158 | 159 | /** 160 | * @notice Encode all of a record's fields. 161 | * @return The static (fixed length) data, encoded into a sequence of bytes. 162 | * @return The lengths of the dynamic fields (packed into a single bytes32 value). 163 | * @return The dynamic (variable length) data, encoded into a sequence of bytes. 164 | */ 165 | function encode(string memory username) internal pure returns (bytes memory, EncodedLengths, bytes memory) { 166 | bytes memory _staticData; 167 | EncodedLengths _encodedLengths = encodeLengths(username); 168 | bytes memory _dynamicData = encodeDynamic(username); 169 | 170 | return (_staticData, _encodedLengths, _dynamicData); 171 | } 172 | 173 | /** 174 | * @notice Encode keys as a bytes32 array using this table's field layout. 175 | */ 176 | function encodeKeyTuple(uint160 entityId) internal pure returns (bytes32[] memory) { 177 | bytes32[] memory _keyTuple = new bytes32[](1); 178 | _keyTuple[0] = bytes32(uint256(entityId)); 179 | 180 | return _keyTuple; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /packages/contracts/src/codegen/world/IAccessSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | /** 7 | * @title IAccessSystem 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 IAccessSystem { 12 | function access(bytes memory accessSignature, string memory username) external; 13 | } 14 | -------------------------------------------------------------------------------- /packages/contracts/src/codegen/world/IAdminSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | /** 7 | * @title IAdminSystem 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 IAdminSystem { 12 | function addLines(uint32 numLinesToAdd) external; 13 | 14 | function banPlayer(address player) external; 15 | 16 | function banUsername(string memory username) external; 17 | } 18 | -------------------------------------------------------------------------------- /packages/contracts/src/codegen/world/IDirectionSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | /** 7 | * @title IDirectionSystem 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 IDirectionSystem { 12 | function setDirection(bool velRight) external; 13 | } 14 | -------------------------------------------------------------------------------- /packages/contracts/src/codegen/world/IJumpSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | /** 7 | * @title IJumpSystem 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 IJumpSystem { 12 | function jumpToLine(bool up) external returns (uint32 newLine); 13 | } 14 | -------------------------------------------------------------------------------- /packages/contracts/src/codegen/world/ISpawnSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | /** 7 | * @title ISpawnSystem 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 ISpawnSystem { 12 | function spawn(uint32 line, uint160 rightNeighbor, bool velRight) external returns (uint160 entityId); 13 | } 14 | -------------------------------------------------------------------------------- /packages/contracts/src/codegen/world/IUtilitiesSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.24; 3 | 4 | /* Autogenerated file. Do not edit manually. */ 5 | 6 | /** 7 | * @title IUtilitiesSystem 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 IUtilitiesSystem { 12 | function getNumLines() external view returns (uint32); 13 | 14 | function poke(uint32 line) external; 15 | } 16 | -------------------------------------------------------------------------------- /packages/contracts/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 | import { IAccessSystem } from "./IAccessSystem.sol"; 8 | import { IAdminSystem } from "./IAdminSystem.sol"; 9 | import { IDirectionSystem } from "./IDirectionSystem.sol"; 10 | import { IJumpSystem } from "./IJumpSystem.sol"; 11 | import { ISpawnSystem } from "./ISpawnSystem.sol"; 12 | import { IUtilitiesSystem } from "./IUtilitiesSystem.sol"; 13 | 14 | /** 15 | * @title IWorld 16 | * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) 17 | * @notice This interface integrates all systems and associated function selectors 18 | * that are dynamically registered in the World during deployment. 19 | * @dev This is an autogenerated file; do not edit manually. 20 | */ 21 | interface IWorld is 22 | IBaseWorld, 23 | IAccessSystem, 24 | IAdminSystem, 25 | IDirectionSystem, 26 | IJumpSystem, 27 | ISpawnSystem, 28 | IUtilitiesSystem 29 | {} 30 | -------------------------------------------------------------------------------- /packages/contracts/src/systems/AccessSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.21; 3 | 4 | import "../codegen/index.sol"; 5 | 6 | import {System} from "@latticexyz/world/src/System.sol"; 7 | 8 | import {ECDSA} from "solady/utils/ECDSA.sol"; 9 | 10 | import {EntityLib} from "../utils/EntityLib.sol"; 11 | 12 | contract AccessSystem is System { 13 | function access(bytes memory accessSignature, string memory username) public { 14 | uint160 caller = EntityLib.toEntityId(_msgSender()); 15 | 16 | // We overload the purpose of this field to both manage access and to prevent spam. 17 | // Here we are using it only for the former, ensuring addresses are not relinked. Access 18 | // can be revoked by setting the field to type(uint32).max, which will also fail this check. 19 | require(Player.getLastJumpBlockNumber(caller) == 0, "ALREADY_AUTHORIZED"); 20 | 21 | bytes32 usernameHash = keccak256(abi.encodePacked(username)); 22 | 23 | // Ensure the username is not already registered to another address. 24 | require(!UsernameHash.get(usernameHash), "USERNAME_TAKEN"); 25 | 26 | bytes32 messageHash = keccak256(abi.encodePacked(_msgSender(), username)); 27 | 28 | // Will revert if the signature is invalid. 29 | address signer = ECDSA.recover(messageHash, accessSignature); 30 | 31 | // Ensure the signer is the access signer. 32 | require(signer == GameConfig.getAccessSigner(), "INVALID_ACCESS_SIGNATURE"); 33 | 34 | // Set last jump block number to non-zero to prevent accessing twice. 35 | Player.setLastJumpBlockNumber(caller, 1); 36 | 37 | // Set username for offchain access. 38 | UsernameOffchain.set(caller, username); 39 | 40 | // Set username hash to prevent registering multiple addresses under the same username. 41 | UsernameHash.set(usernameHash, true); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/contracts/src/systems/AdminSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.21; 3 | 4 | import "../codegen/index.sol"; 5 | import "../codegen/common.sol"; 6 | 7 | import {System} from "@latticexyz/world/src/System.sol"; 8 | 9 | import {MinHeapLib} from "../utils/PriorityQueue96x160Lib.sol"; 10 | import {ConfigLib} from "../utils/ConfigLib.sol"; 11 | import {timeWad} from "../utils/WadTimeLib.sol"; 12 | import {EntityLib} from "../utils/EntityLib.sol"; 13 | import {LineLib} from "../utils/LineLib.sol"; 14 | 15 | // This system is set to openAccess = false in mud.config.ts. 16 | // Thus, only authorized users can call any of its functions. 17 | contract AdminSystem is System { 18 | function addLines(uint32 numLinesToAdd) public { 19 | (uint32 currentNumLines, uint128 lineWidth, uint96 wallMass) = ( 20 | GameState.getNumLines(), 21 | GameConfig.getLineWidth(), 22 | GameConfig.getWallMass() 23 | ); 24 | 25 | uint32 targetNumLines = currentNumLines + numLinesToAdd; // New # of lines. 26 | 27 | for (uint32 newLine = currentNumLines; newLine < targetNumLines; newLine++) { 28 | // Determine the leftmost and rightmost entity IDs for the new line. 29 | (uint160 leftmostEntityId, uint160 rightmostEntityId) = ( 30 | EntityLib.leftmostEntityId(newLine), 31 | EntityLib.rightmostEntityId(newLine) 32 | ); 33 | 34 | // Spawn the leftmost boundary entity. 35 | Entity.setEtype(leftmostEntityId, EntityType.ALIVE); 36 | Entity.setLineId(leftmostEntityId, newLine); 37 | Entity.setLastX(leftmostEntityId, 0); 38 | Entity.setLastTouchedTime(leftmostEntityId, timeWad()); 39 | Entity.setLeftNeighbor(leftmostEntityId, 0); 40 | Entity.setRightNeighbor(leftmostEntityId, rightmostEntityId); 41 | 42 | // Spawn the rightmost boundary entity. 43 | Entity.setEtype(rightmostEntityId, EntityType.ALIVE); 44 | Entity.setLineId(rightmostEntityId, newLine); 45 | Entity.setLastX(rightmostEntityId, lineWidth); 46 | Entity.setLastTouchedTime(rightmostEntityId, timeWad()); 47 | Entity.setLeftNeighbor(rightmostEntityId, leftmostEntityId); 48 | Entity.setRightNeighbor(rightmostEntityId, 0); 49 | 50 | MinHeapLib.MemHeap memory newLineCollisionQueue; // Create a new collision queue for the new line. 51 | 52 | // Spawn a food entity in the middle of the line. 53 | uint160 foodId = EntityLib.toEntityId(uint256(keccak256(abi.encode(newLine, 1)))); 54 | Entity.setEtype(foodId, EntityType.FOOD); 55 | Entity.setMass(foodId, uint128(GameConfig.getMinFoodMass())); 56 | LineLib.spawnEntityIntoLine(newLine, foodId, rightmostEntityId, timeWad(), newLineCollisionQueue); 57 | 58 | // Each line gets 2 wall entities. 59 | for (uint256 i = 0; i < 2; i++) { 60 | uint160 wallId = EntityLib.toEntityId(uint256(keccak256(abi.encode(newLine, i + 2)))); 61 | Entity.setEtype(wallId, EntityType.WALL); 62 | Entity.setMass(wallId, wallMass); 63 | int128 WALL_VEL_MULTIPLIER = int128(int256(uint256(0.025e18 + (wallId % 0.125e18)))); // Random in a range. 64 | Entity.setVelMultiplier(wallId, i % 2 == 0 ? WALL_VEL_MULTIPLIER : -WALL_VEL_MULTIPLIER); 65 | LineLib.spawnEntityIntoLine( 66 | newLine, 67 | wallId, 68 | i % 2 == 0 ? foodId : rightmostEntityId, 69 | timeWad(), 70 | newLineCollisionQueue 71 | ); 72 | } 73 | 74 | Line.setCollisionQueue(newLine, newLineCollisionQueue.data); 75 | } 76 | 77 | GameState.setNumLines(targetNumLines); 78 | } 79 | 80 | function banPlayer(address player) public { 81 | // This field is overloaded to both manage access and to prevent spamming, here 82 | // we are using it for the former, setting it to a magic number which prevents access. 83 | Player.setLastJumpBlockNumber(EntityLib.toEntityId(player), type(uint32).max); 84 | } 85 | 86 | function banUsername(string memory username) public { 87 | // Set username hash to taken to prevent registering from succeeding. 88 | UsernameHash.set(keccak256(abi.encodePacked(username)), true); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /packages/contracts/src/systems/DirectionSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.21; 3 | 4 | import "../codegen/index.sol"; 5 | import "../codegen/common.sol"; 6 | 7 | import {System} from "@latticexyz/world/src/System.sol"; 8 | 9 | import {LineLib} from "../utils/LineLib.sol"; 10 | import {timeWad} from "../utils/WadTimeLib.sol"; 11 | import {EntityLib} from "../utils/EntityLib.sol"; 12 | import {MinHeapLib} from "../utils/PriorityQueue96x160Lib.sol"; 13 | 14 | import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol"; 15 | 16 | contract DirectionSystem is System { 17 | using FixedPointMathLib for *; 18 | 19 | function setDirection(bool velRight) external { 20 | uint160 caller = EntityLib.toEntityId(_msgSender()); 21 | 22 | uint32 line = Entity.getLineId(caller); 23 | 24 | // Done before checking if player is alive, because they could die in a collision. 25 | MinHeapLib.MemHeap memory currentCollisionQueue = LineLib.getCollisionQueue(line); 26 | LineLib.processCollisions(line, currentCollisionQueue); 27 | 28 | require(Entity.getEtype(caller) == EntityType.ALIVE, "CALLER_IS_NOT_ALIVE"); 29 | 30 | // Touch the caller to ensure we don't change its trajectory retroactively. 31 | // We could check Location.getLastTouchedTime(caller) != timeWad() first to 32 | // avoid touching the caller if it's already been touched, but in practice 33 | // it's very unlikely the caller had a collision at the exact calling time. 34 | Entity.setLastX(caller, EntityLib.computeX(caller, timeWad())); 35 | Entity.setLastTouchedTime(caller, timeWad()); 36 | 37 | Entity.setVelMultiplier( 38 | caller, 39 | int128(velRight ? int256(Entity.getVelMultiplier(caller).abs()) : -int256(Entity.getVelMultiplier(caller).abs())) 40 | ); 41 | 42 | // Schedule the entity's collision with its left neighbor. 43 | LineLib.scheduleCollision(Entity.getLeftNeighbor(caller), caller, currentCollisionQueue); 44 | // Schedule the entity's right neighbor's collision with the entity. 45 | LineLib.scheduleCollision(caller, Entity.getRightNeighbor(caller), currentCollisionQueue); 46 | 47 | Line.setCollisionQueue(line, currentCollisionQueue.data); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/contracts/src/systems/JumpSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.21; 3 | 4 | import "../codegen/index.sol"; 5 | import "../codegen/common.sol"; 6 | 7 | import {System} from "@latticexyz/world/src/System.sol"; 8 | 9 | import {LineLib} from "../utils/LineLib.sol"; 10 | import {timeWad} from "../utils/WadTimeLib.sol"; 11 | import {EntityLib} from "../utils/EntityLib.sol"; 12 | import {ConfigLib} from "../utils/ConfigLib.sol"; 13 | import {MinHeapLib} from "../utils/PriorityQueue96x160Lib.sol"; 14 | 15 | import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol"; 16 | 17 | contract JumpSystem is System { 18 | using FixedPointMathLib for *; 19 | 20 | function jumpToLine(bool up) external returns (uint32 newLine) { 21 | uint160 caller = EntityLib.toEntityId(_msgSender()); 22 | 23 | // This field is overloaded to both manage access and to prevent spamming. Here we will use 24 | // its value for both purposes. First to check access, then to prevent rapid consecutive jumps. 25 | uint32 lastJumpBlockNumber = Player.getLastJumpBlockNumber(caller); 26 | 27 | // We only want players to be able to jump once per block, to avoid people being sniped. 28 | // Serves the dual purpose of preventing spam and ensuring access hasn't been revoked, which 29 | // is done by setting the field to type(uint32).max, which is greater than any block number. 30 | require(block.number > lastJumpBlockNumber, "JUMPING_TOO_QUICKLY"); 31 | 32 | unchecked { 33 | // If you're at the topmost line, going up will wrap around to the bottom. 34 | // If you're at the bottommost line, going down will wrap around to the top. 35 | // Otherwise, going up will go up one line, and going down will go down one line. 36 | uint32 currentLine = Entity.getLineId(caller); 37 | uint32 numLines = GameState.getNumLines(); 38 | newLine = up 39 | ? (currentLine == 0 ? numLines - 1 : currentLine - 1) 40 | : (currentLine == numLines - 1 ? 0 : currentLine + 1); 41 | 42 | // 1) Remove the entity from the old line: 43 | 44 | // Done before checking if player is alive, because they could die in a collision. 45 | MinHeapLib.MemHeap memory collisionQueue = LineLib.getCollisionQueue(currentLine); 46 | LineLib.processCollisions(currentLine, collisionQueue); 47 | 48 | require(Entity.getEtype(caller) == EntityType.ALIVE, "CALLER_IS_NOT_ALIVE"); 49 | 50 | // Touch the caller to ensure we don't change its trajectory retroactively. 51 | // We could check Location.getLastTouchedTime(caller) != timeWad() first to 52 | // avoid touching the caller if it's already been touched, but in practice 53 | // it's very unlikely the caller had a collision at the exact calling time. 54 | Entity.setLastX(caller, EntityLib.computeX(caller, timeWad())); 55 | Entity.setLastTouchedTime(caller, timeWad()); 56 | 57 | removeEntityFromLine(caller, collisionQueue); 58 | 59 | // 1.5) Decay the entity's mass according to the line jump decay factor: 60 | 61 | // Mass decays by 1 - lineJumpDecayFactor whenever an entity jumps lines. 62 | uint128 newMass = ConfigLib.computeMassAfterJumpingLine(Entity.getMass(caller)); 63 | // We don't want to allow players to get too small and speedy. 64 | require(newMass >= GameConfig.getMinFoodMass(), "NOT_ENOUGH_MASS"); 65 | // Must be done before inserting the entity into the new line, as the mass 66 | // is used to calculate overlap & schedule its collisions with its neighbors. 67 | Entity.setMass(caller, newMass); 68 | 69 | Line.setCollisionQueue(currentLine, collisionQueue.data); 70 | 71 | // 2) Insert the entity into the new line: 72 | 73 | LineLib.processCollisions(newLine, collisionQueue = LineLib.getCollisionQueue(newLine)); 74 | insertExistingEntityIntoLine(newLine, caller, timeWad(), collisionQueue); 75 | Line.setCollisionQueue(newLine, collisionQueue.data); 76 | } 77 | 78 | // This field is overloaded to both manage access and to prevent 79 | // spam. Here we are using it to prevent a 2nd jump in this block. 80 | Player.setLastJumpBlockNumber(caller, uint32(block.number)); 81 | } 82 | 83 | // Caller must ensure all collisions up to timeWad() are processed before 84 | // calling, otherwise collisions will effectively be retroactively modified. 85 | function removeEntityFromLine(uint160 entity, MinHeapLib.MemHeap memory collisionQueue) internal { 86 | // Get the entity's left and right neighbors. 87 | (uint160 leftNeighbor, uint160 rightNeighbor) = (Entity.getLeftNeighbor(entity), Entity.getRightNeighbor(entity)); 88 | 89 | // Update the neighbors' references to skip the entity. 90 | Entity.setRightNeighbor(leftNeighbor, rightNeighbor); 91 | Entity.setLeftNeighbor(rightNeighbor, leftNeighbor); 92 | 93 | // Schedule entity's right neighbor's collision with its new left neighbor. 94 | LineLib.scheduleCollision(leftNeighbor, rightNeighbor, collisionQueue); 95 | } 96 | 97 | function insertExistingEntityIntoLine( 98 | uint32 line, 99 | uint160 entity, 100 | uint96 wadTime, // Caller must ensure all collisions up to wadTime are processed before calling. 101 | MinHeapLib.MemHeap memory collisionQueue 102 | ) internal { 103 | (uint128 entityLeftEdge, uint128 entityMass) = (EntityLib.computeX(entity, wadTime), Entity.getMass(entity)); 104 | uint128 entityDiameter = ConfigLib.mapMassToDiameter(entityMass); // Could recompute every time, but better to cache. 105 | 106 | // Start the right neighbor search at the right neighbor of the leftmost entity. 107 | uint160 rightNeighbor = Entity.getRightNeighbor(EntityLib.leftmostEntityId(line)); 108 | uint128 rightNeighborLeftEdge = EntityLib.computeX(rightNeighbor, wadTime); 109 | 110 | // Search for the closest right neighbor without overlap. 111 | while (entityLeftEdge + entityDiameter >= rightNeighborLeftEdge) { 112 | require(rightNeighbor != EntityLib.rightmostEntityId(line), "NO_VALID_RIGHT_NEIGHBOR"); // Just in case. 113 | rightNeighborLeftEdge = EntityLib.computeX(rightNeighbor = Entity.getRightNeighbor(rightNeighbor), wadTime); 114 | } 115 | 116 | // Start the left neighbor search at the left neighbor of the selected right neighbor. 117 | uint160 leftNeighbor = Entity.getLeftNeighbor(rightNeighbor); 118 | uint128 leftNeighborRightEdge = EntityLib.computeX(leftNeighbor, wadTime) + EntityLib.computeDiameter(leftNeighbor); 119 | 120 | // Search for a left neighbor without overlap, consuming all with overlap. 121 | while (leftNeighborRightEdge >= entityLeftEdge) { 122 | require(leftNeighbor != EntityLib.leftmostEntityId(line), "NO_VALID_LEFT_NEIGHBOR"); // Just in case. 123 | 124 | // Determine whether the entity can consume the overlapping left neighbor candidate. 125 | (uint128 leftNeighborMass, EntityType leftNeighborType, bool isLeftNeighborPoweredUp) = ( 126 | Entity.getMass(leftNeighbor), 127 | Entity.getEtype(leftNeighbor), 128 | EntityLib.isPoweredUp(leftNeighbor, wadTime) 129 | ); 130 | require( 131 | // prettier-ignore 132 | // 1. Non-{food, power-pellet} entity wins over food/power-pellet 133 | // 2. Powered-up entity wins (if both are powered up, neither wins) 134 | // 3. Entity with greater mass wins 135 | leftNeighborType != EntityType.WALL 136 | && (leftNeighborType == EntityType.FOOD 137 | || leftNeighborType == EntityType.POWER_PELLET 138 | || (EntityLib.isPoweredUp(entity, wadTime) && !isLeftNeighborPoweredUp) 139 | || (entityMass > leftNeighborMass && !isLeftNeighborPoweredUp)), 140 | "WOULD_OVERLAP_WITH_UNCONSUMABLE_ENTITY" // Revert if the candidate is unconsumable. 141 | ); 142 | 143 | // To discourage players from rapidly spawning bots to feed themselves, we cap the mass you gain 144 | // from consuming a player to min(consumedEntityMass, Player.getConsumedMass(consumedEntity)). 145 | if (leftNeighborType == EntityType.ALIVE) 146 | leftNeighborMass = uint128( 147 | // prettier-ignore 148 | leftNeighborMass // Conceptually equal to consumedEntityMass. 149 | .min(Player.getConsumedMass(leftNeighbor)) // consumedEntity = leftNeighbor. 150 | ); 151 | 152 | uint160 consumedEntity = leftNeighbor; // Need to cache as we'll reassign leftNeighbor below. 153 | leftNeighbor = Entity.getLeftNeighbor(consumedEntity); // Try another neighbor to the left. 154 | // Must go after the getLeftNeighbor(...) call, this'll delete the consumedEntity's state. 155 | EntityLib.onConsumeEntity(entity, consumedEntity, wadTime); 156 | 157 | // Retrieve and update the right edge of the new left neighbor candidate. Checked for overlap in the while. 158 | leftNeighborRightEdge = EntityLib.computeX(leftNeighbor, wadTime) + EntityLib.computeDiameter(leftNeighbor); 159 | 160 | // Update the entity's mass and diameter (uncommitted) given it just absorbed the left neighbor. We also 161 | // update the left edge of the entity to account for its larger diameter, see inline comments for more. 162 | uint128 newDiameter = ConfigLib.mapMassToDiameter(entityMass += leftNeighborMass); 163 | entityLeftEdge -= newDiameter - entityDiameter; // See winnerEntityNewLastX in processCollisions for why. 164 | entityDiameter = newDiameter; // This is done last because the line above depends on the old entityDiameter. 165 | } 166 | 167 | // Apply the, until now, uncommitted updates to mass (and thus diameter). 168 | uint128 massConsumed = entityMass - Entity.getMass(entity); 169 | Entity.setMass(entity, entityMass); 170 | Player.setConsumedMass(entity, Player.getConsumedMass(entity) + massConsumed); 171 | 172 | LineLib.insertEntityIntoLine(line, entity, entityLeftEdge, leftNeighbor, rightNeighbor, wadTime, collisionQueue); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /packages/contracts/src/systems/SpawnSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.21; 3 | 4 | import "../codegen/index.sol"; 5 | import "../codegen/common.sol"; 6 | 7 | import {System} from "@latticexyz/world/src/System.sol"; 8 | 9 | import {LineLib} from "../utils/LineLib.sol"; 10 | import {timeWad} from "../utils/WadTimeLib.sol"; 11 | import {EntityLib} from "../utils/EntityLib.sol"; 12 | import {MinHeapLib} from "../utils/PriorityQueue96x160Lib.sol"; 13 | 14 | contract SpawnSystem is System { 15 | function spawn(uint32 line, uint160 rightNeighbor, bool velRight) public returns (uint160 entityId) { 16 | // Ensure the caller has been authorized to access the game. This field is overloaded to both manage 17 | // access and to prevent spam. Here we use it for access, ensuring the player is authorized and not banned. 18 | uint32 lastJumpBlockNumber = Player.getLastJumpBlockNumber(entityId = EntityLib.toEntityId(_msgSender())); 19 | require(lastJumpBlockNumber > 0 && lastJumpBlockNumber != type(uint32).max, "NO_ACCESS"); 20 | 21 | require(line < GameState.getNumLines(), "LINE_OUT_OF_BOUNDS"); 22 | 23 | MinHeapLib.MemHeap memory collisionQueue; 24 | 25 | // If the player is currently alive, they might have just died 26 | // in a collision that hasn't been processed yet, so we'll try 27 | // processing collisions before checking if they're alive/dead. 28 | if (Entity.getEtype(entityId) == EntityType.ALIVE) { 29 | uint32 currentLine = Entity.getLineId(entityId); 30 | LineLib.processCollisions(currentLine, collisionQueue = LineLib.getCollisionQueue(currentLine)); 31 | Line.setCollisionQueue(currentLine, collisionQueue.data); // Will waste some gas if the require fails. 32 | } 33 | 34 | require(Entity.getEtype(entityId) == EntityType.DEAD, "CALLER_IS_ALIVE"); 35 | 36 | LineLib.processCollisions(line, collisionQueue = LineLib.getCollisionQueue(line)); 37 | 38 | // Set fundamental player state. 39 | Entity.setEtype(entityId, EntityType.ALIVE); 40 | Entity.setMass(entityId, GameConfig.getPlayerStartingMass()); 41 | Entity.setVelMultiplier(entityId, velRight ? int128(1e18) : -1e18); 42 | 43 | LineLib.spawnEntityIntoLine(line, entityId, rightNeighbor, timeWad(), collisionQueue); 44 | 45 | Line.setCollisionQueue(line, collisionQueue.data); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/contracts/src/systems/UtilitiesSystem.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.21; 3 | 4 | import "../codegen/index.sol"; 5 | 6 | import {System} from "@latticexyz/world/src/System.sol"; 7 | 8 | import {LineLib} from "../utils/LineLib.sol"; 9 | import {MinHeapLib} from "../utils/PriorityQueue96x160Lib.sol"; 10 | 11 | contract UtilitiesSystem is System { 12 | // For simple bots who aren't going to sync MUD state. 13 | function getNumLines() public view returns (uint32) { 14 | return GameState.getNumLines(); 15 | } 16 | 17 | function poke(uint32 line) public { 18 | require(line < GameState.getNumLines(), "INVALID_LINE"); 19 | MinHeapLib.MemHeap memory currentCollisionQueue = LineLib.getCollisionQueue(line); 20 | LineLib.processCollisions(line, currentCollisionQueue); 21 | Line.setCollisionQueue(line, currentCollisionQueue.data); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/contracts/src/utils/ConfigLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.21; 3 | 4 | import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol"; 5 | 6 | import "../codegen/index.sol"; 7 | 8 | library ConfigLib { 9 | using FixedPointMathLib for *; 10 | 11 | function mapMassToDiameter(uint128 mass) internal pure returns (uint128) { 12 | // Note: This MUST be sub-linear, as otherwise when an entity consumes another entity, 13 | // the fact the diameter of the winner entity could exceed/match the combined diameters of 14 | // the original entities could cause the winner to instantly collide with its neighbor(s). 15 | return uint128(mass.sqrtWad()); 16 | } 17 | 18 | function mapMassToVelocity(uint128 mass) internal view returns (int128) { 19 | if (mass == 0) return 0; // Avoid wasting gas on boundary entities. 20 | 21 | // prettier-ignore 22 | return int128(uint128( 23 | uint256(GameConfig.getVelocityCoefficient()) 24 | .divWad(uint256( 25 | // + 1.0...1e18 to avoid negative and 0 outputs. 26 | log10Wad(int256(int128(mass + 1.000000001e18))) 27 | )) 28 | )); 29 | } 30 | 31 | function computeMassAfterJumpingLine(uint128 mass) internal view returns (uint128) { 32 | return uint128(mass.mulWad(GameConfig.getLineJumpDecayFactor())); 33 | } 34 | 35 | /*////////////////////////////////////////////////////////////// 36 | LOW-LEVEL MATH FUNCTIONS 37 | //////////////////////////////////////////////////////////////*/ 38 | 39 | function log10Wad(int256 x) internal pure returns (int256) { 40 | // via change of base formula, log10(x) = ln(x) / ln(10) 41 | return FixedPointMathLib.lnWad(x).sDivWad(2.302585092994045683e18); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/contracts/src/utils/EntityLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.21; 3 | 4 | import "../codegen/index.sol"; 5 | import "../codegen/common.sol"; 6 | 7 | import {ConfigLib} from "./ConfigLib.sol"; 8 | 9 | import {MinHeapLib} from "solady/utils/MinHeapLib.sol"; 10 | import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol"; 11 | 12 | library EntityLib { 13 | using FixedPointMathLib for *; 14 | using MinHeapLib for MinHeapLib.MemHeap; 15 | 16 | /*////////////////////////////////////////////////////////////// 17 | UP-TO-DATE ENTITY PROPERTY DATA 18 | //////////////////////////////////////////////////////////////*/ 19 | 20 | function computeX(uint160 entity, uint96 wadTime) internal view returns (uint128) { 21 | // x_0 + v * (t - t_0) 22 | int128 computedX = int128(Entity.getLastX(entity)) + 23 | int128(computeVelocity(entity).sMulWad(int96(wadTime) - int96(Entity.getLastTouchedTime(entity)))); 24 | 25 | // Casting a negative position would cause overflow, avoid for safety. 26 | return computedX <= 0 ? 0 : uint128(computedX); 27 | } 28 | 29 | function computeCollisionTime(uint160 leftEntity, uint160 rightEntity) internal view returns (uint96) { 30 | (int128 leftVelocity, int128 rightVelocity) = (computeVelocity(leftEntity), computeVelocity(rightEntity)); 31 | 32 | // If leftVelocity > rightVelocity -> relativeVelocity > 0 -> will collide, since: 33 | // - the left entity is going right faster than the right entity can run away 34 | // - or the right entity is going left faster than the left entity can run away 35 | // - or the left entity is going right and the right entity is going to the left. 36 | // 37 | // If leftVelocity == rightVelocity -> relativeVelocity == 0 -> won't collide, since: 38 | // - the left entity and the right entity are both going at the exact same speed. 39 | // 40 | // If leftVelocity < rightVelocity -> relativeVelocity < 0 -> won't collide, since: 41 | // - the right entity is going right faster than the left entity can catch up 42 | // - or the left entity is going left faster than the right entity can catch up 43 | // - or the left entity is going left while the right entity is going to the right. 44 | int128 relativeVelocity = leftVelocity - rightVelocity; 45 | 46 | // Bail early as no need to compute a time when we know they won't collide. 47 | if (relativeVelocity <= 0) return 0; // Caller will ignore returned times of 0. 48 | 49 | (int128 leftEntityRightEdge, int128 rightEntityLeftEdge) = ( 50 | int128(Entity.getLastX(leftEntity) + computeDiameter(leftEntity)), 51 | int128(Entity.getLastX(rightEntity)) 52 | ); 53 | 54 | // prettier-ignore 55 | return uint96( 56 | // x1_0 + v1 * (t - t1_0) = x2_0 + v2 * (t - t2_0) -> Solve for t 57 | // t = ((x2_0 - x1_0) + ((t1_0 * v1) - (t2_0 * v2))) / (v1 - v2) 58 | // x1 -> leftEntity, x2 -> rightEntity, x1_0 -> lastX + diameter 59 | // Numerator < 0 only if no collision, which is checked for above. 60 | uint256((rightEntityLeftEdge - leftEntityRightEdge) 61 | + int96(Entity.getLastTouchedTime(leftEntity)).sMulWad(leftVelocity) 62 | - int96(Entity.getLastTouchedTime(rightEntity)).sMulWad(rightVelocity) 63 | ).divWad(uint128(relativeVelocity)) // relativeVelocity > 0, so cast is safe. 64 | ); 65 | } 66 | 67 | function isPoweredUp(uint160 entity, uint96 wadTime) internal view returns (bool) { 68 | return wadTime - Player.getLastConsumedPowerPelletTime(entity) <= GameConfig.getPowerPelletEffectTime(); 69 | } 70 | 71 | function computeDiameter(uint160 entity) internal view returns (uint128) { 72 | return ConfigLib.mapMassToDiameter(Entity.getMass(entity)); 73 | } 74 | 75 | function computeVelocity(uint160 entity) internal view returns (int128) { 76 | return int128(ConfigLib.mapMassToVelocity(Entity.getMass(entity)).sMulWad(Entity.getVelMultiplier(entity))); 77 | } 78 | 79 | /*////////////////////////////////////////////////////////////// 80 | ENTITY OPERATIONS 81 | //////////////////////////////////////////////////////////////*/ 82 | 83 | function onConsumeEntity(uint160 consumerEntity, uint160 consumedEntity, uint96 wadTime) internal { 84 | // If the killed consumed entity was a player (vs food, etc): 85 | if (Entity.getEtype(consumedEntity) == EntityType.ALIVE) { 86 | // Enqueue (push only if >min) the consumed entity's consumed mass into its top-k scores. 87 | MinHeapLib.MemHeap memory highScores = MinHeapLib.MemHeap(Player.getHighScores(consumedEntity)); 88 | highScores.enqueue(Player.getConsumedMass(consumedEntity), GameConfig.getHighScoreTopK()); 89 | Player.setHighScores(consumedEntity, highScores.data); 90 | 91 | Player.setConsumedMass(consumedEntity, 0); // Reset the consumed player's consumed mass. 92 | } else if ( 93 | // If the consumed entity was a power pellet and the consumer is a player: 94 | Entity.getEtype(consumedEntity) == EntityType.POWER_PELLET && Entity.getEtype(consumerEntity) == EntityType.ALIVE 95 | ) { 96 | Player.setLastConsumedPowerPelletTime(consumerEntity, wadTime); 97 | } 98 | 99 | Entity.deleteRecord(consumedEntity); // Remove the consumed entity from state. 100 | } 101 | 102 | /*////////////////////////////////////////////////////////////// 103 | ENTITY ID HELPERS 104 | //////////////////////////////////////////////////////////////*/ 105 | 106 | // Bounds a seed number into the range of valid non-boundary entity ids. 107 | function toEntityId(uint256 seed) internal pure returns (uint160) { 108 | unchecked { 109 | // Add 1 to avoid returning 0, so we're sure 110 | // 0 means "not set" in the context of the game. 111 | // We also avoid returning ids over type(uint144).max, 112 | // so we can reserve that id range for boundary entities. 113 | return uint160((seed % type(uint144).max) + 1); 114 | } 115 | } 116 | 117 | // Simple overload for addresses, which are commonly used as seeds. 118 | function toEntityId(address seed) internal pure returns (uint160) { 119 | return EntityLib.toEntityId(uint160(seed)); 120 | } 121 | 122 | function leftmostEntityId(uint32 line) internal pure returns (uint160) { 123 | unchecked { 124 | return uint160(type(uint144).max) + 1 + line; // + 1 to avoid overlap with non-boundary ids. 125 | } 126 | } 127 | 128 | function rightmostEntityId(uint32 line) internal pure returns (uint160) { 129 | unchecked { 130 | return uint160(type(uint152).max) + line; 131 | } 132 | } 133 | 134 | function isRightmostEntity(uint160 entity) internal pure returns (bool) { 135 | return entity >= type(uint152).max; 136 | } 137 | 138 | function isBoundaryEntity(uint160 entity) internal pure returns (bool) { 139 | unchecked { 140 | return entity > type(uint144).max; 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /packages/contracts/src/utils/PriorityQueue96x160Lib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.21; 3 | 4 | import {MinHeapLib} from "solady/utils/MinHeapLib.sol"; 5 | 6 | library PriorityQueue96x160Lib { 7 | using MinHeapLib for MinHeapLib.MemHeap; 8 | 9 | /*////////////////////////////////////////////////////////////// 10 | PACKING 11 | //////////////////////////////////////////////////////////////*/ 12 | 13 | function pack(uint96 priority, uint160 value) internal pure returns (uint256 packed) { 14 | return (uint256(priority) << 160) | uint256(value); 15 | } 16 | 17 | function unpack(uint256 packed) internal pure returns (uint96 priority, uint160 value) { 18 | return (uint96(packed >> 160), uint160(packed)); 19 | } 20 | 21 | /*////////////////////////////////////////////////////////////// 22 | OPERATIONS 23 | //////////////////////////////////////////////////////////////*/ 24 | 25 | function isEmpty(MinHeapLib.MemHeap memory heap) internal pure returns (bool empty) { 26 | assembly { 27 | empty := iszero(mload(mload(heap))) 28 | } 29 | } 30 | 31 | function peek(MinHeapLib.MemHeap memory heap) internal pure returns (uint96 priority, uint160 value) { 32 | return unpack(heap.root()); 33 | } 34 | 35 | function push(MinHeapLib.MemHeap memory heap, uint96 priority, uint160 value) internal pure { 36 | heap.push(pack(priority, value)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/contracts/src/utils/WadTimeLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.21; 3 | 4 | function timeWad() view returns (uint96) { 5 | unchecked { 6 | return uint96(block.timestamp) * 1e18; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/contracts/test/DebugLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.21; 3 | 4 | import {console2} from "forge-std/console2.sol"; 5 | 6 | import {LibString} from "solady/utils/LibString.sol"; 7 | 8 | import "../src/codegen/index.sol"; 9 | import "../src/codegen/common.sol"; 10 | 11 | import {IWorld} from "../src/codegen/world/IWorld.sol"; 12 | 13 | import {timeWad} from "../src/utils/WadTimeLib.sol"; 14 | import {EntityLib} from "../src/utils/EntityLib.sol"; 15 | import {PriorityQueue96x160Lib} from "../src/utils/PriorityQueue96x160Lib.sol"; 16 | 17 | import {Handler} from "./Handler.sol"; 18 | 19 | library DebugLib { 20 | function logStateOfLines(IWorld world, Handler handler) public { 21 | console2.log('\n\n{ type: "snapshot", timeWad:', DebugLib.stringify(timeWad()), ", lines: ["); 22 | for (uint256 i = 0; i < GameState.getNumLines(); i++) { 23 | console2.log("{ line:", DebugLib.stringify(i), ", entities: ["); 24 | world.poke(uint32(i)); 25 | 26 | uint160 entityId = EntityLib.leftmostEntityId(uint32(i)); 27 | 28 | while (entityId != 0) { 29 | console2.log( 30 | "{ entity:", 31 | DebugLib.mapEntityToEmoji(entityId, handler.getAccounts()), 32 | ", leftNeighbor:", 33 | DebugLib.mapEntityToEmoji(Entity.getLeftNeighbor(entityId), handler.getAccounts()) 34 | ); 35 | console2.log( 36 | ", rightNeighbor:", 37 | DebugLib.mapEntityToEmoji(Entity.getRightNeighbor(entityId), handler.getAccounts()), 38 | ", computeX:", 39 | DebugLib.stringify(EntityLib.computeX(entityId, timeWad())) 40 | ); 41 | console2.log( 42 | ", computeDiameter:", 43 | DebugLib.stringify(EntityLib.computeDiameter(entityId)), 44 | ", etype:", 45 | DebugLib.stringify(uint8(Entity.getEtype(entityId))) 46 | ); 47 | console2.log(", velocityMultiplier:", DebugLib.stringify(Entity.getVelMultiplier(entityId))); 48 | console2.log("},\n"); 49 | 50 | entityId = Entity.getRightNeighbor(entityId); 51 | } 52 | 53 | console2.log("], collisionQueue: ["); 54 | uint256[] memory collisionQueue = Line.getCollisionQueue(uint32(i)); 55 | for (uint256 j = 0; j < collisionQueue.length; j++) { 56 | (uint96 priority, uint160 value) = PriorityQueue96x160Lib.unpack(collisionQueue[j]); 57 | console2.log("{ collisionTimeWad:", DebugLib.stringify(priority)); 58 | console2.log(", rightEntity:", DebugLib.mapEntityToEmoji(value, handler.getAccounts()), "},"); 59 | } 60 | console2.log("]},\n"); 61 | } 62 | console2.log("] },"); 63 | } 64 | 65 | function stringify(uint256 value) internal pure returns (string memory) { 66 | return string(abi.encodePacked("'", LibString.toString(value), "'")); 67 | } 68 | 69 | function stringify(int256 value) internal pure returns (string memory) { 70 | return string(abi.encodePacked("'", LibString.toString(value), "'")); 71 | } 72 | 73 | function mapEntityToEmoji(uint160 entity, address[] memory accounts) internal pure returns (string memory) { 74 | if (entity == 0) return '"N/A"'; 75 | 76 | for (uint256 i = 0; i < accounts.length; i++) { 77 | if (entity == EntityLib.toEntityId(uint256(uint160(accounts[i])))) 78 | return string(abi.encodePacked('"[PLAYER ', LibString.toString(i), ']"')); 79 | } 80 | 81 | if (EntityLib.isBoundaryEntity(entity) && !EntityLib.isRightmostEntity(entity)) return '"[LEFT_BOUNDARY]"'; 82 | if (EntityLib.isRightmostEntity(entity)) return '"[RIGHT_BOUNDARY]"'; 83 | 84 | string[103] memory colorfulEmojis = [ 85 | unicode"🕋", // Kaaba (Black) 86 | unicode"🐸", // Frog (Green) 87 | unicode"🍅", // Tomato (Red) 88 | unicode"🍊", // Tangerine (Orange) 89 | unicode"🍋", // Lemon (Yellow) 90 | unicode"🍇", // Grapes (Purple) 91 | unicode"🌸", // Cherry Blossom (Pink) 92 | unicode"🌻", // Sunflower (Yellow) 93 | unicode"🌼", // Blossom (Light Yellow) 94 | unicode"🌿", // Herb (Green) 95 | unicode"🔥", // Fire (Red/Orange) 96 | unicode"💧", // Droplet (Blue) 97 | unicode"🌍", // Globe Showing Europe-Africa (Green/Blue) 98 | unicode"🌙", // Crescent Moon (Yellow) 99 | unicode"⭐", // Star (Yellow) 100 | unicode"🍁", // Maple Leaf (Red) 101 | unicode"🍀", // Four Leaf Clover (Green) 102 | unicode"🌈", // Rainbow 103 | unicode"🌊", // Water Wave (Blue) 104 | unicode"🌌", // Milky Way (Space Colors) 105 | unicode"🎈", // Balloon (Red) 106 | unicode"💎", // Gem Stone (Blue) 107 | unicode"🍑", // Peach (Orange) 108 | unicode"🍒", // Cherries (Red) 109 | unicode"🍓", // Strawberry (Red) 110 | unicode"🌹", // Rose (Red) 111 | unicode"🥑", // Avocado (Green) 112 | unicode"🥥", // Coconut (Brown) 113 | unicode"🫐", // Blueberries (Blue) 114 | unicode"🌺", // Hibiscus (Red) 115 | unicode"🥕", // Carrot (Orange) 116 | unicode"🌽", // Corn (Yellow) 117 | unicode"🍆", // Eggplant (Purple) 118 | unicode"🌶️", // Hot Pepper (Red) 119 | unicode"🥒", // Cucumber (Green) 120 | unicode"🍄", // Mushroom (Red/White) 121 | unicode"🌰", // Chestnut (Brown) 122 | unicode"🍯", // Honey Pot (Yellow) 123 | unicode"🦋", // Butterfly (Blue) 124 | unicode"🐠", // Tropical Fish (Orange/Blue) 125 | unicode"🦜", // Parrot (Red/Green/Yellow) 126 | unicode"🐙", // Octopus (Purple) 127 | unicode"🦚", // Peacock (Green/Blue) 128 | unicode"🌖", // Waning Gibbous Moon (Yellow) 129 | unicode"❄️", // Snowflake (Blue/White) 130 | unicode"🔮", // Crystal Ball (Purple) 131 | unicode"🎃", // Jack-o-lantern (Orange) 132 | unicode"🌟", // Glowing Star (Yellow) 133 | unicode"🌠", // Shooting Star (Yellow) 134 | unicode"🌋", // Volcano (Red/Orange) 135 | unicode"🏜️", // Desert (Yellow/Brown) 136 | unicode"🏝️", // Desert Island (Green/Blue) 137 | unicode"🌅", // Sunrise (Yellow/Blue) 138 | unicode"🌄", // Mountain at Sunrise (Orange/Blue) 139 | unicode"🏞️", // National Park (Green) 140 | unicode"🌐", // Globe with Meridians (Blue/Green) 141 | unicode"🧊", // Ice (Light Blue/White) 142 | unicode"🛸", // Flying Saucer (Grey) 143 | unicode"🎍", // Pine Decoration (Green) 144 | unicode"🎋", // Tanabata Tree (Green) 145 | unicode"🧨", // Firecracker (Red) 146 | unicode"🎏", // Carp Streamer (Red/Blue) 147 | unicode"🏮", // Red Paper Lantern 148 | unicode"🎴", // Flower Playing Cards (Red/Blue), 149 | unicode"🥮", // Moon Cake (Yellow) 150 | unicode"🥭", // Mango (Yellow/Orange) 151 | unicode"🍍", // Pineapple (Yellow) 152 | unicode"🥖", // Baguette Bread (Brown) 153 | unicode"🥨", // Pretzel (Brown) 154 | unicode"🍩", // Doughnut (Brown/Pink) 155 | unicode"🍪", // Cookie (Brown) 156 | unicode"⛩️", // Shinto Shrine (Red) 157 | unicode"🚌", // Bus (Yellow) 158 | unicode"🛶", // Canoe (Brown) 159 | unicode"🛎️", // Bellhop Bell (Gold) 160 | unicode"🍟", // French Fries (Yellow) 161 | unicode"🥣", // Bowl with Spoon (White) 162 | unicode"🧁", // Cupcake (Pink/White) 163 | unicode"🍭", // Lollipop (Rainbow) 164 | unicode"🍬", // Candy (Colorful) 165 | unicode"🦖", // T-Rex (Green) 166 | unicode"🍫", // Chocolate Bar (Brown) 167 | unicode"🦄", // Unicorn (White/Pink) 168 | unicode"🐲", // Dragon Face (Green) 169 | unicode"🎳", // Bowling (White/Red) 170 | unicode"🗽", // Statue of Liberty (Green) 171 | unicode"🎟️", // Admission Tickets (Red) 172 | unicode"🎬", // Clapper Board (Black/White) 173 | unicode"🎨", // Artist Palette (Colorful) 174 | unicode"🧶", // Yarn (Blue) 175 | unicode"🧵", // Thread (Red) 176 | unicode"🪡", // Sewing Needle (Silver) 177 | unicode"🧩", // Puzzle Piece (Blue) 178 | unicode"🎯", // Bullseye (Red/White) 179 | unicode"🎱", // Pool 8 Ball (Black/White) 180 | unicode"🚧", // Construction (Yellow/Black) 181 | unicode"⚓", // Anchor (Black) 182 | unicode"⛵", // Sailboat (White) 183 | unicode"📟", // Pager (Grey) 184 | unicode"📚", // Books (Colorful) 185 | unicode"🎙️", // Studio Microphone (Grey) 186 | unicode"💽", // Computer Disk (Black) 187 | unicode"🎽" // Running Shirt (Blue) 188 | ]; 189 | 190 | return string(abi.encodePacked('"', colorfulEmojis[uint256(entity) % colorfulEmojis.length], '"')); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /packages/contracts/test/Handler.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.21; 3 | 4 | import {CommonBase} from "forge-std/Base.sol"; 5 | import {StdCheats} from "forge-std/StdCheats.sol"; 6 | import {StdUtils} from "forge-std/StdUtils.sol"; 7 | import {console2} from "forge-std/console2.sol"; 8 | 9 | import {StoreSwitch} from "@latticexyz/store/src/StoreSwitch.sol"; 10 | 11 | import "../src/codegen/index.sol"; 12 | import "../src/codegen/common.sol"; 13 | 14 | import {IWorld} from "../src/codegen/world/IWorld.sol"; 15 | 16 | import {EntityLib} from "../src/utils/EntityLib.sol"; 17 | import {timeWad} from "../src/utils/WadTimeLib.sol"; 18 | 19 | import {DebugLib} from "./DebugLib.sol"; 20 | 21 | contract Handler is StdCheats, StdUtils, CommonBase { 22 | address[] public accounts; 23 | 24 | IWorld world; 25 | 26 | function getAccounts() public view returns (address[] memory) { 27 | return accounts; 28 | } 29 | 30 | constructor(IWorld _world) { 31 | world = _world; 32 | 33 | accounts.push(address(0x1111111111111111111111111111111111111111)); 34 | accounts.push(address(0x2222222222222222222222222222222222222222)); 35 | accounts.push(address(0x3333333333333333333333333333333333333333)); 36 | accounts.push(address(0x4444444444444444444444444444444444444444)); 37 | 38 | StoreSwitch.setStoreAddress(address(world)); 39 | } 40 | 41 | function spawn(uint8 accountId, uint8 rightNeighborOption, bool velRight) public { 42 | accountId = accountId % uint8(accounts.length); 43 | 44 | uint32 lineId; 45 | uint160 rightNeighborEntityId; 46 | if (rightNeighborOption > 100) { 47 | rightNeighborEntityId = EntityLib.toEntityId(uint160(accounts[(rightNeighborOption - 100) % accounts.length])); 48 | lineId = Entity.getLineId(rightNeighborEntityId); 49 | } else { 50 | lineId = rightNeighborOption % GameState.getNumLines(); 51 | 52 | if (rightNeighborOption % 3 == 0) { 53 | rightNeighborEntityId = EntityLib.toEntityId(uint256(keccak256(abi.encode(lineId, 2)))); 54 | } else if (rightNeighborOption % 3 == 1) { 55 | rightNeighborEntityId = EntityLib.toEntityId(uint256(keccak256(abi.encode(lineId, 3)))); 56 | } else { 57 | rightNeighborEntityId = EntityLib.rightmostEntityId(lineId); 58 | } 59 | } 60 | 61 | vm.prank(accounts[accountId]); 62 | world.spawn(lineId, rightNeighborEntityId, velRight); 63 | 64 | console2.log( 65 | "{ type: 'spawn' , entity: ", 66 | DebugLib.mapEntityToEmoji(EntityLib.toEntityId(uint256(uint160(accounts[accountId]))), accounts) 67 | ); 68 | console2.log(", rightNeighbor: ", DebugLib.mapEntityToEmoji(rightNeighborEntityId, accounts)); 69 | console2.log(", timeWad: ", DebugLib.stringify(timeWad())); 70 | console2.log(", velRight: ", velRight, "},"); 71 | } 72 | 73 | function setDirection(uint8 accountId, bool velRight) public { 74 | vm.prank(accounts[accountId % accounts.length]); 75 | world.setDirection(velRight); 76 | 77 | console2.log( 78 | "{ type: 'setDirection' , entity: ", 79 | DebugLib.mapEntityToEmoji( 80 | EntityLib.toEntityId(uint256(uint160(accounts[accountId % accounts.length]))), 81 | accounts 82 | ) 83 | ); 84 | console2.log(", timeWad: ", DebugLib.stringify(timeWad())); 85 | console2.log(", velRight: ", velRight, "},"); 86 | } 87 | 88 | function pass1Second() public { 89 | vm.warp(block.timestamp + 1); 90 | 91 | console2.log("{ type: 'pass1Second' , timeWad: ", DebugLib.stringify(timeWad()), "},"); 92 | } 93 | 94 | function jumpToLine(uint8 accountId, bool up) public { 95 | vm.prank(accounts[accountId % accounts.length]); 96 | world.jumpToLine(up); 97 | 98 | console2.log( 99 | "{ type: 'jumpToLine' , entity: ", 100 | DebugLib.mapEntityToEmoji( 101 | EntityLib.toEntityId(uint256(uint160(accounts[accountId % accounts.length]))), 102 | accounts 103 | ) 104 | ); 105 | console2.log(", timeWad: ", DebugLib.stringify(timeWad())); 106 | console2.log(", up: ", up, "},"); 107 | } 108 | 109 | //////////////////////////////////////////////////////// 110 | 111 | function jumpToLineParallel2(uint8 accountId1, uint8 accountId2, bool up1, bool up2) public { 112 | jumpToLine(accountId1, up1); 113 | jumpToLine(accountId2, up2); 114 | } 115 | 116 | function jumpToLineParallel3(uint8 accountId1, uint8 accountId2, uint8 accountId3, bool up1, bool up2, bool up3) public { 117 | jumpToLine(accountId1, up1); 118 | jumpToLine(accountId2, up2); 119 | jumpToLine(accountId3, up3); 120 | } 121 | 122 | function jumpToLineParallel4( 123 | uint8 accountId1, 124 | uint8 accountId2, 125 | uint8 accountId3, 126 | uint8 accountId4, 127 | bool up1, 128 | bool up2, 129 | bool up3, 130 | bool up4 131 | ) public { 132 | jumpToLine(accountId1, up1); 133 | jumpToLine(accountId2, up2); 134 | jumpToLine(accountId3, up3); 135 | jumpToLine(accountId4, up4); 136 | } 137 | 138 | //////////////////////////////////////////////////////// 139 | 140 | function setDirectionParallel2(uint8 accountId1, uint8 accountId2, bool velRight1, bool velRight2) public { 141 | setDirection(accountId1, velRight1); 142 | setDirection(accountId2, velRight2); 143 | } 144 | 145 | function setDirectionParallel3( 146 | uint8 accountId1, 147 | uint8 accountId2, 148 | uint8 accountId3, 149 | bool velRight1, 150 | bool velRight2, 151 | bool velRight3 152 | ) public { 153 | setDirection(accountId1, velRight1); 154 | setDirection(accountId2, velRight2); 155 | setDirection(accountId3, velRight3); 156 | } 157 | 158 | function setDirectionParallel4( 159 | uint8 accountId1, 160 | uint8 accountId2, 161 | uint8 accountId3, 162 | uint8 accountId4, 163 | bool velRight1, 164 | bool velRight2, 165 | bool velRight3, 166 | bool velRight4 167 | ) public { 168 | setDirection(accountId1, velRight1); 169 | setDirection(accountId2, velRight2); 170 | setDirection(accountId3, velRight3); 171 | setDirection(accountId4, velRight4); 172 | } 173 | 174 | /////////////////////////////////////////////////////////// 175 | 176 | function spawnParallel2( 177 | uint8 accountId1, 178 | uint8 accountId2, 179 | uint8 rightNeighborOption1, 180 | uint8 rightNeighborOption2, 181 | bool velRight1, 182 | bool velRight2 183 | ) public { 184 | spawn(accountId1, rightNeighborOption1, velRight1); 185 | spawn(accountId2, rightNeighborOption2, velRight2); 186 | } 187 | 188 | function spawnParallel3( 189 | uint8 accountId1, 190 | uint8 accountId2, 191 | uint8 accountId3, 192 | uint8 rightNeighborOption1, 193 | uint8 rightNeighborOption2, 194 | uint8 rightNeighborOption3, 195 | bool velRight1, 196 | bool velRight2, 197 | bool velRight3 198 | ) public { 199 | spawn(accountId1, rightNeighborOption1, velRight1); 200 | spawn(accountId2, rightNeighborOption2, velRight2); 201 | spawn(accountId3, rightNeighborOption3, velRight3); 202 | } 203 | 204 | function spawnParallel4( 205 | uint8 accountId1, 206 | uint8 accountId2, 207 | uint8 accountId3, 208 | uint8 accountId4, 209 | uint8 rightNeighborOption1, 210 | uint8 rightNeighborOption2, 211 | uint8 rightNeighborOption3, 212 | uint8 rightNeighborOption4, 213 | bool velRight1, 214 | bool velRight2, 215 | bool velRight3, 216 | bool velRight4 217 | ) public { 218 | spawn(accountId1, rightNeighborOption1, velRight1); 219 | spawn(accountId2, rightNeighborOption2, velRight2); 220 | spawn(accountId3, rightNeighborOption3, velRight3); 221 | spawn(accountId4, rightNeighborOption4, velRight4); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /packages/contracts/test/Invariants.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.21; 3 | 4 | import {console2} from "forge-std/console2.sol"; 5 | 6 | import {MudTest} from "@latticexyz/world/test/MudTest.t.sol"; 7 | 8 | import "../src/codegen/index.sol"; 9 | import "../src/codegen/common.sol"; 10 | 11 | import {IWorld} from "../src/codegen/world/IWorld.sol"; 12 | 13 | import {PriorityQueue96x160Lib, MinHeapLib} from "../src/utils/PriorityQueue96x160Lib.sol"; 14 | import {EntityLib} from "../src/utils/EntityLib.sol"; 15 | import {timeWad} from "../src/utils/WadTimeLib.sol"; 16 | import {LineLib} from "../src/utils/LineLib.sol"; 17 | 18 | import {Handler} from "./Handler.sol"; 19 | import {DebugLib} from "./DebugLib.sol"; 20 | 21 | contract InvariantsTest is MudTest { 22 | Handler public handler; 23 | 24 | IWorld public world; 25 | 26 | function setUp() public override { 27 | super.setUp(); 28 | world = IWorld(worldAddress); 29 | handler = new Handler(world); 30 | targetContract(address(handler)); 31 | 32 | // If you see tests fail with "failed to set up invariant testing environment: EvmError: Revert" 33 | // ensure that FORCE_DETERMINISTIC_TIMESTAMP was true when the PostDeploy.s.sol script was run. 34 | require( 35 | vm.envBool("FORCE_DETERMINISTIC_TIMESTAMP"), 36 | "FORCE_DETERMINISTIC_TIMESTAMP should be set for invariant tests." 37 | ); 38 | 39 | vm.warp(9999999999); // Value from PostDeploy.s.sol. 40 | } 41 | 42 | function test_add_lines_fails(uint32 numLines) public { 43 | vm.expectRevert(); 44 | world.addLines(numLines); 45 | } 46 | 47 | function invariant_no_entities_have_invalid_coordinates() public { 48 | for (uint256 i = 0; i < GameState.getNumLines(); i++) { 49 | world.poke(uint32(i)); 50 | 51 | uint160 entityId = EntityLib.leftmostEntityId(uint32(i)); 52 | uint160 previousEntityId = 0; 53 | 54 | while (entityId != 0) { 55 | if (entityId != EntityLib.leftmostEntityId(uint32(i))) { 56 | assertNotEq(Entity.getLeftNeighbor(entityId), 0, "LEFT_NEIGHBOR_IS_ZERO"); 57 | assertGt(EntityLib.computeX(entityId, timeWad()), 0, "ENTITY_X_IS_ZERO"); 58 | assertGt(Entity.getLastX(entityId), 0, "ENTITY_LAST_X_IS_ZERO"); 59 | } 60 | if (entityId != EntityLib.rightmostEntityId(uint32(i))) { 61 | assertNotEq(Entity.getRightNeighbor(entityId), 0, "RIGHT_NEIGHBOR_IS_ZERO"); 62 | assertLt(EntityLib.computeX(entityId, timeWad()), GameConfig.getLineWidth(), "ENTITY_X_IS_TOO_LARGE"); 63 | assertLt(Entity.getLastX(entityId), GameConfig.getLineWidth(), "ENTITY_LAST_X_IS_TOO_LARGE"); 64 | } 65 | 66 | assertNotEq(Entity.getLeftNeighbor(entityId), EntityLib.rightmostEntityId(Entity.getLineId(entityId))); 67 | assertNotEq(Entity.getRightNeighbor(entityId), EntityLib.leftmostEntityId(Entity.getLineId(entityId))); 68 | 69 | assertNotEq(Entity.getLeftNeighbor(entityId), entityId); 70 | assertNotEq(Entity.getRightNeighbor(entityId), entityId); 71 | 72 | if (previousEntityId != 0) { 73 | assertEq( 74 | Entity.getLeftNeighbor(entityId), 75 | previousEntityId, 76 | "LEFT_NEIGHBOR_DOES_NOT_MATCH_PREVIOUS_ENTITY_RIGHT_NEIGHBOR" 77 | ); 78 | 79 | uint128 previousEntityRightEdge = EntityLib.computeX(previousEntityId, timeWad()) + 80 | EntityLib.computeDiameter(previousEntityId); 81 | uint128 currentEntityLeftEdge = EntityLib.computeX(entityId, timeWad()); 82 | 83 | assertGt(currentEntityLeftEdge, previousEntityRightEdge, "ENTITY_OVERLAPS_WITH_PREVIOUS_NEIGHBOR"); 84 | } 85 | 86 | previousEntityId = entityId; 87 | entityId = Entity.getRightNeighbor(entityId); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/contracts/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 | -------------------------------------------------------------------------------- /packages/contracts/worlds.json: -------------------------------------------------------------------------------- 1 | { 2 | "911867": { 3 | "address": "0xDA6628C889Cb78b5364D979B01A2CB877Cb43B8c", 4 | "blockNumber": 20072784 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/contracts/worlds.json.d.ts: -------------------------------------------------------------------------------- 1 | declare const worlds: Partial>; 2 | export default worlds; 3 | -------------------------------------------------------------------------------- /packages/example-bot/.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | .env -------------------------------------------------------------------------------- /packages/example-bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-bot", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "tsx src/bot.ts" 9 | }, 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@types/cli-progress": "^3.11.6", 13 | "@types/node": "^18.15.11", 14 | "@types/yargs": "^17.0.32", 15 | "tsx": "^4.16.2" 16 | }, 17 | "dependencies": { 18 | "@franciscokloganb/local-storage-polyfill": "^0.1.0", 19 | "@latticexyz/store-sync": "2.2.22-3baa3fd86f5917471729ba6551f12c17cdca53e3", 20 | "cli-progress": "^3.12.0", 21 | "viem": "^2.29.1", 22 | "yargs": "^17.7.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/example-bot/src/bot.ts: -------------------------------------------------------------------------------- 1 | // See https://hackmd.io/@t11s/rethmatch for more info on how to get started! 2 | 3 | import "dotenv/config"; 4 | import cliProgress from "cli-progress"; 5 | import { http, fallback, webSocket, createPublicClient } from "viem"; 6 | 7 | import { forwardStateTo, LiveState, parseSyncStateGivenTables } from "../../client/src/utils/sync"; 8 | import { EntityType } from "../../client/src/utils/game/entityLib"; 9 | import { GameConfig } from "../../client/src/utils/game/configLib"; 10 | import { stash } from "../../client/src/mud/stash"; 11 | import { timeWad } from "../../client/src/utils/timeLib"; 12 | import { ODYSSEY_CHAIN } from "../../client/src/utils/chains"; 13 | 14 | import { syncToStash } from "@latticexyz/store-sync/internal"; 15 | import { Stash } from "@latticexyz/stash/internal"; 16 | 17 | import Worlds from "../../contracts/worlds.json"; 18 | 19 | const chain = ODYSSEY_CHAIN; 20 | const WORLD_ADDRESS = Worlds[chain.id]?.address as `0x${string}`; 21 | if (!WORLD_ADDRESS) throw new Error(`No world address found for chain ${chain.id}`); 22 | const START_BLOCK = Worlds[chain.id]!.blockNumber!; 23 | 24 | function onBlock(liveState: LiveState, gameConfig: GameConfig, wadTime: bigint) { 25 | const { lines, gameState } = liveState; 26 | 27 | // 28 | 29 | // Demo: Visualize each line by printing players with their usernames. 30 | lines.forEach((line, idx) => { 31 | const players = line 32 | .filter( 33 | (entity) => entity.etype === EntityType.ALIVE && gameState.usernames.has(entity.entityId) 34 | ) 35 | .map((entity) => { 36 | return gameState.usernames.get(entity.entityId); 37 | }); 38 | 39 | console.log( 40 | `Line ${idx} has players: ${players.length > 0 ? players.join(", ") : "No players"}` 41 | ); 42 | }); 43 | } 44 | 45 | export async function main() { 46 | const progressBar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic); 47 | const { storedBlockLogs$ } = await syncToStash({ 48 | stash: stash as Stash, 49 | startSync: true, 50 | address: WORLD_ADDRESS, 51 | startBlock: BigInt(START_BLOCK ?? 0), 52 | publicClient: createPublicClient({ 53 | chain, 54 | transport: fallback([webSocket(), http()]), 55 | pollingInterval: 100, 56 | cacheTime: 100, 57 | }), 58 | indexerUrl: chain.indexerUrl, 59 | }); 60 | progressBar.start(100, 0); 61 | 62 | storedBlockLogs$.subscribe(() => { 63 | const { syncProgress, data } = parseSyncStateGivenTables(stash.get()); 64 | 65 | if (syncProgress.step === "live" && data) { 66 | if (progressBar.isActive) { 67 | progressBar.update(100); 68 | progressBar.stop(); 69 | console.log("\n✅Caught up!"); 70 | } 71 | 72 | const syncedState = { 73 | lastSyncedTime: performance.now(), 74 | lastProcessedTime: -1n, 75 | lines: data.lines, 76 | lineStates: data.lineStates, 77 | gameState: data.gameState, 78 | }; 79 | 80 | const liveState = forwardStateTo(syncedState, data.gameConfig, false, null, { 81 | stopAtIteration: null, 82 | stopAtTimestampWad: null, 83 | }); 84 | 85 | console.log("\n📥 Got block:", Number(syncProgress.latestBlockNumber), "\n"); 86 | 87 | onBlock(liveState, data.gameConfig, timeWad()); 88 | } else { 89 | if (syncProgress.step === "snapshot") progressBar.update(0); 90 | else progressBar.update(Math.round(syncProgress.percentage)); 91 | } 92 | }); 93 | } 94 | 95 | console.log("🤖 Starting bot... (this may take a couple seconds)", WORLD_ADDRESS, START_BLOCK); 96 | console.log(); 97 | await main(); 98 | -------------------------------------------------------------------------------- /packages/example-bot/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "esModuleInterop": true, 7 | "jsx": "react" 8 | }, 9 | "include": ["src"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/infra/.env.example: -------------------------------------------------------------------------------- 1 | CLERK_PUBLISHABLE_KEY=pk_test_.... 2 | CLERK_SECRET_KEY=sk_test_.... 3 | SIGNING_PRIVATE_KEY=0x... 4 | POKING_PRIVATE_KEY=0x... -------------------------------------------------------------------------------- /packages/infra/.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | .env -------------------------------------------------------------------------------- /packages/infra/ecosystem.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: "poke", 5 | script: "pnpm", 6 | args: "start:poke", 7 | cron_restart: "0 * * * *", // Restart every hour. 8 | time: true, 9 | }, 10 | { 11 | name: "indexer", 12 | script: "pnpm", 13 | args: "start:indexer", 14 | cron_restart: "0 * * * *", // Restart every hour. 15 | time: true, 16 | }, 17 | { 18 | name: "auth", 19 | script: "pnpm", 20 | args: "start:auth", 21 | cron_restart: "0 * * * *", // Restart every hour. 22 | time: true, 23 | }, 24 | ], 25 | }; 26 | -------------------------------------------------------------------------------- /packages/infra/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "infra", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "tsc", 8 | "start:poke": "tsx src/poke.ts", 9 | "start:auth": "tsx src/auth.ts", 10 | "start:indexer": "bash src/indexer.sh", 11 | "reset:indexer": "rm -f indexer.db", 12 | "start": "pm2 start ecosystem.config.cjs" 13 | }, 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@types/body-parser": "^1.19.5", 17 | "@types/cors": "^2.8.18", 18 | "@types/express": "^5.0.2", 19 | "@types/node": "^18.15.11", 20 | "@types/yargs": "^17.0.32", 21 | "tsx": "^4.16.2" 22 | }, 23 | "dependencies": { 24 | "@clerk/express": "^1.5.2", 25 | "@franciscokloganb/local-storage-polyfill": "^0.1.0", 26 | "body-parser": "^2.2.0", 27 | "cors": "^2.8.5", 28 | "dotenv": "^16.4.5", 29 | "express": "^5.1.0", 30 | "random-words": "^2.0.1", 31 | "viem": "^2.29.1", 32 | "yargs": "^17.7.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/infra/src/auth.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import express from "express"; 3 | import { clerkClient, requireAuth, getAuth, clerkMiddleware } from "@clerk/express"; 4 | import cors from "cors"; 5 | import bodyParser from "body-parser"; 6 | import { privateKeyToAccount, sign } from "viem/accounts"; 7 | import { encodePacked, getAddress, isAddress, keccak256 } from "viem"; 8 | 9 | const signingPrivateKey = process.env.SIGNING_PRIVATE_KEY as `0x${string}`; 10 | if (!signingPrivateKey) { 11 | throw new Error("SIGNING_PRIVATE_KEY is not set"); 12 | } 13 | 14 | const signingAccount = privateKeyToAccount(signingPrivateKey); 15 | 16 | console.log("Using signing account:", signingAccount.address); 17 | 18 | const app = express(); 19 | const PORT = 3002; 20 | 21 | app.use(cors()); 22 | app.use(bodyParser.json({ limit: "1mb" })); 23 | app.use(bodyParser.urlencoded({ extended: true })); 24 | app.use(clerkMiddleware()); 25 | 26 | app.post("/generateAccessSignature", requireAuth(), async (req, res) => { 27 | if (!req.body) { 28 | res.status(400).json({ message: "Request body is required" }); 29 | return; 30 | } 31 | 32 | const address = req.body.address; 33 | if (!address) { 34 | res.status(400).json({ message: "Address is required" }); 35 | return; 36 | } 37 | if (!isAddress(address)) { 38 | res.status(400).json({ message: "Invalid address" }); 39 | return; 40 | } 41 | 42 | const { userId } = getAuth(req); 43 | if (!userId) { 44 | res.status(401).json({ message: "Invalid request, please sign in" }); 45 | return; 46 | } 47 | 48 | const clerkUser = await clerkClient.users.getUser(userId); 49 | if (!clerkUser) { 50 | res.status(401).json({ message: "User does not exist." }); 51 | return; 52 | } 53 | if (!clerkUser.username) { 54 | res.status(401).json({ message: "User does not have a username." }); 55 | return; 56 | } 57 | 58 | console.log("Generating access signature for:", clerkUser.username, "with address:", address); 59 | 60 | const accessSignature = await sign({ 61 | // getAddress() is used to ensure the address is in the correct checksum format. 62 | hash: keccak256( 63 | encodePacked(["address", "string"], [getAddress(address), clerkUser.username.toLowerCase()]) 64 | ), 65 | privateKey: signingPrivateKey, 66 | to: "hex", 67 | }); 68 | 69 | // Link the address to the user in Clerk for convenience. 70 | await clerkClient.users.updateUserMetadata(userId, { 71 | privateMetadata: { [Date.now().toString()]: address }, 72 | }); 73 | 74 | res.json({ accessSignature: accessSignature }); 75 | }); 76 | 77 | app.get("/", async (req, res) => { 78 | res.status(401).json({ 79 | message: "Unauthorized, please sign in", 80 | }); 81 | }); 82 | 83 | // Start the server and listen on the specified port 84 | app.listen(PORT, () => { 85 | console.log(`Auth listening at http://localhost:${PORT}`); 86 | }); 87 | -------------------------------------------------------------------------------- /packages/infra/src/indexer.sh: -------------------------------------------------------------------------------- 1 | # Sleep for 10 seconds for logging purposes. 2 | sleep 10 3 | 4 | # Default to Odyssey. 5 | CHAIN_ID=${CHAIN_ID:-911867} 6 | CHAIN_URL=${CHAIN_URL:-odyssey.ithaca.xyz} 7 | 8 | # Extract address and blockNumber from worlds.json. 9 | STORE_ADDRESS=$(jq -r ".[\"$CHAIN_ID\"][\"address\"]" ../contracts/worlds.json) 10 | START_BLOCK=$(jq -r ".[\"$CHAIN_ID\"][\"blockNumber\"]" ../contracts/worlds.json) 11 | 12 | if [ -z "$STORE_ADDRESS" ] || [ -z "$START_BLOCK" ]; then 13 | echo "Error: STORE_ADDRESS or START_BLOCK are unset." 14 | exit 1 15 | fi 16 | 17 | echo "Starting store indexer for chain $CHAIN_ID with params:" 18 | echo "STORE_ADDRESS: $STORE_ADDRESS" 19 | echo "START_BLOCK: $START_BLOCK" 20 | echo "" 21 | 22 | # Options: https://mud.dev/indexer/sqlite 23 | RPC_HTTP_URL="https://$CHAIN_URL" \ 24 | RPC_WS_URL="wss://$CHAIN_URL" \ 25 | STORE_ADDRESS="$STORE_ADDRESS" \ 26 | START_BLOCK="$START_BLOCK" \ 27 | MAX_BLOCK_RANGE=500 \ 28 | PORT=3001 \ 29 | npx -y -p @latticexyz/store-indexer sqlite-indexer -------------------------------------------------------------------------------- /packages/infra/src/poke.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createPublicClient, 3 | http, 4 | fallback, 5 | webSocket, 6 | createWalletClient, 7 | formatGwei, 8 | } from "viem"; 9 | import { privateKeyToAccount } from "viem/accounts"; 10 | import "dotenv/config"; 11 | 12 | import IWorldAbi from "../../contracts/out/IWorld.sol/IWorld.abi.json"; 13 | import Worlds from "../../contracts/worlds.json"; 14 | 15 | import { ODYSSEY_CHAIN } from "../../client/src/utils/chains"; 16 | const chain = ODYSSEY_CHAIN; 17 | const WORLD_ADDRESS = Worlds[chain.id]?.address as `0x${string}`; 18 | if (!WORLD_ADDRESS) { 19 | throw new Error(`No world address found for chain ${chain.id}`); 20 | } 21 | 22 | const publicClient = createPublicClient({ 23 | chain, 24 | transport: fallback([webSocket(), http()]), 25 | pollingInterval: 100, 26 | cacheTime: 100, 27 | }); 28 | 29 | const privateKey = process.env.POKING_PRIVATE_KEY as `0x${string}`; 30 | if (!privateKey) { 31 | throw new Error("POKING_PRIVATE_KEY is not set"); 32 | } 33 | 34 | const account = privateKeyToAccount(privateKey); 35 | 36 | console.log( 37 | "Poking account address:", 38 | account.address, 39 | "balance:", 40 | await publicClient.getBalance({ address: account.address }) 41 | ); 42 | 43 | const client = createWalletClient({ 44 | account, 45 | chain, 46 | transport: fallback([webSocket(), http()]), 47 | }); 48 | 49 | console.log("\n-------- USING CHAIN:", client.chain.name, "--------\n"); 50 | console.log("WORLD_ADDRESS:", WORLD_ADDRESS); 51 | 52 | process.on("SIGINT", () => { 53 | console.log("Keyboard interruption detected. Exiting gracefully..."); 54 | process.exit(); 55 | }); 56 | 57 | async function getNumLines() { 58 | const numLines = await publicClient 59 | // @ts-ignore 60 | .readContract({ 61 | address: WORLD_ADDRESS, 62 | abi: IWorldAbi, 63 | functionName: "getNumLines", 64 | }) 65 | .catch((error) => { 66 | console.error("❌ Error getting number of lines:", stripViemErrorOfABI(error)); 67 | process.exit(1); 68 | }); 69 | 70 | return numLines; 71 | } 72 | 73 | let numLines = -1; // Will be set in the first iteration below. 74 | 75 | for (let iteration = 0; ; iteration++) { 76 | if (iteration % 5 === 0) { 77 | numLines = await getNumLines(); 78 | 79 | publicClient.getBalance({ address: account.address }).then((balance) => { 80 | console.log("\n💰 Balance remaining:", (Number(balance) / 1e18).toFixed(2), "eth\n"); 81 | }); 82 | } 83 | 84 | console.log("\n--------------------------------------------\n"); 85 | 86 | for (let line = 0; line < numLines; line++) { 87 | try { 88 | await new Promise((resolve) => setTimeout(resolve, 2000)); // Sleep. 89 | 90 | const tx = await client.writeContract({ 91 | address: WORLD_ADDRESS, 92 | abi: IWorldAbi, 93 | functionName: "poke", 94 | args: [line], 95 | gas: 29_000_000n, 96 | }); 97 | 98 | console.log("📤 Poking line:", line, "with tx:", tx); 99 | 100 | publicClient 101 | .waitForTransactionReceipt({ 102 | hash: tx, 103 | }) 104 | .then((receipt) => { 105 | console.log( 106 | "⛽️ Line", 107 | line, 108 | "poked — gas used:", 109 | Number(receipt.gasUsed).toLocaleString(), 110 | "gas price:", 111 | formatGwei(receipt.effectiveGasPrice), 112 | "block:", 113 | Number(receipt.blockNumber), 114 | "hash:", 115 | receipt.transactionHash.slice(0, 6) + "..." + receipt.transactionHash.slice(-4) 116 | ); 117 | 118 | if (receipt.status === "reverted") { 119 | console.log("❌ Line", line, "reverted"); 120 | } 121 | }) 122 | .catch((error) => { 123 | console.error("❌ Error poking line", line, ":", stripViemErrorOfABI(error)); 124 | }); 125 | } catch (error) { 126 | console.error("❌ Error poking line", line, ":", stripViemErrorOfABI(error)); 127 | } 128 | } 129 | } 130 | 131 | // Errors can get really long without this. 132 | function stripViemErrorOfABI(error: any) { 133 | return error.toString().split("abi: [")[0]; 134 | } 135 | -------------------------------------------------------------------------------- /packages/infra/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "esModuleInterop": true, 7 | "jsx": "react" 8 | }, 9 | "include": ["src"] 10 | } 11 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "moduleResolution": "Bundler", 5 | "target": "es2021", 6 | "lib": [ 7 | "ESNext", 8 | // This is temporarily added until we can unbundle JS files and only include this when we import things that use DOM APIs 9 | // TODO: fix me 10 | "DOM" 11 | ], 12 | "noEmit": true, 13 | "skipLibCheck": true, 14 | "strict": true, 15 | "declaration": true, 16 | "esModuleInterop": true, 17 | "noErrorTruncation": true, 18 | "resolveJsonModule": true, 19 | "forceConsistentCasingInFileNames": true, 20 | "sourceMap": true 21 | }, 22 | "exclude": ["**/dist", "**/node_modules", "**/docs", "**/e2e"] 23 | } 24 | --------------------------------------------------------------------------------