This commit is contained in:
Morten Olsen
2023-05-19 14:51:17 +02:00
commit b574c375af
89 changed files with 11574 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
{
"name": "@shipped/playground",
"version": "1.0.0",
"description": "",
"scripts": {
"build:esm": "tsc -p tsconfig.json",
"build": "pnpm build:esm"
},
"keywords": [],
"author": "",
"license": "GPL-3.0-or-later",
"types": "./dist/esm/types/index.d.ts",
"main": "./dist/esm/index.js",
"files": [
"dist/**/*"
],
"_exports": {
".": {
"import": {
"types": "./dist/esm/types/index.d.ts",
"default": "./dist/esm/index.mjs"
},
"require": {
"types": "./dist/cjs/types/index.d.ts",
"default": "./dist/cjs/index.js"
}
}
},
"devDependencies": {
"@shipped/config": "workspace:^",
"@types/pathfinding": "^0.0.6",
"@types/react": "^18.0.28",
"@types/styled-components": "^5.1.26",
"react": "^18.2.0",
"typescript": "^5.0.4"
},
"dependencies": {
"@monaco-editor/react": "^4.5.1",
"@shipped/engine": "workspace:^",
"@shipped/fleet-map": "workspace:^",
"eventemitter3": "^5.0.1",
"pathfinding": "^0.4.18",
"styled-components": "6.0.0-rc.1"
},
"peerDependencies": {
"react": "^18.2.0"
}
}

View File

@@ -0,0 +1,123 @@
import { FC, createContext, useCallback, useEffect, useState } from "react"
import { Bridge, BridgeProvider, Connection } from "@shipped/fleet-map";
import { StateOptions, Vessel } from "@shipped/engine";
class WorkerConnection extends Connection {
#worker: Worker;
constructor(worker: Worker) {
super();
this.#worker = worker;
this.#worker.addEventListener('message', this.#onMessage);
}
#onMessage = (event: MessageEvent) => {
const { type, payload } = JSON.parse(event.data);
switch (type) {
case 'sync': {
this.emit('sync', payload);
break;
}
case 'update': {
this.emit('update', payload);
break;
}
}
}
}
type PlaygroundContextValue = {
bridge?: Bridge;
running?: boolean;
errors: string[];
run: (script: string, data?: Partial<Omit<Vessel, 'captain'>>) => void;
launch: (script: string, data?: Partial<Omit<Vessel, 'captain'>>) => void;
stop: () => void;
};
type PlaygroundProviderProps = {
children: React.ReactNode;
map?: StateOptions;
createWorker: () => Worker;
onRun?: () => void;
};
const PlaygroundContext = createContext<PlaygroundContextValue>(undefined as any);
const PlaygroundProvider: FC<PlaygroundProviderProps> = ({ children, map, onRun, createWorker }) => {
const [worker, setWorker] = useState<Worker>();
const [bridge, setBridge] = useState<Bridge>();
const [running, setRunning] = useState<boolean>(false);
const run = useCallback((script: string, data?: Partial<Omit<Vessel, 'captain'>>) => {
setRunning(false);
const run = async () => {
const nextWorker = createWorker();
nextWorker.addEventListener('message', (event) => {
const { type } = JSON.parse(event.data);
switch (type) {
case 'setup': {
setRunning(true);
nextWorker.postMessage(JSON.stringify({ type: 'run', payload: {
script,
data,
}}));
break;
}
}
});
const connection = new WorkerConnection(nextWorker);
const bridge = new Bridge(connection);
nextWorker.postMessage(JSON.stringify({ type: 'setup', payload: map }));
if (onRun) {
onRun();
}
setWorker(nextWorker);
setBridge(bridge);
}
run();
}, [map]);
const launch = useCallback((script: string, data?: Partial<Omit<Vessel, 'captain'>>) => {
const run = async () => {
if (!worker) {
return;
}
worker.postMessage(JSON.stringify({ type: 'run', payload: {
script,
data,
}}));
}
run();
}, [worker]);
const stop = useCallback(() => {
worker?.terminate();
setWorker(undefined);
}, [worker]);
useEffect(() => {
return () => {
stop();
}
}, []);
return (
<PlaygroundContext.Provider value={{
bridge,
launch,
running,
run,
stop,
errors: [],
}}>
<BridgeProvider bridge={bridge}>
{children}
</BridgeProvider>
</PlaygroundContext.Provider>
)
}
export { PlaygroundContext, PlaygroundProvider };

