diff --git a/bin/presubmit.sh b/bin/presubmit.sh new file mode 100755 index 0000000..509647e --- /dev/null +++ b/bin/presubmit.sh @@ -0,0 +1,15 @@ +#! /usr/bin/env bash + +echo "Presubmit: Running Prettier..."; +prettier --write src || exit 1; + +echo "Presubmit: Running ESLint..."; +eslint --fix src --max-warnings=0 || exit 1; + +echo "Presubmit: Building..."; +react-scripts build || exit 1; + +echo "Presubmit: Running all tests..."; +react-scripts test --all --watchAll=false --coverage || exit 1; + +echo "Presubmit succeeded. Remember to commit any changes." \ No newline at end of file diff --git a/package.json b/package.json index 0fa36f2..abcd17e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "groundhog", - "version": "0.10.9", + "version": "0.10.10", "homepage": "https://charredutensil.github.io/groundhog", "private": true, "dependencies": { @@ -23,11 +23,12 @@ "test": "react-scripts test", "predeploy": "react-scripts build", "deploy": "gh-pages -d build", - "presubmit": "prettier --write src && eslint --fix src --max-warnings=0 && react-scripts build && react-scripts test --all", + "presubmit": "bin/presubmit.sh", "eject": "react-scripts eject", "gen": "tsx bin/groundhog-gen", "architect-stats": "tsx bin/architect-stats", - "lore-dump": "tsx bin/lore-dump" + "lore-dump": "tsx bin/lore-dump", + "update-goldens": "UPDATE_GOLDENS=1 react-scripts test --all --watchAll=false" }, "eslintConfig": { "extends": [ diff --git a/src/core/architects/utils/script.test.ts b/src/core/architects/utils/script.test.ts index 637ad3f..52d9e65 100644 --- a/src/core/architects/utils/script.test.ts +++ b/src/core/architects/utils/script.test.ts @@ -1,4 +1,6 @@ -import { sanitizeString } from "./script"; +import goldenTest from "../../test_utils/golden"; +import { PreprogrammedCavern } from "../../transformers/04_ephemera/03_preprogram"; +import { mkScriptBuilder, mkVars, sanitizeString } from "./script"; describe("escapeString", () => { it("handles the empty string", () => { @@ -23,3 +25,171 @@ describe("escapeString", () => { ); }); }); + +describe("ScriptBuilder", () => { + const cavern = {} as PreprogrammedCavern; + + it("declares variables", () => { + const sb = mkScriptBuilder(cavern); + sb.declareArrow("arw"); + sb.declareBuilding("bld"); + sb.declareCreature("cr"); + sb.declareInt("num", 42); + sb.declareString("str", "Message!"); + expect(sb.build()).toEqual(`\ +arrow arw +building bld +creature cr +int num=42 +string str="Message!"`); + }); + + it("declares an event", () => { + const sb = mkScriptBuilder(cavern); + sb.event("foo", "crystals+=1;"); + expect(sb.build()).toEqual(`\ +foo::; +crystals+=1; +`); + }); + + it("declares a simple if trigger", () => { + const sb = mkScriptBuilder(cavern); + sb.if("crystals>10", "crystals-=5;"); + expect(sb.build()).toEqual("if(crystals>10)[crystals-=5]"); + }); + + it("declares a simple when trigger", () => { + const sb = mkScriptBuilder(cavern); + sb.when("crystals>50", "crystals-=1;"); + expect(sb.build()).toEqual("when(crystals>50)[crystals-=1]"); + }); + + it("creates an anonymous event chain to handle multiple statements", () => { + const sb = mkScriptBuilder(cavern); + sb.if("crystals>10", "wait:5;", "crystals-=5;"); + expect(sb.build()).toEqual(`\ +if(crystals>10)[t0] +t0::; +wait:5; +crystals-=5; +`); + }); + + it("creates an anonymous event chain to handle repeated triggers", () => { + const sb = mkScriptBuilder(cavern); + sb.when("ore>50", "ore-=20;"); + sb.when("ore>50", "crystals+=1;"); + expect(sb.build()).toEqual(`\ +when(ore>50)[tw0] +tw0::; +ore-=20; +crystals+=1; +`); + }); + + it("creates anonymous event chains to handle repeated triggers with multiple statements", () => { + const sb = mkScriptBuilder(cavern); + sb.when("ore>50", "ore-=20;"); + sb.when("ore>50", "wait:5;", "crystals+=1;"); + sb.when("ore>50", "wait:10;", "crystals+=1;"); + expect(sb.build()).toEqual(`\ +when(ore>50)[tw2] +tw2::; +ore-=20; +t0; +t1; + +t0::; +wait:5; +crystals+=1; + +t1::; +wait:10; +crystals+=1; +`); + }); + + it("handles if and when triggers on the same condition 1", () => { + const sb = mkScriptBuilder(cavern); + sb.if("ore>50", "msg:msgFirstAlchemy;"); + sb.when("ore>50", "ore-=20;", "wait:5;", "crystals+=1;"); + expect(sb.build()).toEqual(`\ +int tf0=0 +if(tf0>0)[msg:msgFirstAlchemy] +when(ore>50)[tw2] +tw2::; +tf0=1; +t1; + +t1::; +ore-=20; +wait:5; +crystals+=1; +`); + }); + + it("handles if and when triggers on the same condition 2", () => { + const sb = mkScriptBuilder(cavern); + sb.if("ore>50", "shake:1;", "msg:msgFirstAlchemy;"); + sb.when("ore>50", "ore-=20;"); + expect(sb.build()).toEqual(`\ +int tf0=0 +if(tf0>0)[t1] +t1::; +shake:1; +msg:msgFirstAlchemy; + +when(ore>50)[tw2] +tw2::; +tf0=1; +ore-=20; +`); + }); + + goldenTest("fizzbuzz", () => { + // This code is bad but idk - it's more about testing the builder. + + const sb = mkScriptBuilder(cavern); + const v = mkVars("fbz", [ + "n", + "lock", + "m", + "three", + "five", + "msgFizz", + "msgBuzz", + "msgFizzBuzz", + ]); + sb.declareInt(v.n, 0); + sb.declareInt(v.m, 0); + sb.declareInt(v.lock, 0); + sb.declareInt(v.three, 0); + sb.declareInt(v.five, 0); + + sb.when("enter:1,1", `${v.lock}+=1;`); + sb.when( + `${v.lock}==1`, + + `pan:${v.three},${v.five};`, + + `${v.n}+=1;`, + `${v.m}=0;`, + + `${v.three}+=1;`, + `((${v.three}==3))${v.m}=1;`, + `((${v.three}>=3))${v.three}-=3;`, + + `${v.five}+=1;`, + `((${v.five}==5))${v.m}+=2;`, + `((${v.five}>=5))${v.five}-=5;`, + + `((${v.m}==1))msg:${sb.declareString(v.msgFizz, "Fizz")};`, + `((${v.m}==2))msg:${sb.declareString(v.msgBuzz, "Buzz")};`, + `((${v.m}==3))msg:${sb.declareString(v.msgFizzBuzz, "Fizz Buzz")};`, + + `${v.lock}=0;`, + ); + return sb.build(); + }); +}); diff --git a/src/core/architects/utils/script.ts b/src/core/architects/utils/script.ts index 5c3a26d..a136c79 100644 --- a/src/core/architects/utils/script.ts +++ b/src/core/architects/utils/script.ts @@ -118,9 +118,9 @@ export type ScriptBuilder = { }; type Trigger = { - kind: "if" | "when"; condition: string; - bodies: `${string};`[]; + ifs: `${string};`[]; + whens: `${string};`[]; }; type BuildableScriptBuilder = ScriptBuilder & { build(): string }; @@ -148,20 +148,39 @@ export function mkScriptBuilder( } const tx: Trigger | undefined = triggers.byCondition[condition]; if (tx) { - if (tx.kind !== kind) { - throw new Error( - `Attempted to redefine trigger \`${tx.kind}(${condition})\` as a \`${kind}\` trigger`, - ); - } - triggers.byCondition[condition].bodies.push(body); + triggers.byCondition[condition][`${kind}s`].push(body); return; } - const t: Trigger = { kind, condition, bodies: [body] }; + const t: Trigger = + kind === "if" + ? { condition, ifs: [body], whens: [] } + : { condition, ifs: [], whens: [body] }; triggers.inOrder.push(t); triggers.byCondition[condition] = t; } - function buildTrigger({ kind, condition, bodies }: Trigger) { + function buildTrigger({ condition, ifs, whens }: Trigger) { + if (ifs.length && whens.length) { + // If there are both ifs and whens, need to trigger them separately. + const v = `tf${uid++}`; + return [ + `int ${v}=0`, + buildTriggerHelper("if", `${v}>0`, ifs), + buildTriggerHelper("when", condition, [`${v}=1;`, ...whens]), + ].join("\n"); + } else if (ifs.length) { + return buildTriggerHelper("if", condition, ifs); + } else if (whens.length) { + return buildTriggerHelper("when", condition, whens); + } + return ""; + } + + function buildTriggerHelper( + kind: "if" | "when", + condition: Trigger["condition"], + bodies: `${string};`[], + ) { const calls: `${string};`[] = []; const extra: string[] = []; bodies.forEach((body) => { diff --git a/src/core/e2e_tests/golden.ts b/src/core/test_utils/golden.ts similarity index 90% rename from src/core/e2e_tests/golden.ts rename to src/core/test_utils/golden.ts index 201cdf5..1d7a45a 100644 --- a/src/core/e2e_tests/golden.ts +++ b/src/core/test_utils/golden.ts @@ -11,7 +11,7 @@ const updateGoldenFile = async (filePath: string, actual: string) => { }; const goldenTest = async (name: string, fn: () => string) => { - const filePath = path.resolve(__dirname, `${GOLDEN_DIR}/${name}.dat`); + const filePath = path.resolve(__dirname, GOLDEN_DIR, name); test(name, async () => { const actual = fn(); if (process.env[UPDATE_GOLDENS]) { diff --git a/src/core/e2e_tests/goldens/building_zoo.dat b/src/core/test_utils/goldens/building_zoo.dat similarity index 99% rename from src/core/e2e_tests/goldens/building_zoo.dat rename to src/core/test_utils/goldens/building_zoo.dat index 3dd6aa6..abe7739 100644 --- a/src/core/e2e_tests/goldens/building_zoo.dat +++ b/src/core/test_utils/goldens/building_zoo.dat @@ -1,7 +1,7 @@ comments{ Cavern generated by groundHog [VERSION] https://github.com/charredUtensil/groundhog - initialContext: {} + initialContext = {} } info{ rowcount:14 diff --git a/src/core/e2e_tests/goldens/entity_zoo.dat b/src/core/test_utils/goldens/entity_zoo.dat similarity index 99% rename from src/core/e2e_tests/goldens/entity_zoo.dat rename to src/core/test_utils/goldens/entity_zoo.dat index 0086849..6423a9d 100644 --- a/src/core/e2e_tests/goldens/entity_zoo.dat +++ b/src/core/test_utils/goldens/entity_zoo.dat @@ -1,7 +1,7 @@ comments{ Cavern generated by groundHog [VERSION] https://github.com/charredUtensil/groundhog - initialContext: {} + initialContext = {} } info{ rowcount:14 diff --git a/src/core/test_utils/goldens/fizzbuzz b/src/core/test_utils/goldens/fizzbuzz new file mode 100644 index 0000000..e63b9a2 --- /dev/null +++ b/src/core/test_utils/goldens/fizzbuzz @@ -0,0 +1,24 @@ +int fbz_n=0 +int fbz_m=0 +int fbz_lock=0 +int fbz_three=0 +int fbz_five=0 +string fbz_msgFizz="Fizz" +string fbz_msgBuzz="Buzz" +string fbz_msgFizzBuzz="Fizz Buzz" +when(enter:1,1)[fbz_lock+=1] +when(fbz_lock==1)[t0] +t0::; +pan:fbz_three,fbz_five; +fbz_n+=1; +fbz_m=0; +fbz_three+=1; +((fbz_three==3))fbz_m=1; +((fbz_three>=3))fbz_three-=3; +fbz_five+=1; +((fbz_five==5))fbz_m+=2; +((fbz_five>=5))fbz_five-=5; +((fbz_m==1))msg:fbz_msgFizz; +((fbz_m==2))msg:fbz_msgBuzz; +((fbz_m==3))msg:fbz_msgFizzBuzz; +fbz_lock=0; diff --git a/src/core/e2e_tests/goldens/mvp.dat b/src/core/test_utils/goldens/mvp.dat similarity index 98% rename from src/core/e2e_tests/goldens/mvp.dat rename to src/core/test_utils/goldens/mvp.dat index 88f87a9..e751159 100644 --- a/src/core/e2e_tests/goldens/mvp.dat +++ b/src/core/test_utils/goldens/mvp.dat @@ -1,7 +1,7 @@ comments{ Cavern generated by groundHog [VERSION] https://github.com/charredUtensil/groundhog - initialContext: {} + initialContext = {} } info{ rowcount:6 diff --git a/src/core/e2e_tests/serialize.test.ts b/src/core/transformers/04_ephemera/05_serialize.test.ts similarity index 91% rename from src/core/e2e_tests/serialize.test.ts rename to src/core/transformers/04_ephemera/05_serialize.test.ts index c09997e..8aa1cf5 100644 --- a/src/core/e2e_tests/serialize.test.ts +++ b/src/core/transformers/04_ephemera/05_serialize.test.ts @@ -1,5 +1,5 @@ -import { EAST, NORTH, Point, SOUTH, WEST } from "../common/geometry"; -import { MutableGrid } from "../common/grid"; +import { EAST, NORTH, Point, SOUTH, WEST } from "../../common/geometry"; +import { MutableGrid } from "../../common/grid"; import { CANTEEN, DOCKS, @@ -12,8 +12,8 @@ import { TELEPORT_PAD, TOOL_STORE, UPGRADE_STATION, -} from "../models/building"; -import { Cavern } from "../models/cavern"; +} from "../../models/building"; +import { Cavern } from "../../models/cavern"; import { BAT, CreatureFactory, @@ -22,16 +22,16 @@ import { ROCK_MONSTER, SLIMY_SLUG, SMALL_SPIDER, -} from "../models/creature"; -import { Erosion, Landslide } from "../models/hazards"; -import { MinerFactory } from "../models/miner"; -import { atCenterOfTile, position } from "../models/position"; -import { Tile } from "../models/tiles"; -import discover from "../transformers/03_plastic/01_discover"; -import fence from "../transformers/03_plastic/00_fence"; -import serialize from "../transformers/04_ephemera/05_serialize"; -import goldenTest from "./golden"; -import strataflux from "../transformers/03_plastic/03_strataflux"; +} from "../../models/creature"; +import { Erosion, Landslide } from "../../models/hazards"; +import { MinerFactory } from "../../models/miner"; +import { atCenterOfTile, position } from "../../models/position"; +import { Tile } from "../../models/tiles"; +import discover from "../03_plastic/01_discover"; +import fence from "../03_plastic/00_fence"; +import serialize from "./05_serialize"; +import goldenTest from "../../test_utils/golden"; +import strataflux from "../03_plastic/03_strataflux"; import { CARGO_CARRIER, CHROME_CRUSHER, @@ -46,7 +46,7 @@ import { TUNNEL_SCOUT, TUNNEL_TRANSPORT, VehicleFactory, -} from "../models/vehicle"; +} from "../../models/vehicle"; function fill( grid: MutableGrid, @@ -107,7 +107,7 @@ function ds(args: Partial) { ).serialized.replace(/groundHog v\S+/, "groundHog [VERSION]"); } -goldenTest("mvp", () => { +goldenTest("mvp.dat", () => { const crystals = new MutableGrid(); const tiles = new MutableGrid(); const openCaveFlags = new MutableGrid(); @@ -133,7 +133,7 @@ goldenTest("mvp", () => { }); }); -goldenTest("building_zoo", () => { +goldenTest("building_zoo.dat", () => { const tiles = new MutableGrid(); const openCaveFlags = new MutableGrid(); @@ -200,7 +200,7 @@ goldenTest("building_zoo", () => { }); }); -goldenTest("entity_zoo", () => { +goldenTest("entity_zoo.dat", () => { const tiles = new MutableGrid(); const openCaveFlags = new MutableGrid(); diff --git a/src/core/transformers/04_ephemera/05_serialize.ts b/src/core/transformers/04_ephemera/05_serialize.ts index 5a90aac..4af6838 100644 --- a/src/core/transformers/04_ephemera/05_serialize.ts +++ b/src/core/transformers/04_ephemera/05_serialize.ts @@ -33,7 +33,7 @@ function indent(it: string, prefix: string) { function comments(cavern: ProgrammedCavern) { return `Cavern generated by groundHog v${process.env.REACT_APP_VERSION} https://github.com/charredUtensil/groundhog -initialContext: ${JSON.stringify(cavern.initialContext, null, 2)}`; +initialContext = ${JSON.stringify(cavern.initialContext, null, 2)}`; } /**