Files
systemic-game-design-course/course/11-conditions/tests/ts/test_11.test.ts
T
Lucas WintherandClaude Opus 5.5 34c655c1a4 feat: Module 11, Conditions (NOUN ON NOUN IS ..., checked per object at use; NEAR as the Expansion Drop)
Adds course/11-conditions: lesson, scenarios, nine test_11 tests in both
tracks, the cumulative reference (Module 10's plus conditions) and the NEAR
drop. The engine learns a condition role, rules carry conditions, and
Level.hasProperty and conversion evaluate content's condition checks against
the board whenever a rule is used. Resolution pairs only rules with the same
conditions. Catalog, harness contract, research notes and the diff-tracks
generator follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 13:00:26 +01:00

306 lines
7.8 KiB
TypeScript

// Module 11: Conditions. Each test matches a heading in scenarios.md.
import { expect, test } from "vitest";
import { newBaba, type Baba } from "../src/harness";
/** An empty level `width` wide and `height` high. */
const emptyLevel = (width: number, height: number) => ({ width, height, objects: [] });
/** Place one text object per word: `[word, x, y]`. */
function write(b: Baba, words: [string, number, number][]): void {
for (const [word, x, y] of words) b.place(`text:${word}`, x, y);
}
test("test_11_condition_gates_property", () => {
const b = newBaba(emptyLevel(6, 4));
write(b, [
["KEY", 0, 0],
["ON", 1, 0],
["ROCK", 2, 0],
["IS", 3, 0],
["WIN", 4, 0],
]);
const onRock = b.place("key", 0, 2);
const rock = b.place("rock", 0, 2);
const alone = b.place("key", 2, 2);
expect(b.rules()).toEqual(["KEY ON ROCK IS WIN", "TEXT IS PUSH"]);
expect(b.activeRules()).toEqual(["KEY ON ROCK IS WIN", "TEXT IS PUSH"]);
expect(b.hasProperty(onRock, "WIN")).toBe(true);
expect(b.hasProperty(alone, "WIN")).toBe(false);
expect(b.hasProperty(rock, "WIN")).toBe(false);
});
test("test_11_condition_follows_the_object", () => {
const b = newBaba(emptyLevel(6, 5));
write(b, [
["BABA", 0, 0],
["IS", 1, 0],
["YOU", 2, 0],
]);
write(b, [
["LAVA", 0, 1],
["IS", 1, 1],
["HOT", 2, 1],
]);
write(b, [
["BABA", 0, 2],
["ON", 1, 2],
["KEY", 2, 2],
["IS", 3, 2],
["MELT", 4, 2],
]);
const toKey = b.place("baba", 0, 4);
b.place("key", 1, 4);
b.place("lava", 1, 4);
const toLava = b.place("baba", 3, 4);
b.place("lava", 4, 4);
const before = b.activeRules();
expect(b.hasProperty(toKey, "MELT")).toBe(false);
expect(b.hasProperty(toLava, "MELT")).toBe(false);
b.step("right");
expect(b.activeRules()).toEqual(before);
expect(b.posOf(toKey)).toBeNull();
expect(b.kindsAt(1, 4)).toEqual(["key", "lava"]);
expect(b.posOf(toLava)).toEqual([4, 4]);
expect(b.kindsAt(4, 4)).toEqual(["baba", "lava"]);
});
test("test_11_and_requires_both", () => {
const b = newBaba(emptyLevel(7, 4));
write(b, [
["KEY", 0, 0],
["ON", 1, 0],
["ROCK", 2, 0],
["AND", 3, 0],
["FLAG", 4, 0],
["IS", 5, 0],
["WIN", 6, 0],
]);
const withRock = b.place("key", 0, 2);
b.place("rock", 0, 2);
const withFlag = b.place("key", 2, 2);
b.place("flag", 2, 2);
const withBoth = b.place("key", 4, 2);
b.place("rock", 4, 2);
b.place("flag", 4, 2);
expect(b.rules()).toEqual(["KEY ON ROCK AND FLAG IS WIN", "TEXT IS PUSH"]);
expect(b.hasProperty(withRock, "WIN")).toBe(false);
expect(b.hasProperty(withFlag, "WIN")).toBe(false);
expect(b.hasProperty(withBoth, "WIN")).toBe(true);
});
test("test_11_and_counts_objects", () => {
const b = newBaba(emptyLevel(7, 4));
write(b, [
["KEY", 0, 0],
["ON", 1, 0],
["ROCK", 2, 0],
["AND", 3, 0],
["ROCK", 4, 0],
["IS", 5, 0],
["WIN", 6, 0],
]);
const oneRock = b.place("key", 0, 2);
b.place("rock", 0, 2);
const twoRocks = b.place("key", 2, 2);
b.place("rock", 2, 2);
b.place("rock", 2, 2);
expect(b.rules()).toEqual(["KEY ON ROCK AND ROCK IS WIN", "TEXT IS PUSH"]);
expect(b.hasProperty(oneRock, "WIN")).toBe(false);
expect(b.hasProperty(twoRocks, "WIN")).toBe(true);
});
test("test_11_conditional_not_only_where_it_holds", () => {
const b = newBaba(emptyLevel(6, 7));
write(b, [
["BABA", 0, 0],
["IS", 1, 0],
["YOU", 2, 0],
]);
write(b, [
["BABA", 0, 2],
["ON", 1, 2],
["KEY", 2, 2],
["IS", 3, 2],
["NOT", 4, 2],
["YOU", 5, 2],
]);
const onKey = b.place("baba", 0, 6);
b.place("key", 0, 6);
const free = b.place("baba", 2, 6);
expect(b.activeRules()).toEqual(["BABA IS YOU", "BABA ON KEY IS NOT YOU", "TEXT IS PUSH"]);
expect(b.hasProperty(onKey, "YOU")).toBe(false);
expect(b.hasProperty(free, "YOU")).toBe(true);
write(b, [
["BABA", 0, 4],
["ON", 1, 4],
["KEY", 2, 4],
["IS", 3, 4],
["YOU", 4, 4],
]);
expect(b.rules()).toContain("BABA ON KEY IS YOU");
expect(b.activeRules()).toEqual(["BABA IS YOU", "BABA ON KEY IS NOT YOU", "TEXT IS PUSH"]);
b.step("right");
expect(b.posOf(onKey)).toEqual([0, 6]);
expect(b.posOf(free)).toEqual([3, 6]);
});
test("test_11_conditional_conversion", () => {
const b = newBaba(emptyLevel(6, 5));
write(b, [
["BABA", 0, 0],
["IS", 1, 0],
["YOU", 2, 0],
]);
write(b, [
["ROCK", 0, 1],
["IS", 1, 1],
["PUSH", 2, 1],
]);
write(b, [
["KEY", 0, 2],
["ON", 1, 2],
["ROCK", 2, 2],
["IS", 3, 2],
["FLAG", 4, 2],
]);
const resting = b.place("key", 0, 4);
b.place("rock", 0, 4);
const baba = b.place("baba", 2, 4);
const pushed = b.place("rock", 3, 4);
const reached = b.place("key", 4, 4);
const alone = b.place("key", 5, 4);
b.step("right");
expect(b.posOf(baba)).toEqual([3, 4]);
expect(b.posOf(pushed)).toEqual([4, 4]);
expect(b.kindsAt(0, 4)).toEqual(["flag", "rock"]);
expect(b.posOf(resting)).toEqual([0, 4]);
expect(b.kindsAt(4, 4)).toEqual(["flag", "rock"]);
expect(b.posOf(reached)).toEqual([4, 4]);
expect(b.kindsAt(5, 4)).toEqual(["key"]);
expect(b.posOf(alone)).toEqual([5, 4]);
});
test("test_11_condition_and_stacked_text", () => {
const b = newBaba(emptyLevel(6, 7));
write(b, [
["BABA", 0, 0],
["ON", 1, 0],
["KEY", 2, 0],
["ROCK", 2, 0],
["IS", 3, 0],
["WIN", 4, 0],
]);
write(b, [
["FLAG", 0, 2],
["ON", 1, 2],
["AND", 1, 2],
["KEY", 2, 2],
["IS", 3, 2],
["PUSH", 4, 2],
]);
const onRock = b.place("baba", 0, 4);
b.place("rock", 0, 4);
const onKey = b.place("baba", 2, 4);
b.place("key", 2, 4);
const alone = b.place("baba", 4, 4);
expect(b.rules()).toEqual([
"BABA ON KEY IS WIN",
"BABA ON ROCK IS WIN",
"FLAG IS PUSH",
"FLAG ON KEY IS PUSH",
"KEY IS PUSH",
"TEXT IS PUSH",
]);
expect(b.hasProperty(onRock, "WIN")).toBe(true);
expect(b.hasProperty(onKey, "WIN")).toBe(true);
expect(b.hasProperty(alone, "WIN")).toBe(false);
});
test("test_11_on_group_noun", () => {
const b = newBaba(emptyLevel(8, 4));
write(b, [
["ROCK", 0, 0],
["ON", 1, 0],
["ALL", 2, 0],
["IS", 3, 0],
["WIN", 4, 0],
]);
const alone = b.place("rock", 0, 2);
const onText = b.place("rock", 2, 2);
b.place("text:BABA", 2, 2);
const onKey = b.place("rock", 4, 2);
b.place("key", 4, 2);
const first = b.place("rock", 6, 2);
const second = b.place("rock", 6, 2);
expect(b.rules()).toEqual(["ROCK ON ALL IS WIN", "TEXT IS PUSH"]);
expect(b.hasProperty(alone, "WIN")).toBe(false);
expect(b.hasProperty(onText, "WIN")).toBe(false);
expect(b.hasProperty(onKey, "WIN")).toBe(true);
expect(b.hasProperty(first, "WIN")).toBe(true);
expect(b.hasProperty(second, "WIN")).toBe(true);
});
test("test_11_condition_rechecked_between_hazards", () => {
const b = newBaba(emptyLevel(6, 8));
write(b, [
["BABA", 0, 0],
["IS", 1, 0],
["YOU", 2, 0],
]);
write(b, [
["BABA", 0, 1],
["AND", 1, 1],
["LAVA", 2, 1],
["IS", 3, 1],
["FLOAT", 4, 1],
]);
write(b, [
["LAVA", 0, 2],
["IS", 1, 2],
["HOT", 2, 2],
]);
write(b, [
["KEY", 0, 3],
["IS", 1, 3],
["SINK", 2, 3],
]);
write(b, [
["BABA", 0, 4],
["ON", 1, 4],
["KEY", 2, 4],
["IS", 3, 4],
["MELT", 4, 4],
]);
const baba = b.place("baba", 2, 6);
const lava = b.place("lava", 3, 6);
const key = b.place("key", 3, 6);
const rock = b.place("rock", 3, 6);
expect(b.rules()).toContain("BABA ON KEY IS MELT");
expect(b.hasProperty(baba, "MELT")).toBe(false);
b.step("right");
expect(b.posOf(key)).toBeNull();
expect(b.posOf(rock)).toBeNull();
expect(b.posOf(lava)).toEqual([3, 6]);
expect(b.posOf(baba)).toEqual([3, 6]);
expect(b.kindsAt(3, 6)).toEqual(["baba", "lava"]);
expect(b.lost()).toBe(false);
});