Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Expand Kitten Engineer Operator Library #632

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Large diffs are not rendered by default.

711 changes: 711 additions & 0 deletions packages/kitten-engineers/examples/test.gv

Large diffs are not rendered by default.

Binary file added packages/kitten-engineers/examples/test.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
134 changes: 134 additions & 0 deletions packages/kitten-engineers/source/GraphDotPrinter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { NamedPalettes, Palette } from "@oliversalzburg/js-utils/graphics/palette.js";
import { GraphInfo } from "./GraphInfo.js";
import { Operator, Solution, Solutions } from "./GraphSolver.js";
import { Pause } from "./operators/pause.js";
import { Reset } from "./operators/reset.js";
import { UnlockSolarRevolution } from "./operators/unlock-solar-revolution.js";

const render = (node: Operator, color = 0xffffffff) => {
const _hexColor = `"#${color.toString(16).padStart(8, "0")}"`;
if (node instanceof UnlockSolarRevolution) {
return `"${node.name}"`;
}
if (node instanceof Pause) {
return `"${node.name}"`;
}
if (node instanceof Reset) {
return `"${node.name}"`;
}
if (node.name.startsWith("assign")) {
return `"${node.name}"`;
}
if (node.name.startsWith("build")) {
return `"${node.name}"`;
}
if (node.name.startsWith("craft")) {
return `"${node.name}"`;
}
if (node.name.startsWith("research")) {
return `"${node.name}"`;
}
if (node.name.startsWith("take")) {
return `"${node.name}"`;
}
if (node.name.startsWith("trade")) {
return `"${node.name}"`;
}
if (node.name.startsWith("unlock")) {
return `"${node.name}"`;
}

return `"${node.name}"`;
};

export class GraphDotPrinter {
palette = new Palette(NamedPalettes.Seaside);
operatorColors = new Map<string, number>();

print(
node: Operator,
info: GraphInfo,
seen = new Set<Operator>(),
indent = 0,
dotBuffer = [
"digraph kittens {",
' label="Kitten Engineers Decision Graph"',
' fontname="Helvetica,Arial,sans-serif"',
" layout=sfdp",
" splines=true",
' edge [fontname="Helvetica,Arial,sans-serif"]',
' node [fontname="Helvetica,Arial,sans-serif"]',
" graph [overlap=prism, overlap_scaling=5, overlap_shrink=true]",
],
): Array<string> {
const limit = 150;
if (limit < indent) {
throw new Error(`Reached max recursion depth (${limit}) in graph.`);
}

if (seen.has(node)) {
return dotBuffer;
}

seen.add(node);

if (0 < node.requires.length && node.children.size === 0) {
console.warn(`${node.name} 🗲 unsolved!`);
return dotBuffer;
}

dotBuffer.push(" " + render(node, this.palette.someColor() >>> 0));

if (node.requires.length === 0) {
return dotBuffer;
}

const requirements = new Set(node.requires);
for (const child of node.children) {
this.print(child, info, seen, indent + 1, dotBuffer);
dotBuffer.push(` "${child.name}" -> "${node.name}"`);
for (const solution of child.solves) {
requirements.delete(solution);
}
}
if (0 < requirements.size) {
console.warn(
`${" ".repeat(indent)}🗲 parent was left partially unsolved: ${[...requirements.values()].join(", ")}`,
);
}

return indent === 0 ? [...dotBuffer, "}"] : dotBuffer;
}

printEx(
info: GraphInfo,
dotBuffer = [
"digraph kittens {",
' label="Kitten Engineers Decision Graph"',
' fontname="Helvetica,Arial,sans-serif"',
" layout=sfdp",
" splines=true",
' edge [fontname="Helvetica,Arial,sans-serif"]',
' node [fontname="Helvetica,Arial,sans-serif"]',
" graph [overlap=prism, overlap_scaling=5, overlap_shrink=true]",
],
): Array<string> {
for (const node of info.nodes) {
dotBuffer.push(" " + render(node, this.palette.someColor() >>> 0));
}
const edgeColors = new Map<Solution, string>(
Solutions.map(solution => [
solution,
`#${(this.palette.someColor() >>> 0).toString(16).padStart(8, "0")}`,
]),
);
const coloredEdges = info.edges.sort((a, b) => a.solution.localeCompare(b.solution));
for (const edge of coloredEdges) {
dotBuffer.push(
` "${edge.nodes[0].name}" -> "${edge.nodes[1].name}" [color="${edgeColors.get(edge.solution)}"]`,
);
}

return [...dotBuffer, "}"];
}
}
49 changes: 49 additions & 0 deletions packages/kitten-engineers/source/GraphInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Operator, Solution } from "./GraphSolver.js";

export interface GraphEdge {
nodes: [Operator, Operator];
solution: Solution;
}

export interface GraphInfo {
nodes: Array<Operator>;
edges: Array<GraphEdge>;
}

