#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const repoRoot = path.resolve(__dirname, '..'); const rulesPath = path.join(repoRoot, 'database', 'rules', 'rules-database.json'); const data = JSON.parse(fs.readFileSync(rulesPath, 'utf8')); const rules = data.rules || []; const scenarios = [ { actor: 'Player', moment: 'Tyranid Warrior hits Brother Lucian; player checks whether a Reaction can cancel it.', query: 'dodge', expected: ['Dodge'] }, { actor: 'GM', moment: 'Heavy bolter lays down fire across a corridor.', query: 'suppressing fire pinning', expected: ['Suppressing Fire', 'Pinning'] }, { actor: 'Player', moment: 'Devastator fires full auto and needs additional hit rules.', query: 'full auto burst', expected: ['Full Auto Burst'] }, { actor: 'GM', moment: 'A mob of gaunts becomes one mass enemy.', query: 'horde magnitude damage', expected: ['Hordes'] }, { actor: 'Player', moment: 'Librarian rolls psychic backlash.', query: 'perils of the warp', expected: ['Perils of the Warp'] }, { actor: 'GM', moment: 'Kill-team takes blast damage in Squad Mode.', query: 'cohesion damage', expected: ['Cohesion Damage'] }, { actor: 'Player', moment: 'Mission prep pauses to buy gear.', query: 'requisition renown', expected: ['Requisition', 'Renown'] }, { actor: 'GM', moment: 'Enemy causes Fear and the table needs the modifier.', query: 'fear test', expected: ['Fear'] } ]; function includes(value, term) { return String(value || '').toLowerCase().includes(term); } function score(rule, query) { const term = query.toLowerCase(); const tokens = term.split(/\s+/).filter(Boolean); const title = String(rule.title || '').toLowerCase(); const summary = String(rule.summary || '').toLowerCase(); const content = String(rule.content || '').toLowerCase(); const tags = (rule.tags || []).map(v => String(v).toLowerCase()); const aliases = (rule.aliases || []).map(v => String(v).toLowerCase()); const tokenHits = tokens.reduce((sum, token) => { return sum + (title.includes(token) ? 8 : 0) + (aliases.some(a => a.includes(token)) ? 6 : 0) + (tags.some(a => a.includes(token)) ? 4 : 0) + (summary.includes(token) ? 3 : 0) + (content.includes(token) ? 1 : 0); }, 0); return (title === term ? 100 : 0) + (aliases.includes(term) ? 90 : 0) + (title.startsWith(term) ? 50 : 0) + (includes(rule.title, term) ? 20 : 0) + (aliases.some(a => a.includes(term)) ? 18 : 0) + (tags.some(t => t.includes(term)) ? 15 : 0) + (includes(rule.summary, term) ? 8 : 0) + Number(rule.midgamePriority || 0) + tokenHits + (includes(rule.content, term) ? 1 : 0); } function search(query, limit = 5) { const term = query.toLowerCase(); const tokens = term.split(/\s+/).filter(Boolean); return rules .filter(rule => { const haystacks = [ rule.title, rule.summary, rule.content, rule.category, ...(rule.tags || []), ...(rule.aliases || []) ].map(v => String(v || '').toLowerCase()); return tokens.every(token => haystacks.some(v => v.includes(token))) || haystacks.some(v => v.includes(term)); }) .sort((a, b) => score(b, query) - score(a, query)) .slice(0, limit); } let failures = 0; console.log('Mid-play rules lookup simulation\n'); for (const scenario of scenarios) { const results = search(scenario.query); const titles = results.map(r => r.title); const ok = scenario.expected.some(expected => titles.slice(0, 3).includes(expected)); if (!ok) failures += 1; console.log(`${scenario.actor}: ${scenario.moment}`); console.log(` Query: ${scenario.query}`); console.log(` Top results: ${titles.slice(0, 3).join(' | ') || '(none)'}`); if (results[0]) { const page = results[0].page ? ` p.${results[0].page}${results[0].pageEnd && results[0].pageEnd !== results[0].page ? `-${results[0].pageEnd}` : ''}` : ''; console.log(` Opens: ${results[0].sourceAbbr || results[0].source}${page} [${results[0].sourceMethod}, ${Math.round(Number(results[0].confidence || 0) * 100)}%]`); } console.log(` ${ok ? 'PASS' : 'FAIL'}\n`); } if (failures) { console.error(`${failures} mid-play lookup scenario(s) failed.`); process.exit(1); } console.log('All mid-play lookup scenarios passed.');