feat: init

This commit is contained in:
Morten Olsen
2023-09-06 10:56:36 +02:00
commit 1f3401a961
55 changed files with 6098 additions and 0 deletions

2
packages/algorithm/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/node_modules/
/dist/

View File

@@ -0,0 +1,4 @@
> bob-the-algorithm@ build /Users/alice/work/private/bob/packages/algorithm
> tsc --build configs/tsconfig.libs.json

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"declarationDir": "../dist/cjs/types",
"outDir": "../dist/cjs"
},
"extends": "@bob-the-algorithm/config/tsconfig.cjs.json",
"include": [
"../src/**/*"
]
}

View File

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

View File

@@ -0,0 +1,11 @@
{
"include": [],
"references": [
{
"path": "./tsconfig.cjs.json"
},
{
"path": "./tsconfig.esm.json"
}
]
}

View File

@@ -0,0 +1,27 @@
{
"devDependencies": {
"@bob-the-algorithm/config": "workspace:^",
"typescript": "^5.0.4"
},
"exports": {
".": {
"import": {
"default": "./dist/esm/index.js",
"types": "./dist/esm/types/index.d.ts"
},
"require": {
"default": "./dist/cjs/index.js",
"types": "./dist/cjs/types/index.d.ts"
}
}
},
"files": [
"dist/**/*"
],
"main": "./dist/cjs/index.js",
"name": "bob-the-algorithm",
"scripts": {
"build": "tsc --build configs/tsconfig.libs.json"
},
"types": "./dist/cjs/types/index.d.ts"
}

View File

@@ -0,0 +1,87 @@
import { Attributes, GraphNode } from '../types/node';
import { Planable } from '../types/planable';
import { PluginAttributes, Plugins } from '../types/plugin';
import { expandNode } from './expand-node';
type CalulationOptions<TPlugins extends Plugins> = {
location: string;
time: number;
planables: Planable<PluginAttributes<TPlugins>>[];
plugins: TPlugins;
heuristic?: (result: any) => boolean;
onUpdated?: (result: any) => void;
};
type CalulationResult<TAttributes extends Attributes> = {
root: GraphNode<TAttributes>;
nodes: GraphNode<TAttributes>[];
completed: GraphNode<TAttributes>[];
planables: Planable<TAttributes>[];
};
const idGen = () => {
let id = 0;
return () => {
id += 1;
return id.toString();
};
};
const calulation = async <TPlugins extends Plugins>({
location,
time,
planables,
plugins,
heuristic,
onUpdated,
}: CalulationOptions<TPlugins>): Promise<
CalulationResult<PluginAttributes<TPlugins>>
> => {
const generateId = idGen();
let exploreId = 1;
const root: GraphNode<PluginAttributes<TPlugins>> = {
id: generateId(),
type: 'root',
score: 0,
parent: null,
duration: 0,
time,
location,
exploreId: 0,
remaining: planables,
};
const nodes: GraphNode<PluginAttributes<TPlugins>>[] = [root];
const leafNodes: GraphNode<PluginAttributes<TPlugins>>[] = [root];
const completed: GraphNode<PluginAttributes<TPlugins>>[] = [];
const popHighestScore = () => {
const highestScore = Math.max(...leafNodes.map((n) => n.score));
const highestScoreNode = leafNodes.find((n) => n.score === highestScore);
if (!highestScoreNode) {
throw new Error('No highest score node');
}
leafNodes.splice(leafNodes.indexOf(highestScoreNode), 1);
return highestScoreNode;
};
while (leafNodes.length > 0) {
const node = popHighestScore();
node.exploreId = exploreId++;
const expanded = await expandNode({ node, generateId, plugins });
nodes.push(...expanded);
completed.push(...expanded.filter((n) => n.remaining.length === 0));
leafNodes.push(...expanded.filter((n) => n.remaining.length > 0));
if (heuristic && heuristic({ root, nodes, completed, planables })) {
break;
}
if (onUpdated) {
onUpdated({ root, nodes, completed, planables });
}
}
return { root, nodes, completed, planables };
};
export type { CalulationOptions, CalulationResult };
export { calulation };

View File

@@ -0,0 +1,83 @@
import { Attributes, GraphNode } from '../types/node';
import { Plugin } from '../types/plugin';
import { hasImpossible } from './is-impossible';
type ExpandOptions<TAttributes extends Attributes> = {
node: GraphNode<TAttributes>;
generateId: () => string;
plugins: Plugin[];
};
const expandNode = async <TAttributes extends Attributes>({
node,
generateId,
plugins,
}: ExpandOptions<TAttributes>): Promise<GraphNode<TAttributes>[]> => {
const isImpossible = hasImpossible({ node });
if (isImpossible) {
node.deadEnd = true;
return [];
}
const metaNodes = await Promise.all(
plugins.map(async (plugin) => {
if (!plugin.getMetaNodes) {
return [];
}
const pluginNodes = await plugin.getMetaNodes(node);
return pluginNodes.map(
(pluginNode) =>
({
...pluginNode,
parent: node.id,
exploreId: 0,
id: generateId(),
} as GraphNode<TAttributes>),
);
}),
);
const planables = node.remaining.filter((planable) => {
const hasNonPlanable = plugins.some(
(plugin) => plugin.isPlanable && !plugin.isPlanable(node, planable),
);
return !hasNonPlanable;
});
const planableNodes = planables.map<GraphNode<TAttributes>>((planable) => {
const decreased = node.remaining.map((remainingPlanable) => {
if (remainingPlanable === planable) {
return {
...remainingPlanable,
count: (remainingPlanable.count || 1) - 1,
};
}
return remainingPlanable;
});
const remaining = decreased.filter(
(remainingPlanable) => remainingPlanable.count !== 0,
);
const startTime = Math.max(
node.time + node.duration,
planable.start?.min || 0,
);
return {
...node,
type: 'planable',
exploreId: 0,
id: generateId(),
score: node.score + planable.score,
planable: planable.id,
time: startTime,
duration: planable.duration,
remaining,
completed: remaining.length === 0,
parent: node.id,
};
});
return [...planableNodes, ...metaNodes.flat()];
};
export { expandNode };