export const analyzeGraph = (root: Operator): GraphInfo => {
const collectNodes = (node: Operator, log = new Set<Operator>()): Array<Operator> => {
log.add(node);
return [
node,
...[...node.children.values()].flatMap(child =>
log.has(child) ? [] : collectNodes(child, log),
),
];
};
const collectEdges = (node: Operator, log = new Set<Operator>()): Array<GraphEdge> => {
log.add(node);
return [
...node.requires
.map((requirement): [Solution, Array<Operator>] => [
requirement,
[...node.children.values()].filter(child => child.solves.includes(requirement)),
])
.flatMap(([requirement, solutions]) => {
return solutions.map(
(solution): GraphEdge => ({
nodes: [node, solution],
solution: requirement,
}),
);
}),
...[...node.children.values()].flatMap(child =>
log.has(child) ? [] : collectEdges(child, log),
),
];
};

return {
nodes: collectNodes(root),
edges: collectEdges(root),
};
};
35 changes: 35 additions & 0 deletions packages/kitten-engineers/source/GraphJudge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { EngineState } from "@kitten-science/kitten-scientists/Engine.js";
import { Game } from "@kitten-science/kitten-scientists/types/index.js";
import { GraphInfo } from "./GraphInfo.js";
import { Operator, SnapshotCollection, Solution } from "./GraphSolver.js";

export class GraphJudge<TRoot extends Operator> {
graphRoot: TRoot;
graphInfo: GraphInfo;

constructor(graphRoot: TRoot, graphInfo: GraphInfo) {
this.graphRoot = graphRoot;
this.graphInfo = graphInfo;
}

judge(
node: TRoot | Operator = this.graphRoot,
game: Game,
state: EngineState,
snapshots: SnapshotCollection,
): number {
return node.scoreSolution(node.solves[0], this, game, state, snapshots);
}

judgeChildren(
node: Operator,
requirement: Solution,
game: Game,
state: EngineState,
snapshots: SnapshotCollection,
): Array<number> {
return [...node.children.values()]
.filter(child => child.solves.includes(requirement))
.map(child => child.scoreSolution(requirement, this, game, state, snapshots));
}
}
2 changes: 1 addition & 1 deletion packages/kitten-engineers/source/GraphPrinter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class GraphPrinter {
}
if (0 < requirements.size) {
console.warn(
`${" ".repeat(indent)}🗲 parent was left partially unsolved: ${[...requirements].join(", ")}`,
`${" ".repeat(indent)}🗲 parent was left partially unsolved: ${[...requirements.values()].join(", ")}`,
);
}
}
Expand Down
77 changes: 67 additions & 10 deletions packages/kitten-engineers/source/GraphSolver.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,51 @@
import { PayloadBuildings } from "@kitten-science/kitten-analysts/KittenAnalysts.js";
import { EngineState } from "@kitten-science/kitten-scientists/Engine.js";
import { Game } from "@kitten-science/kitten-scientists/types/game.js";
import { Buildings, Resources } from "@kitten-science/kitten-scientists/types/index.js";
import {
Buildings,
ChronoForgeUpgrades,
Jobs,
Policies,
ReligionUpgrades,
Resources,
SpaceBuildings,
StagedBuildings,
Technologies,
TranscendenceUpgrades,
Upgrades,
VoidSpaceUpgrades,
ZiggurathUpgrades,
} from "@kitten-science/kitten-scientists/types/index.js";
import { TreeNode } from "@oliversalzburg/js-utils/data/tree.js";
import { GraphJudge } from "./GraphJudge";

export const Solutions = [...Buildings, ...Resources] as const;
export const Solutions = [
...Buildings,
...ChronoForgeUpgrades,
...Jobs,
...Policies,
...ReligionUpgrades,
...Resources,
...SpaceBuildings,
...StagedBuildings,
...Technologies,
...TranscendenceUpgrades,
...Upgrades,
...VoidSpaceUpgrades,
...ZiggurathUpgrades,
"energy",
"epiphany",
"happiness",
"necrocornDeficit",
"transcendenceTier",
"worship",
] as const;
export type Solution = (typeof Solutions)[number];

export interface SnapshotCollection {
buildings: PayloadBuildings;
}

export interface Operator extends TreeNode<Operator> {
name: string;

Expand All @@ -15,22 +54,40 @@ export interface Operator extends TreeNode<Operator> {

ancestors: Set<Operator>;

calculateCost: () => number;
execute: (
game: Game,
scoreSolution: (
solution: Solution,
judge: GraphJudge<Operator>,
_game: Game,
state: EngineState,
snapshots: { buildings: PayloadBuildings },
) => EngineState;
snapshots: SnapshotCollection,
) => number;
execute: (game: Game, state: EngineState, snapshots: SnapshotCollection) => EngineState;
}

export class GraphSolver {
operators: Iterable<Operator>;
constructor(operators: Iterable<Operator>) {
threshold: number;
constructor(operators: Iterable<Operator>, threshold: number) {
this.operators = operators;
this.threshold = threshold;
}

solve(node: Operator, root: Operator = node, parents: Iterable<Operator> = []): Operator {
solve(
node: Operator,
root: Operator = node,
parents: Iterable<Operator> = [],
depth = 0,
): Operator {
if (depth > this.threshold) {
return root;
}

for (const operator of this.operators) {
// We might want to allow some operators to solve themselves,
// but it seems counter-productive for the time being.
if (operator === node) {
continue;
}
if (!operator.solves.some(solution => node.requires.includes(solution))) {
continue;
}
Expand All @@ -46,7 +103,7 @@ export class GraphSolver {
}

root.ancestors.add(operator);
this.solve(operator, root, [...parents, node]);
this.solve(operator, root, [...parents, node], depth + 1);
}

return root;
Expand Down
Loading
Loading