feat: rules midgame priority system, mission enhancements, and requisition shop improvements
- 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
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
#!/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.');
|
||||
Reference in New Issue
Block a user