View File

@@ -0,0 +1,21 @@
import { Attributes, GraphNode } from '../types/node';
import { Planable } from '../types/planable';
type IsPlanableOptions<TAttributes extends Attributes> = {
node: GraphNode<TAttributes>;
};
const hasImpossible = <TAttributes extends Attributes>({
node,
}: IsPlanableOptions<TAttributes>): boolean => {
const impossible = node.remaining.find((planable: Planable) => {
if (planable.start) {
return planable.start.max < node.time + node.duration;
}
return false;
});
return !!impossible;
};
export { hasImpossible };

View File

@@ -0,0 +1,5 @@
export type { GraphNode } from './types/node';
export type { Planable } from './types/planable';
export { expandNode } from './algorithm/expand-node';
export { calulation, type CalulationResult } from './algorithm/calulation';
export { plugins } from './plugins/index';

View File

@@ -0,0 +1,9 @@
import { Attributes } from '../types/node';
import { Plugin } from '../types/plugin';
import { transport } from './transport';
const plugins = {
transport,
} satisfies Record<string, (...args: any[]) => Plugin<Attributes>>;
export { plugins };

View File

@@ -0,0 +1,64 @@
import { GraphNode } from '../types/node';
import { Plugin } from '../types/plugin';
type GetTravelTime = (from: string, to: string) => Promise<number>;
type TransportOptions = {
getTravelTime: GetTravelTime;
};
type TransportAttributes = {
locations?: string[];
};
const transport = ({
getTravelTime,
}: TransportOptions): Plugin<TransportAttributes> => ({
getMetaNodes: async (node) => {
const locations =
(node.type !== 'travel' &&
[
...new Set(
node.remaining
.map((planable) => planable.attributes?.locations)
.flat(),
),
]
.filter((location) => location !== node.location)
.filter(Boolean)
.map((l) => l!)) ||
[];
const travelNodes = await Promise.all(
locations.map<Promise<GraphNode<TransportAttributes>>>(
async (location) => {
const travelTime = await getTravelTime(node.location, location);
return {
...node,
type: 'travel',
planable: undefined,
location,
exploreId: 0,
score: node.score - 20,
time: node.time + node.duration,
duration: travelTime,
parent: node.id,
};
},
),
);
return travelNodes;
},
isPlanable: (node, planable) => {
if (
planable.attributes?.locations &&
!planable.attributes?.locations.includes(node.location)
) {
return false;
}
return true;
},
});
export { transport };

View File

@@ -0,0 +1,21 @@
import type { Planable } from './planable';
type Attributes = any;
type GraphNode<TAttributes extends Attributes = Attributes> = {
id: string;
type: 'root' | 'planable' | 'travel';
score: number;
location: string;
parent: string | null;
time: number;
exploreId: number;
duration: number;
planable?: string;
remaining: Planable<TAttributes>[];
deadEnd?: boolean;
completed?: boolean;
};
export type { Attributes };
export { GraphNode };

View File

@@ -0,0 +1,17 @@
import { Attributes } from './node';
type Planable<TAttributes extends Attributes = Attributes> = {
id: string;
duration: number;
score: number;
count?: number;
start?: {
min: number;
max: number;
};
attributes: TAttributes;
required?: boolean;
locations?: string[];
};
export type { Planable };

View File

@@ -0,0 +1,25 @@
import { Attributes, GraphNode } from './node';
import { Planable } from './planable';
type Plugin<TAttributes extends Attributes = Attributes> = {
getMetaNodes?: (
node: GraphNode<TAttributes>,
) => Promise<GraphNode<TAttributes>[]>;
isImpossible?: (node: GraphNode<TAttributes>) => Promise<boolean>;
isPlanable?: (
node: GraphNode<TAttributes>,
planable: Planable<TAttributes>,
) => boolean;
};
type Plugins = Plugin[];
type PluginAttributes<TPlugins extends Plugins> = {
[K in keyof TPlugins]: TPlugins[K] extends Plugin<infer TAttributes>
? TAttributes extends Attributes
? TAttributes
: never
: never;
}[number];
export type { Plugin, Plugins, PluginAttributes };

View File

@@ -0,0 +1,3 @@
{
"extends": "./configs/tsconfig.cjs.json"
}