View File

@@ -0,0 +1,102 @@
import styled from 'styled-components';
import { FC, useEffect, useRef, useState } from "react";
import MonacoEditor from '@monaco-editor/react';
import { usePlaygroundRun } from "../hooks";
import { FleetMap } from "@shipped/fleet-map";
import { typings } from './types';
type Props = {
value: string,
onValueChange: (value: string) => void,
className?: string,
onRun?: () => void,
}
const Wrapper = styled.div`
position: relative;
`;
const EditorWrapper = styled.div`
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
`;
const Button = styled.div`
position: absolute;
bottom: 10px;
right: 10px;
z-index: 1;
color: #222;
background: #badc58;
padding: 5px 10px;
border-radius: 5px;
cursor: pointer;
`;
const Editor: FC<Props> = ({ className, value, onValueChange, onRun }) => {
const ref = useRef<HTMLDivElement>(null);
const [editor, setEditor] = useState<any>(null);
useEffect(() => {
if (!ref.current || !editor) return;
console.log('init')
const observer = new ResizeObserver(() => {
console.log('changed')
editor.layout();
});
observer.observe(ref.current);
return () => observer.disconnect();
}, [editor]);
return (
<Wrapper className={className} ref={ref}>
<EditorWrapper>
<MonacoEditor
value={value}
path="file:///src/index.tsx"
onChange={(value) => onValueChange(value || '')}
beforeMount={(monaco) => {
for (let [path, typing] of Object.entries(typings)) {
monaco.languages.typescript.typescriptDefaults.addExtraLib(
typing,
`file:///node_modules/${path}`,
)
}
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.ESNext,
allowNonTsExtensions: true,
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
module: monaco.languages.typescript.ModuleKind.CommonJS,
noEmit: true,
esModuleInterop: true,
typeRoots: ["node_modules/@types"]
});
}}
onMount={(editor) => {
setEditor(editor);
}}
theme="vs-dark"
language="typescript"
height="100%"
width="100%"
options={{
minimap: {
enabled: false
}
}}
/>
</EditorWrapper>
{onRun && (
<Button
onClick={() => onRun()}
>
Run
</Button>
)}
</Wrapper>
)
};
export { Editor };

View File

@@ -0,0 +1,41 @@
// eslint-disable @typescript-eslint/ban-ts-comment
// @ts-ignore
import pathfinding from '@types/pathfinding/index.d.ts?raw';
// @ts-ignore
import engine from '@shipped/engine/dist/esm/types/index.d.ts?raw';
// @ts-ignore
import engineVessel from '@shipped/engine/dist/esm/types/types/vessel.d.ts?raw';
// @ts-ignore
import engineCaptain from '@shipped/engine/dist/esm/types/types/captain.d.ts?raw';
// @ts-ignore
import engineMap from '@shipped/engine/dist/esm/types/types/map.d.ts?raw';
// @ts-ignore
import enginePort from '@shipped/engine/dist/esm/types/types/port.d.ts?raw';
// @ts-ignore
import engineMath from '@shipped/engine/dist/esm/types/types/math.d.ts?raw';
// @ts-ignore
import engineUtilsMath from '@shipped/engine/dist/esm/types/utils/math.d.ts?raw';
// @ts-ignore
import engineUtilsMap from '@shipped/engine/dist/esm/types/utils/map.d.ts?raw';
// @ts-ignore
import engineUtilsPort from '@shipped/engine/dist/esm/types/utils/port.d.ts?raw';
// @ts-ignore
import engineUtilsVessel from '@shipped/engine/dist/esm/types/utils/vessel.d.ts?raw';
const typings = {
pathfinding,
'@shipped/engine/types/captain.d.ts': engineCaptain,
'@shipped/engine/types/vessel.d.ts': engineVessel,
'@shipped/engine/types/map.d.ts': engineMap,
'@shipped/engine/types/port.d.ts': enginePort,
'@shipped/engine/types/math.d.ts': engineMath,
'@shipped/engine/utils/math.d.ts': engineUtilsMath,
'@shipped/engine/utils/map.d.ts': engineUtilsMap,
'@shipped/engine/utils/port.d.ts': engineUtilsPort,
'@shipped/engine/utils/vessel.d.ts': engineUtilsVessel,
'@shipped/engine/index.d.ts': engine,
}
export { typings };

