mirror of
https://github.com/morten-olsen/shipped.git
synced 2026-02-07 23:26:23 +01:00
init
This commit is contained in:
48
packages/playground/package.json
Normal file
48
packages/playground/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
123
packages/playground/src/context.tsx
Normal file
123
packages/playground/src/context.tsx
Normal 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 };
|
||||
102
packages/playground/src/editor/index.tsx
Normal file
102
packages/playground/src/editor/index.tsx
Normal 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 };
|
||||
41
packages/playground/src/editor/types.ts
Normal file
41
packages/playground/src/editor/types.ts
Normal 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 };
|
||||
19
packages/playground/src/hooks.ts
Normal file
19
packages/playground/src/hooks.ts
Normal 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 }
|
||||
3
packages/playground/src/index.ts
Normal file
3
packages/playground/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { PlaygroundProvider } from './context';
|
||||
export { usePlaygroundRun, usePlaygroundLaunch, usePlaygroundRunning } from './hooks';
|
||||
export { Editor } from './editor';
|
||||
100
packages/playground/src/runner/index.ts
Normal file
100
packages/playground/src/runner/index.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
10
packages/playground/tsconfig.json
Normal file
10
packages/playground/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "@shipped/config/esm",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/esm",
|
||||
"declarationDir": "./dist/esm/types"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user