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,37 @@
{
"name": "@shipped/engine",
"version": "1.0.0",
"description": "",
"scripts": {
"build:cjs": "tsc -p tsconfig.json",
"build:esm": "tsc -p tsconfig.esm.json",
"build": "pnpm build:cjs && pnpm build:esm"
},
"keywords": [],
"author": "",
"license": "GPL-3.0-or-later",
"types": "./dist/cjs/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:^",
"typescript": "^5.0.4"
},
"dependencies": {
"eventemitter3": "^5.0.1"
}
}

View File

@@ -0,0 +1,31 @@
export {
State,
type StateOptions,
} from './state';
export type {
MapTile,
PortMapTile,
} from './types/map';
export {
Position,
} from './types/math';
export type {
Vessel
} from './types/vessel';
export type {
Port
} from './types/port';
export type {
CaptainAI,
CaptainCommand,
CaptainView,
} from './types/captain';
export {
getVesselStats,
} from './utils/vessel';
export {
createMap,
} from './utils/map';
export {
calculatePrice,
} from './utils/port';

View File

@@ -0,0 +1,190 @@
import { EventEmitter } from "eventemitter3";
import { Vessel } from "../types/vessel"
import { move } from "../utils/math";
import { getCurrentTile, getVesselTravelDelta, getVisiblePorts } from "../utils/vessel";
import { Captain, CaptainCommand } from "../types/captain";
import { Port } from "../types/port";
import { calculatePrice, findPort } from "../utils/port";
import { Effect } from "../types/effects";
import { MapTile } from "../types/map";
type StateEvents = {
update: () => void;
}
type StateOptions = {
size?: { width: number, height: number };
map?: MapTile[][];
ports?: Port[];
}
class State extends EventEmitter<StateEvents> {
#size: { width: number, height: number };
#vessels: Vessel[] = [];
#ports: Port[] = [];
#effects: Effect[] = [];
#captains: Record<string, Captain> = {};
#map: MapTile[][] = [];
constructor(options: StateOptions = {}) {
super();
this.#size = options.size || { width: 50, height: 50 };
this.#ports = options.ports || new Array(10).fill(0).map((_, i) => ({
id: `port-${i}`,
fuelPrice: 1,
amount: Math.round(Math.random() * 100),
}));
this.#map = options.map || this.#generateMap();
}
#generateMap = () => {
const map: MapTile[][] = [];
const getData = () => {
const draw = Math.random();
if (draw < 0.01 && this.#ports.length > 0) return { type: 'port', id: this.#ports[Math.floor(Math.random() * this.#ports.length)].id };
if (draw < 0.2) return { type: 'land' };
return { type: 'water' };
}
for (let x = 0; x < Math.round(this.#size.width / 1); x++) {
map[x] = [];
for (let y = 0; y < Math.round(this.#size.height / 1); y++) {
map[x][y] = {
...getData() as any,
};
}
}
return map;
}
#transact = (vessel: Vessel, amount: number) => {
if (vessel.cash + amount < 0) return false;
vessel.cash += amount;
return true;
};
#applyCommand = (vessel: Vessel, command: CaptainCommand) => {
const tile = getCurrentTile(vessel, this.#map);
switch (command.type) {
case "update-plan":
vessel.plan = command.plan;
break;
case "update-power":
vessel.power = command.power;
break;
case "fuel-up": {
const port = findPort(tile, this.#ports);
if (!port) return;
const amount = command.amount ?? vessel.fuel.capacity - vessel.fuel.current;
const cost = amount * port.fuelPrice;
if (this.#transact(vessel, -cost)) {
vessel.fuel.current += amount;
}
break;
}
case "buy": {
const port = findPort(tile, this.#ports);
if (!port) return;
if (command.amount > port.amount) return;
const cost = calculatePrice(port) * command.amount;
if (this.#transact(vessel, -cost)) {
vessel.goods += command.amount;
port.amount -= command.amount;
}
break;
}
case "sell": {
const port = findPort(tile, this.#ports);
if (!port) return;
if (command.amount > vessel.goods) return;
const cost = calculatePrice(port) * command.amount;
this.#transact(vessel, cost);
vessel.goods -= command.amount;
port.amount += command.amount;
break;
}
case 'record': {
vessel.data[command.name] = command.data;
break;
}
}
}
public addPort = (port: Port) => {
this.#ports.push(port);
this.emit("update");
}
public addVessel = (vessel: Vessel) => {
this.#vessels.push(vessel);
this.emit("update");
}
public addCaptain = (id: string, captain: Captain) => {
this.#captains[id] = captain;
this.emit("update");
}
public addEffect = (effect: Effect) => {
this.#effects.push(effect);
this.emit("update");
}
public update = () => {
for (const vessel of this.#vessels) {
const captain = this.#captains[vessel.captain];
if (!captain) continue;
const currentTile = getCurrentTile(vessel, this.#map);
if (currentTile.type === 'land') continue;
const currentPort = findPort(currentTile, this.#ports);
const visiblePorts = currentPort ? this.#ports : getVisiblePorts(vessel, this.#map, this.#ports);
const commands = captain.command({
vessel,
currentPort,
ports: visiblePorts,
size: this.#size,
map: this.#map,
});
if (commands) {
for (const command of commands) {
this.#applyCommand(vessel, command);
}
}
const next = vessel.plan?.[0];
if (next && vessel.fuel.current > 0) {
const delta = getVesselTravelDelta(vessel);
const moved = move(vessel.position, next, delta.speed);
vessel.fuel.current -= delta.fuel;
vessel.score.fuelUsed += delta.fuel;
vessel.score.distanceTravelled += moved.travelled;
vessel.position = moved.position;
vessel.direction = moved.direction;
if (moved.reached) {
vessel.plan?.shift();
}
}
vessel.score.rounds++;
}
this.emit("update");
}
public getState = () => {
return {
vessels: this.#vessels,
ports: this.#ports,
}
};
public getWorld = () => {
return {
size: this.#size,
map: this.#map,
}
}
}
export type { StateOptions };
export { State }

View File

@@ -0,0 +1,42 @@
import { MapTile } from "./map";
import { Port } from "./port";
import { Vessel } from "./vessel";
type CaptainCommandList = [{
type: 'update-plan',
plan: Vessel['plan']
}, {
type: 'update-power',
power: Vessel['power']
}, {
type: 'fuel-up',
amount?: number,
}, {
type: 'buy',
amount: number,
}, {
type: 'sell',
amount: number,
}, {
type: 'record',
name: string,
data: string,
}]
type CaptainCommand = CaptainCommandList[number]
type CaptainView = {
size: { width: number, height: number };
currentPort?: Port;
vessel: Vessel;
ports: Port[];
map: MapTile[][];
}
type CaptainAI = (view: CaptainView) => CaptainCommand[] | void;
type Captain = {
command: CaptainAI;
}
export type { Captain, CaptainCommand, CaptainView, CaptainAI }

View File

@@ -0,0 +1,8 @@
import { Position, Size } from "./math"
type Effect = {
position: Position;
size: Size;
}
export type { Effect }

View File

@@ -0,0 +1,10 @@
type PortMapTile = {
type: 'port';
id: string;
};
type MapTile = {
type: 'water' | 'land';
} | PortMapTile;
export type { MapTile, PortMapTile };

View File

@@ -0,0 +1,8 @@
type Position = {
x: number;
y: number;
};
type Size = number;
export type { Position, Size }

View File

@@ -0,0 +1,9 @@
import { Position } from "./math";
type Port = {
id: string;
fuelPrice: number;
amount: number;
}
export type { Port }

View File

@@ -0,0 +1,23 @@
import { Position } from "./math"
type Vessel = {
position: Position;
plan: Position[];
data: Record<string, string>;
direction: number;
power: number;
cash: number;
fuel: {
current: number;
capacity: number;
};
score: {
fuelUsed: number;
distanceTravelled: number;
rounds: number;
};
captain: string;
goods: number;
}
export type { Vessel }

View File

@@ -0,0 +1,51 @@
import { MapTile } from "../types/map";
import { Port } from "../types/port";
type CreateMapOptions = {
ports?: {
[key: `${number},${number}`]: {
name?: string;
};
}
}
const createMap = (width: number, height: number, options: CreateMapOptions = {}) => {
const { ports } = options;
const generatedPorts: Record<string, Port> = {};
const generatedMap = new Array(height)
.fill(0)
.map(() => new Array(width)
.fill(0)
.map(() => ({
type: 'water',
} as MapTile))
);
for (const [key, port] of Object.entries(ports ?? {})) {
const [x, y] = key.split(',').map(Number);
const id = port.name ?? `${x},${y}`;
if (!generatedPorts[id]) {
generatedPorts[id] = {
id,
fuelPrice: 1,
amount: 100,
};
}
generatedMap[y][x] = {
type: 'port',
id,
};
}
return {
map: generatedMap,
ports: Object.values(generatedPorts),
size: {
width,
height,
},
}
};
export { createMap };

View File

@@ -0,0 +1,35 @@
import { Position } from "../types/math";
const distance = (a: Position, b: Position) => {
return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
}
const direction = (a: Position, b: Position) => {
return Math.atan2(b.y - a.y, b.x - a.x);
}
const intersect = (aposition: Position, asize: number, bposition: Position, bsize: number) => {
const dist = distance(aposition, bposition);
return dist < asize + bsize;
}
const move = (a: Position, b: Position, length: number) => {
const dist = distance(a, b);
const dir = direction(a, b);
if (dist < length) return {
position: b,
direction: dir,
reached: true,
travelled: dist,
};
const x = a.x + Math.cos(dir) * Math.min(dist, length);
const y = a.y + Math.sin(dir) * Math.min(dist, length);
return {
position: { x, y },
travelled: length,
direction: dir,
reached: false,
};
}
export { distance, direction, move, intersect }

View File

@@ -0,0 +1,14 @@
import { MapTile } from "../types/map";
import { Port } from "../types/port";
const calculatePrice = (port: Port) => {
return Math.max(100 - port.amount, 1);
}
const findPort = (tile: MapTile, ports: Port[]) => {
const id = tile.type === 'port' ? tile.id : undefined;
if (!id) return;
return ports.find(p => p.id === id);
};
export { calculatePrice, findPort };

View File

@@ -0,0 +1,39 @@
import { MapTile } from "../types/map";
import { Port } from "../types/port";
import { Vessel } from "../types/vessel";
import { intersect } from "./math";
const getVesselStats = (vessel: Vessel) => {
return {
visibility: 500,
energy: 1,
power: 1,
};
};
const getCurrentTile = (vessel: Vessel, map: MapTile[][]) => {
const x = Math.max(Math.floor(vessel.position.x), 0);
const y = Math.max(Math.floor(vessel.position.y), 0);
return map[y][x];
};
const getVisiblePorts = (vessel: Vessel, map: MapTile[][], ports: Port[]) => {
const tiles = map.map((row, x) => row.filter(t => t.type === 'port').map((tile, y) => ({ tile, x, y }))).flat();
const stats = getVesselStats(vessel);
const visibleTiles = tiles.filter((port) => intersect(vessel.position, stats.visibility, { x: port.x, y: port.y }, 1));
const ids = [...new Set(visibleTiles.map(t => t.tile.type === 'port' ? t.tile.id : ''))];
return ports.filter(p => ids.includes(p.id));
};
const getVesselTravelDelta = (vessel: Vessel) => {
const stats = getVesselStats(vessel);
const speed = stats.power * vessel.power * 0.1;
const fuel = stats.energy * vessel.power;
return {
speed,
fuel,
}
}
export { getVesselStats, getVesselTravelDelta, getVisiblePorts, getCurrentTile };

View File

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

View File

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