From 62e3e1129b9d0a69f301d62c29632b3701e7119e Mon Sep 17 00:00:00 2001 From: EehMauro Date: Wed, 13 Nov 2024 11:31:30 -0300 Subject: [PATCH] Added challenge parametrization and leaderboard real data --- backend/docs/example.sql | 21 ------ backend/docs/table.sql | 15 ---- backend/src/main.rs | 93 +++++++++++++----------- frontend/package-lock.json | 32 +++++++- frontend/package.json | 3 +- frontend/src/components/NavBar.tsx | 90 ++++++++++++++--------- frontend/src/pages/_app.tsx | 27 ++++++- frontend/src/pages/index.tsx | 27 ++++--- frontend/src/pages/leaderboard/index.tsx | 45 ++++++------ frontend/src/stores/challenge.ts | 41 +++++++++++ frontend/tailwind.config.ts | 4 +- godot-visualizer/scripts/main.gd | 21 ++++-- 12 files changed, 261 insertions(+), 158 deletions(-) delete mode 100644 backend/docs/example.sql delete mode 100644 backend/docs/table.sql create mode 100644 frontend/src/stores/challenge.ts diff --git a/backend/docs/example.sql b/backend/docs/example.sql deleted file mode 100644 index bd76ef0..0000000 --- a/backend/docs/example.sql +++ /dev/null @@ -1,21 +0,0 @@ --- Insert the central RewardPot -INSERT INTO MapObjects (id, class, positionX, positionY, totalRewards) -VALUES ('rewardpot01', 'RewardPot', 0, 0, 1000.0); - --- Ships -INSERT INTO MapObjects (id, class, positionX, positionY, fuel, shipyardPolicy, shipTokenName, pilotTokenName) -VALUES ('ship01', 'Ship', 1, 0, 100, 'policy01', 'tokenName01', 'pilotName01'), - ('ship02', 'Ship', -1, 0, 120, 'policy02', 'tokenName02', 'pilotName02'), - ('ship03', 'Ship', 0, 1, 110, 'policy03', 'tokenName03', 'pilotName03'), - ('ship04', 'Ship', 0, -1, 130, 'policy04', 'tokenName04', 'pilotName04'), - ('ship05', 'Ship', 2, 1, 140, 'policy05', 'tokenName05', 'pilotName05'), - ('ship06', 'Ship', -2, -1, 150, 'policy06', 'tokenName06', 'pilotName06'); - --- FuelPellets -INSERT INTO MapObjects (id, class, positionX, positionY, fuel) -VALUES ('fuel01', 'FuelPellet', 1, 1, 50), - ('fuel02', 'FuelPellet', 2, 0, 60), - ('fuel03', 'FuelPellet', -1, 2, 70), - ('fuel04', 'FuelPellet', -2, -2, 80), - ('fuel05', 'FuelPellet', 3, -1, 90), - ('fuel06', 'FuelPellet', -3, 1, 100); diff --git a/backend/docs/table.sql b/backend/docs/table.sql deleted file mode 100644 index cb2b387..0000000 --- a/backend/docs/table.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE TABLE MapObjects ( - id VARCHAR(255) PRIMARY KEY, -- Utxo (Hash#Index) as a unique identifier - class VARCHAR(50) NOT NULL, -- 'Ship', 'FuelPellet', 'RewardPot' - positionX INT NOT NULL, -- X coordinate - positionY INT NOT NULL, -- Y coordinate - fuel INT, -- Fuel amount, only relevant for ships and fuel pellets - shipyardPolicy VARCHAR(255), -- Policy ID relevant for all types - shipTokenName VARCHAR(255), -- Only relevant for ships - pilotTokenName VARCHAR(255), -- Only relevant for ships - totalRewards INT -- Only relevant for the reward pot -); - --- Indexes for performance optimization -CREATE INDEX idx_mapobjects_class ON MapObjects(class); -CREATE INDEX idx_mapobjects_position ON MapObjects(positionX, positionY); diff --git a/backend/src/main.rs b/backend/src/main.rs index 61d479f..0192b35 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -11,7 +11,6 @@ use async_graphql::*; use async_graphql_rocket::GraphQLRequest as Request; use async_graphql_rocket::GraphQLResponse as Response; use rocket::State; -use rand::{distributions::Alphanumeric, Rng}; use rocket::fairing::{Fairing, Info, Kind}; use rocket::http::{Header, Method, Status}; @@ -40,7 +39,6 @@ impl Fairing for CORS { struct QueryRoot; pub struct ChainParameters { - shipyard_policy_id: String, ship_address: String, fuel_address: String, asteria_address: String, @@ -48,8 +46,6 @@ pub struct ChainParameters { impl ChainParameters { pub fn from_env() -> Self { Self { - shipyard_policy_id: env::var("SHIPYARD_POLICY_ID") - .expect("SHIPYARD_POLICY_ID must be set in the environment"), ship_address: env::var("SHIP_ADDRESS") .expect("SHIP_ADDRESS must be set in the environment"), fuel_address: env::var("FUEL_ADDRESS") @@ -104,7 +100,7 @@ impl Data { self.fuels.clone()[offset..offset + limit].to_vec() } - pub fn objects_in_radius(self, center: Position, radius: i32) -> Vec { + pub fn objects_in_radius(self, center: Position, radius: i32, _shipyard_policy_id: String) -> Vec { let mut retval = Vec::new(); for ship in self.ships { @@ -252,7 +248,6 @@ pub struct LeaderboardRecord { ship_name: String, pilot_name: String, fuel: i32, - movements: i32, distance: i32, } @@ -327,6 +322,7 @@ impl QueryRoot { ctx: &Context<'_>, center: PositionInput, radius: i32, + shipyard_policy_id: String, ) -> Result, Error> { // Access the connection pool from the GraphQL context let pool = ctx @@ -413,7 +409,7 @@ impl QueryRoot { center.x, center.y, radius, - chain_parameters.shipyard_policy_id, + shipyard_policy_id, chain_parameters.ship_address, chain_parameters.fuel_address, chain_parameters.asteria_address, @@ -536,43 +532,54 @@ impl QueryRoot { async fn leaderboard( &self, - _ctx: &Context<'_>, - ) -> Vec { - let mut results = Vec::new(); - - for i in 0..100 { - let address: String = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(64) - .map(char::from) - .collect(); - - let ship_name: String = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(6) - .map(char::from) - .collect(); - - let pilot_name: String = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(6) - .map(char::from) - .collect(); - - results.push( - LeaderboardRecord { - ranking: i + 1, - address: address, - ship_name: ship_name.to_uppercase(), - pilot_name: pilot_name.to_uppercase(), - fuel: rand::thread_rng().gen_range(0..100), - movements: i * 10, - distance: i * 10 - } - ); - } + ctx: &Context<'_>, + shipyard_policy_id: String, + ) -> Result, Error> { + let pool = ctx + .data::() + .map_err(|e| Error::new(e.message))?; - results + let chain_parameters = ctx + .data::() + .map_err(|e| Error::new(e.message))?; + + let fetched_objects = sqlx::query!( + " + SELECT + id, + CAST(utxo_plutus_data(era, cbor) -> 'fields' -> 0 ->> 'int' AS INTEGER) AS fuel, + CAST(utxo_plutus_data(era, cbor) -> 'fields' -> 3 ->> 'bytes' AS TEXT) AS ship_token_name, + CAST(utxo_plutus_data(era, cbor) -> 'fields' -> 4 ->> 'bytes' AS TEXT) AS pilot_token_name, + ABS(CAST(utxo_plutus_data(era, cbor) -> 'fields' -> 1 ->> 'int' AS INTEGER)) + ABS(CAST(utxo_plutus_data(era, cbor) -> 'fields' -> 2 ->> 'int' AS INTEGER)) AS distance + FROM + utxos + WHERE + utxo_address(era, cbor) = from_bech32($2::varchar) + AND utxo_has_policy_id(era, cbor, decode($1::varchar, 'hex')) + AND spent_slot IS NULL + ORDER BY distance ASC + ", + shipyard_policy_id, + chain_parameters.ship_address, + ) + .fetch_all(pool) + .await + .map_err(|e| Error::new(e.to_string()))?; + + let map_objects: Vec = fetched_objects + .into_iter() + .enumerate() + .map(|(i, record)| LeaderboardRecord { + ranking: i as i32 + 1, + address: record.id, + ship_name: record.ship_token_name.unwrap_or_default(), + pilot_name: record.pilot_token_name.unwrap_or_default(), + fuel: record.fuel.unwrap_or(0), + distance: record.distance.unwrap_or(0) + }) + .collect(); + + Ok(map_objects) } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e2279b8..11f8c6a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,7 +17,8 @@ "next": "14.2.15", "next-mdx-remote": "^5.0.0", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "zustand": "^5.0.1" }, "devDependencies": { "@tailwindcss/forms": "^0.5.9", @@ -4817,6 +4818,35 @@ "zen-observable": "0.8.15" } }, + "node_modules/zustand": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.1.tgz", + "integrity": "sha512-pRET7Lao2z+n5R/HduXMio35TncTlSW68WsYBq2Lg1ASspsNGjpwLAsij3RpouyV6+kHMwwwzP0bZPD70/Jx/w==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index efeaa5d..4aefa4b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,7 +18,8 @@ "next": "14.2.15", "next-mdx-remote": "^5.0.0", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "zustand": "^5.0.1" }, "devDependencies": { "@tailwindcss/forms": "^0.5.9", diff --git a/frontend/src/components/NavBar.tsx b/frontend/src/components/NavBar.tsx index 0087096..72f2b03 100644 --- a/frontend/src/components/NavBar.tsx +++ b/frontend/src/components/NavBar.tsx @@ -3,50 +3,70 @@ import React from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; +import { useChallengeStore } from '@/stores/challenge'; const NavBar: React.FunctionComponent = () => { + const { challenges, selected, select } = useChallengeStore(); const pathname = usePathname() || ''; const isActive = (route: string): string => pathname.includes(route) ? 'text-[#FFF75D]' : 'text-[#F1E9D9]'; + const handleSelect = (event: React.FormEvent) => { + select(parseInt(event.currentTarget.value)); + } + return ( -
-
-
- - +
+
+
+
+ + + +
+ {pathname !== '/' && ( + + By TxPipe + + )} +
+
+ + + + + + + + + +
- {pathname !== '/' && ( - - By TxPipe - - )} -
-
- - - - - - - - - - - -
-
- +
); diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index e8500be..5e1b1ff 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -1,9 +1,11 @@ +import { useEffect } from 'react'; import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client'; import type { ReactElement, ReactNode } from 'react'; import type { NextPage } from 'next'; import type { AppProps } from 'next/app'; import NavBar from "@/components/NavBar"; +import { useChallengeStore } from '@/stores/challenge'; import "./globals.css"; @@ -21,10 +23,31 @@ const client = new ApolloClient({ }); export default function App({ Component, pageProps }: AppPropsWithLayout) { + const { select, selected } = useChallengeStore(); + + useEffect(() => { + const request = window.indexedDB.open('/userfs'); + request.onsuccess = () => { + const db = request.result; + db.transaction('FILE_DATA', 'readwrite').objectStore('FILE_DATA').put( + { + contents: new TextEncoder().encode(process.env.API_URL), + timestamp: new Date(), + mode: 33206, + }, + '/userfs/godot/app_userdata/visualizer/api_url' + ).onsuccess = () => select(0); + }; + }, []); + return ( - - + { selected !== null && ( + <> + + + + )} ); } \ No newline at end of file diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 051e7ea..b6abf3f 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -1,6 +1,9 @@ import Link from "next/link"; +import { useChallengeStore } from '@/stores/challenge'; export default function Landing() { + const { challenges, selected, select } = useChallengeStore(); + return (
@@ -81,22 +84,26 @@ export default function Landing() { Available Challenges -
- {[0,1,2].map(index => -
+
+ {challenges.slice(0, 3).map((challenge, index) => +
-

- Network | Preview +

+ Network | { challenge.network }

-

- Shipyard Policy | 00000 +

+ Shipyard Policy | { challenge.policyId }

-

- Builder fest workshop +

+ { challenge.label }

-

- Pilot: {props.record.pilotName}
- Ship: {props.record.shipName} + Pilot: {props.record.pilotName.toUpperCase().slice(-6)}
+ Ship: {props.record.shipName.toUpperCase().slice(-6)}

{`${props.record.distance}km`} @@ -66,20 +65,17 @@ const LeaderboardRow: React.FunctionComponent = (props: RecordProps - {props.record.address} + {props.record.address.replace('#0', '')} - {props.record.pilotName} + {props.record.pilotName.toUpperCase().slice(-6)} - {props.record.shipName} + {props.record.shipName.toUpperCase().slice(-6)} {props.record.fuel} - - {props.record.movements} - {`${props.record.distance}km`} @@ -89,7 +85,8 @@ const LeaderboardRow: React.FunctionComponent = (props: RecordProps ); export default function Leaderboard() { - const { loading, error, data } = useQuery(GET_LEADERBOARD_RECORDS); + const { current } = useChallengeStore(); + const { loading, error, data } = useQuery(GET_LEADERBOARD_RECORDS, { variables: { shipyardPolicyId: current().policyId } }); const [ offset, setOffset ] = useState(0); const hasNextPage = () => { @@ -127,7 +124,7 @@ export default function Leaderboard() {