├── .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 |
2 |
3 | ---
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | [](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 |
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 | onStartGame()}>
181 | Start
182 |
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 |
onResetGame()} className="mt-10">
203 | Retry
204 |
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 |
15 | Ignite
16 |
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 |
95 |
96 | {rows.map((row, i) => (
97 |
105 | ))}
106 |
107 |
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 |
27 | );
28 | break;
29 | case "outline":
30 | path = (
31 | <>
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | >
43 | );
44 | break;
45 | case "line":
46 | default:
47 | path = (
48 |
49 | );
50 | break;
51 | }
52 | return (
53 |
54 | {path}
55 |
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 |
29 | );
30 | break;
31 | case "arrow":
32 | default:
33 | path = (
34 |
39 | );
40 | break;
41 | }
42 | return (
43 |
44 | {path}
45 |
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 |
32 | <>
33 |
41 |
46 | >
47 |
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 | ? " "
11 | : " ";
12 |
13 | return `${path} `;
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 ` `;
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 } & IconProps) => {
11 | return (
12 |
13 | {children}
14 |
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::"
76 | },
77 | {
78 | "name": "values",
79 | "type": "core::array::Span::"
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::"
96 | }
97 | ],
98 | "outputs": [
99 | {
100 | "type": "core::array::Array::"
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::"
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::"
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::array::Span::>)"
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>;
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 {
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 |
11 |
12 |
13 |
14 |
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 | ///
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) -> Lander {
27 | let mut raw_old_lander = world
28 | .entity('Lander'.into(), query.span(), 0, dojo::SerdeLen::::len());
29 |
30 | let des = world.entity('Lander', query.span(), 0, dojo::SerdeLen::::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::::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 = 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::::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 |
--------------------------------------------------------------------------------