View File

@@ -0,0 +1,19 @@
import { useContext } from "react"
import { PlaygroundContext } from "./context";
const usePlaygroundRun = () => {
const { run } = useContext(PlaygroundContext);
return run;
}
const usePlaygroundRunning = () => {
const { running } = useContext(PlaygroundContext);
return running;
}
const usePlaygroundLaunch = () => {
const { launch } = useContext(PlaygroundContext);
return launch;
}
export { usePlaygroundRun, usePlaygroundRunning, usePlaygroundLaunch }

View File

@@ -0,0 +1,3 @@
export { PlaygroundProvider } from './context';
export { usePlaygroundRun, usePlaygroundLaunch, usePlaygroundRunning } from './hooks';
export { Editor } from './editor';

View File

@@ -0,0 +1,100 @@
/// <reference lib="webworker" />
import PF from 'pathfinding';
import { State, calculatePrice } from '@shipped/engine';
import { transpileModule, ModuleKind } from 'typescript';
const dependencies = {
pathfinding: PF,
'@shipped/engine': {
calculatePrice,
},
};
declare const self: DedicatedWorkerGlobalScope;
let state: State;
const setup = (payload: any) => {
if (state) {
state.removeAllListeners();
}
state = new State(payload);
state.on('update', () => {
self.postMessage(JSON.stringify({ type: 'update', payload: {
delta: state.getState(),
}}));
});
self.postMessage(JSON.stringify({ type: 'sync', payload: {
state: state.getState(),
world: state.getWorld(),
}}));
self.postMessage(JSON.stringify({ type: 'setup' }));
};
let captainId = 0;
self.onmessage = (event) => {
const { type, payload } = JSON.parse(event.data)
switch (type) {
case 'setup': {
setup(payload);
const update = () => {
state.update();
setTimeout(update, 100);
}
update();
break;
}
case 'run': {
const { outputText: script } = transpileModule(payload.script, {
compilerOptions: {
module: ModuleKind.CommonJS,
esModuleInterop: true,
allowSyntheticDefaultImports: true,
},
});
const exports = {} as any;
const module = { exports };
const api = {
module,
exports,
require: (name: string) => {
const m = dependencies[name as keyof typeof dependencies];
return m;
},
};
const fn = new Function(...Object.keys(api), script);
fn(...Object.values(api));
state.addCaptain(`captain-${captainId++}`, {
command: module.exports.default || module.exports,
})
const waterTiles = state.getWorld().map.map((row, y) => row.map((tile, x) => ({ x, y, tile })).filter(({ tile }) => tile.type === 'water')).flat();
const start = waterTiles[Math.floor(Math.random() * waterTiles.length)];
state.addVessel({
captain: `captain-${captainId - 1}`,
position: {
x: start.x,
y: start.y,
},
plan: [],
data: {},
direction: 0,
power: 1,
cash: 100000,
fuel: {
current: 100000,
capacity: 100000,
},
score: {
fuelUsed: 0,
distanceTravelled: 0,
rounds: 0,
},
goods: 0,
...payload.data,
})
break;
}
}
};

View File

@@ -0,0 +1,10 @@
{
"extends": "@shipped/config/esm",
"compilerOptions": {
"outDir": "dist/esm",
"declarationDir": "./dist/esm/types"
},
"include": [
"src/**/*"
]
}