├── .github ├── lander.png ├── mark-dark.svg ├── mark-light.svg └── workflows │ └── build.yml ├── README.md ├── client ├── .eslintrc.cjs ├── .gitignore ├── index.html ├── package.json ├── postcss.config.js ├── public │ ├── Generic Icons.svg │ ├── Lander.svg │ ├── STARK.svg │ ├── fonts │ │ ├── ChicagoFLF.ttf │ │ ├── DOS_VGA.ttf │ │ ├── DOS_VGA_WIN.ttf │ │ ├── PPMondwest-Regular.otf │ │ ├── PixelscriptPro.ttf │ │ └── broken-console-broken-console-regular-400.ttf │ ├── vite.svg │ └── worker.js ├── src │ ├── App.tsx │ ├── Body.tsx │ ├── assets │ │ ├── react.svg │ │ └── svg │ │ │ ├── HappyFace.tsx │ │ │ ├── Icon.tsx │ │ │ ├── Lander.tsx │ │ │ ├── Skull.tsx │ │ │ └── Stark.tsx │ ├── components │ │ ├── Button.tsx │ │ ├── Control.tsx │ │ ├── Header.tsx │ │ ├── Prompt.tsx │ │ ├── Slider.tsx │ │ ├── Table.tsx │ │ └── icons │ │ │ ├── Arrow.tsx │ │ │ ├── ArrowEnclosed.tsx │ │ │ ├── ArrowInput.tsx │ │ │ ├── BorderImage.tsx │ │ │ ├── BorderImagePixelated.tsx │ │ │ └── index.tsx │ ├── dojo │ │ ├── abi.json │ │ └── setupNetwork.ts │ ├── index.css │ ├── main.tsx │ ├── theme │ │ ├── colors.ts │ │ ├── components │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── container.tsx │ │ │ ├── divider.tsx │ │ │ ├── index.ts │ │ │ ├── input.tsx │ │ │ └── slider.tsx │ │ ├── index.ts │ │ └── styles.ts │ ├── types │ │ └── components.ts │ ├── utils │ │ ├── math.ts │ │ └── ui.ts │ └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock └── contracts ├── .gitignore ├── LICENSE ├── Scarb.toml ├── lander ├── Scarb.toml ├── readme.md └── src │ ├── lib.cairo │ └── math.cairo ├── readme.md └── src ├── components.cairo ├── components ├── fuel.cairo └── lander.cairo ├── lib.cairo ├── systems.cairo ├── systems ├── burn.cairo ├── position.cairo └── start.cairo ├── tests.cairo └── tests └── start.cairo /.github/lander.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dojoengine/stark-lander/e1f425d1cd3227c7158137f45f92af3fa21dc5a4/.github/lander.png -------------------------------------------------------------------------------- /.github/mark-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.github/mark-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Scarb project 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | env: 10 | SCARB_VERSION: 0.5.1 11 | 12 | jobs: 13 | build-contracts: 14 | name: Build Adventurer 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Setup Scarb 20 | run: | 21 | curl --proto '=https' --tlsv1.2 -sSf https://docs.swmansion.com/scarb/install.sh | bash -s -- -v ${{ env.SCARB_VERSION }} 22 | 23 | - name: Scarb build 24 | run: | 25 | cd contracts && scarb build && scarb cairo-test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Dojo logo 2 | 3 | --- 4 | 5 | 6 | Dojo logo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | [![discord](https://img.shields.io/badge/join-dojo-green?logo=discord&logoColor=white)](https://discord.gg/PwDa2mKhR4) 17 | [![Telegram Chat][tg-badge]][tg-url] 18 | 19 | [tg-badge]: https://img.shields.io/endpoint?color=neon&logo=telegram&label=chat&style=flat-square&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fdojoengine 20 | [tg-url]: https://t.me/dojoengine 21 | 22 | ## Stark Lander 23 | 24 | An onchain interpretation of the classic game [Lunar Lander](https://en.wikipedia.org/wiki/Lunar_Lander_(video_game_genre)). Try and land on the ground with a velocity of 0.1m/s. All computation is calculated in [Cairo](https://book.cairo-lang.org/title-page.html) and the game is built using the Dojo engine. 25 | 26 | 27 | ### Systems 28 | - `Start`: Spawns a Lander with some random coordinates 29 | - `Burn`: Adjusts the trajectory of the Lander according to inputs 30 | - `Position`: Returns live position of the Lander 31 | - `Win`: Create Win condition 32 | 33 | ### Components 34 | - `Lander`: Lander state and computed values 35 | - `Fuel` : TODO: abstract from Lander component 36 | 37 | ### Game loop 38 | 1. Players spawn a lander with `start` 39 | 2. Input thrust and angle on each action 40 | 3. Compute position according to block and tick forward at constant rate 41 | 4. Determine if lander arrives at surface of planet at the correct angle and correct speed 42 | 43 | ## Pre-requisites 44 | 45 | ### Clone 46 | 47 | ```console 48 | git clone https://github.com/dojoengine/stark-lander.git 49 | ``` 50 | 51 | ### Install Dojo 52 | 53 | ```console 54 | curl -L https://install.dojoengine.org | bash 55 | 56 | dojoup 57 | ``` 58 | 59 | ## Running the game 60 | 61 | ### Katana 62 | Run Katana in a terminal window using the following command: 63 | 64 | ```console 65 | katana --allow-zero-max-fee --block-time 1 66 | ``` 67 | 68 | ### Contract 69 | Switch to a new terminal window and run the following commands: 70 | 71 | ```console 72 | cd contract 73 | 74 | sozo build // Build World 75 | 76 | sozo migrate // Migrate World 77 | ``` 78 | 79 | ### Client 80 | In another terminal window, start the client server by running the following command: 81 | 82 | ```console 83 | cd client 84 | 85 | yarn 86 | 87 | yarn dev 88 | ``` 89 | 90 | -------------------------------------------------------------------------------- /client/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | module.exports = { 4 | root: true, 5 | env: { browser: true, es2020: true }, 6 | extends: [ 7 | 'eslint:recommended', 8 | 'plugin:@typescript-eslint/recommended', 9 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 10 | 'plugin:react-hooks/recommended', 11 | ], 12 | parser: '@typescript-eslint/parser', 13 | parserOptions: { 14 | ecmaVersion: 'latest', 15 | sourceType: 'module', 16 | project: true, 17 | tsconfigRootDir: __dirname, 18 | }, 19 | plugins: ['react-refresh'], 20 | rules: { 21 | 'react-refresh/only-export-components': [ 22 | 'warn', 23 | { allowConstantExport: true }, 24 | ], 25 | '@typescript-eslint/no-non-null-assertion': 'off', 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@chakra-ui/react": "^2.7.1", 14 | "@dojoengine/core": "^0.0.6", 15 | "@emotion/react": "^11.11.1", 16 | "@emotion/styled": "^11.11.0", 17 | "framer-motion": "^10.12.21", 18 | "influence-utils": "^1.14.0", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "starknet": "^5.14.1" 22 | }, 23 | "devDependencies": { 24 | "@types/react": "^18.2.14", 25 | "@types/react-dom": "^18.2.6", 26 | "@typescript-eslint/eslint-plugin": "^5.61.0", 27 | "@typescript-eslint/parser": "^5.61.0", 28 | "@vitejs/plugin-react": "^4.0.1", 29 | "autoprefixer": "^10.4.14", 30 | "eslint": "^8.44.0", 31 | "eslint-plugin-react-hooks": "^4.6.0", 32 | "eslint-plugin-react-refresh": "^0.4.1", 33 | "postcss": "^8.4.26", 34 | "tailwindcss": "^3.3.3", 35 | "typescript": "^5.0.2", 36 | "vite": "^4.4.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /client/public/Generic Icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /client/public/Lander.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/public/STARK.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /client/public/fonts/ChicagoFLF.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dojoengine/stark-lander/e1f425d1cd3227c7158137f45f92af3fa21dc5a4/client/public/fonts/ChicagoFLF.ttf -------------------------------------------------------------------------------- /client/public/fonts/DOS_VGA.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dojoengine/stark-lander/e1f425d1cd3227c7158137f45f92af3fa21dc5a4/client/public/fonts/DOS_VGA.ttf -------------------------------------------------------------------------------- /client/public/fonts/DOS_VGA_WIN.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dojoengine/stark-lander/e1f425d1cd3227c7158137f45f92af3fa21dc5a4/client/public/fonts/DOS_VGA_WIN.ttf -------------------------------------------------------------------------------- /client/public/fonts/PPMondwest-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dojoengine/stark-lander/e1f425d1cd3227c7158137f45f92af3fa21dc5a4/client/public/fonts/PPMondwest-Regular.otf -------------------------------------------------------------------------------- /client/public/fonts/PixelscriptPro.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dojoengine/stark-lander/e1f425d1cd3227c7158137f45f92af3fa21dc5a4/client/public/fonts/PixelscriptPro.ttf -------------------------------------------------------------------------------- /client/public/fonts/broken-console-broken-console-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dojoengine/stark-lander/e1f425d1cd3227c7158137f45f92af3fa21dc5a4/client/public/fonts/broken-console-broken-console-regular-400.ttf -------------------------------------------------------------------------------- /client/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/public/worker.js: -------------------------------------------------------------------------------- 1 | let startTime = new Date(); 2 | let seconds = 0; 3 | 4 | setInterval(() => { 5 | const time = getTime(); 6 | // send a message to notify for game loop 7 | self.postMessage(time); 8 | }, 1000); 9 | 10 | function getTime() { 11 | let now = new Date(); 12 | let totalSeconds = Math.floor((now - startTime) / 1000); 13 | 14 | let minutes = Math.floor(totalSeconds / 60); 15 | let seconds = totalSeconds % 60; 16 | 17 | let timeString = `${String(minutes).padStart(2, "0")}:${String(seconds).padStart( 18 | 2, 19 | "0" 20 | )}`; 21 | 22 | return timeString; 23 | } 24 | -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import Header from "./components/Header"; 2 | import Body from "./Body"; 3 | 4 | function App() { 5 | return ( 6 |
7 |
8 |
9 | 10 |
11 |
12 | ); 13 | } 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /client/src/Body.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import Button from "./components/Button"; 3 | 4 | import { KATANA_ACCOUNT_1_ADDRESS, setupNetwork } from "./dojo/setupNetwork"; 5 | import Table, { RowData } from "./components/Table"; 6 | import Control from "./components/Control"; 7 | import { Lander, parseRawCalldataAsLander } from "./types/components"; 8 | import { Center, VStack } from "@chakra-ui/react"; 9 | import Prompt from "./components/Prompt"; 10 | 11 | enum Stage { 12 | Idle, 13 | Playing, 14 | End, 15 | } 16 | 17 | export enum EndState { 18 | Success, 19 | Failure, 20 | } 21 | 22 | function Body() { 23 | const [gameLoopWorker, setGameLoopWorker] = useState(null); 24 | 25 | const [endState, setEndState] = useState(null); 26 | const [stage, setStage] = useState(Stage.Idle); 27 | 28 | // stores the game id for the current game 29 | const [gameId, setGameId] = useState(0); 30 | 31 | const [angle, setAngle] = useState(0); 32 | const [rows, setRows] = useState([]); 33 | 34 | // Handler for resetting the game state when 35 | // the game is over and the user wants to play again. 36 | const onNewGame = function () { 37 | setStage(Stage.Playing); 38 | setRows([ 39 | { 40 | fuel: 100, 41 | height: 120000, 42 | angle: 45, 43 | speed: 100, 44 | time: "00:00", 45 | }, 46 | ]); 47 | }; 48 | 49 | // Handler for resetting the game state. 50 | const onResetGame = function () { 51 | setGameId((prev) => prev + 1); 52 | setStage(Stage.Idle); 53 | setEndState(null); 54 | setAngle(0); 55 | }; 56 | 57 | // Handler for starting a new game. 58 | // A game is identified by the game id. 59 | const onStartGame = function () { 60 | setupNetwork() 61 | .execute("start", []) 62 | .then(onNewGame) 63 | .catch((error) => { 64 | console.log(error); 65 | }); 66 | }; 67 | 68 | // Handler for fetching the lander position of the current game. 69 | // The lander position is based on the current block timestamp. 70 | const onFetchLanderPosition = function (callback: (lander: Lander) => void) { 71 | setupNetwork() 72 | .call_execute(["8101821151424638830", [gameId, KATANA_ACCOUNT_1_ADDRESS]]) 73 | .then((result) => { 74 | const data = result as string[]; 75 | console.log("raw", parseRawCalldataAsLander(data)); 76 | callback(parseRawCalldataAsLander(data)); 77 | }) 78 | .catch((error) => { 79 | console.log("error on fetching lander position", error); 80 | }); 81 | }; 82 | 83 | const onIgnite = function () { 84 | const angle_mag = Math.abs(angle).toString(); 85 | const angle_sign = angle >= 0 ? "0" : "1"; 86 | 87 | setupNetwork() 88 | .execute("burn", ["0", "1", angle_mag, angle_sign, "1"]) 89 | .then((result) => console.log("execute ", result)) 90 | .catch((error) => { 91 | console.log("error on executing burn system", error); 92 | }); 93 | }; 94 | 95 | const onChangeAngle = function (angle: number) { 96 | setAngle(angle); 97 | }; 98 | 99 | // Hanlder for adding more rows to the table. 100 | const onAddRow = function (data: RowData) { 101 | setRows((rows) => [...rows, data]); 102 | }; 103 | 104 | // Fetch World uuid 105 | useEffect(() => { 106 | setupNetwork() 107 | .call("uuid", []) 108 | .then((result) => { 109 | const data = result as string; 110 | setGameId(parseInt(BigInt(data).toString())); 111 | }) 112 | .catch((error) => { 113 | console.log("error in fetching World uuid", error); 114 | }); 115 | }, []); 116 | 117 | useEffect(() => { 118 | console.log("stage", stage); 119 | }, [stage]); 120 | 121 | useEffect(() => { 122 | console.log("game id", gameId); 123 | }, [gameId]); 124 | 125 | useEffect(() => { 126 | if (stage !== Stage.Playing) return; 127 | 128 | const height = rows[rows.length - 1].height; 129 | const velocity = rows[rows.length - 1].speed; 130 | 131 | if (height <= 0 || velocity <= 0) { 132 | setStage(Stage.End); 133 | 134 | // only if the lander has landed properly 135 | // we can end the game in success. 136 | if (height === 0 && velocity === 0) { 137 | setEndState(EndState.Success); 138 | } else { 139 | setEndState(EndState.Failure); 140 | } 141 | } 142 | }, [stage, rows]); 143 | 144 | // Initialize the game loop 145 | useEffect(() => { 146 | if (stage !== Stage.Playing) { 147 | if (gameLoopWorker !== null) { 148 | gameLoopWorker.terminate(); 149 | setGameLoopWorker(null); 150 | } 151 | 152 | return; 153 | } 154 | 155 | // Initialize a web worker 156 | const myWorker = new Worker("worker.js"); 157 | 158 | // Listen for messages from the worker 159 | myWorker.onmessage = function (e) { 160 | onFetchLanderPosition((lander) => { 161 | onAddRow({ 162 | time: e.data as string, 163 | speed: Math.abs(parseInt(lander.velocity_y)), 164 | angle: parseInt(lander.angle) , 165 | fuel: lander.fuel, 166 | height: parseInt(lander.position_y), 167 | }); 168 | }); 169 | }; 170 | 171 | setGameLoopWorker(myWorker); 172 | }, [stage]); 173 | 174 | return ( 175 |
176 | {/* this is where u put all the main components */} 177 | 178 | {stage === Stage.Idle && ( 179 |
180 | 183 |
184 | )} 185 | 186 | {stage !== Stage.Idle && ( 187 | 188 | 189 | 190 | 191 | )} 192 | 193 | {stage == Stage.Playing && ( 194 | 199 | )} 200 | 201 | {stage === Stage.End && ( 202 | 205 | )} 206 | 207 | ); 208 | } 209 | 210 | export default Body; 211 | -------------------------------------------------------------------------------- /client/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/assets/svg/HappyFace.tsx: -------------------------------------------------------------------------------- 1 | function SvgComponent() { 2 | return ( 3 | 4 | 8 | 12 | 16 | 17 | ); 18 | } 19 | 20 | export default SvgComponent; 21 | -------------------------------------------------------------------------------- /client/src/assets/svg/Icon.tsx: -------------------------------------------------------------------------------- 1 | function SvgComponent() { 2 | return ( 3 | 4 | 10 | 14 | 15 | ); 16 | } 17 | 18 | export default SvgComponent; 19 | -------------------------------------------------------------------------------- /client/src/assets/svg/Lander.tsx: -------------------------------------------------------------------------------- 1 | function SvgComponent() { 2 | return ( 3 | 4 | 8 | 12 | 16 | 20 | 24 | 28 | 29 | ); 30 | } 31 | 32 | export default SvgComponent; 33 | -------------------------------------------------------------------------------- /client/src/assets/svg/Skull.tsx: -------------------------------------------------------------------------------- 1 | function SvgComponent() { 2 | return ( 3 | 4 | 8 | 9 | ); 10 | } 11 | 12 | export default SvgComponent; 13 | -------------------------------------------------------------------------------- /client/src/assets/svg/Stark.tsx: -------------------------------------------------------------------------------- 1 | function SvgComponent() { 2 | return ( 3 | 4 | 8 | 12 | 16 | 20 | 24 | 25 | ); 26 | } 27 | 28 | export default SvgComponent; 29 | -------------------------------------------------------------------------------- /client/src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { Button as ChakraButton, ButtonProps, StyleProps, Text } from "@chakra-ui/react"; 2 | import { ReactNode } from "react"; 3 | 4 | // Can't seem to set first-letter css correctly on button in chakra theme 5 | // so we do it here on text... 6 | const Button = ({ 7 | children, 8 | ...props 9 | }: { children: ReactNode } & StyleProps & ButtonProps) => ( 10 | { 13 | props.onClick && props.onClick(e); 14 | }} 15 | > 16 | 23 | {children} 24 | 25 | 26 | ); 27 | 28 | export default Button; 29 | -------------------------------------------------------------------------------- /client/src/components/Control.tsx: -------------------------------------------------------------------------------- 1 | import Button from "./Button"; 2 | import Slider from "./Slider"; 3 | 4 | interface Props { 5 | angle?: number; 6 | onIgnite: () => void; 7 | onChangeAngle: (angle: number) => void; 8 | } 9 | 10 | function Control({ angle, onChangeAngle, onIgnite }: Props) { 11 | return ( 12 | <> 13 | 14 | 17 | 18 | ); 19 | } 20 | 21 | export default Control; 22 | -------------------------------------------------------------------------------- /client/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import Stark from "../assets/svg/Stark"; 2 | import Icon from "../assets/svg/Icon"; 3 | import Lander from "../assets/svg/Lander"; 4 | 5 | function Header() { 6 | return ( 7 |
8 |
9 | 10 | 11 | 12 |
13 |
14 | ); 15 | } 16 | 17 | export default Header; 18 | -------------------------------------------------------------------------------- /client/src/components/Prompt.tsx: -------------------------------------------------------------------------------- 1 | import { EndState } from "../Body"; 2 | import Skull from "../assets/svg/Skull"; 3 | import Face from "../assets/svg/HappyFace"; 4 | import { HStack, Card, Center } from "@chakra-ui/react"; 5 | 6 | interface Props { 7 | gameEndState: EndState | null; 8 | } 9 | 10 | function Prompt({ gameEndState }: Props) { 11 | return ( 12 | 13 | 14 |
15 | {gameEndState === EndState.Failure ? : } 16 |
17 |
18 | 19 | 20 |
21 | {gameEndState === null && 22 | "That's one small step for a man, one giant leap for provable gaming."} 23 | 24 | {gameEndState === EndState.Success && 25 | "In space, no one can hear you clap, but I certainly can!"} 26 | 27 | {gameEndState === EndState.Failure && 28 | "Moon rocks called: they're filing a complaint about your landing technique."} 29 |
30 |
31 |
32 | ); 33 | } 34 | 35 | export default Prompt; 36 | -------------------------------------------------------------------------------- /client/src/components/Slider.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Slider as ChakraSlider, 3 | SliderFilledTrack, 4 | SliderTrack, 5 | HStack, 6 | Text, 7 | Card, 8 | SliderThumb, 9 | Box, 10 | } from "@chakra-ui/react"; 11 | import { ArrowEnclosed } from "./icons"; 12 | 13 | interface Props { 14 | angle?: number; 15 | onChangeAngle: (angle: number) => void; 16 | } 17 | 18 | function Slider({ angle = 0, onChangeAngle }: Props) { 19 | const onUp = (angle: number) => onChangeAngle(angle + 1); 20 | const onDown = (angle: number) => onChangeAngle(angle - 1); 21 | 22 | return ( 23 | 24 | Angle: 25 | 30 | {angle} 31 | 32 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | onDown(angle - 1)} 51 | color="neon.500" 52 | _hover={{ 53 | color: "neon.300", 54 | }} 55 | > 56 | 62 | 63 | onUp(angle + 1)} 66 | color="neon.500" 67 | _hover={{ 68 | color: "neon.300", 69 | }} 70 | > 71 | 72 | 73 | 74 | 75 | ); 76 | } 77 | 78 | export default Slider; 79 | -------------------------------------------------------------------------------- /client/src/components/Table.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Card, Divider, HStack, VStack } from "@chakra-ui/react"; 2 | import { useEffect, useRef } from "react"; 3 | 4 | function Title() { 5 | return ( 6 | 14 | 15 |
Time
(s)
16 |
17 | 18 |
Height
(ft)
19 |
20 | 21 |
Speed
(mph)
22 |
23 | 24 |
Roll
(deg)
25 |
26 | 27 |
Fuel
(lbs)
28 |
29 |
30 | ); 31 | } 32 | 33 | type RowProps = RowData; 34 | 35 | function Row({ time, height, speed, angle, fuel }: RowProps) { 36 | return ( 37 | 45 | {time} 46 | {height} 47 | {speed} 48 | {angle} 49 | {fuel} 50 | 51 | ); 52 | } 53 | 54 | export type RowData = RowDataWithoutTime & { 55 | time: string; 56 | }; 57 | 58 | export interface RowDataWithoutTime { 59 | height: number; 60 | speed: number; 61 | angle: number; 62 | fuel: number; 63 | } 64 | 65 | interface Props { 66 | rows: RowData[]; 67 | } 68 | 69 | function Table({ rows }: Props) { 70 | const tableRef = useRef(null); 71 | 72 | // scroll to the bottom of the table when a new row is added 73 | useEffect(() => { 74 | if (tableRef.current) { 75 | const { scrollHeight, scrollTop, clientHeight } = tableRef.current; 76 | 77 | if (scrollHeight - scrollTop < clientHeight + 150) { 78 | tableRef.current.scrollTo(0, tableRef.current.scrollHeight); 79 | } 80 | } 81 | }, [rows]); 82 | 83 | return ( 84 | 91 |
92 | 93 | <Divider /> 94 | </div> 95 | <VStack align="normal" position="relative" className=" py-1 px-5"> 96 | {rows.map((row, i) => ( 97 | <Row 98 | key={i} 99 | time={row.time} 100 | height={row.height} 101 | speed={row.speed} 102 | angle={row.angle} 103 | fuel={row.fuel} 104 | /> 105 | ))} 106 | </VStack> 107 | </Card> 108 | ); 109 | } 110 | 111 | export default Table; 112 | -------------------------------------------------------------------------------- /client/src/components/icons/Arrow.tsx: -------------------------------------------------------------------------------- 1 | import { Icon, IconProps } from "../icons"; 2 | 3 | type RotateType = { 4 | [key: string]: string; 5 | }; 6 | 7 | const rotate: RotateType = { 8 | up: "rotate(90deg)", 9 | down: "rotate(270deg)", 10 | right: "rotate(180deg)", 11 | left: "rotate(0deg)", 12 | }; 13 | 14 | type StyleType = "line" | "outline" | "pixel"; 15 | 16 | export interface ArrowProps { 17 | direction?: string; 18 | style?: StyleType; 19 | } 20 | 21 | export const Arrow = ({ direction, style, ...props }: ArrowProps & IconProps) => { 22 | let path; 23 | switch (style) { 24 | case "pixel": 25 | path = ( 26 | <path d="M20.7265 23.9998L20.7265 12L18.9088 12V14.0002L17.0911 14.0002L17.0911 16.0003H15.2734L15.2734 20.0006H17.0911L17.0911 21.9998H18.9088L18.9088 24L20.7265 23.9998Z" /> 27 | ); 28 | break; 29 | case "outline": 30 | path = ( 31 | <> 32 | <path d="M16.8891 10.2226H14.6673V12.4444H16.8891V10.2226Z" /> 33 | <path d="M25.7774 12.4449V23.5555H19.1107V25.7773H27.9995V10.2218L19.1107 10.2218V12.4437L25.7774 12.4449Z" /> 34 | <path d="M16.8891 10.2226L19.1117 10.223V8L16.8887 8L16.8891 10.2226Z" /> 35 | <path d="M8.00049 16.8882L8.00049 19.1112H10.2235V16.8882H8.00049Z" /> 36 | <path d="M14.6673 12.4444L12.4452 12.4446V14.6665H14.6671L14.6673 12.4444Z" /> 37 | <path d="M16.8887 28H19.1117V25.777H16.8887V28Z" /> 38 | <path d="M14.6673 23.5548V25.7767L16.8887 25.777L16.8891 23.5548H14.6673Z" /> 39 | <path d="M12.4452 21.3334V23.5553L14.6673 23.5548L14.6671 21.3334H12.4452Z" /> 40 | <path d="M12.4452 14.6665L10.2238 14.6666L10.2235 16.8882L12.4457 16.8885L12.4452 14.6665Z" /> 41 | <path d="M10.2235 19.1112L10.2232 21.3332L12.4452 21.3334L12.445 19.1114L10.2235 19.1112Z" /> 42 | </> 43 | ); 44 | break; 45 | case "line": 46 | default: 47 | path = ( 48 | <path d="M9.91758 18.9131L9 17.9996L9.91354 17.086L15.7343 11.2652L16.6519 10.3477L18.479 12.1788L17.5655 13.0923L13.9517 16.7061H25.7065H27V19.2931H25.7065H13.9558L17.5695 22.9068L18.479 23.8204L16.6519 25.6515L15.7384 24.738L9.91758 18.9172V18.9131Z" /> 49 | ); 50 | break; 51 | } 52 | return ( 53 | <Icon transform={rotate[direction || "left"]} {...props}> 54 | {path} 55 | </Icon> 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /client/src/components/icons/ArrowEnclosed.tsx: -------------------------------------------------------------------------------- 1 | import { Icon, IconProps } from "../icons"; 2 | 3 | type RotateType = { 4 | [key: string]: string; 5 | }; 6 | 7 | const rotate: RotateType = { 8 | up: "rotate(0deg)", 9 | down: "rotate(180deg)", 10 | right: "rotate(90deg)", 11 | left: "rotate(270deg)", 12 | }; 13 | 14 | export interface ArrowEnclosedProps { 15 | direction?: string; 16 | variant?: "arrow" | "caret" | string; 17 | } 18 | 19 | export const ArrowEnclosed = ({ 20 | direction, 21 | variant, 22 | ...props 23 | }: ArrowEnclosedProps & IconProps) => { 24 | let path; 25 | switch (variant) { 26 | case "caret": 27 | path = ( 28 | <path d="M-1.06561e-06 18C-1.50015e-06 27.9411 8.05887 36 18 36V36C27.9411 36 36 27.9411 36 18V18C36 8.05887 27.9411 -3.52265e-07 18 -7.86805e-07V-7.86805e-07C8.05887 -1.22135e-06 -6.31067e-07 8.05887 -1.06561e-06 18V18ZM24.2771 20.0916L20.0917 20.0916L20.0917 15.908L15.9292 15.908L15.9292 20.0916L11.7667 20.0916L11.7667 24.2541L7.58308 24.2541L7.58308 20.0916L11.7667 20.0916L11.7667 15.908L15.9292 15.908L15.9292 11.7455L20.0917 11.7455L20.0917 15.908L24.2771 15.908L24.2771 20.0916L28.4395 20.0916L28.4395 24.2541L24.277 24.2541L24.2771 20.0916Z" /> 29 | ); 30 | break; 31 | case "arrow": 32 | default: 33 | path = ( 34 | <path 35 | fillRule="evenodd" 36 | clipRule="evenodd" 37 | d="M6 23.9997L8.00008 23.9997L8.00008 12.0003L6 12.0003L6 23.9997ZM8.00008 12.0003L10.0002 12.0003L10.0002 10.0002L8.00008 10.0002L8.00008 12.0003ZM10.0002 10.0002L12.0003 10.0002L12.0003 8.00008L10.0002 8.00008L10.0002 10.0002ZM12.0003 8.00008L23.9997 8.00008L23.9997 6L12.0003 6L12.0003 8.00008ZM23.9997 8.00008L23.9997 10.0002L25.9998 10.0002L25.9998 8.00008L23.9997 8.00008ZM25.9998 10.0002L25.9998 12.0003L27.9999 12.0003L27.9999 10.0002L25.9998 10.0002ZM27.9999 12.0003L27.9999 23.9997L30 23.9997L30 12.0003L27.9999 12.0003ZM27.9999 23.9997L25.9998 23.9997L25.9998 25.9998L27.9999 25.9998L27.9999 23.9997ZM25.9998 25.9998L23.9997 25.9998L23.9997 27.9999L25.9998 27.9999L25.9998 25.9998ZM23.9997 27.9999L12.0003 27.9999L12.0003 30L23.9997 30L23.9997 27.9999ZM12.0003 27.9999L12.0003 25.9998L10.0002 25.9998L10.0002 27.9999L12.0003 27.9999ZM10.0002 25.9998L10.0002 23.9997L8.00008 23.9997L8.00008 25.9998L10.0002 25.9998ZM12.0003 18.0004L15.9994 18.0004L15.9994 23.9998L19.9994 23.9998L19.9994 18.0004L23.9995 18.0004L23.9995 16.0003L21.9994 16.0003L21.9994 14.0002L19.9993 14.0002L19.9993 12.0002L15.9993 12.0002L15.9993 14.0002L14.0001 14.0002L14.0001 16.0003L12 16.0003L12.0003 18.0004Z" 38 | /> 39 | ); 40 | break; 41 | } 42 | return ( 43 | <Icon transform={rotate[direction || "up"]} {...props}> 44 | {path} 45 | </Icon> 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /client/src/components/icons/ArrowInput.tsx: -------------------------------------------------------------------------------- 1 | import { Icon, IconProps } from "."; 2 | import { Box } from "@chakra-ui/react"; 3 | 4 | type RotateType = { 5 | [key: string]: string; 6 | }; 7 | 8 | const rotate: RotateType = { 9 | up: "rotate(0deg)", 10 | down: "rotate(180deg)", 11 | right: "rotate(90deg)", 12 | left: "rotate(270deg)", 13 | }; 14 | 15 | export interface ArrowInputProps { 16 | direction?: string; 17 | disabled?: boolean; 18 | } 19 | 20 | export const ArrowInput = ({ 21 | direction, 22 | disabled, 23 | ...props 24 | }: ArrowInputProps & IconProps) => { 25 | return ( 26 | <Icon 27 | transform={rotate[direction || "up"]} 28 | role="group" 29 | pointerEvents={disabled ? "none" : "auto"} 30 | {...props} 31 | > 32 | <> 33 | <Box 34 | as="path" 35 | d="M12.4681 0.516602V3.51535H6.47065V6.5141H3.47189V12.5116H0.473145V24.5216H3.47189V30.5191H6.47065V33.5178H12.4681V36.5166H24.4781V33.5178H30.4756V30.5191H33.4744V24.5216H36.4731V12.5116H33.4744V6.5141H30.4756V3.51535H24.4781V0.516602H12.4681Z" 36 | fill={disabled ? "neon.700" : "neon.600"} 37 | _groupHover={{ 38 | fill: "neon.500", 39 | }} 40 | /> 41 | <Box 42 | as="path" 43 | d="M9.47668 22.1602H12.058V19.5916H14.6266V17.0231H17.1951V14.4546H19.7508V17.0231H22.3193V19.5916H24.8878V22.1602H27.4692V19.5788H24.9007V17.0103H22.3321V14.4417H19.7636V11.8732H17.1823V14.4417H14.6137V17.0103H12.0452V19.5788H9.47668V22.1602Z" 44 | fill={disabled ? "neon.500" : "neon.200"} 45 | /> 46 | </> 47 | </Icon> 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /client/src/components/icons/BorderImage.tsx: -------------------------------------------------------------------------------- 1 | // This icon is used by button and card layerstyle only 2 | const BorderImage = ({ 3 | color, 4 | isPressed, 5 | }: { 6 | color: string; 7 | isPressed: boolean; 8 | }) => { 9 | const path = isPressed 10 | ? "<path d='M2,2h2v2H2V2ZM4,0h2V2h-2V0Zm6,4h2v2h-2v-2ZM0,4H2v2H0v-2ZM6,0h2V2h-2V0Zm2,2h2v2h-2V2Zm0,6h2v2h-2v-2Zm-2,2h2v2h-2v-2ZM0,6H2v2H0v-2Zm10,0h2v2h-2v-2Zm-6,4h2v2h-2v-2Zm-2-2h2v2H2v-2Z'/>" 11 | : "<path d='M2,2h2v2H2V2ZM4,0h2V2h-2V0Zm6,4h2v2h-2v-2ZM0,4H2v2H0v-2ZM6,0h2V2h-2V0Zm2,2h2v2h-2V2Zm0,6h2v2h-2v-2Zm-2,2h2v2h-2v-2ZM0,6H2v2H0v-2Zm10,0h2v2h-2v-2Zm-6,4h2v2h-2v-2Zm-2-2h2v2H2v-2Zm6-2h2v2h-2v-2Zm-2,2h2v2h-2v-2Zm2-4h2v2h-2v-2Zm-4,4h2v2h-2v-2Zm2-2h2v2h-2v-2Z'/>"; 12 | 13 | return `<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='${encodeURIComponent( 14 | color, 15 | )}' >${path}</svg>`; 16 | }; 17 | 18 | export default BorderImage; 19 | -------------------------------------------------------------------------------- /client/src/components/icons/BorderImagePixelated.tsx: -------------------------------------------------------------------------------- 1 | // This icon is used by ... 2 | 3 | // no space allowed in svg content !!! 4 | const BorderImagePixelated = ({ color }: { color: string }) => { 5 | return `<svg xmlns='http://www.w3.org/2000/svg' width='16' height='17' fill='${encodeURIComponent( 6 | color 7 | )}'><path d='M8 8.5V16.5H10.0075V14.5025H12.005V12.505H14.0025V10.5075H16V8.5H8Z' /><path d='M8 8.5L16 8.5V6.49251L14.0025 6.49251V4.49501L12.005 4.49501V2.4975L10.0075 2.4975V0.5L8 0.5L8 8.5Z' /><path d='M8 8.5L8 0.5L5.99251 0.5V2.4975L3.99501 2.4975L3.99501 4.49501H1.9975L1.9975 6.49251H1.755e-07L0 8.5L8 8.5Z' /><path d='M8 8.5L1.755e-07 8.5L0 10.5075L1.9975 10.5075L1.9975 12.505H3.99501V14.5025L5.99251 14.5025V16.5H8L8 8.5Z'/></svg>`; 8 | }; 9 | 10 | export default BorderImagePixelated; 11 | -------------------------------------------------------------------------------- /client/src/components/icons/index.tsx: -------------------------------------------------------------------------------- 1 | import { Icon as ChakraIcon, IconProps as ChakraIconProps } from "@chakra-ui/react"; 2 | import { ThemingProps } from "@chakra-ui/styled-system"; 3 | import React from "react"; 4 | 5 | export interface IconProps extends ChakraIconProps, ThemingProps {} 6 | 7 | export const Icon = ({ 8 | children, 9 | ...rest 10 | }: { children: React.ReactElement<SVGPathElement> } & IconProps) => { 11 | return ( 12 | <ChakraIcon viewBox="0 0 36 36" fill="currentColor" {...rest}> 13 | {children} 14 | </ChakraIcon> 15 | ); 16 | }; 17 | 18 | export * from "./Arrow"; 19 | export * from "./ArrowEnclosed"; 20 | export * from "./ArrowInput"; 21 | -------------------------------------------------------------------------------- /client/src/dojo/abi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "function", 4 | "name": "component", 5 | "inputs": [ 6 | { 7 | "name": "name", 8 | "type": "core::felt252" 9 | } 10 | ], 11 | "outputs": [ 12 | { 13 | "type": "core::starknet::class_hash::ClassHash" 14 | } 15 | ], 16 | "state_mutability": "view" 17 | }, 18 | { 19 | "type": "function", 20 | "name": "register_component", 21 | "inputs": [ 22 | { 23 | "name": "class_hash", 24 | "type": "core::starknet::class_hash::ClassHash" 25 | } 26 | ], 27 | "outputs": [], 28 | "state_mutability": "external" 29 | }, 30 | { 31 | "type": "function", 32 | "name": "system", 33 | "inputs": [ 34 | { 35 | "name": "name", 36 | "type": "core::felt252" 37 | } 38 | ], 39 | "outputs": [ 40 | { 41 | "type": "core::starknet::class_hash::ClassHash" 42 | } 43 | ], 44 | "state_mutability": "view" 45 | }, 46 | { 47 | "type": "function", 48 | "name": "register_system", 49 | "inputs": [ 50 | { 51 | "name": "class_hash", 52 | "type": "core::starknet::class_hash::ClassHash" 53 | } 54 | ], 55 | "outputs": [], 56 | "state_mutability": "external" 57 | }, 58 | { 59 | "type": "function", 60 | "name": "uuid", 61 | "inputs": [], 62 | "outputs": [ 63 | { 64 | "type": "core::integer::u32" 65 | } 66 | ], 67 | "state_mutability": "view" 68 | }, 69 | { 70 | "type": "function", 71 | "name": "emit", 72 | "inputs": [ 73 | { 74 | "name": "keys", 75 | "type": "core::array::Span::<core::felt252>" 76 | }, 77 | { 78 | "name": "values", 79 | "type": "core::array::Span::<core::felt252>" 80 | } 81 | ], 82 | "outputs": [], 83 | "state_mutability": "view" 84 | }, 85 | { 86 | "type": "function", 87 | "name": "execute", 88 | "inputs": [ 89 | { 90 | "name": "system", 91 | "type": "core::felt252" 92 | }, 93 | { 94 | "name": "calldata", 95 | "type": "core::array::Array::<core::felt252>" 96 | } 97 | ], 98 | "outputs": [ 99 | { 100 | "type": "core::array::Array::<core::felt252>" 101 | } 102 | ], 103 | "state_mutability": "view" 104 | }, 105 | { 106 | "type": "function", 107 | "name": "entity", 108 | "inputs": [ 109 | { 110 | "name": "component", 111 | "type": "core::felt252" 112 | }, 113 | { 114 | "name": "query", 115 | "type": "dojo::database::query::Query" 116 | }, 117 | { 118 | "name": "offset", 119 | "type": "core::integer::u8" 120 | }, 121 | { 122 | "name": "length", 123 | "type": "core::integer::u32" 124 | } 125 | ], 126 | "outputs": [ 127 | { 128 | "type": "core::array::Span::<core::felt252>" 129 | } 130 | ], 131 | "state_mutability": "view" 132 | }, 133 | { 134 | "type": "function", 135 | "name": "set_entity", 136 | "inputs": [ 137 | { 138 | "name": "component", 139 | "type": "core::felt252" 140 | }, 141 | { 142 | "name": "query", 143 | "type": "dojo::database::query::Query" 144 | }, 145 | { 146 | "name": "offset", 147 | "type": "core::integer::u8" 148 | }, 149 | { 150 | "name": "value", 151 | "type": "core::array::Span::<core::felt252>" 152 | } 153 | ], 154 | "outputs": [], 155 | "state_mutability": "external" 156 | }, 157 | { 158 | "type": "function", 159 | "name": "entities", 160 | "inputs": [ 161 | { 162 | "name": "component", 163 | "type": "core::felt252" 164 | }, 165 | { 166 | "name": "partition", 167 | "type": "core::felt252" 168 | }, 169 | { 170 | "name": "length", 171 | "type": "core::integer::u32" 172 | } 173 | ], 174 | "outputs": [ 175 | { 176 | "type": "(core::array::Span::<core::felt252>, core::array::Span::<core::array::Span::<core::felt252>>)" 177 | } 178 | ], 179 | "state_mutability": "view" 180 | }, 181 | { 182 | "type": "function", 183 | "name": "set_executor", 184 | "inputs": [ 185 | { 186 | "name": "contract_address", 187 | "type": "core::starknet::contract_address::ContractAddress" 188 | } 189 | ], 190 | "outputs": [], 191 | "state_mutability": "external" 192 | }, 193 | { 194 | "type": "function", 195 | "name": "executor", 196 | "inputs": [], 197 | "outputs": [ 198 | { 199 | "type": "core::starknet::contract_address::ContractAddress" 200 | } 201 | ], 202 | "state_mutability": "view" 203 | }, 204 | { 205 | "type": "function", 206 | "name": "delete_entity", 207 | "inputs": [ 208 | { 209 | "name": "component", 210 | "type": "core::felt252" 211 | }, 212 | { 213 | "name": "query", 214 | "type": "dojo::database::query::Query" 215 | } 216 | ], 217 | "outputs": [], 218 | "state_mutability": "external" 219 | }, 220 | { 221 | "type": "function", 222 | "name": "origin", 223 | "inputs": [], 224 | "outputs": [ 225 | { 226 | "type": "core::starknet::contract_address::ContractAddress" 227 | } 228 | ], 229 | "state_mutability": "view" 230 | }, 231 | { 232 | "type": "function", 233 | "name": "is_owner", 234 | "inputs": [ 235 | { 236 | "name": "account", 237 | "type": "core::starknet::contract_address::ContractAddress" 238 | }, 239 | { 240 | "name": "target", 241 | "type": "core::felt252" 242 | } 243 | ], 244 | "outputs": [ 245 | { 246 | "type": "core::bool" 247 | } 248 | ], 249 | "state_mutability": "view" 250 | }, 251 | { 252 | "type": "function", 253 | "name": "grant_owner", 254 | "inputs": [ 255 | { 256 | "name": "account", 257 | "type": "core::starknet::contract_address::ContractAddress" 258 | }, 259 | { 260 | "name": "target", 261 | "type": "core::felt252" 262 | } 263 | ], 264 | "outputs": [], 265 | "state_mutability": "external" 266 | }, 267 | { 268 | "type": "function", 269 | "name": "revoke_owner", 270 | "inputs": [ 271 | { 272 | "name": "account", 273 | "type": "core::starknet::contract_address::ContractAddress" 274 | }, 275 | { 276 | "name": "target", 277 | "type": "core::felt252" 278 | } 279 | ], 280 | "outputs": [], 281 | "state_mutability": "external" 282 | }, 283 | { 284 | "type": "function", 285 | "name": "is_writer", 286 | "inputs": [ 287 | { 288 | "name": "component", 289 | "type": "core::felt252" 290 | }, 291 | { 292 | "name": "system", 293 | "type": "core::felt252" 294 | } 295 | ], 296 | "outputs": [ 297 | { 298 | "type": "core::bool" 299 | } 300 | ], 301 | "state_mutability": "view" 302 | }, 303 | { 304 | "type": "function", 305 | "name": "grant_writer", 306 | "inputs": [ 307 | { 308 | "name": "component", 309 | "type": "core::felt252" 310 | }, 311 | { 312 | "name": "system", 313 | "type": "core::felt252" 314 | } 315 | ], 316 | "outputs": [], 317 | "state_mutability": "external" 318 | }, 319 | { 320 | "type": "function", 321 | "name": "revoke_writer", 322 | "inputs": [ 323 | { 324 | "name": "component", 325 | "type": "core::felt252" 326 | }, 327 | { 328 | "name": "system", 329 | "type": "core::felt252" 330 | } 331 | ], 332 | "outputs": [], 333 | "state_mutability": "external" 334 | } 335 | ] 336 | -------------------------------------------------------------------------------- /client/src/dojo/setupNetwork.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Account, 3 | RpcProvider, 4 | num, 5 | InvokeFunctionResponse, 6 | Provider, 7 | Contract, 8 | } from "starknet"; 9 | 10 | import abi from "./abi.json"; 11 | 12 | export const KATANA_ACCOUNT_1_ADDRESS = 13 | "0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0"; 14 | export const KATANA_ACCOUNT_1_PRIVATEKEY = 15 | "0x0300001800000000300000180000000000030000000000003006001800006600"; 16 | export const WORLD_ADDRESS = 17 | "0x37789dc51b4d31948b9994f92fdfd72c800002e1823d96e3d77df437659d08f"; 18 | export const EVENT_KEY = 19 | "0x1a2f334228cee715f1f0f54053bb6b5eac54fa336e0bc1aacf7516decb0471d"; 20 | 21 | export const KATANA_RPC = "http://localhost:5050"; 22 | 23 | export type SetupNetworkResult = Awaited<ReturnType<typeof setupNetwork>>; 24 | 25 | export function setupNetwork() { 26 | const provider = new RpcProvider({ 27 | nodeUrl: KATANA_RPC, 28 | }); 29 | 30 | const signer = new Account( 31 | provider, 32 | KATANA_ACCOUNT_1_ADDRESS, 33 | KATANA_ACCOUNT_1_PRIVATEKEY 34 | ); 35 | 36 | return { 37 | provider, 38 | signer, 39 | execute: async (system: string, call_data: num.BigNumberish[]) => 40 | execute(signer, system, call_data), 41 | call_execute: async (call_data: any[]) => call_execute(provider, call_data), 42 | call: async (selector: string, call_data: any[]) => 43 | call(provider, selector, call_data), 44 | }; 45 | } 46 | 47 | async function execute( 48 | account: Account, 49 | system: string, 50 | call_data: num.BigNumberish[] 51 | ): Promise<InvokeFunctionResponse> { 52 | const nonce = await account?.getNonce(); 53 | const call = await account?.execute( 54 | { 55 | contractAddress: WORLD_ADDRESS, 56 | entrypoint: "execute", 57 | calldata: [strTofelt252Felt(system), call_data.length, ...call_data], 58 | }, 59 | undefined, 60 | { 61 | nonce: nonce, 62 | maxFee: 0, 63 | } 64 | ); 65 | return call; 66 | } 67 | 68 | function call_execute(provider: RpcProvider, call_data: any[]) { 69 | return new Contract(abi, WORLD_ADDRESS, provider).call("execute", call_data); 70 | } 71 | 72 | function call(provider: RpcProvider, selector: string, call_data: any[]) { 73 | return new Contract(abi, WORLD_ADDRESS, provider).call(selector, call_data); 74 | } 75 | 76 | export function strTofelt252Felt(str: string): string { 77 | const encoder = new TextEncoder(); 78 | const strB = encoder.encode(str); 79 | return BigInt( 80 | strB.reduce((memo, byte) => { 81 | memo += byte.toString(16); 82 | return memo; 83 | }, "0x") 84 | ).toString(); 85 | } 86 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 7 | line-height: 1.5; 8 | font-weight: 400; 9 | 10 | color-scheme: light dark; 11 | color: rgba(255, 255, 255, 0.87); 12 | background-color: #172217; 13 | 14 | font-synthesis: none; 15 | text-rendering: optimizeLegibility; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | -webkit-text-size-adjust: 100%; 19 | } 20 | 21 | a { 22 | font-weight: 500; 23 | text-decoration: inherit; 24 | } 25 | a:hover { 26 | color: #535bf2; 27 | } 28 | 29 | body { 30 | /* margin: 0; 31 | display: flex; 32 | place-items: center; 33 | min-width: 320px; 34 | min-height: 100vh; */ 35 | } 36 | 37 | h1 { 38 | font-size: 3.2em; 39 | line-height: 1.1; 40 | } 41 | 42 | button { 43 | border-radius: 8px; 44 | border: 1px solid transparent; 45 | padding: 0.6em 1.2em; 46 | font-size: 1em; 47 | font-weight: 500; 48 | font-family: inherit; 49 | background-color: #1a1a1a; 50 | cursor: pointer; 51 | transition: border-color 0.25s; 52 | } 53 | 54 | @media (prefers-color-scheme: light) { 55 | :root { 56 | color: #213547; 57 | background-color: #ffffff; 58 | } 59 | a:hover { 60 | color: #747bff; 61 | } 62 | button { 63 | background-color: #f9f9f9; 64 | } 65 | } 66 | 67 | @font-face { 68 | font-family: "dos-vga"; 69 | font-weight: 400; 70 | font-style: normal; 71 | src: url("/fonts/DOS_VGA.ttf"); 72 | } 73 | 74 | @font-face { 75 | font-family: "pixel-script"; 76 | font-weight: 400; 77 | font-style: normal; 78 | src: url("/fonts/PixelscriptPro.ttf"); 79 | } 80 | 81 | @font-face { 82 | font-family: "broken-console"; 83 | font-weight: 400; 84 | font-style: normal; 85 | src: url("/fonts/broken-console-broken-console-regular-400.ttf"); 86 | } 87 | 88 | @font-face { 89 | font-family: "ppmondwest"; 90 | font-weight: 400; 91 | font-style: normal; 92 | src: url("/fonts/PPMondwest-Regular.otf") format("opentype"); 93 | } 94 | 95 | @font-face { 96 | font-family: "chicago-flf"; 97 | font-weight: 500; 98 | font-style: normal; 99 | src: url("/fonts/ChicagoFLF.ttf"); 100 | } 101 | -------------------------------------------------------------------------------- /client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.tsx"; 4 | import "./index.css"; 5 | 6 | import { ChakraProvider } from "@chakra-ui/react"; 7 | import theme from "./theme/index.ts"; 8 | 9 | ReactDOM.createRoot(document.getElementById("root")!).render( 10 | <React.StrictMode> 11 | <ChakraProvider theme={theme}> 12 | <App /> 13 | </ChakraProvider> 14 | </React.StrictMode> 15 | ); 16 | -------------------------------------------------------------------------------- /client/src/theme/colors.ts: -------------------------------------------------------------------------------- 1 | export type ColorsType = { 2 | [key: string | number]: string | ColorsType; 3 | }; 4 | 5 | const colors: ColorsType = { 6 | neon: { 7 | 200: "#11ED83", 8 | 300: "#16C973", 9 | 500: "#157342", 10 | 600: "#1F422A", 11 | 700: "#202F20", 12 | 800: "#1C291C", 13 | 900: "#172217", 14 | }, 15 | yellow: { 16 | 400: "#FBCB4A", 17 | }, 18 | black: "#000000", 19 | white: "#FFFFFF", 20 | whiteAlpha: { 21 | 100: "rgba(255, 255, 255, 0.03)", 22 | }, 23 | }; 24 | 25 | export default colors; 26 | -------------------------------------------------------------------------------- /client/src/theme/components/button.tsx: -------------------------------------------------------------------------------- 1 | import BorderImage from "../../components/icons/BorderImage"; 2 | import { generatePixelBorderPath } from "../../utils/ui"; 3 | import { ComponentStyleConfig } from "@chakra-ui/react"; 4 | 5 | import colors from "../colors"; 6 | 7 | export const Button: ComponentStyleConfig = { 8 | defaultProps: { 9 | variant: "primary", 10 | }, 11 | baseStyle: { 12 | fontWeight: "400", 13 | textTransform: "uppercase", 14 | position: "relative", 15 | borderStyle: "solid", 16 | borderWidth: "2px", 17 | borderImageSlice: "4", 18 | borderImageWidth: "4px", 19 | px: "40px", 20 | gap: "10px", 21 | bgColor: "neon.900", 22 | transition: "none", 23 | _active: { 24 | top: "2px", 25 | left: "2px", 26 | }, 27 | _disabled: { 28 | pointerEvents: "none", 29 | }, 30 | }, 31 | variants: { 32 | primary: { 33 | color: "neon.200", 34 | borderImageSource: `url("data:image/svg+xml,${BorderImage({ 35 | color: colors.neon["200"].toString(), 36 | isPressed: false, 37 | })}")`, 38 | _hover: { 39 | color: "neon.300", 40 | borderImageSource: `url("data:image/svg+xml,${BorderImage({ 41 | color: colors.neon["300"].toString(), 42 | isPressed: false, 43 | })}")`, 44 | }, 45 | _active: { 46 | borderImageSource: `url("data:image/svg+xml,${BorderImage({ 47 | color: colors.neon["300"].toString(), 48 | isPressed: true, 49 | })}")`, 50 | }, 51 | }, 52 | pixelated: { 53 | border: 0, 54 | bg: "neon.700", 55 | lineHeight: "1em", 56 | clipPath: `polygon(${generatePixelBorderPath()})`, 57 | _hover: { 58 | bg: "neon.600", 59 | }, 60 | }, 61 | default: {}, 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /client/src/theme/components/card.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentMultiStyleConfig } from "@chakra-ui/theme"; 2 | import { cardStyle, cardPixelatedStyle } from "../styles"; 3 | 4 | export const Card: ComponentMultiStyleConfig = { 5 | parts: ["container", "header", "body", "footer"], 6 | baseStyle: { 7 | container: { 8 | ...cardStyle, 9 | }, 10 | header: { 11 | textAlign: "center", 12 | }, 13 | body: { 14 | display: "flex", 15 | justifyContent: "center", 16 | alignContent: "center", 17 | }, 18 | footer: {}, 19 | }, 20 | variants: { 21 | primary: { 22 | header: { 23 | py: "12px", 24 | }, 25 | body: { 26 | p: "0", 27 | }, 28 | footer: { 29 | px: "20px", 30 | py: "12px", 31 | }, 32 | }, 33 | pixelated: { 34 | container: { 35 | ...cardPixelatedStyle({ 36 | color: "neon.700", 37 | }), 38 | }, 39 | // body: { 40 | // bg: "neon.700", 41 | // }, 42 | }, 43 | }, 44 | defaultProps: { 45 | variant: "primary", 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /client/src/theme/components/container.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentStyleConfig } from "@chakra-ui/theme"; 2 | 3 | export const Container: ComponentStyleConfig = { 4 | baseStyle: { 5 | maxWidth: "1240px", 6 | padding: "12px", 7 | h: "100vh", 8 | justifyContent: "center", 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /client/src/theme/components/divider.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentStyleConfig } from "@chakra-ui/react"; 2 | 3 | export const Divider: ComponentStyleConfig = { 4 | baseStyle: { 5 | borderWidth: "2px", 6 | borderColor: "neon.600", 7 | opacity: "1", 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /client/src/theme/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./button"; 2 | export * from "./card"; 3 | export * from "./container"; 4 | export * from "./slider"; 5 | export * from "./input"; 6 | export * from "./divider"; 7 | -------------------------------------------------------------------------------- /client/src/theme/components/input.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentStyleConfig } from "@chakra-ui/theme"; 2 | import { StyleFunctionProps } from "@chakra-ui/theme-tools"; 3 | 4 | export const Input: ComponentStyleConfig = { 5 | variants: { 6 | primary: (props: StyleFunctionProps) => ({ 7 | field: { 8 | px: "0", 9 | color: "neon.200", 10 | borderColor: "none", 11 | bgColor: "transparent", 12 | fontSize: "16px", 13 | _placeholder: { 14 | color: "neon.500", 15 | }, 16 | }, 17 | }), 18 | caret: (props: StyleFunctionProps) => ({ 19 | field: {}, 20 | }), 21 | }, 22 | defaultProps: { 23 | variant: "primary", 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /client/src/theme/components/slider.tsx: -------------------------------------------------------------------------------- 1 | import { cardPixelatedStyleOutset } from "../styles"; 2 | 3 | import { sliderAnatomy } from "@chakra-ui/anatomy"; 4 | import { createMultiStyleConfigHelpers } from "@chakra-ui/react"; 5 | 6 | import { generatePixelBorderPath } from "../../utils/ui"; 7 | 8 | const { definePartsStyle, defineMultiStyleConfig } = createMultiStyleConfigHelpers( 9 | sliderAnatomy.keys 10 | ); 11 | 12 | const baseStyle = definePartsStyle({ 13 | container: { 14 | // this will style the Slider component 15 | }, 16 | track: { 17 | // this will style the SliderTrack component 18 | height: "3px", 19 | //...cardPixelatedStyle({radius:2}), 20 | ...cardPixelatedStyleOutset({ borderImageWidth: 6, color: "#202F20" }), 21 | }, 22 | thumb: { 23 | // this will style the SliderThumb component 24 | height: "23px", 25 | width: "23px", 26 | bg: "neon.200", 27 | }, 28 | filledTrack: { 29 | // this will style the SliderFilledTrack component 30 | bg: "neon.700", 31 | height: "3px", 32 | borderRadius: 0, 33 | clipPath: `polygon(${generatePixelBorderPath(2, 2)})`, 34 | }, 35 | mark: { 36 | // this will style the SliderMark component 37 | }, 38 | }); 39 | // export the base styles in the component theme 40 | export const Slider = defineMultiStyleConfig({ baseStyle }); 41 | -------------------------------------------------------------------------------- /client/src/theme/index.ts: -------------------------------------------------------------------------------- 1 | // 1. Import `extendTheme` 2 | import { extendTheme } from "@chakra-ui/react"; 3 | import * as Components from "./components"; 4 | import colors from "./colors"; 5 | import { styles, textStyles, layerStyles } from "./styles"; 6 | 7 | // 2. Call `extendTheme` and pass your custom values 8 | const theme = extendTheme({ 9 | colors, 10 | styles, 11 | textStyles, 12 | layerStyles, 13 | components: { 14 | ...Components, 15 | }, 16 | }); 17 | 18 | export default theme; 19 | -------------------------------------------------------------------------------- /client/src/theme/styles.ts: -------------------------------------------------------------------------------- 1 | import BorderImage from "../components/icons/BorderImage"; 2 | import BorderImagePixelated from "../components/icons/BorderImagePixelated"; 3 | 4 | import { generatePixelBorderPath } from "../utils/ui"; 5 | 6 | export const styles = { 7 | global: { 8 | body: { 9 | height: "100vh", 10 | bgColor: "neon.900", 11 | color: "neon.200", 12 | letterSpacing: "0.04em", 13 | WebkitTapHighlightColor: "transparent", 14 | }, 15 | }, 16 | }; 17 | 18 | // applied layerStyles below and also chakra's Card component 19 | export const cardStyle = { 20 | position: "relative", 21 | color: "neon.200", 22 | bgColor: "none", 23 | borderStyle: "solid", 24 | borderWidth: "2px", 25 | borderImageSlice: "4", 26 | borderImageWidth: "4px", 27 | borderImageSource: `url("data:image/svg+xml,${BorderImage({ 28 | color: "#157342", 29 | isPressed: true, 30 | })}")`, 31 | }; 32 | 33 | // use clipPath to "cut" corners 34 | export const cardPixelatedStyle = ({ 35 | color = "#11ED83", 36 | pixelSize = 4, 37 | radius = 4, 38 | }: { 39 | color?: string; 40 | pixelSize?: number; 41 | radius?: number; 42 | }) => ({ 43 | w: "full", 44 | bg: color, 45 | borderWidth: "0", 46 | borderRadius: "0", 47 | 48 | borderImageSource: "none", 49 | _hover: { 50 | borderImageSource: `none`, 51 | }, 52 | _active: { 53 | top: 0, 54 | left: 0, 55 | borderImageSource: `none`, 56 | }, 57 | clipPath: `polygon(${generatePixelBorderPath(radius, pixelSize)})`, 58 | }); 59 | 60 | // use borderImage & borderImageOutset to display border with outset 61 | export const cardPixelatedStyleOutset = ({ 62 | color = "#11ED83", 63 | borderImageWidth = 8, 64 | }: { 65 | color?: string; 66 | borderImageWidth?: number; 67 | }) => ({ 68 | w: "full", 69 | bg: color, 70 | borderWidth: "0", 71 | borderRadius: "0", 72 | borderImageWidth: `${borderImageWidth}px`, 73 | borderImageOutset: `${borderImageWidth}px`, 74 | borderImageSlice: 7, 75 | 76 | borderImageSource: `url("data:image/svg+xml,${BorderImagePixelated({ 77 | color, 78 | })}")`, 79 | 80 | _hover: { 81 | borderImageSource: `url("data:image/svg+xml,${BorderImagePixelated({ 82 | color, 83 | })}")`, 84 | }, 85 | _active: { 86 | top: 0, 87 | left: 0, 88 | borderImageSource: `url("data:image/svg+xml,${BorderImagePixelated({ 89 | color, 90 | })}")`, 91 | }, 92 | }); 93 | 94 | //layer styles 95 | export const layerStyles = { 96 | card: cardStyle, 97 | rounded: { 98 | p: "6px", 99 | borderRadius: "6px", 100 | bgColor: "neon.700", 101 | }, 102 | fill: { 103 | position: "absolute", 104 | top: "0", 105 | left: "0", 106 | boxSize: "full", 107 | }, 108 | }; 109 | 110 | //text styles 111 | export const textStyles = { 112 | "upper-bold": { 113 | fontWeight: "700", 114 | textTransform: "uppercase", 115 | }, 116 | subheading: { 117 | textTransform: "uppercase", 118 | fontFamily: "broken-console", 119 | letterSpacing: "0.25em", 120 | }, 121 | }; 122 | -------------------------------------------------------------------------------- /client/src/types/components.ts: -------------------------------------------------------------------------------- 1 | import math from "../utils/math"; 2 | 3 | export type Lander = { 4 | last_update: string; 5 | position_x: string; 6 | position_y: string; 7 | velocity_x: string; 8 | velocity_y: string; 9 | angle: string; 10 | fuel: number; 11 | }; 12 | 13 | export function parseRawCalldataAsLander(calldata: string[]): Lander { 14 | 15 | console.log(calldata[11]) 16 | return { 17 | last_update: BigInt(calldata[1]).toString(), 18 | position_x: parseMagAndSign(calldata[2], calldata[3]), 19 | position_y: parseMagAndSign(calldata[4], calldata[5]), 20 | velocity_x: parseMagAndSign(calldata[6], calldata[7]), 21 | velocity_y: parseMagAndSign(calldata[8], calldata[9]), 22 | angle: parseMagAndSign(calldata[10], calldata[11]), 23 | fuel: parseInt(calldata[12]) , 24 | }; 25 | } 26 | 27 | function parseMagAndSign(magValue: string, signValue: string) { 28 | const sign = BigInt(signValue) === BigInt(0) ? "" : "-"; 29 | return math.fromFixed(`${sign}${magValue}`).toFixed(2); 30 | } 31 | `` -------------------------------------------------------------------------------- /client/src/utils/math.ts: -------------------------------------------------------------------------------- 1 | export const FIXED_SIZE = 42535295865117307932921825928971026432n; // 2^125 2 | export const ONE = 2305843009213693952n; // 2^61 3 | export const PRIME = 3618502788666131213697322783095070105623107215331596699973092056135872020481n; 4 | export const PRIME_HALF = PRIME / 2n; 5 | 6 | // Converts to a felt representation 7 | export const toFelt = (num: string) => BigInt(num); 8 | 9 | // Converts to Cairo 64.61 representation 10 | export const toFixed = (num: string) => { 11 | const res = BigInt(num) * ONE; 12 | if (res > FIXED_SIZE || res <= FIXED_SIZE * -1n) throw new Error('Number is out of valid range') 13 | return toFelt(res.toString()); 14 | }; 15 | 16 | // Negative values are returned by Starknet so no need to wrap 17 | export const fromFixed = (num: string) => { 18 | let res = BigInt(num); 19 | res = res > PRIME_HALF ? res - PRIME : res; 20 | const int = Number(res / ONE); 21 | const frac = Number(res % ONE) / Number(ONE); 22 | return int + frac; 23 | } 24 | 25 | export default { 26 | FIXED_SIZE, 27 | ONE, 28 | PRIME, 29 | PRIME_HALF, 30 | toFelt, 31 | toFixed, 32 | fromFixed 33 | }; -------------------------------------------------------------------------------- /client/src/utils/ui.ts: -------------------------------------------------------------------------------- 1 | type Point = { 2 | x: number; 3 | y: number; 4 | }; 5 | // from https://pixelcorners.lukeb.co.uk/ 6 | export function generatePixelBorderPath(radius = 4, pixelSize = 4) { 7 | const points = generatePoints(radius, pixelSize); 8 | const flipped = flipCoords(points); 9 | 10 | return generatePath(flipped); 11 | } 12 | 13 | function generateInnerPath( 14 | radius: number, 15 | pixelSize: number, 16 | offset: number, 17 | reverse = false 18 | ) { 19 | const points = generatePoints(radius, pixelSize); 20 | const inset = 21 | offset < radius 22 | ? insetCoords(points, pixelSize, offset) 23 | : generatePoints(2, pixelSize, offset); 24 | const flipped = flipCoords(inset); 25 | const corners = addCorners(flipped); 26 | 27 | return generatePath(corners, reverse); 28 | } 29 | 30 | function generatePath(coords: Point[], reverse = false) { 31 | const mirroredCoords = mirrorCoords(coords); 32 | 33 | return (reverse ? mirroredCoords : mirroredCoords.reverse()) 34 | .map((point) => { 35 | return `${point.x} ${point.y}`; 36 | }) 37 | .join(",\n "); 38 | } 39 | 40 | function generatePoints(radius: number, pixelSize: number, offset = 0) { 41 | const coords = []; 42 | 43 | const lastCoords = { 44 | x: -1, 45 | y: -1, 46 | }; 47 | 48 | for (let i = 270; i > 225; i--) { 49 | const x = 50 | Math.floor(radius * Math.sin((2 * Math.PI * i) / 360) + radius + 0.5) * 51 | pixelSize; 52 | const y = 53 | Math.floor(radius * Math.cos((2 * Math.PI * i) / 360) + radius + 0.5) * 54 | pixelSize; 55 | 56 | if (x !== lastCoords.x || y !== lastCoords.y) { 57 | lastCoords.x = x; 58 | lastCoords.y = y; 59 | 60 | coords.push({ 61 | x: x + offset * pixelSize, 62 | y: y + offset * pixelSize, 63 | }); 64 | } 65 | } 66 | 67 | const mergedCoords = mergeCoords(coords); 68 | const corners = addCorners(mergedCoords); 69 | 70 | return corners; 71 | } 72 | 73 | function flipCoords(coords: Point[]) { 74 | return [...coords, ...coords.map(({ x, y }) => ({ x: y, y: x })).reverse()].filter( 75 | ({ x, y }, i, arr) => { 76 | return !i || arr[i - 1].x !== x || arr[i - 1].y !== y; 77 | } 78 | ); 79 | } 80 | 81 | function insetCoords(coords: Point[], pixelSize: number, offset: number) { 82 | return coords 83 | .map(({ x, y }) => ({ 84 | x: x + pixelSize * offset, 85 | y: y + pixelSize * Math.floor(offset / 2), 86 | })) 87 | .reduce((ret: Point[], item) => { 88 | if (ret.length > 0 && ret[ret.length - 1].x === ret[ret.length - 1].y) { 89 | return ret; 90 | } 91 | 92 | ret.push(item); 93 | 94 | return ret; 95 | }, []); 96 | } 97 | 98 | function mergeCoords(coords: Point[]) { 99 | return coords.reduce((result: Point[], point: Point, index: number) => { 100 | if (index !== coords.length - 1 && point.x === 0 && coords[index + 1].x === 0) { 101 | return result; 102 | } 103 | 104 | if (index !== 0 && point.y === 0 && coords[index - 1].y === 0) { 105 | return result; 106 | } 107 | 108 | if ( 109 | index !== 0 && 110 | index !== coords.length - 1 && 111 | point.x === coords[index - 1].x && 112 | point.x === coords[index + 1].x 113 | ) { 114 | return result; 115 | } 116 | 117 | result.push(point); 118 | return result; 119 | }, []); 120 | } 121 | 122 | function addCorners(coords: Point[]) { 123 | return coords.reduce((result: Point[], point: Point, i: number) => { 124 | result.push(point); 125 | 126 | if ( 127 | coords.length > 1 && 128 | i < coords.length - 1 && 129 | coords[i + 1].x !== point.x && 130 | coords[i + 1].y !== point.y 131 | ) { 132 | result.push({ 133 | x: coords[i + 1].x, 134 | y: point.y, 135 | }); 136 | } 137 | 138 | return result; 139 | }, []); 140 | } 141 | 142 | function mirrorCoords(coords: Point[], offset = 0) { 143 | return [ 144 | ...coords.map(({ x, y }) => ({ 145 | x: offset ? `${x + offset}px` : `${x}px`, 146 | y: offset ? `${y + offset}px` : `${y}px`, 147 | })), 148 | ...coords.map(({ x, y }) => ({ 149 | x: edgeCoord(y, offset), 150 | y: offset ? `${x + offset}px` : `${x}px`, 151 | })), 152 | ...coords.map(({ x, y }) => ({ 153 | x: edgeCoord(x, offset), 154 | y: edgeCoord(y, offset), 155 | })), 156 | ...coords.map(({ x, y }) => ({ 157 | x: offset ? `${y + offset}px` : `${y}px`, 158 | y: edgeCoord(x, offset), 159 | })), 160 | ]; 161 | } 162 | 163 | function edgeCoord(n: number, offset: number) { 164 | if (offset) { 165 | return n === 0 ? `calc(100% - ${offset}px)` : `calc(100% - ${offset + n}px)`; 166 | } 167 | 168 | return n === 0 ? "100%" : `calc(100% - ${n}px)`; 169 | } 170 | 171 | export function formatQuantity(quantity: number): string { 172 | return Intl.NumberFormat("en-US", { 173 | notation: "compact", 174 | maximumFractionDigits: 1, 175 | }).format(quantity); 176 | } 177 | 178 | export function formatCash(cash: number): string { 179 | return Intl.NumberFormat("en-US", { 180 | style: "currency", 181 | currency: "USD", 182 | maximumFractionDigits: 0, 183 | }).format(cash); 184 | } 185 | -------------------------------------------------------------------------------- /client/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="vite/client" /> 2 | -------------------------------------------------------------------------------- /client/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | colors: { 6 | primary: "#11ED83", 7 | "primary-muted": "#157342", 8 | background: "#172217", 9 | brown: "#887837", 10 | accent: "#FBCB4A", 11 | alert: "#FB744A", 12 | secondary: "#202F20", 13 | }, 14 | extend: { 15 | fontFamily: { 16 | body: `'dos-vga', san-serif`, 17 | heading: `'ppmondwest', san-serif`, 18 | }, 19 | }, 20 | }, 21 | plugins: [], 22 | }; 23 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["**/*.ts", "**/*.tsx"], 27 | "references": [{ "path": "./tsconfig.node.json" }] 28 | } 29 | -------------------------------------------------------------------------------- /client/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /contracts/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /contracts/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Dojo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /contracts/Scarb.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | cairo-version = "2.0.1" 3 | name = "stark_lander" 4 | version = "0.1.0" 5 | 6 | [cairo] 7 | sierra-replace-ids = true 8 | 9 | [dependencies] 10 | # Locking to latest commit (so future builds work even with incompatible changes in cubit) 11 | cubit = { git = "https://github.com/influenceth/cubit", rev = "f888156" } 12 | dojo = { git = "https://github.com/dojoengine/dojo" } 13 | lander_math = { path = "./lander" } 14 | 15 | [[target.dojo]] 16 | 17 | [tool.dojo] 18 | initializer_class_hash = "0xbeef" 19 | 20 | [tool.dojo.env] 21 | account_address = "0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0" 22 | private_key = "0x0300001800000000300000180000000000030000000000003006001800006600" 23 | rpc_url = "http://localhost:5050/" 24 | # world_address = "0x37789dc51b4d31948b9994f92fdfd72c800002e1823d96e3d77df437659d08f" 25 | 26 | # keystore_path = "../keystore.json" 27 | # keystore_password = "password" 28 | -------------------------------------------------------------------------------- /contracts/lander/Scarb.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lander_math" 3 | version = "0.1.0" 4 | 5 | [dependencies] 6 | cubit = { git = "https://github.com/influenceth/cubit", rev = "f888156" } 7 | -------------------------------------------------------------------------------- /contracts/lander/readme.md: -------------------------------------------------------------------------------- 1 | ## Stark Lander 2 | 3 | Game loop 4 | 1. Random x,y location on new game 5 | 2. Input thrust and angle on each action 6 | 3. Compute position according to block and tick forward at constant rate 7 | 4. Determine if lander arrives at surface of planet at the correct angle and correct speed -------------------------------------------------------------------------------- /contracts/lander/src/lib.cairo: -------------------------------------------------------------------------------- 1 | mod math; -------------------------------------------------------------------------------- /contracts/lander/src/math.cairo: -------------------------------------------------------------------------------- 1 | use debug::PrintTrait; 2 | use cubit::f128::types::fixed::{ 3 | Fixed, FixedTrait, FixedAdd, FixedSub, FixedMul, FixedDiv, ONE_u128 4 | }; 5 | use cubit::f128::types::vec2::{Vec2, Vec2Trait, Vec2Add, Vec2Sub, Vec2Mul, Vec2Div}; 6 | use cubit::f128::math::trig::{cos, sin, PI_u128}; 7 | use traits::{TryInto, Into}; 8 | 9 | // Constants 10 | const GRAVITY: u128 = 10; // Gravity force 11 | const THRUST_FORCE: u128 = 1; // Thrust force applied on each update 12 | const LANDER_WIDTH: u32 = 100; // Width of the lander 13 | const LANDER_HEIGHT: u32 = 100; // Height of the lander 14 | const FUEL_CONSUMPTION_RATE: u128 = 10; // Fuel consumption rate per second 15 | 16 | #[derive(Copy, Drop)] 17 | struct LanderMath { 18 | position: Vec2, 19 | velocity: Vec2, 20 | angle: Fixed, 21 | fuel: Fixed 22 | } 23 | 24 | fn deg_to_rad(theta_deg: Fixed) -> Fixed { 25 | let pi = FixedTrait::new(PI_u128, false); 26 | let one_eighty = FixedTrait::new(180 * ONE_u128, false); 27 | theta_deg * pi / one_eighty 28 | } 29 | 30 | fn gravity(gravity: u128) -> Vec2 { 31 | Vec2Trait::new(FixedTrait::new(0, false), FixedTrait::new_unscaled(gravity, true)) 32 | } 33 | 34 | #[generate_trait] 35 | impl ImplLanderMath of ILanderMath { 36 | fn new(position: Vec2, velocity: Vec2, angle: Fixed, fuel: Fixed) -> LanderMath { 37 | LanderMath { position, velocity, angle, fuel } 38 | } 39 | 40 | // adjusts the lander's position, velocity, and fuel based on the thrust and angle 41 | fn burn( 42 | ref self: LanderMath, thrust: Fixed, angle_deg: Fixed, delta_time: Fixed 43 | ) -> LanderMath { 44 | let angle = deg_to_rad(angle_deg); 45 | 46 | // Update gravity ----------------------------- 47 | 48 | let gravity_force = gravity(GRAVITY); 49 | 50 | // Update force ----------------------------- 51 | 52 | let thrust_force = Vec2Trait::new(thrust * cos(angle), thrust * sin(angle)); 53 | let total_force = gravity_force + thrust_force; 54 | 55 | // Update velocity ----------------------------- 56 | 57 | let old_velocity = self.velocity; 58 | let delta_velocity = Vec2Trait::new(total_force.x * delta_time, total_force.y * delta_time); 59 | self.velocity = old_velocity + delta_velocity; 60 | 61 | // Update position ----------------------------- 62 | 63 | let two = FixedTrait::new(2 * ONE_u128, false); 64 | let avg_velocity = Vec2Trait::new( 65 | (old_velocity.x + self.velocity.x) / two, (old_velocity.y + self.velocity.y) / two 66 | ); 67 | let delta_position = Vec2Trait::new( 68 | avg_velocity.x * delta_time, avg_velocity.y * delta_time 69 | ); 70 | self.position = self.position + delta_position; 71 | 72 | // Update fuel ----------------------------- 73 | 74 | let fuel_consumption = FixedTrait::new_unscaled(FUEL_CONSUMPTION_RATE, false); 75 | 76 | self.fuel -= fuel_consumption * delta_time; 77 | 78 | // self.fuel.mag.print(); 79 | // self.fuel.sign.print(); 80 | 81 | // overflow fuel check 82 | if (self.fuel.sign == true) { 83 | self.fuel = FixedTrait::new(0, false); 84 | } 85 | 86 | self.angle = angle_deg; 87 | 88 | self 89 | } 90 | 91 | // returns the lander's position at the given time 92 | fn position(ref self: LanderMath, delta_time: Fixed) -> LanderMath { 93 | // Update gravity ----------------------------- 94 | let gravity_force = gravity(GRAVITY); 95 | 96 | // Update force ----------------------------- 97 | let total_force = gravity_force; 98 | 99 | // Update velocity ----------------------------- 100 | let old_velocity = self.velocity; 101 | let delta_velocity = Vec2Trait::new(total_force.x * delta_time, total_force.y * delta_time); 102 | self.velocity = old_velocity + delta_velocity; 103 | 104 | // Update position ----------------------------- 105 | let two = FixedTrait::new(2 * ONE_u128, false); 106 | let avg_velocity = Vec2Trait::new( 107 | (old_velocity.x + self.velocity.x) / two, (old_velocity.y + self.velocity.y) / two 108 | ); 109 | let delta_position = Vec2Trait::new( 110 | avg_velocity.x * delta_time, avg_velocity.y * delta_time 111 | ); 112 | self.position = self.position + delta_position; 113 | 114 | self 115 | } 116 | fn print(ref self: LanderMath) { 117 | self.position.x.mag.print(); 118 | self.position.x.sign.print(); 119 | self.position.y.mag.print(); 120 | self.position.y.sign.print(); 121 | 122 | self.velocity.x.mag.print(); 123 | self.velocity.x.sign.print(); 124 | self.velocity.y.mag.print(); 125 | self.velocity.y.sign.print(); 126 | 127 | self.angle.mag.print(); 128 | self.angle.sign.print(); 129 | 130 | self.fuel.mag.print(); 131 | self.fuel.sign.print(); 132 | } 133 | 134 | fn print_unscaled(ref self: LanderMath) { 135 | (self.position.x.mag / ONE_u128).print(); 136 | (self.position.x.sign).print(); 137 | (self.position.y.mag / ONE_u128).print(); 138 | self.position.y.sign.print(); 139 | 140 | (self.velocity.x.mag / ONE_u128).print(); 141 | self.velocity.x.sign.print(); 142 | (self.velocity.y.mag / ONE_u128).print(); 143 | self.velocity.y.sign.print(); 144 | 145 | (self.angle.mag / ONE_u128).print(); 146 | self.angle.sign.print(); 147 | 148 | (self.fuel.mag / ONE_u128).print(); 149 | self.fuel.sign.print(); 150 | } 151 | } 152 | 153 | #[test] 154 | #[available_gas(500000000)] 155 | fn test_update() { 156 | let position = Vec2 { 157 | x: FixedTrait::new_unscaled(1000, false), y: FixedTrait::new_unscaled(12000, false) 158 | }; 159 | 160 | let velocity = Vec2 { 161 | x: FixedTrait::new_unscaled(0, false), y: FixedTrait::new_unscaled(10, true) 162 | }; 163 | 164 | let angle = FixedTrait::new_unscaled(45, true); 165 | let fuel = FixedTrait::new_unscaled(10000, false); 166 | 167 | let mut lander = ImplLanderMath::new(position, velocity, angle, fuel); 168 | 169 | let thrust = FixedTrait::new_unscaled(10, false); 170 | let delta_time_burn = FixedTrait::new_unscaled(20, false); 171 | 172 | let angle = FixedTrait::new_unscaled(45, true); 173 | 174 | lander.burn(thrust, angle, delta_time_burn); 175 | let delta_time_position = FixedTrait::new_unscaled(10, false); 176 | lander.position(delta_time_position); 177 | lander.print_unscaled(); 178 | 179 | lander.burn(thrust, angle, delta_time_burn); 180 | let delta_time_position = FixedTrait::new_unscaled(10, false); 181 | lander.position(delta_time_position); 182 | lander.print_unscaled(); 183 | } 184 | -------------------------------------------------------------------------------- /contracts/readme.md: -------------------------------------------------------------------------------- 1 | ```console 2 | sozo execute start 3 | ``` 4 | 5 | ```console 6 | sozo execute start 7 | ``` -------------------------------------------------------------------------------- /contracts/src/components.cairo: -------------------------------------------------------------------------------- 1 | mod lander; 2 | mod fuel; -------------------------------------------------------------------------------- /contracts/src/components/fuel.cairo: -------------------------------------------------------------------------------- 1 | // TODO: Abstract out Fuel from Lander Component 2 | #[derive(Component, Copy, Drop, Serde, SerdeLen)] 3 | struct Fuel { 4 | #[key] 5 | key: felt252, 6 | gallons: u128 7 | } 8 | -------------------------------------------------------------------------------- /contracts/src/components/lander.cairo: -------------------------------------------------------------------------------- 1 | use core::debug::PrintTrait; 2 | use lander_math::math::{ImplLanderMath, ILanderMath, LanderMath}; 3 | use box::BoxTrait; 4 | use option::OptionTrait; 5 | use traits::{Into, TryInto}; 6 | use cubit::f128::types::fixed::{ 7 | Fixed, FixedTrait, FixedAdd, FixedSub, FixedMul, FixedDiv, ONE_u128 8 | }; 9 | use cubit::f128::types::vec2::{Vec2, Vec2Trait, Vec2Add, Vec2Sub, Vec2Mul, Vec2Div}; 10 | use cubit::f128::math::trig::{cos, sin, PI_u128}; 11 | 12 | // TODO: Make multiple components for the lander 13 | #[derive(Component, Copy, Drop, Serde, SerdeLen)] 14 | struct Lander { 15 | #[key] 16 | key: felt252, 17 | last_update: u64, 18 | position_x: u128, 19 | position_x_sign: bool, 20 | position_y: u128, 21 | position_y_sign: bool, 22 | velocity_x: u128, 23 | velocity_x_sign: bool, 24 | velocity_y: u128, 25 | velocity_y_sign: bool, 26 | angle: u128, 27 | angle_sign: bool, 28 | fuel: u128 29 | } 30 | 31 | #[generate_trait] 32 | impl ImplLander of LanderTrait { 33 | fn burn(ref self: Lander, thrust: Fixed, angle_deg: Fixed, delta_time: Fixed) -> Lander { 34 | let info = starknet::get_block_info().unbox(); 35 | 36 | let mut landerMath = self.to_math(); 37 | 38 | landerMath.burn(thrust, angle_deg, delta_time); 39 | 40 | Lander { 41 | key: self.key, 42 | last_update: info.block_timestamp, 43 | position_x: landerMath.position.x.mag, 44 | position_x_sign: landerMath.position.x.sign, 45 | position_y: landerMath.position.y.mag, 46 | position_y_sign: landerMath.position.y.sign, 47 | velocity_x: landerMath.velocity.x.mag, 48 | velocity_x_sign: landerMath.velocity.x.sign, 49 | velocity_y: landerMath.velocity.y.mag, 50 | velocity_y_sign: landerMath.velocity.y.sign, 51 | angle: landerMath.angle.mag, 52 | angle_sign: landerMath.angle.sign, 53 | fuel: landerMath.fuel.try_into().unwrap() 54 | } 55 | } 56 | fn position(ref self: Lander, elapsed: u64) -> Lander { 57 | let mut landerMath = self.to_math(); 58 | 59 | // we calculate the position of the lander according to the elapsed time from the library 60 | landerMath.position(FixedTrait::new_unscaled(elapsed.into(), false)); 61 | 62 | Lander { 63 | key: self.key, 64 | last_update: self.last_update, 65 | position_x: landerMath.position.x.mag, 66 | position_x_sign: landerMath.position.x.sign, 67 | position_y: landerMath.position.y.mag, 68 | position_y_sign: landerMath.position.y.sign, 69 | velocity_x: landerMath.velocity.x.mag, 70 | velocity_x_sign: landerMath.velocity.x.sign, 71 | velocity_y: landerMath.velocity.y.mag, 72 | velocity_y_sign: landerMath.velocity.y.sign, 73 | angle: self.angle, 74 | angle_sign: self.angle_sign, 75 | fuel: self.fuel 76 | } 77 | } 78 | // TODO: check if the lander is within the landing zone 79 | // TODO: check if within velocity range 80 | fn has_landed(self: @Lander, elapsed: u64) -> bool { 81 | let mut landerMath = self.to_math(); 82 | 83 | let current_position = landerMath.position(FixedTrait::new_unscaled(elapsed.into(), false)); 84 | 85 | if (current_position.position.y.sign == false) { 86 | return false; 87 | } 88 | 89 | true 90 | } 91 | fn to_math(self: @Lander) -> LanderMath { 92 | let position = Vec2 { 93 | x: FixedTrait::new(*self.position_x, *self.position_x_sign), 94 | y: FixedTrait::new(*self.position_y, *self.position_y_sign) 95 | }; 96 | 97 | let velocity = Vec2 { 98 | x: FixedTrait::new(*self.velocity_x, *self.velocity_x_sign), 99 | y: FixedTrait::new(*self.velocity_y, *self.velocity_y_sign) 100 | }; 101 | 102 | let angle = FixedTrait::new(*self.angle, *self.angle_sign); 103 | 104 | let fuel = FixedTrait::new_unscaled(*self.fuel, false); 105 | 106 | ImplLanderMath::new(position, velocity, angle, fuel) 107 | } 108 | } 109 | 110 | 111 | #[test] 112 | #[available_gas(500000000)] 113 | fn check_landed() { 114 | let position_x = FixedTrait::new_unscaled(1000, false); 115 | let position_y = FixedTrait::new_unscaled(12000, false); 116 | let velocity_x = FixedTrait::new_unscaled(1, false); 117 | let velocity_y = FixedTrait::new_unscaled(1, true); 118 | let angle = FixedTrait::new_unscaled(90, true); 119 | let fuel = FixedTrait::new_unscaled(10000, false); 120 | 121 | let mut lander = Lander { 122 | key: 0x1, 123 | last_update: 1000, 124 | position_x: position_x.mag, 125 | position_x_sign: position_x.sign, 126 | position_y: position_y.mag, 127 | position_y_sign: position_y.sign, 128 | velocity_x: velocity_x.mag, 129 | velocity_x_sign: velocity_x.sign, 130 | velocity_y: velocity_y.mag, 131 | velocity_y_sign: velocity_y.sign, 132 | angle: angle.mag, 133 | angle_sign: angle.sign, 134 | fuel: fuel.try_into().unwrap() 135 | }; 136 | 137 | // has not landed 138 | assert(!lander.has_landed(1), 'has not landed'); 139 | 140 | // has landed or exploded 141 | assert(lander.has_landed(100), 'has not landed'); 142 | } 143 | 144 | 145 | #[test] 146 | #[available_gas(500000000)] 147 | fn check_burn() { 148 | let position_x = FixedTrait::new_unscaled(1000, false); 149 | let position_y = FixedTrait::new_unscaled(12000, false); 150 | let velocity_x = FixedTrait::new_unscaled(1, false); 151 | let velocity_y = FixedTrait::new_unscaled(1, true); 152 | let angle = FixedTrait::new_unscaled(90, true); 153 | let fuel = FixedTrait::new_unscaled(10000, false); 154 | 155 | let mut lander = Lander { 156 | key: 0x1, 157 | last_update: 1000, 158 | position_x: position_x.mag, 159 | position_x_sign: position_x.sign, 160 | position_y: position_y.mag, 161 | position_y_sign: position_y.sign, 162 | velocity_x: velocity_x.mag, 163 | velocity_x_sign: velocity_x.sign, 164 | velocity_y: velocity_y.mag, 165 | velocity_y_sign: velocity_y.sign, 166 | angle: angle.mag, 167 | angle_sign: angle.sign, 168 | fuel: fuel.try_into().unwrap() 169 | }; 170 | 171 | let thrust = FixedTrait::new_unscaled(1, false); 172 | let angle = FixedTrait::new_unscaled(45, false); 173 | let delta_time = FixedTrait::new_unscaled(10, false); 174 | 175 | let burnt = lander.burn(thrust, angle, delta_time); 176 | 177 | assert(lander.fuel > burnt.fuel, 'no fuel burnt'); 178 | } 179 | -------------------------------------------------------------------------------- /contracts/src/lib.cairo: -------------------------------------------------------------------------------- 1 | mod components; 2 | mod systems; 3 | mod tests; -------------------------------------------------------------------------------- /contracts/src/systems.cairo: -------------------------------------------------------------------------------- 1 | mod burn; 2 | mod start; 3 | mod position; 4 | 5 | -------------------------------------------------------------------------------- /contracts/src/systems/burn.cairo: -------------------------------------------------------------------------------- 1 | #[system] 2 | mod burn { 3 | use core::debug::PrintTrait; 4 | use array::ArrayTrait; 5 | use box::BoxTrait; 6 | use traits::Into; 7 | 8 | use dojo::world::Context; 9 | 10 | use cubit::f128::types::fixed::{ 11 | Fixed, FixedTrait, FixedAdd, FixedSub, FixedMul, FixedDiv, ONE_u128 12 | }; 13 | 14 | use stark_lander::components::lander::{Lander, LanderTrait}; 15 | use stark_lander::components::fuel::Fuel; 16 | 17 | fn execute( 18 | ctx: Context, 19 | game_id: u32, 20 | thrust_felt: u128, 21 | angle_deg_felt: u128, 22 | angle_deg_sign: bool, 23 | delta_time_felt: u128 24 | ) { 25 | let info = starknet::get_block_info().unbox(); 26 | 27 | let player_id: felt252 = ctx.origin.into(); 28 | 29 | let key = player_id + game_id.into(); 30 | 31 | // TODO: check auth 32 | 33 | // get current state 34 | let mut lander: Lander = get!(ctx.world, key, Lander); 35 | 36 | // assert fuel 37 | assert(lander.fuel > 0, 'no fuel'); 38 | 39 | let elapsed = info.block_timestamp - lander.last_update; 40 | 41 | // since time has elapsed we need to find out where the lander actually is 42 | // then compute position based on elapsed time and last update 43 | let mut new_position = lander.position(elapsed); 44 | 45 | // convert to fixed math 46 | let thrust = FixedTrait::new_unscaled(thrust_felt, false); 47 | let angle = FixedTrait::new_unscaled(angle_deg_felt, angle_deg_sign); 48 | let delta_time = FixedTrait::new_unscaled(delta_time_felt, false); 49 | 50 | // burn!! 51 | let mut new = new_position.burn(thrust, angle, delta_time); 52 | 53 | new.fuel.print(); 54 | 55 | // save new state of Lander 56 | set!( 57 | ctx.world, 58 | (Lander { 59 | key, 60 | last_update: new.last_update, 61 | position_x: new.position_x, 62 | position_x_sign: new.position_x_sign, 63 | position_y: new.position_y, 64 | position_y_sign: new.position_y_sign, 65 | velocity_x: new.velocity_x, 66 | velocity_x_sign: new.velocity_x_sign, 67 | velocity_y: new.velocity_y, 68 | velocity_y_sign: new.velocity_y_sign, 69 | angle: new.angle, 70 | angle_sign: new.angle_sign, 71 | fuel: new.fuel 72 | }) 73 | ); 74 | 75 | return (); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /contracts/src/systems/position.cairo: -------------------------------------------------------------------------------- 1 | #[system] 2 | mod position { 3 | use core::debug::PrintTrait; 4 | use array::ArrayTrait; 5 | use box::BoxTrait; 6 | use traits::Into; 7 | use starknet::ContractAddress; 8 | 9 | use dojo::world::Context; 10 | 11 | use stark_lander::components::lander::{Lander, LanderTrait}; 12 | use stark_lander::components::fuel::Fuel; 13 | 14 | fn execute(ctx: Context, game_id: u32, player_id: ContractAddress) -> Lander { 15 | // block 16 | let info = starknet::get_block_info().unbox(); 17 | 18 | let player_id: felt252 = player_id.into(); 19 | 20 | // define query 21 | let key = player_id + game_id.into(); 22 | 23 | // get current state 24 | let mut lander: Lander = get!(ctx.world, key, Lander); 25 | 26 | // get elapsed between last update 27 | let elapsed = info.block_timestamp - lander.last_update; 28 | 29 | // compute position according to time 30 | let mut new_position = lander.position(elapsed); 31 | 32 | new_position 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /contracts/src/systems/start.cairo: -------------------------------------------------------------------------------- 1 | // start new game - game starts immediately 2 | // random velocity and position 3 | // game is based on player address and incrementing game id 4 | #[system] 5 | mod start { 6 | use array::ArrayTrait; 7 | use box::BoxTrait; 8 | use traits::Into; 9 | 10 | use dojo::world::Context; 11 | 12 | use cubit::f128::types::fixed::{Fixed, FixedTrait}; 13 | 14 | use stark_lander::components::lander::Lander; 15 | use stark_lander::components::fuel::Fuel; 16 | use debug::PrintTrait; 17 | fn execute(ctx: Context) -> (u32, felt252) { 18 | let info = starknet::get_block_info().unbox(); 19 | 20 | let player_id: felt252 = ctx.origin.into(); 21 | 22 | let game_id = ctx.world.uuid(); 23 | let key = player_id + game_id.into(); 24 | 25 | // TODO: make rando 26 | let position_x = FixedTrait::new_unscaled(1000, false); 27 | let position_y = FixedTrait::new_unscaled(120000, false); 28 | let velocity_x = FixedTrait::new_unscaled(1, false); 29 | let velocity_y = FixedTrait::new_unscaled(1, true); 30 | let angle = FixedTrait::new_unscaled(90, true); 31 | 32 | set!( 33 | ctx.world, 34 | (Lander { 35 | key, 36 | last_update: info.block_timestamp, 37 | position_x: position_x.mag, 38 | position_x_sign: position_x.sign, 39 | position_y: position_y.mag, 40 | position_y_sign: position_y.sign, 41 | velocity_x: velocity_x.mag, 42 | velocity_x_sign: velocity_x.sign, 43 | velocity_y: velocity_y.mag, 44 | velocity_y_sign: velocity_y.sign, 45 | angle: angle.mag, 46 | angle_sign: angle.sign, 47 | fuel: 10000 48 | }) 49 | ); 50 | return (game_id, player_id); 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /contracts/src/tests.cairo: -------------------------------------------------------------------------------- 1 | mod start; -------------------------------------------------------------------------------- /contracts/src/tests/start.cairo: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use traits::{Into, TryInto}; 4 | use core::result::ResultTrait; 5 | use array::{serialize_array_helper, ArrayTrait, SpanTrait}; 6 | use option::OptionTrait; 7 | use box::BoxTrait; 8 | use clone::Clone; 9 | use debug::{print, PrintTrait}; 10 | 11 | use starknet::testing; 12 | use starknet::{contract_address_const}; 13 | 14 | use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; 15 | use dojo::test_utils::spawn_test_world; 16 | 17 | use stark_lander::components::lander::{lander, Lander}; 18 | use stark_lander::systems::burn::burn; 19 | use stark_lander::systems::start::start; 20 | use stark_lander::systems::position::position; 21 | 22 | use cubit::f128::types::fixed::{Fixed, FixedTrait}; 23 | 24 | const STARTING_BLOCKTIME: u64 = 600; 25 | 26 | fn get_lander_entity(world: IWorldDispatcher, query: Array<felt252>) -> Lander { 27 | let mut raw_old_lander = world 28 | .entity('Lander'.into(), query.span(), 0, dojo::SerdeLen::<Lander>::len()); 29 | 30 | let des = world.entity('Lander', query.span(), 0, dojo::SerdeLen::<Lander>::len()); 31 | let mut _des = array::ArrayTrait::new(); 32 | array::serialize_array_helper(query.span(), ref _des); 33 | array::serialize_array_helper(des, ref _des); 34 | let mut des = array::ArrayTrait::span(@_des); 35 | option::OptionTrait::expect(serde::Serde::<Lander>::deserialize(ref des), '{deser_err_msg}') 36 | } 37 | 38 | 39 | #[test] 40 | #[available_gas(30000000)] 41 | fn test_start() { 42 | testing::set_block_timestamp(STARTING_BLOCKTIME); 43 | 44 | testing::set_caller_address(contract_address_const::<0xb0b>()); 45 | 46 | // components 47 | let mut components: Array = Default::default(); 48 | components.append(lander::TEST_CLASS_HASH); 49 | 50 | // systems 51 | let mut systems: Array = Default::default(); 52 | systems.append(burn::TEST_CLASS_HASH); 53 | systems.append(start::TEST_CLASS_HASH); 54 | systems.append(position::TEST_CLASS_HASH); 55 | 56 | // deploy executor, world and register components/systems 57 | let world = spawn_test_world(components, systems); 58 | let start_call_data: Array<felt252> = Default::default(); 59 | let mut res = world.execute('start'.into(), start_call_data); 60 | 61 | assert(res.len() > 0, 'did not spawn'); 62 | 63 | let (game_id, player_id) = serde::Serde::<(u32, felt252)>::deserialize(ref res) 64 | .expect('create deserialization failed'); 65 | 66 | let mut query: Array = Default::default(); 67 | query.append(player_id + game_id.into()); 68 | 69 | let old_lander = get_lander_entity(world, query); 70 | 71 | // old_lander.fuel.print(); 72 | 73 | // shift time forward 74 | testing::set_block_timestamp(STARTING_BLOCKTIME + 10); 75 | 76 | let mut burn_call_data: Array = ArrayTrait::<felt252>::new(); 77 | burn_call_data.append(game_id.into()); 78 | burn_call_data.append(10.into()); 79 | burn_call_data.append(0.into()); 80 | burn_call_data.append(0.into()); 81 | burn_call_data.append(5.into()); 82 | 83 | let mut res = world.execute('burn'.into(), burn_call_data); 84 | 85 | testing::set_block_timestamp(STARTING_BLOCKTIME + 10); 86 | let mut query: Array = Default::default(); 87 | query.append(player_id + game_id.into()); 88 | 89 | let new_lander = get_lander_entity(world, query); 90 | 91 | // new_lander.fuel.print(); 92 | // old_lander.fuel.print(); 93 | 94 | assert(new_lander.fuel < old_lander.fuel, 'fuel did not burn'); 95 | } 96 | } 97 | --------------------------------------------------------------------------------