- Add midgame priority ranking for rules (midgamePriority field) - Enhance rules search to support midgame filtering and sorting - Improve mission simulation and midgame tracking - Fix RequisitionShop player name handling - Add rules midplay simulation tests - Update MissionTab with midgame scene tracking - Add backend logging for rules updates
140 lines
4.4 KiB
JavaScript
140 lines
4.4 KiB
JavaScript
#!/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.');
|