diff --git a/Cargo.lock b/Cargo.lock index 60beb03..fd6c53c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -252,6 +252,7 @@ dependencies = [ "axum", "chrono", "clap", + "petgraph 0.6.4", "prisma-client-rust", "serde", "serde_json", @@ -1250,6 +1251,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86d4de0081402f5e88cdac65c8dcdcc73118c1a7a465e2a05f0da05843a8ea33" +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.0.28" @@ -3002,10 +3009,20 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3659d1ee90221741f65dd128d9998311b0e40c5d3c23a62445938214abce4f" dependencies = [ - "fixedbitset", + "fixedbitset 0.1.9", "ordermap", ] +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset 0.4.2", + "indexmap 2.1.0", +] + [[package]] name = "phf" version = "0.11.2" @@ -3447,7 +3464,7 @@ dependencies = [ "once_cell", "opentelemetry", "parking_lot 0.12.1", - "petgraph", + "petgraph 0.4.13", "pin-utils", "prisma-models", "prisma-value", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index d112556..62f1691 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -14,3 +14,4 @@ prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust serde = { version = "1.0.193", features = ["derive"] } tokio = { version = "1.35.1", features = ["full"] } serde_json = "1.0.108" +petgraph = "0.6.4" diff --git a/backend/src/generate.rs b/backend/src/generate.rs new file mode 100644 index 0000000..766dc2c --- /dev/null +++ b/backend/src/generate.rs @@ -0,0 +1,67 @@ +use std::{collections::HashMap, f64::consts::PI}; + +use anyhow::Result; +use chrono::Utc; +use petgraph::{graph::UnGraph, graphmap::UnGraphMap}; +use triangle::{triangulate, Point, TrianglulateOpts}; + +use crate::{ + prisma::PrismaClient, + state::{GameCoreState, StarSystemCoreState, StarSystemId, UniverseId}, +}; + +pub async fn generate_initial_game_state( + client: &PrismaClient, +) -> Result { + // Generate a new triangulation + let result = { + // Generate a circle of points + let radius = 1_000.0; + const NUM_POINTS: usize = 30; + let point_list = (0..NUM_POINTS) + .map(|n| n as f64 * (2.0 * PI) / NUM_POINTS as f64) + .map(|theta| Point { + x: radius * theta.cos(), + y: radius * theta.sin(), + }); + + triangulate( + TrianglulateOpts::builder() + .point_list(point_list) + .voronoi(true) + .maximum_triangle_area(6_000.0) + .build()?, + )? + }; + + // Generate a new universe ID + let universe = client.universe().create(json!({}), vec![]).exec().await?; + + let empires = HashMap::new(); + + // Format the star systems + let mut star_systems = HashMap::new(); + for (idx, point) in result.point_list.into_iter().enumerate() { + let id = StarSystemId(idx as i32); + let star_system = StarSystemCoreState { + id, + position: (point.x, point.y), + }; + star_systems.insert(id, star_system); + } + + let mut hyperlanes = UnGraphMap::default(); + for indexes in result.triangle_list.into_iter() { + hyperlanes.add_edge(indexes[0], indexes[1], ()); + hyperlanes.add_edge(indexes[0], indexes[2], ()); + hyperlanes.add_edge(indexes[1], indexes[2], ()); + } + + Ok(GameCoreState { + universe_id: UniverseId(universe.id), + current_instant: Utc::now(), + empires, + star_systems, + hyperlanes, + }) +} diff --git a/backend/src/main.rs b/backend/src/main.rs index 8746638..d3e002e 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,22 +1,22 @@ #[macro_use] extern crate serde_json; +mod generate; #[allow(unused_imports, dead_code)] pub mod prisma; mod routes; pub mod state; -use std::{collections::HashMap, f64::consts::PI, sync::Arc}; +use std::sync::Arc; use anyhow::Result; use axum::{routing::get, Router}; use clap::{Parser, Subcommand}; use prisma::PrismaClient; -use prisma_client_rust::serde_json::json; -use routes::{universe_list, universe_map}; -use triangle::{triangulate, Point, TrianglulateOpts}; -use crate::prisma::{star_system, star_system_edges}; +use routes::{universe_list, universe_map}; + +use crate::generate::generate_initial_game_state; #[derive(Debug, Parser)] struct Opt { @@ -57,101 +57,8 @@ async fn main() -> Result<()> { } Command::Seed => { - // Generate a new triangulation - let result = { - // Generate a circle of points - let radius = 1000.0; - const NUM_POINTS: usize = 30; - let point_list = (0..NUM_POINTS) - .map(|n| n as f64 * (2.0 * PI) / NUM_POINTS as f64) - .map(|theta| Point { - x: radius * theta.cos(), - y: radius * theta.sin(), - }); - - triangulate( - TrianglulateOpts::builder() - .point_list(point_list) - .voronoi(true) - .maximum_triangle_area(4000.0) - .build()?, - )? - }; - - println!("Result: {result:?}"); - - // Insert into database - let universe = client.universe().create(json!({}), vec![]).exec().await?; - client - .star_system() - .create_many( - result - .point_list - .into_iter() - .enumerate() - .map(|(idx, point)| { - star_system::create_unchecked( - universe.id.clone(), - idx as i32, - point.x, - point.y, - vec![], - ) - }) - .collect(), - ) - .exec() - .await?; - - // Get the idx -> star system id mapping - let star_system_map = client - .star_system() - .find_many(vec![]) - .select(star_system::select!({ id gen_index })) - .exec() - .await? - .into_iter() - .map(|star_system| (star_system.gen_index as usize, star_system.id)) - .collect::>(); - - // Insert edges into database - macro_rules! create_edge { - ($from:expr, $to:expr) => {{ - println!("WTF? {}, {}", $from, $to); - star_system_edges::create_unchecked( - universe.id.clone(), - star_system_map.get(&$from).unwrap().to_owned(), - $from as i32, - star_system_map.get(&$to).unwrap().to_owned(), - $to as i32, - vec![], - ) - }}; - } - client - .star_system_edges() - .create_many( - result - .triangle_list - .into_iter() - .flat_map(|points| { - let a = points[0]; - let b = points[1]; - let c = points[2]; - [ - create_edge!(a, b), - create_edge!(a, c), - create_edge!(b, a), - create_edge!(b, c), - create_edge!(c, a), - create_edge!(c, b), - ] - }) - .collect(), - ) - .skip_duplicates() - .exec() - .await?; + let game_state = generate_initial_game_state(&client).await?; + game_state.save_to_database(&client).await?; println!("Done."); } diff --git a/backend/src/routes/mod.rs b/backend/src/routes/mod.rs index 8f8041c..f7fda66 100644 --- a/backend/src/routes/mod.rs +++ b/backend/src/routes/mod.rs @@ -7,6 +7,7 @@ use serde_json::Value; use crate::{ prisma::{star_system, star_system_edges}, + state::UniverseId, AppState, }; @@ -25,17 +26,17 @@ pub async fn universe_list(state: State) -> Json { } pub async fn universe_map( - Path((universe_id,)): Path<(String,)>, + Path((universe_id,)): Path<(i32,)>, state: State, ) -> Json { - println!("universe id {universe_id}"); + let universe_id = UniverseId(universe_id); let points = state .prisma .star_system() - .find_many(vec![star_system::universe_id::equals(universe_id.clone())]) - .order_by(star_system::gen_index::order(Direction::Asc)) - .select(star_system::select!({ id coord_x coord_y })) + .find_many(vec![star_system::universe_id::equals(universe_id.0)]) + .order_by(star_system::index::order(Direction::Asc)) + .select(star_system::select!({ index coord_x coord_y })) .exec() .await .unwrap(); @@ -43,10 +44,8 @@ pub async fn universe_map( let edges = state .prisma .star_system_edges() - .find_many(vec![star_system_edges::universe_id::equals( - universe_id.clone(), - )]) - .select(star_system_edges::select!({ from_system_id to_system_id from_index to_index })) + .find_many(vec![star_system_edges::universe_id::equals(universe_id.0)]) + .select(star_system_edges::select!({ from_system_id to_system_id })) .exec() .await .unwrap(); diff --git a/backend/src/state/game.rs b/backend/src/state/game.rs index 3b22fb8..978773d 100644 --- a/backend/src/state/game.rs +++ b/backend/src/state/game.rs @@ -1,15 +1,87 @@ use std::collections::HashMap; +use anyhow::Result; use chrono::{DateTime, Utc}; +use petgraph::{graph::UnGraph, graphmap::UnGraphMap}; -use super::{EmpireCoreState, EmpireId}; +use crate::prisma::{star_system, star_system_edges, PrismaClient}; + +use super::{EmpireCoreState, EmpireId, StarSystemCoreState, StarSystemId}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct UniverseId(pub i32); pub struct GameCoreState { - instant: DateTime, - decision_time: DateTime, - empires: HashMap, + pub universe_id: UniverseId, + pub current_instant: DateTime, + pub empires: HashMap, + pub star_systems: HashMap, + pub hyperlanes: UnGraphMap, } pub struct GameDerivedState<'a> { core: &'a GameCoreState, } + +impl GameCoreState { + pub async fn save_to_database(&self, client: &PrismaClient) -> Result<()> { + println!("Self {:?} {:?}", self.universe_id, self.star_systems); + // Save star systems to database + client + .star_system() + .create_many( + self + .star_systems + .values() + .map(|star_system| { + star_system::create_unchecked( + star_system.id.0, + self.universe_id.0, + star_system.position.0, + star_system.position.1, + vec![], + ) + }) + .collect(), + ) + .skip_duplicates() + .exec() + .await?; + + // Insert edges into database + macro_rules! create_edge { + ($from:expr, $to:expr) => {{ + println!("WTF? {}, {}", $from, $to); + star_system_edges::create_unchecked( + universe.id.clone(), + star_system_map.get(&$from).unwrap().to_owned(), + $from as i32, + star_system_map.get(&$to).unwrap().to_owned(), + $to as i32, + vec![], + ) + }}; + } + client + .star_system_edges() + .create_many( + self + .hyperlanes + .all_edges() + .map(|edge| { + star_system_edges::create_unchecked( + self.universe_id.0, + edge.0 as i32, + edge.1 as i32, + vec![], + ) + }) + .collect(), + ) + .skip_duplicates() + .exec() + .await?; + + Ok(()) + } +} diff --git a/backend/src/state/macros.rs b/backend/src/state/macros.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/src/state/macros.rs @@ -0,0 +1 @@ + diff --git a/backend/src/state/mod.rs b/backend/src/state/mod.rs index 94c1543..6902bec 100644 --- a/backend/src/state/mod.rs +++ b/backend/src/state/mod.rs @@ -1,3 +1,6 @@ +#[macro_use] +mod macros; + mod empire; mod fleet; mod game; diff --git a/backend/src/state/starsystem.rs b/backend/src/state/starsystem.rs index 7d5d096..49e2c28 100644 --- a/backend/src/state/starsystem.rs +++ b/backend/src/state/starsystem.rs @@ -1,3 +1,11 @@ -pub struct StarSystemCoreState {} +/// This is only guaranteed to be unique for a specific UniverseId +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct StarSystemId(pub i32); + +#[derive(Debug)] +pub struct StarSystemCoreState { + pub id: StarSystemId, + pub position: (f64, f64), +} pub struct StarSystemDerivedState {} diff --git a/frontend/package.json b/frontend/package.json index 8602970..ecd8526 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,11 +18,13 @@ "@blueprintjs/table": "^5.0.20", "@react-three/drei": "^9.92.7", "@react-three/fiber": "^8.15.12", + "@reduxjs/toolkit": "^2.0.1", "@types/three": "^0.160.0", "normalize.css": "^8.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-query": "^3.39.3", + "react-redux": "^9.0.4", "react-router-dom": "^6.21.1", "three": "^0.160.0" }, diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 0288fa6..f3f3281 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -20,6 +20,9 @@ dependencies: '@react-three/fiber': specifier: ^8.15.12 version: 8.15.12(react-dom@18.2.0)(react@18.2.0)(three@0.160.0) + '@reduxjs/toolkit': + specifier: ^2.0.1 + version: 2.0.1(react-redux@9.0.4)(react@18.2.0) '@types/three': specifier: ^0.160.0 version: 0.160.0 @@ -35,6 +38,9 @@ dependencies: react-query: specifier: ^3.39.3 version: 3.39.3(react-dom@18.2.0)(react@18.2.0) + react-redux: + specifier: ^9.0.4 + version: 9.0.4(@types/react@18.2.46)(react@18.2.0)(redux@5.0.1) react-router-dom: specifier: ^6.21.1 version: 6.21.1(react-dom@18.2.0)(react@18.2.0) @@ -591,6 +597,25 @@ packages: zustand: 3.7.2(react@18.2.0) dev: false + /@reduxjs/toolkit@2.0.1(react-redux@9.0.4)(react@18.2.0): + resolution: {integrity: sha512-fxIjrR9934cmS8YXIGd9e7s1XRsEU++aFc9DVNMFMRTM5Vtsg2DCRMj21eslGtDt43IUf9bJL3h5bwUlZleibA==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + dependencies: + immer: 10.0.3 + react: 18.2.0 + react-redux: 9.0.4(@types/react@18.2.46)(react@18.2.0)(redux@5.0.1) + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.0.1 + dev: false + /@remix-run/router@1.14.1: resolution: {integrity: sha512-Qg4DMQsfPNAs88rb2xkdk03N3bjK4jgX5fR24eHCTR9q6PrhZQZ4UJBPzCHJkIpTRN1UKxx2DzjZmnC+7Lj0Ow==} engines: {node: '>=14.0.0'} @@ -883,6 +908,10 @@ packages: meshoptimizer: 0.18.1 dev: false + /@types/use-sync-external-store@0.0.3: + resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} + dev: false + /@types/webxr@0.5.10: resolution: {integrity: sha512-n3u5sqXQJhf1CS68mw3Wf16FQ4cRPNBBwdYLFzq3UddiADOim1Pn3Y6PBdDilz1vOJF3ybLxJ8ZEDlLIzrOQZg==} dev: false @@ -1633,6 +1662,10 @@ packages: engines: {node: '>= 4'} dev: true + /immer@10.0.3: + resolution: {integrity: sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==} + dev: false + /immutable@4.3.4: resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==} dev: true @@ -2093,6 +2126,28 @@ packages: scheduler: 0.21.0 dev: false + /react-redux@9.0.4(@types/react@18.2.46)(react@18.2.0)(redux@5.0.1): + resolution: {integrity: sha512-9J1xh8sWO0vYq2sCxK2My/QO7MzUMRi3rpiILP/+tDr8krBHixC6JMM17fMK88+Oh3e4Ae6/sHIhNBgkUivwFA==} + peerDependencies: + '@types/react': ^18.2.25 + react: ^18.0 + react-native: '>=0.69' + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-native: + optional: true + redux: + optional: true + dependencies: + '@types/react': 18.2.46 + '@types/use-sync-external-store': 0.0.3 + react: 18.2.0 + redux: 5.0.1 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false + /react-router-dom@6.21.1(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-QCNrtjtDPwHDO+AO21MJd7yIcr41UetYt5jzaB9Y1UYaPTCnVuJq6S748g1dE11OQlCFIQg+RtAA1SEZIyiBeA==} engines: {node: '>=14.0.0'} @@ -2155,6 +2210,18 @@ packages: picomatch: 2.3.1 dev: true + /redux-thunk@3.1.0(redux@5.0.1): + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + dependencies: + redux: 5.0.1 + dev: false + + /redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + dev: false + /regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} dev: false @@ -2168,6 +2235,10 @@ packages: engines: {node: '>=0.10.0'} dev: false + /reselect@5.0.1: + resolution: {integrity: sha512-D72j2ubjgHpvuCiORWkOUxndHJrxDaSolheiz5CO+roz8ka97/4msh2E8F5qay4GawR5vzBt5MkbDHT+Rdy/Wg==} + dev: false + /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2428,6 +2499,14 @@ packages: punycode: 2.3.1 dev: true + /use-sync-external-store@1.2.0(react@18.2.0): + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + /utility-types@3.10.0: resolution: {integrity: sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==} engines: {node: '>= 4'} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ee04752..e421e4f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,15 +2,19 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; import { QueryClient, QueryClientProvider } from "react-query"; import "./App.css"; import routes from "./routes"; +import { Provider as ReduxProvider } from "react-redux"; +import { store } from "./store"; const router = createBrowserRouter(routes); const queryClient = new QueryClient(); function App() { return ( - - - + + + + + ); } diff --git a/frontend/src/Layout.tsx b/frontend/src/Layout.tsx index 6c1ec00..39cdf6f 100644 --- a/frontend/src/Layout.tsx +++ b/frontend/src/Layout.tsx @@ -1,8 +1,22 @@ import { Outlet, Link, useParams } from "react-router-dom"; -import { Alignment, Icon, Navbar } from "@blueprintjs/core"; +import { + Alignment, + Button, + Icon, + InputGroup, + Menu, + Navbar, + Popover, +} from "@blueprintjs/core"; +import { useAppSelector } from "./store"; +import EmpireSelector from "./components/EmpireSelector"; export default function Layout() { const { universeId } = useParams(); + const activeEmpireId = useAppSelector( + (state) => state.someShit.activeEmpireId, + ); + return ( <> @@ -33,6 +47,10 @@ export default function Layout() { )} + + + + diff --git a/frontend/src/components/EmpireSelector.tsx b/frontend/src/components/EmpireSelector.tsx new file mode 100644 index 0000000..26ceb6f --- /dev/null +++ b/frontend/src/components/EmpireSelector.tsx @@ -0,0 +1,27 @@ +import { Button, InputGroup, Menu, Popover } from "@blueprintjs/core"; + +export default function EmpireSelector() { + return ( + + + ... + + } + placement="bottom-end" + > +