yarn-spinner-runner-ts
Version:
TypeScript parser, compiler, and runtime for Yarn Spinner 3.x with React adapter [NPM package](https://www.npmjs.com/package/yarn-spinner-runner-ts)
259 lines (229 loc) • 7.59 kB
text/typescript
import { test } from "node:test";
import { strictEqual } from "node:assert";
import { parseYarn, compile, YarnRunner } from "../index.js";
test("variables, flow control, and commands", () => {
const script = `
title: Start
---
<<set $score to 10>>
<<if $score >= 10>>
Narrator: High
<<else>>
Narrator: Low
<<endif>>
===
`;
const doc = parseYarn(script);
const ir = compile(doc);
const runner = new YarnRunner(ir, { startAt: "Start" });
// After command, expect if-branch 'High'
// First result should be command emission
const a = runner.currentResult!;
strictEqual(a.type, "command", "First result should be command");
runner.advance();
const b = runner.currentResult!;
strictEqual(b.type, "text", "Should be text after command");
if (b.type === "text") strictEqual(/High/.test(b.text), true, "Expected High branch");
strictEqual(runner.getVariable("score"), 10, "Variable should be set");
});
test("equality operators support ==, !=, and single =", () => {
const script = `
title: Start
---
<<set $doorOpen to true>>
<<if $doorOpen = true>>
Narrator: Single equals ok
<<endif>>
<<if $doorOpen == true>>
Narrator: Double equals ok
<<endif>>
<<if $doorOpen != false>>
Narrator: Not equals ok
<<endif>>
===
`;
const doc = parseYarn(script);
const ir = compile(doc);
const runner = new YarnRunner(ir, { startAt: "Start" });
const seen: string[] = [];
let guard = 25;
while (guard-- > 0) {
const result = runner.currentResult;
if (!result) break;
if (result.type === "text" && result.text.trim()) {
seen.push(result.text.trim());
}
if (result.isDialogueEnd) {
break;
}
if (result.type === "options") {
runner.advance(0);
} else {
runner.advance();
}
}
strictEqual(seen.includes("Single equals ok"), true, "Single equals comparison should succeed");
strictEqual(seen.includes("Double equals ok"), true, "Double equals comparison should succeed");
strictEqual(seen.includes("Not equals ok"), true, "Not equals comparison should succeed");
});
test("set command supports equals syntax with arithmetic reassignment", () => {
const script = `
title: StreetCred
---
<<set $reputation = 100>>
<<set $reputation = $reputation - 25 >>
Narrator: Current street cred: {$reputation}
===
`;
const doc = parseYarn(script);
const ir = compile(doc);
const runner = new YarnRunner(ir, { startAt: "StreetCred" });
const seen: string[] = [];
for (let guard = 0; guard < 20; guard++) {
const result = runner.currentResult;
if (!result) break;
if (result.type === "text" && result.text.trim()) {
seen.push(result.text.trim());
}
if (result.isDialogueEnd) break;
if (result.type === "options") {
runner.advance(0);
} else {
runner.advance();
}
}
strictEqual(seen.includes("Current street cred: 75"), true, "Should reflect arithmetic subtraction");
strictEqual(runner.getVariable("reputation"), 75, "Variable should store updated numeric value");
});
test("set command respects arithmetic precedence and parentheses", () => {
const script = `
title: MathChecks
---
<<set $score = 10>>
<<set $score = $score + 10 * 2>>
<<set $score = ($score + 10) / 2>>
Narrator: Score now {$score}
===
`;
const doc = parseYarn(script);
const ir = compile(doc);
const runner = new YarnRunner(ir, { startAt: "MathChecks" });
const lines: string[] = [];
for (let guard = 0; guard < 20; guard++) {
const result = runner.currentResult;
if (!result) break;
if (result.type === "text" && result.text.trim()) {
lines.push(result.text.trim());
}
if (result.isDialogueEnd) break;
if (result.type === "options") {
runner.advance(0);
} else {
runner.advance();
}
}
strictEqual(lines.includes("Score now 20"), true, "Should honor operator precedence and parentheses");
strictEqual(runner.getVariable("score"), 20, "Final numeric value should be 20");
});
test("variables passed from host accept $ prefix and mutate via arithmetic set", () => {
const script = `
title: HostVars
---
Narrator: Start {$reputation}
<<set $reputation = $reputation - 25 >>
Narrator: After {$reputation}
===
`;
const doc = parseYarn(script);
const ir = compile(doc);
const runner = new YarnRunner(ir, { startAt: "HostVars", variables: { $reputation: 100 } });
const lines: string[] = [];
for (let guard = 0; guard < 20; guard++) {
const result = runner.currentResult;
if (!result) break;
if (result.type === "text" && result.text.trim()) {
lines.push(result.text.trim());
}
if (result.isDialogueEnd) break;
if (result.type === "options") {
runner.advance(0);
} else {
runner.advance();
}
}
strictEqual(lines.includes("Start 100"), true, "Initial host variable should be visible");
strictEqual(lines.includes("After 75"), true, "Arithmetic mutation should be reflected");
strictEqual(runner.getVariable("reputation"), 75, "Runner variable store should update");
});
test("host variables work with math helpers and propagate results", () => {
const script = `
title: MathHost
---
Narrator: Incoming {$energy}
<<set $energy = max($energy, 50)>>
<<set $residual = floor($energy / 3)>>
Narrator: After max {$energy}
Narrator: Residual {$residual}
===
`;
const doc = parseYarn(script);
const ir = compile(doc);
const runner = new YarnRunner(ir, { startAt: "MathHost", variables: { $energy: 37 } });
const lines: string[] = [];
for (let guard = 0; guard < 20; guard++) {
const result = runner.currentResult;
if (!result) break;
if (result.type === "text" && result.text.trim()) {
lines.push(result.text.trim());
}
if (result.isDialogueEnd) break;
if (result.type === "options") {
runner.advance(0);
} else {
runner.advance();
}
}
strictEqual(lines.includes("Incoming 37"), true, "Should read initial host variable");
strictEqual(lines.includes("After max 50"), true, "max() should clamp the variable");
strictEqual(lines.includes("Residual 16"), true, "floor division should be reflected");
strictEqual(runner.getVariable("energy"), 50, "Host variable should hold updated max result");
strictEqual(runner.getVariable("residual"), 16, "New variables from math operations should be stored");
});
test("host variables use custom add/subtract functions", () => {
const script = `
title: HostMathFns
---
Narrator: Credits {$credits}
<<set $credits = add($credits, 25)>>
<<set $credits = subtract($credits, 10)>>
Narrator: Final {$credits}
===
`;
const doc = parseYarn(script);
const ir = compile(doc);
const runner = new YarnRunner(ir, {
startAt: "HostMathFns",
variables: { $credits: 15 },
functions: {
add: (a: unknown, b: unknown) => Number(a) + Number(b),
subtract: (a: unknown, b: unknown) => Number(a) - Number(b),
},
});
const lines: string[] = [];
for (let guard = 0; guard < 20; guard++) {
const result = runner.currentResult;
if (!result) break;
if (result.type === "text" && result.text.trim()) {
lines.push(result.text.trim());
}
if (result.isDialogueEnd) break;
if (result.type === "options") {
runner.advance(0);
} else {
runner.advance();
}
}
strictEqual(lines.includes("Credits 15"), true, "Should read initial credits");
strictEqual(lines.includes("Final 30"), true, "Custom add/subtract functions should apply math");
strictEqual(runner.getVariable("credits"), 30, "Stored variable should reflect final value");
});