Factor eventide graph demo data to a new module (#71)

* Factor evertide graph demo data to a new module

It would be helpful to make our standard tiny graph available to other
test and demo instances, outside of just graph.test.js. This way we can
use it as a test case for the Graph Explorer.
This commit is contained in:
Dandelion Mané 2018-03-05 20:58:47 -08:00 committed by GitHub
parent 7ea8bdd964
commit fb00c35823
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 275 additions and 230 deletions

View File

@ -3,6 +3,7 @@
import type {Address, Addressable} from "./address";
import {sortedByAddress} from "./address";
import {Graph} from "./graph";
import * as demoData from "./graphDemoData";
describe("graph", () => {
describe("#Graph", () => {
@ -13,127 +14,23 @@ describe("graph", () => {
expect(sortedByAddress(xs)).toEqual(sortedByAddress(ys));
}
// A Seafood Fruit Mix is made by cooking Mighty Bananas (picked
// from a tree) and a Razorclaw Crab (grabbed from the beach). In
// this graph, an edge from `u` to `v` means that `u` thanks `v` for
// a particular contribution. For example, the meal thanks the hero
// for cooking it, as well as thanking the bananas and the crab for
// composing it.
function makeAddress(id: string): Address {
return {
repositoryName: "sourcecred/eventide",
pluginName: "hill_cooking_pot",
id,
};
}
const heroNode = () => ({
address: makeAddress("hero_of_time#0"),
payload: {},
});
const bananasNode = () => ({
address: makeAddress("mighty_bananas#1"),
payload: {},
});
const crabNode = () => ({
address: makeAddress("razorclaw_crab#2"),
payload: {},
});
const mealNode = () => ({
address: makeAddress("seafood_fruit_mix#3"),
payload: {
effect: ["attack_power", 1],
},
});
const pickEdge = () => ({
address: makeAddress("hero_of_time#0@picks@mighty_bananas#1"),
src: bananasNode().address,
dst: heroNode().address,
payload: {},
});
const grabEdge = () => ({
address: makeAddress("hero_of_time#0@grabs@razorclaw_crab#2"),
src: crabNode().address,
dst: heroNode().address,
payload: {},
});
const cookEdge = () => ({
address: makeAddress("hero_of_time#0@cooks@seafood_fruit_mix#3"),
src: mealNode().address,
dst: heroNode().address,
payload: {
crit: false,
},
});
const bananasIngredientEdge = () => ({
address: makeAddress("mighty_bananas#1@included_in@seafood_fruit_mix#3"),
src: mealNode().address,
dst: bananasNode().address,
payload: {},
});
const crabIngredientEdge = () => ({
address: makeAddress("razorclaw_crab#2@included_in@seafood_fruit_mix#3"),
src: mealNode().address,
dst: crabNode().address,
payload: {},
});
const eatEdge = () => ({
address: makeAddress("hero_of_time#0@eats@seafood_fruit_mix#3"),
src: heroNode().address,
dst: mealNode().address,
payload: {},
});
const simpleMealGraph = () =>
new Graph()
.addNode(heroNode())
.addNode(bananasNode())
.addNode(crabNode())
.addNode(mealNode())
.addEdge(pickEdge())
.addEdge(grabEdge())
.addEdge(cookEdge())
.addEdge(bananasIngredientEdge())
.addEdge(crabIngredientEdge())
.addEdge(eatEdge());
const crabLoopEdge = () => ({
address: makeAddress("crab-self-assessment"),
src: crabNode().address,
dst: crabNode().address,
payload: {evaluation: "not effective at avoiding hero"},
});
const duplicateCookEdge = () => ({
address: makeAddress("hero_of_time#0@again_cooks@seafood_fruit_mix#3"),
src: mealNode().address,
dst: heroNode().address,
payload: {
crit: true,
saveScummed: true,
},
});
const advancedMealGraph = () =>
simpleMealGraph()
.addEdge(crabLoopEdge())
.addEdge(duplicateCookEdge());
describe("construction", () => {
it("works for a simple graph", () => {
simpleMealGraph();
demoData.simpleMealGraph();
});
it("works for an advanced graph", () => {
advancedMealGraph();
demoData.advancedMealGraph();
});
it("forbids adding an edge with dangling `dst`", () => {
expect(() => {
simpleMealGraph().addEdge({
address: makeAddress(
demoData.simpleMealGraph().addEdge({
address: demoData.makeAddress(
"treasure_octorok#5@helps_cook@seafood_fruit_mix#3"
),
src: mealNode().address,
dst: makeAddress("treasure_octorok#5"),
src: demoData.mealNode().address,
dst: demoData.makeAddress("treasure_octorok#5"),
payload: {},
});
}).toThrow(/does not exist/);
@ -141,10 +38,12 @@ describe("graph", () => {
it("forbids adding an edge with dangling `src`", () => {
expect(() => {
simpleMealGraph().addEdge({
address: makeAddress("health_bar#6@healed_by@seafood_fruit_mix#3"),
src: makeAddress("health_bar#6"),
dst: mealNode().address,
demoData.simpleMealGraph().addEdge({
address: demoData.makeAddress(
"health_bar#6@healed_by@seafood_fruit_mix#3"
),
src: demoData.makeAddress("health_bar#6"),
dst: demoData.mealNode().address,
payload: {},
});
}).toThrow(/does not exist/);
@ -192,44 +91,54 @@ describe("graph", () => {
describe("getting nodes and edges", () => {
it("correctly gets nodes in the simple graph", () => {
const g = simpleMealGraph();
[heroNode(), bananasNode(), crabNode(), mealNode()].forEach((x) => {
const g = demoData.simpleMealGraph();
[
demoData.heroNode(),
demoData.bananasNode(),
demoData.crabNode(),
demoData.mealNode(),
].forEach((x) => {
expect(g.getNode(x.address)).toEqual(x);
});
});
it("correctly gets nodes in the advanced graph", () => {
const g = advancedMealGraph();
[heroNode(), bananasNode(), crabNode(), mealNode()].forEach((x) => {
const g = demoData.advancedMealGraph();
[
demoData.heroNode(),
demoData.bananasNode(),
demoData.crabNode(),
demoData.mealNode(),
].forEach((x) => {
expect(g.getNode(x.address)).toEqual(x);
});
});
it("correctly gets edges in the simple graph", () => {
const g = simpleMealGraph();
const g = demoData.simpleMealGraph();
[
pickEdge(),
grabEdge(),
cookEdge(),
bananasIngredientEdge(),
crabIngredientEdge(),
eatEdge(),
demoData.pickEdge(),
demoData.grabEdge(),
demoData.cookEdge(),
demoData.bananasIngredientEdge(),
demoData.crabIngredientEdge(),
demoData.eatEdge(),
].forEach((x) => {
expect(g.getEdge(x.address)).toEqual(x);
});
});
it("correctly gets edges in the advanced graph", () => {
const g = advancedMealGraph();
const g = demoData.advancedMealGraph();
[
pickEdge(),
grabEdge(),
cookEdge(),
bananasIngredientEdge(),
crabIngredientEdge(),
eatEdge(),
crabLoopEdge(),
duplicateCookEdge(),
demoData.pickEdge(),
demoData.grabEdge(),
demoData.cookEdge(),
demoData.bananasIngredientEdge(),
demoData.crabIngredientEdge(),
demoData.eatEdge(),
demoData.crabLoopEdge(),
demoData.duplicateCookEdge(),
].forEach((x) => {
expect(g.getEdge(x.address)).toEqual(x);
});
@ -237,36 +146,47 @@ describe("graph", () => {
it("returns `undefined` for nodes that do not exist", () => {
expect(
simpleMealGraph().getNode(makeAddress("treasure_octorok#5"))
demoData
.simpleMealGraph()
.getNode(demoData.makeAddress("treasure_octorok#5"))
).toBeUndefined();
});
it("returns `undefined` for edges that do not exist", () => {
expect(
simpleMealGraph().getNode(
makeAddress("treasure_octorok#5@helps_cook@seafood_fruit_mix#3")
)
demoData
.simpleMealGraph()
.getNode(
demoData.makeAddress(
"treasure_octorok#5@helps_cook@seafood_fruit_mix#3"
)
)
).toBeUndefined();
});
it("gets all nodes", () => {
const expected = [heroNode(), bananasNode(), crabNode(), mealNode()];
const actual = advancedMealGraph().getAllNodes();
const expected = [
demoData.heroNode(),
demoData.bananasNode(),
demoData.crabNode(),
demoData.mealNode(),
];
const actual = demoData.advancedMealGraph().getAllNodes();
expectSameSorted(expected, actual);
});
it("gets all edges", () => {
const expected = [
pickEdge(),
grabEdge(),
cookEdge(),
bananasIngredientEdge(),
crabIngredientEdge(),
eatEdge(),
crabLoopEdge(),
duplicateCookEdge(),
demoData.pickEdge(),
demoData.grabEdge(),
demoData.cookEdge(),
demoData.bananasIngredientEdge(),
demoData.crabIngredientEdge(),
demoData.eatEdge(),
demoData.crabLoopEdge(),
demoData.duplicateCookEdge(),
];
const actual = advancedMealGraph().getAllEdges();
const actual = demoData.advancedMealGraph().getAllEdges();
expectSameSorted(expected, actual);
});
});
@ -274,8 +194,8 @@ describe("graph", () => {
describe("creating nodes and edges", () => {
it("forbids adding a node with existing address", () => {
expect(() =>
simpleMealGraph().addNode({
address: crabNode().address,
demoData.simpleMealGraph().addNode({
address: demoData.crabNode().address,
payload: {anotherCrab: true},
})
).toThrow(/already exists/);
@ -283,29 +203,31 @@ describe("graph", () => {
it("forbids adding an edge with existing address", () => {
expect(() =>
simpleMealGraph().addEdge({
address: cookEdge().address,
src: crabNode().address,
dst: crabNode().address,
demoData.simpleMealGraph().addEdge({
address: demoData.cookEdge().address,
src: demoData.crabNode().address,
dst: demoData.crabNode().address,
payload: {},
})
).toThrow(/already exists/);
});
it("allows creating self-loops", () => {
const g = simpleMealGraph();
g.addEdge(crabLoopEdge());
expect(g.getOutEdges(crabNode().address)).toContainEqual(
crabLoopEdge()
const g = demoData.simpleMealGraph();
g.addEdge(demoData.crabLoopEdge());
expect(g.getOutEdges(demoData.crabNode().address)).toContainEqual(
demoData.crabLoopEdge()
);
expect(g.getInEdges(demoData.crabNode().address)).toContainEqual(
demoData.crabLoopEdge()
);
expect(g.getInEdges(crabNode().address)).toContainEqual(crabLoopEdge());
});
it("allows creating multiple edges between the same nodes", () => {
const g = simpleMealGraph();
g.addEdge(duplicateCookEdge());
[cookEdge(), duplicateCookEdge()].forEach((e) => {
expect(g.getOutEdges(mealNode().address)).toContainEqual(e);
const g = demoData.simpleMealGraph();
g.addEdge(demoData.duplicateCookEdge());
[demoData.cookEdge(), demoData.duplicateCookEdge()].forEach((e) => {
expect(g.getOutEdges(demoData.mealNode().address)).toContainEqual(e);
expect(g.getEdge(e.address)).toEqual(e);
});
});
@ -315,16 +237,16 @@ describe("graph", () => {
// the namespaces to be forced to be disjoint. In that case, we can
// certainly change these tests.
it("allows adding an edge with an existing node's address", () => {
simpleMealGraph().addEdge({
address: crabNode().address,
src: crabNode().address,
dst: crabNode().address,
demoData.simpleMealGraph().addEdge({
address: demoData.crabNode().address,
src: demoData.crabNode().address,
dst: demoData.crabNode().address,
payload: {message: "thanks for being you"},
});
});
it("allows adding a node with an existing edge's address", () => {
simpleMealGraph().addNode({
address: cookEdge().address,
demoData.simpleMealGraph().addNode({
address: demoData.cookEdge().address,
payload: {},
});
});
@ -333,21 +255,21 @@ describe("graph", () => {
describe("in- and out-edges", () => {
it("gets out-edges", () => {
const nodeAndExpectedEdgePairs = [
[heroNode(), [eatEdge()]],
[bananasNode(), [pickEdge()]],
[crabNode(), [grabEdge(), crabLoopEdge()]],
[demoData.heroNode(), [demoData.eatEdge()]],
[demoData.bananasNode(), [demoData.pickEdge()]],
[demoData.crabNode(), [demoData.grabEdge(), demoData.crabLoopEdge()]],
[
mealNode(),
demoData.mealNode(),
[
bananasIngredientEdge(),
crabIngredientEdge(),
cookEdge(),
duplicateCookEdge(),
demoData.bananasIngredientEdge(),
demoData.crabIngredientEdge(),
demoData.cookEdge(),
demoData.duplicateCookEdge(),
],
],
];
nodeAndExpectedEdgePairs.forEach(([node, expectedEdges]) => {
const actual = advancedMealGraph().getOutEdges(node.address);
const actual = demoData.advancedMealGraph().getOutEdges(node.address);
expectSameSorted(actual, expectedEdges);
});
});
@ -355,62 +277,76 @@ describe("graph", () => {
it("gets in-edges", () => {
const nodeAndExpectedEdgePairs = [
[
heroNode(),
[pickEdge(), grabEdge(), cookEdge(), duplicateCookEdge()],
demoData.heroNode(),
[
demoData.pickEdge(),
demoData.grabEdge(),
demoData.cookEdge(),
demoData.duplicateCookEdge(),
],
],
[bananasNode(), [bananasIngredientEdge()]],
[crabNode(), [crabIngredientEdge(), crabLoopEdge()]],
[mealNode(), [eatEdge()]],
[demoData.bananasNode(), [demoData.bananasIngredientEdge()]],
[
demoData.crabNode(),
[demoData.crabIngredientEdge(), demoData.crabLoopEdge()],
],
[demoData.mealNode(), [demoData.eatEdge()]],
];
nodeAndExpectedEdgePairs.forEach(([node, expectedEdges]) => {
const actual = advancedMealGraph().getInEdges(node.address);
const actual = demoData.advancedMealGraph().getInEdges(node.address);
expectSameSorted(actual, expectedEdges);
});
});
it("fails to get out-edges for a nonexistent node", () => {
expect(() => {
simpleMealGraph().getOutEdges(makeAddress("hinox"));
demoData.simpleMealGraph().getOutEdges(demoData.makeAddress("hinox"));
}).toThrow(/no node for address/);
});
it("fails to get in-edges for a nonexistent node", () => {
expect(() => {
simpleMealGraph().getInEdges(makeAddress("hinox"));
demoData.simpleMealGraph().getInEdges(demoData.makeAddress("hinox"));
}).toThrow(/no node for address/);
});
});
describe("#equals", () => {
it("returns true for identity-equal graphs", () => {
const g = advancedMealGraph();
const g = demoData.advancedMealGraph();
expect(g.equals(g)).toBe(true);
});
it("returns true for deep-equal graphs", () => {
expect(advancedMealGraph().equals(advancedMealGraph())).toBe(true);
expect(
demoData.advancedMealGraph().equals(demoData.advancedMealGraph())
).toBe(true);
});
it("returns false when the LHS has nodes missing in the RHS", () => {
expect(advancedMealGraph().equals(simpleMealGraph())).toBe(false);
expect(
demoData.advancedMealGraph().equals(demoData.simpleMealGraph())
).toBe(false);
});
it("returns false when the RHS has nodes missing in the LHS", () => {
expect(simpleMealGraph().equals(advancedMealGraph())).toBe(false);
expect(
demoData.simpleMealGraph().equals(demoData.advancedMealGraph())
).toBe(false);
});
const extraNode1 = () => ({
address: makeAddress("octorok"),
address: demoData.makeAddress("octorok"),
payload: {},
});
const extraNode2 = () => ({
address: makeAddress("hinox"),
address: demoData.makeAddress("hinox"),
payload: {status: "sleeping"},
});
it("returns false when the LHS has edges missing in the RHS", () => {
const g1 = advancedMealGraph();
const g2 = advancedMealGraph().addNode(extraNode1());
const g1 = demoData.advancedMealGraph();
const g2 = demoData.advancedMealGraph().addNode(extraNode1());
expect(g1.equals(g2)).toBe(false);
});
it("returns false when the LHS has edges missing in the RHS", () => {
const g1 = advancedMealGraph().addNode(extraNode1());
const g2 = advancedMealGraph();
const g1 = demoData.advancedMealGraph().addNode(extraNode1());
const g2 = demoData.advancedMealGraph();
expect(g1.equals(g2)).toBe(false);
});
it("returns true when nodes are added in different orders", () => {
@ -468,33 +404,32 @@ describe("graph", () => {
}
it("conservatively recomposes a neighborhood decomposition", () => {
const result = neighborhoodDecomposition(advancedMealGraph()).reduce(
(g1, g2) => Graph.mergeConservative(g1, g2),
new Graph()
);
expect(result.equals(advancedMealGraph())).toBe(true);
const result = neighborhoodDecomposition(
demoData.advancedMealGraph()
).reduce((g1, g2) => Graph.mergeConservative(g1, g2), new Graph());
expect(result.equals(demoData.advancedMealGraph())).toBe(true);
});
it("conservatively recomposes an edge decomposition", () => {
const result = edgeDecomposition(advancedMealGraph()).reduce(
const result = edgeDecomposition(demoData.advancedMealGraph()).reduce(
(g1, g2) => Graph.mergeConservative(g1, g2),
new Graph()
);
expect(result.equals(advancedMealGraph())).toBe(true);
expect(result.equals(demoData.advancedMealGraph())).toBe(true);
});
it("conservatively merges a graph with itself", () => {
const result = Graph.mergeConservative(
advancedMealGraph(),
advancedMealGraph()
demoData.advancedMealGraph(),
demoData.advancedMealGraph()
);
expect(result.equals(advancedMealGraph())).toBe(true);
expect(result.equals(demoData.advancedMealGraph())).toBe(true);
});
it("conservatively rejects a graph with conflicting nodes", () => {
const makeGraph: (nodePayload: string) => Graph = (nodePayload) =>
new Graph().addNode({
address: makeAddress("conflicting-node"),
address: demoData.makeAddress("conflicting-node"),
payload: nodePayload,
});
const g1 = makeGraph("one");
@ -505,14 +440,14 @@ describe("graph", () => {
});
it("conservatively rejects a graph with conflicting edges", () => {
const srcAddress = makeAddress("src");
const dstAddress = makeAddress("dst");
const srcAddress = demoData.makeAddress("src");
const dstAddress = demoData.makeAddress("dst");
const makeGraph: (edgePayload: string) => Graph = (edgePayload) =>
new Graph()
.addNode({address: srcAddress, payload: {}})
.addNode({address: dstAddress, payload: {}})
.addEdge({
address: makeAddress("conflicting-edge"),
address: demoData.makeAddress("conflicting-edge"),
src: srcAddress,
dst: dstAddress,
payload: edgePayload,
@ -530,20 +465,20 @@ describe("graph", () => {
it("has the empty graph as a left identity", () => {
const merged = Graph.merge(
new Graph(),
advancedMealGraph(),
demoData.advancedMealGraph(),
assertNotCalled,
assertNotCalled
);
expect(merged.equals(advancedMealGraph())).toBe(true);
expect(merged.equals(demoData.advancedMealGraph())).toBe(true);
});
it("has the empty graph as a right identity", () => {
const merged = Graph.merge(
advancedMealGraph(),
demoData.advancedMealGraph(),
new Graph(),
assertNotCalled,
assertNotCalled
);
expect(merged.equals(advancedMealGraph())).toBe(true);
expect(merged.equals(demoData.advancedMealGraph())).toBe(true);
});
it("trivially merges the empty graph with itself", () => {
const merged = Graph.merge(
@ -558,39 +493,43 @@ describe("graph", () => {
describe("JSON functions", () => {
it("should serialize a simple graph", () => {
expect(advancedMealGraph().toJSON()).toMatchSnapshot();
expect(demoData.advancedMealGraph().toJSON()).toMatchSnapshot();
});
it("should work transparently with JSON.stringify", () => {
// (This is guaranteed by the `JSON.stringify` API, and is more
// as documentation than actual test.)
expect(JSON.stringify(advancedMealGraph())).toEqual(
JSON.stringify(advancedMealGraph().toJSON())
expect(JSON.stringify(demoData.advancedMealGraph())).toEqual(
JSON.stringify(demoData.advancedMealGraph().toJSON())
);
});
it("should canonicalize away node insertion order", () => {
const g1 = new Graph().addNode(heroNode()).addNode(mealNode());
const g2 = new Graph().addNode(mealNode()).addNode(heroNode());
const g1 = new Graph()
.addNode(demoData.heroNode())
.addNode(demoData.mealNode());
const g2 = new Graph()
.addNode(demoData.mealNode())
.addNode(demoData.heroNode());
expect(g1.toJSON()).toEqual(g2.toJSON());
});
it("should canonicalize away edge insertion order", () => {
const g1 = new Graph()
.addNode(heroNode())
.addNode(mealNode())
.addEdge(cookEdge())
.addEdge(duplicateCookEdge());
.addNode(demoData.heroNode())
.addNode(demoData.mealNode())
.addEdge(demoData.cookEdge())
.addEdge(demoData.duplicateCookEdge());
const g2 = new Graph()
.addNode(heroNode())
.addNode(mealNode())
.addEdge(duplicateCookEdge())
.addEdge(cookEdge());
.addNode(demoData.heroNode())
.addNode(demoData.mealNode())
.addEdge(demoData.duplicateCookEdge())
.addEdge(demoData.cookEdge());
expect(g1.toJSON()).toEqual(g2.toJSON());
});
it("should no-op on a serialization--deserialization roundtrip", () => {
const g = () => advancedMealGraph();
const g = () => demoData.advancedMealGraph();
expect(Graph.fromJSON(g().toJSON()).equals(g())).toBe(true);
});
it("should no-op on a deserialization--serialization roundtrip", () => {
const json = () => advancedMealGraph().toJSON();
const json = () => demoData.advancedMealGraph().toJSON();
expect(Graph.fromJSON(json()).toJSON()).toEqual(json());
});
});

View File

@ -0,0 +1,106 @@
// @flow
// This module provides some small demo graphs, which report
// on a hero's adventures in cooking a seafood fruit mix.
// It is factored as its own module so that it may be depended on by
// multiple test and demo consumers.
import type {Address} from "./address";
import {Graph} from "./graph";
export function makeAddress(id: string): Address {
return {
repositoryName: "sourcecred/eventide",
pluginName: "hill_cooking_pot",
id,
};
}
export const heroNode = () => ({
address: makeAddress("hero_of_time#0"),
payload: {},
});
export const bananasNode = () => ({
address: makeAddress("mighty_bananas#1"),
payload: {},
});
export const crabNode = () => ({
address: makeAddress("razorclaw_crab#2"),
payload: {},
});
export const mealNode = () => ({
address: makeAddress("seafood_fruit_mix#3"),
payload: {
effect: ["attack_power", 1],
},
});
export const pickEdge = () => ({
address: makeAddress("hero_of_time#0@picks@mighty_bananas#1"),
src: bananasNode().address,
dst: heroNode().address,
payload: {},
});
export const grabEdge = () => ({
address: makeAddress("hero_of_time#0@grabs@razorclaw_crab#2"),
src: crabNode().address,
dst: heroNode().address,
payload: {},
});
export const cookEdge = () => ({
address: makeAddress("hero_of_time#0@cooks@seafood_fruit_mix#3"),
src: mealNode().address,
dst: heroNode().address,
payload: {
crit: false,
},
});
export const bananasIngredientEdge = () => ({
address: makeAddress("mighty_bananas#1@included_in@seafood_fruit_mix#3"),
src: mealNode().address,
dst: bananasNode().address,
payload: {},
});
export const crabIngredientEdge = () => ({
address: makeAddress("razorclaw_crab#2@included_in@seafood_fruit_mix#3"),
src: mealNode().address,
dst: crabNode().address,
payload: {},
});
export const eatEdge = () => ({
address: makeAddress("hero_of_time#0@eats@seafood_fruit_mix#3"),
src: heroNode().address,
dst: mealNode().address,
payload: {},
});
export const simpleMealGraph = () =>
new Graph()
.addNode(heroNode())
.addNode(bananasNode())
.addNode(crabNode())
.addNode(mealNode())
.addEdge(pickEdge())
.addEdge(grabEdge())
.addEdge(cookEdge())
.addEdge(bananasIngredientEdge())
.addEdge(crabIngredientEdge())
.addEdge(eatEdge());
export const crabLoopEdge = () => ({
address: makeAddress("crab-self-assessment"),
src: crabNode().address,
dst: crabNode().address,
payload: {evaluation: "not effective at avoiding hero"},
});
export const duplicateCookEdge = () => ({
address: makeAddress("hero_of_time#0@again_cooks@seafood_fruit_mix#3"),
src: mealNode().address,
dst: heroNode().address,
payload: {
crit: true,
saveScummed: true,
},
});
export const advancedMealGraph = () =>
simpleMealGraph()
.addEdge(crabLoopEdge())
.addEdge(duplicateCookEdge());