mirror of
https://github.com/morten-olsen/bob.git
synced 2026-02-08 01:46:29 +01:00
feat: init
This commit is contained in:
2
packages/algorithm/.gitignore
vendored
Normal file
2
packages/algorithm/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/node_modules/
|
||||
/dist/
|
||||
4
packages/algorithm/.turbo/turbo-build.log
Normal file
4
packages/algorithm/.turbo/turbo-build.log
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
> bob-the-algorithm@ build /Users/alice/work/private/bob/packages/algorithm
|
||||
> tsc --build configs/tsconfig.libs.json
|
||||
|
||||
10
packages/algorithm/configs/tsconfig.cjs.json
Normal file
10
packages/algorithm/configs/tsconfig.cjs.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"declarationDir": "../dist/cjs/types",
|
||||
"outDir": "../dist/cjs"
|
||||
},
|
||||
"extends": "@bob-the-algorithm/config/tsconfig.cjs.json",
|
||||
"include": [
|
||||
"../src/**/*"
|
||||
]
|
||||
}
|
||||
10
packages/algorithm/configs/tsconfig.esm.json
Normal file
10
packages/algorithm/configs/tsconfig.esm.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"declarationDir": "../dist/esm/types",
|
||||
"outDir": "../dist/esm"
|
||||
},
|
||||
"extends": "@bob-the-algorithm/config/tsconfig.esm.json",
|
||||
"include": [
|
||||
"../src/**/*"
|
||||
]
|
||||
}
|
||||
11
packages/algorithm/configs/tsconfig.libs.json
Normal file
11
packages/algorithm/configs/tsconfig.libs.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.cjs.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.esm.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
27
packages/algorithm/package.json
Normal file
27
packages/algorithm/package.json
Normal 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"
|
||||
}
|
||||
87
packages/algorithm/src/algorithm/calulation.ts
Normal file
87
packages/algorithm/src/algorithm/calulation.ts
Normal 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 };
|
||||
83
packages/algorithm/src/algorithm/expand-node.ts
Normal file
83
packages/algorithm/src/algorithm/expand-node.ts
Normal 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 };
|
||||
21
packages/algorithm/src/algorithm/is-impossible.ts
Normal file
21
packages/algorithm/src/algorithm/is-impossible.ts
Normal 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 };
|
||||
5
packages/algorithm/src/index.ts
Normal file
5
packages/algorithm/src/index.ts
Normal 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';
|
||||
9
packages/algorithm/src/plugins/index.ts
Normal file
9
packages/algorithm/src/plugins/index.ts
Normal 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 };
|
||||
64
packages/algorithm/src/plugins/transport.ts
Normal file
64
packages/algorithm/src/plugins/transport.ts
Normal 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 };
|
||||
21
packages/algorithm/src/types/node.ts
Normal file
21
packages/algorithm/src/types/node.ts
Normal 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 };
|
||||
17
packages/algorithm/src/types/planable.ts
Normal file
17
packages/algorithm/src/types/planable.ts
Normal 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 };
|
||||
25
packages/algorithm/src/types/plugin.ts
Normal file
25
packages/algorithm/src/types/plugin.ts
Normal 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 };
|
||||
3
packages/algorithm/tsconfig.json
Normal file
3
packages/algorithm/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "./configs/tsconfig.cjs.json"
|
||||
}
|
||||
Reference in New Issue
Block a user