feat: card/level schemas and verified catalog
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
9a00b8ac18
commit
a4b72ad110
@@ -0,0 +1,388 @@
|
||||
[
|
||||
{
|
||||
"name": "Armorsmith",
|
||||
"kind": "card",
|
||||
"type": "minion",
|
||||
"cost": 2,
|
||||
"attack": 1,
|
||||
"health": 4,
|
||||
"text": "Whenever a friendly minion takes damage, gain 1 Armor.",
|
||||
"source": "https://hearthstone.wiki.gg/wiki/Armorsmith"
|
||||
},
|
||||
{
|
||||
"name": "Acolyte of Pain",
|
||||
"kind": "card",
|
||||
"type": "minion",
|
||||
"cost": 3,
|
||||
"attack": 1,
|
||||
"health": 4,
|
||||
"text": "Whenever this minion takes damage, draw a card.",
|
||||
"source": "https://hearthstone.wiki.gg/wiki/Acolyte_of_Pain"
|
||||
},
|
||||
{
|
||||
"name": "Frothing Berserker",
|
||||
"kind": "card",
|
||||
"type": "minion",
|
||||
"cost": 3,
|
||||
"attack": 2,
|
||||
"health": 4,
|
||||
"text": "Whenever a minion takes damage, gain +1 Attack.",
|
||||
"source": "https://hearthstone.wiki.gg/wiki/Frothing_Berserker"
|
||||
},
|
||||
{
|
||||
"name": "Loot Hoarder",
|
||||
"kind": "card",
|
||||
"type": "minion",
|
||||
"cost": 2,
|
||||
"attack": 2,
|
||||
"health": 1,
|
||||
"text": "Deathrattle: Draw a card.",
|
||||
"source": "https://hearthstone.wiki.gg/wiki/Loot_Hoarder"
|
||||
},
|
||||
{
|
||||
"name": "Baron Rivendare",
|
||||
"kind": "card",
|
||||
"type": "minion",
|
||||
"cost": 4,
|
||||
"attack": 1,
|
||||
"health": 7,
|
||||
"text": "Your minions trigger their Deathrattles twice.",
|
||||
"source": "https://hearthstone.wiki.gg/wiki/Baron_Rivendare"
|
||||
},
|
||||
{
|
||||
"name": "Twisting Nether",
|
||||
"kind": "card",
|
||||
"type": "spell",
|
||||
"cost": 8,
|
||||
"text": "Destroy all minions and locations.",
|
||||
"source": "https://hearthstone.wiki.gg/wiki/Twisting_Nether"
|
||||
},
|
||||
{
|
||||
"name": "Whirlwind",
|
||||
"kind": "card",
|
||||
"type": "spell",
|
||||
"cost": 1,
|
||||
"text": "Deal 1 damage to ALL minions.",
|
||||
"source": "https://hearthstone.wiki.gg/wiki/Whirlwind"
|
||||
},
|
||||
{
|
||||
"name": "Unstable Ghoul",
|
||||
"kind": "card",
|
||||
"type": "minion",
|
||||
"cost": 2,
|
||||
"attack": 1,
|
||||
"health": 3,
|
||||
"text": "Taunt. Deathrattle: Deal 1 damage to all minions.",
|
||||
"source": "https://hearthstone.wiki.gg/wiki/Unstable_Ghoul"
|
||||
},
|
||||
{
|
||||
"name": "Harvest Golem",
|
||||
"kind": "card",
|
||||
"type": "minion",
|
||||
"cost": 3,
|
||||
"attack": 2,
|
||||
"health": 3,
|
||||
"text": "Deathrattle: Summon a 2/1 Damaged Golem.",
|
||||
"source": "https://hearthstone.wiki.gg/wiki/Harvest_Golem"
|
||||
},
|
||||
{
|
||||
"name": "Damaged Golem",
|
||||
"kind": "card",
|
||||
"type": "minion",
|
||||
"cost": 1,
|
||||
"attack": 2,
|
||||
"health": 1,
|
||||
"text": "",
|
||||
"source": "https://hearthstone.wiki.gg/wiki/Damaged_Golem",
|
||||
"uncollectible": true
|
||||
},
|
||||
{
|
||||
"name": "Brann Bronzebeard",
|
||||
"kind": "card",
|
||||
"type": "minion",
|
||||
"cost": 3,
|
||||
"attack": 2,
|
||||
"health": 4,
|
||||
"text": "Your Battlecries trigger twice.",
|
||||
"source": "https://hearthstone.wiki.gg/wiki/Brann_Bronzebeard"
|
||||
},
|
||||
{
|
||||
"name": "Drakkari Enchanter",
|
||||
"kind": "card",
|
||||
"type": "minion",
|
||||
"cost": 3,
|
||||
"attack": 1,
|
||||
"health": 5,
|
||||
"text": "Your end of turn effects trigger twice.",
|
||||
"source": "https://hearthstone.wiki.gg/wiki/Drakkari_Enchanter"
|
||||
},
|
||||
{
|
||||
"name": "Novice Engineer",
|
||||
"kind": "card",
|
||||
"type": "minion",
|
||||
"cost": 2,
|
||||
"attack": 1,
|
||||
"health": 1,
|
||||
"text": "Battlecry: Draw a card.",
|
||||
"source": "https://hearthstone.wiki.gg/wiki/Novice_Engineer"
|
||||
},
|
||||
{
|
||||
"name": "Dread Infernal",
|
||||
"kind": "card",
|
||||
"type": "minion",
|
||||
"cost": 6,
|
||||
"attack": 6,
|
||||
"health": 6,
|
||||
"text": "Battlecry: Deal 1 damage to ALL other characters.",
|
||||
"source": "https://hearthstone.wiki.gg/wiki/Dread_Infernal"
|
||||
},
|
||||
{
|
||||
"name": "Fleshshape",
|
||||
"kind": "card",
|
||||
"type": "spell",
|
||||
"cost": 3,
|
||||
"text": "Assemble a Deathrattle minion. Give all minions in your hand +2/+2.",
|
||||
"source": "https://hearthstone.wiki.gg/wiki/Fleshshape"
|
||||
},
|
||||
{
|
||||
"name": "Test Echo",
|
||||
"kind": "card",
|
||||
"type": "minion",
|
||||
"cost": 1,
|
||||
"attack": 1,
|
||||
"health": 5,
|
||||
"text": "Whenever a minion takes damage, deal 1 damage to the enemy hero.",
|
||||
"fictional": true
|
||||
},
|
||||
{
|
||||
"name": "Test Reactor",
|
||||
"kind": "card",
|
||||
"type": "minion",
|
||||
"cost": 1,
|
||||
"attack": 1,
|
||||
"health": 5,
|
||||
"text": "Whenever a hero takes damage, gain +1 Attack.",
|
||||
"fictional": true
|
||||
},
|
||||
{
|
||||
"name": "Test Filler",
|
||||
"kind": "card",
|
||||
"type": "minion",
|
||||
"cost": 1,
|
||||
"attack": 1,
|
||||
"health": 1,
|
||||
"text": "",
|
||||
"fictional": true
|
||||
},
|
||||
{
|
||||
"name": "BABA",
|
||||
"kind": "word",
|
||||
"role": "noun",
|
||||
"text": "the Baba creature",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "ROCK",
|
||||
"kind": "word",
|
||||
"role": "noun",
|
||||
"text": "a pushable boulder",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "WALL",
|
||||
"kind": "word",
|
||||
"role": "noun",
|
||||
"text": "a wall",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "FLAG",
|
||||
"kind": "word",
|
||||
"role": "noun",
|
||||
"text": "a flag",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "LAVA",
|
||||
"kind": "word",
|
||||
"role": "noun",
|
||||
"text": "lava",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "KEY",
|
||||
"kind": "word",
|
||||
"role": "noun",
|
||||
"text": "a key",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "DOOR",
|
||||
"kind": "word",
|
||||
"role": "noun",
|
||||
"text": "a door",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "SKULL",
|
||||
"kind": "word",
|
||||
"role": "noun",
|
||||
"text": "a skull",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "WATER",
|
||||
"kind": "word",
|
||||
"role": "noun",
|
||||
"text": "water",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "IS",
|
||||
"kind": "word",
|
||||
"role": "operator",
|
||||
"text": "links a noun to a property or another noun",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "AND",
|
||||
"kind": "word",
|
||||
"role": "operator",
|
||||
"text": "joins nouns or properties",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "NOT",
|
||||
"kind": "word",
|
||||
"role": "operator",
|
||||
"text": "negates the property or noun after IS",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "YOU",
|
||||
"kind": "word",
|
||||
"role": "property",
|
||||
"text": "player controls these",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "WIN",
|
||||
"kind": "word",
|
||||
"role": "property",
|
||||
"text": "touching a YOU object wins",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "PUSH",
|
||||
"kind": "word",
|
||||
"role": "property",
|
||||
"text": "can be pushed",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "STOP",
|
||||
"kind": "word",
|
||||
"role": "property",
|
||||
"text": "blocks movement",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "OPEN",
|
||||
"kind": "word",
|
||||
"role": "property",
|
||||
"text": "opens SHUT objects on contact",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "SHUT",
|
||||
"kind": "word",
|
||||
"role": "property",
|
||||
"text": "blocks until opened",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "HOT",
|
||||
"kind": "word",
|
||||
"role": "property",
|
||||
"text": "melts MELT objects",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "MELT",
|
||||
"kind": "word",
|
||||
"role": "property",
|
||||
"text": "destroyed by HOT",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "SINK",
|
||||
"kind": "word",
|
||||
"role": "property",
|
||||
"text": "destroys itself and what shares its tile",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "DEFEAT",
|
||||
"kind": "word",
|
||||
"role": "property",
|
||||
"text": "destroys YOU objects on contact",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "FLOAT",
|
||||
"kind": "word",
|
||||
"role": "property",
|
||||
"text": "shares a tile layer with non-floating objects",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "WEAK",
|
||||
"kind": "word",
|
||||
"role": "property",
|
||||
"text": "destroyed on contact",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "PULL",
|
||||
"kind": "word",
|
||||
"role": "property",
|
||||
"text": "pulled by a mover",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "DOUBLE",
|
||||
"kind": "word",
|
||||
"role": "property",
|
||||
"text": "effects apply twice",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "TEXT",
|
||||
"kind": "word",
|
||||
"role": "meta",
|
||||
"text": "every text object (a noun about text itself)",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "ALL",
|
||||
"kind": "word",
|
||||
"role": "meta",
|
||||
"text": "every object",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "EMPTY",
|
||||
"kind": "word",
|
||||
"role": "meta",
|
||||
"text": "empty tiles",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md"
|
||||
},
|
||||
{
|
||||
"name": "DEATHRATTLE",
|
||||
"kind": "word",
|
||||
"role": "noun",
|
||||
"text": "a trigger type, so DEATHRATTLE IS DOUBLE doubles deathrattles",
|
||||
"source": "https://github.com/utilForever/baba-is-auto/blob/main/Documents/rule-language.md",
|
||||
"game": "spellgrid"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"$comment": "Structured card text. A trigger clause applies to the effect clauses after it until the next trigger/keyword/aura. split divides top (text[0:split]) from bottom (text[split:]).",
|
||||
"type": "object",
|
||||
"required": ["name", "type", "cost", "text", "split"],
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"type": { "enum": ["minion", "spell"] },
|
||||
"cost": { "type": "integer", "minimum": 0 },
|
||||
"attack": { "type": "integer", "minimum": 0 },
|
||||
"health": { "type": "integer", "minimum": 1 },
|
||||
"noun": { "type": "string" },
|
||||
"split": { "type": "integer", "minimum": 0 },
|
||||
"fictional": { "type": "boolean" },
|
||||
"game": { "type": "string" },
|
||||
"text": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{ "type": "object", "required": ["kind", "keyword"], "properties": { "kind": { "const": "keyword" }, "keyword": { "type": "string" } } },
|
||||
{ "type": "object", "required": ["kind", "on"], "properties": {
|
||||
"kind": { "const": "trigger" },
|
||||
"on": { "enum": ["deathrattle", "battlecry", "selfDamaged", "friendlyMinionDamaged", "anyMinionDamaged", "heroDamaged", "endOfTurn"] } } },
|
||||
{ "type": "object", "required": ["kind", "modifier"], "properties": {
|
||||
"kind": { "const": "aura" },
|
||||
"modifier": { "type": "object", "required": ["type", "trigger", "times", "stacking"], "properties": {
|
||||
"type": { "const": "triggerMultiplier" },
|
||||
"trigger": { "enum": ["deathrattle", "battlecry", "selfDamaged", "friendlyMinionDamaged", "anyMinionDamaged", "heroDamaged", "endOfTurn"] },
|
||||
"times": { "type": "integer", "minimum": 2 },
|
||||
"stacking": { "enum": ["max", "additive"] } } } } },
|
||||
{ "type": "object", "required": ["kind", "effect"], "properties": {
|
||||
"kind": { "const": "effect" },
|
||||
"effect": { "oneOf": [
|
||||
{ "type": "object", "required": ["type", "target", "amount"], "properties": { "type": { "const": "dealDamage" }, "target": { "enum": ["self", "allMinions", "allOtherMinions", "allOtherCharacters", "enemyHero", "chosen"] }, "amount": { "type": "integer", "minimum": 0 } } },
|
||||
{ "type": "object", "required": ["type", "count"], "properties": { "type": { "const": "draw" }, "count": { "type": "integer", "minimum": 1 } } },
|
||||
{ "type": "object", "required": ["type", "card", "count"], "properties": { "type": { "const": "summon" }, "card": { "type": "string" }, "count": { "type": "integer", "minimum": 1 } } },
|
||||
{ "type": "object", "required": ["type", "amount"], "properties": { "type": { "const": "gainArmor" }, "amount": { "type": "integer", "minimum": 1 } } },
|
||||
{ "type": "object", "required": ["type", "target", "amount"], "properties": { "type": { "const": "gainAttack" }, "target": { "enum": ["self", "allMinions", "allOtherMinions", "allOtherCharacters", "enemyHero", "chosen"] }, "amount": { "type": "integer" } } },
|
||||
{ "type": "object", "required": ["type", "target"], "properties": { "type": { "const": "destroy" }, "target": { "enum": ["self", "allMinions", "allOtherMinions", "allOtherCharacters", "enemyHero", "chosen"] } } },
|
||||
{ "type": "object", "required": ["type", "word", "at"], "properties": { "type": { "const": "spawnText" }, "word": { "type": "string" }, "at": { "const": "self" } } }
|
||||
] } } }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$comment": "x grows right, y grows down.",
|
||||
"type": "object",
|
||||
"required": ["width", "height", "objects"],
|
||||
"properties": {
|
||||
"width": { "type": "integer", "minimum": 1 },
|
||||
"height": { "type": "integer", "minimum": 1 },
|
||||
"objects": { "type": "array", "items": { "type": "object", "required": ["kind", "x", "y"],
|
||||
"properties": { "kind": { "type": "string" }, "x": { "type": "integer", "minimum": 0 }, "y": { "type": "integer", "minimum": 0 } } } },
|
||||
"cards": { "type": "array", "items": { "type": "object", "required": ["card", "x", "y"],
|
||||
"properties": { "card": { "type": "string" }, "x": { "type": "integer", "minimum": 0 }, "y": { "type": "integer", "minimum": 0 } } } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Minimal JSON-schema subset validator (node built-ins only).
|
||||
// Supports: type, required, enum, const, properties, items, minimum, oneOf, anyOf.
|
||||
const typeOk = (t, v) =>
|
||||
t === "array" ? Array.isArray(v)
|
||||
: t === "null" ? v === null
|
||||
: t === "integer" ? Number.isInteger(v)
|
||||
: t === "object" ? v !== null && typeof v === "object" && !Array.isArray(v)
|
||||
: typeof v === t;
|
||||
|
||||
export function validate(schema, value, path = "$") {
|
||||
const errs = [];
|
||||
if (schema.type) {
|
||||
const types = [].concat(schema.type);
|
||||
if (!types.some((t) => typeOk(t, value))) return [`${path}: expected ${types.join("|")}`];
|
||||
}
|
||||
if (schema.enum && !schema.enum.includes(value)) errs.push(`${path}: not in enum ${JSON.stringify(schema.enum)}`);
|
||||
if ("const" in schema && value !== schema.const) errs.push(`${path}: expected ${JSON.stringify(schema.const)}`);
|
||||
if (schema.minimum !== undefined && typeof value === "number" && value < schema.minimum) errs.push(`${path}: below minimum ${schema.minimum}`);
|
||||
if (typeOk("object", value)) {
|
||||
for (const k of schema.required || []) if (!(k in value)) errs.push(`${path}: missing "${k}"`);
|
||||
for (const [k, s] of Object.entries(schema.properties || {})) if (k in value) errs.push(...validate(s, value[k], `${path}.${k}`));
|
||||
}
|
||||
if (Array.isArray(value) && schema.items) value.forEach((v, i) => errs.push(...validate(schema.items, v, `${path}[${i}]`)));
|
||||
if (schema.anyOf && !schema.anyOf.some((s) => validate(s, value, path).length === 0)) errs.push(`${path}: matches no anyOf branch`);
|
||||
if (schema.oneOf) {
|
||||
const n = schema.oneOf.filter((s) => validate(s, value, path).length === 0).length;
|
||||
if (n !== 1) errs.push(`${path}: matches ${n} oneOf branches (need 1)`);
|
||||
}
|
||||
return errs;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"name":"Armorsmith","type":"minion","cost":2,"attack":1,"health":4,"split":1,"text":[{"kind":"trigger","on":"friendlyMinionDamaged"},{"kind":"effect","effect":{"type":"gainArmor","amount":1}}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"name":"Baron Rivendare","type":"minion","cost":4,"attack":1,"health":7,"split":0,"text":[{"kind":"aura","modifier":{"type":"triggerMultiplier","trigger":"deathrattle","times":2,"stacking":"max"}}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"name":"Dread Infernal","type":"minion","cost":6,"attack":6,"health":6,"split":1,"text":[{"kind":"trigger","on":"battlecry"},{"kind":"effect","effect":{"type":"dealDamage","target":"allOtherCharacters","amount":1}}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"name":"Harvest Golem","type":"minion","cost":3,"attack":2,"health":3,"split":1,"text":[{"kind":"trigger","on":"deathrattle"},{"kind":"effect","effect":{"type":"summon","card":"Damaged Golem","count":1}}]}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"name":"Loot Hoarder","type":"minion","cost":2,"attack":2,"health":1,"split":1,"text":[{"kind":"trigger","on":"deathrattle"},{"kind":"effect","effect":{"type":"draw","count":1}}]}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { validate } from "../schema.mjs";
|
||||
import { loadCatalog } from "../lib.mjs";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const json = (p) => JSON.parse(readFileSync(p, "utf8"));
|
||||
const cardSchema = json(join(here, "../../course/schema/card.schema.json"));
|
||||
const levelSchema = json(join(here, "../../course/schema/level.schema.json"));
|
||||
const entry = {
|
||||
type: "object",
|
||||
required: ["name", "kind", "text", "source"],
|
||||
properties: { name: { type: "string" }, kind: { enum: ["card", "word"] }, text: { type: "string" } },
|
||||
};
|
||||
const cardEntry = {
|
||||
type: "object",
|
||||
required: ["type", "cost"],
|
||||
properties: { type: { enum: ["minion", "spell"] }, cost: { type: "integer", minimum: 0 }, attack: { type: "integer" }, health: { type: "integer" } },
|
||||
};
|
||||
const wordEntry = { type: "object", required: ["role"], properties: { role: { enum: ["noun", "operator", "property", "meta"] } } };
|
||||
|
||||
test("catalog_valid", () => {
|
||||
const cat = loadCatalog();
|
||||
assert.ok(Array.isArray(cat) && cat.length > 0);
|
||||
const names = new Set();
|
||||
for (const e of cat) {
|
||||
assert.deepEqual(validate({ ...entry, required: e.fictional ? ["name", "kind", "text"] : entry.required }, e), [], e.name);
|
||||
assert.deepEqual(validate(e.kind === "card" ? cardEntry : wordEntry, e), [], e.name);
|
||||
assert.ok(!names.has(e.name), `duplicate ${e.name}`);
|
||||
names.add(e.name);
|
||||
if (e.kind === "card" && !e.fictional) assert.match(e.source, /^https:\/\/hearthstone\.wiki\.gg\//, e.name);
|
||||
if (e.kind === "card" && e.name.startsWith("Test ")) assert.equal(e.fictional, true, e.name);
|
||||
if (e.fictional) assert.ok(e.name.startsWith("Test "), e.name);
|
||||
}
|
||||
for (const n of ["Baron Rivendare", "Fleshshape", "Test Echo", "BABA", "IS", "DEATHRATTLE"]) assert.ok(names.has(n), n);
|
||||
});
|
||||
|
||||
test("card_schema_accepts_fixtures", () => {
|
||||
const dir = join(here, "fixtures/cards");
|
||||
const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
|
||||
assert.ok(files.length >= 5);
|
||||
const cat = new Map(loadCatalog().map((e) => [e.name, e]));
|
||||
for (const f of files) {
|
||||
const c = json(join(dir, f));
|
||||
assert.deepEqual(validate(cardSchema, c), [], f);
|
||||
const m = cat.get(c.name);
|
||||
assert.ok(m, `${c.name} in catalog`);
|
||||
assert.deepEqual([c.cost, c.attack, c.health], [m.cost, m.attack, m.health], `${f} matches catalog stats`);
|
||||
}
|
||||
});
|
||||
|
||||
test("card_schema_rejects_bad", () => {
|
||||
const bad = { name: "X", type: "minion", cost: 1, split: 0, text: [{ kind: "trigger", on: "bogus" }] };
|
||||
assert.notDeepEqual(validate(cardSchema, bad), []);
|
||||
assert.notDeepEqual(validate(cardSchema, { name: "X" }), []);
|
||||
});
|
||||
|
||||
test("level_schema", () => {
|
||||
assert.deepEqual(validate(levelSchema, { width: 3, height: 1, objects: [{ kind: "baba", x: 0, y: 0 }] }), []);
|
||||
assert.notDeepEqual(validate(levelSchema, { width: 3, height: 1, objects: [{ kind: "baba", x: 0 }] }), []);
|
||||
});
|
||||
Reference in New Issue
Block a user