MatchMaster/tools/count-level-blocks.cjs
2026-09-16 12:44:01 +08:00

101 lines
8.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Run from the project root: node tools/count-level-blocks.cjs
// Counts level-configured blocks, including stacked upper blocks and spawn queues.
const fs = require('node:fs');
const path = require('node:path');
const assert = require('node:assert/strict');
const root = path.resolve(__dirname, '..');
const out = path.join(root, 'reports', 'level-block-counts');
const read = p => fs.readFileSync(path.join(root, p), 'utf8');
const colors = ['蓝(月亮)', '紫(星星)', '浅绿(圆)', '米黄(方形)', '青(三叶)', '橙(三角)', '深绿(菱形)', '黄(六边形)', '粉(爱心)', '红(皇冠)', '灰(障碍)'];
const blockCode = read('assets/Script/Block.ts');
const shapeCode = blockCode.split(' initBlocks() {')[1].split('\n }')[0];
const shapes = [...shapeCode.matchAll(/case (\d+):([\s\S]*?)break;/g)].map(m => {
const points = [...m[2].matchAll(/cc\.v2\((-?\d+), (-?\d+)\)/g)].map(p => [Number(p[1]), Number(p[2])]);
const xs = points.map(p => p[0]), ys = points.map(p => p[1]);
const width = Math.max(...xs) - Math.min(...xs) + 1;
const height = Math.max(...ys) - Math.min(...ys) + 1;
const diagram = [];
for (let y = Math.max(...ys); y >= Math.min(...ys); y--) {
let row = '';
for (let x = Math.min(...xs); x <= Math.max(...xs); x++) row += points.some(p => p[0] === x && p[1] === y) ? '■' : '·';
diagram.push(row);
}
return { id: Number(m[1]), width, height, cells: points.length, diagram: diagram.join('/') };
});
assert.equal(shapes.length, 23);
const fresh = () => ({ levels: 0, raw: 0, barriers: 0, initialUpper: 0, spawnBase: 0, spawnUpper: 0, gates: 0, cycles: 0, matrix: Array.from({ length: 23 }, () => Array(11).fill(0)) });
const results = {}, levelDetails = [];
for (const dir of ['Json', 'Json2']) {
const total = fresh();
const files = fs.readdirSync(path.join(root, 'assets/custom', dir)).filter(f => /^level\d+\.json$/.test(f)).sort((a, b) => Number(a.match(/\d+/)[0]) - Number(b.match(/\d+/)[0]));
total.levels = files.length;
const ids = files.map(f => Number(f.match(/\d+/)[0]));
total.missing = Array.from({ length: Math.max(...ids) }, (_, i) => i + 1).filter(id => !ids.includes(id));
for (const file of files) {
const data = JSON.parse(read(`assets/custom/${dir}/${file}`));
assert.equal(data.BLOCK_INFO.length, 1, file);
const item = fresh();
function add(block, stage) {
assert.ok(Number.isInteger(block.block) && block.block >= 0 && block.block <= 23, file);
if (stage === 'initial') item.raw++;
else item.spawnBase++;
if (block.block === 23) { item.barriers++; return; }
let color = block.color;
if (block.colorArray != null && String(block.colorArray).length) {
color = Number(String(block.colorArray)[0]) + 1;
item.cycles++;
assert.equal(color, block.color, `${file}: initial cycle color differs`);
}
assert.ok(Number.isInteger(color) && color >= 1 && color <= 11, file);
item.matrix[block.block][color - 1]++;
if (block.type === 1) {
assert.ok(Number.isInteger(block.stacking) && block.stacking >= 1 && block.stacking <= 11, file);
item.matrix[block.block][block.stacking - 1]++;
item[stage === 'initial' ? 'initialUpper' : 'spawnUpper']++;
}
}
data.BLOCK_INFO[0].forEach(b => add(b, 'initial'));
for (const level of data.LEVEL_INFO) for (const gate of level.spawnGates || []) {
item.gates++;
for (const batch of gate.spawnQueue) batch.members.forEach(b => add(b, 'spawn'));
}
for (const key of ['raw', 'barriers', 'initialUpper', 'spawnBase', 'spawnUpper', 'gates', 'cycles']) total[key] += item[key];
for (let s = 0; s < 23; s++) for (let c = 0; c < 11; c++) total.matrix[s][c] += item.matrix[s][c];
levelDetails.push({ source: dir, file, ...item });
}
results[dir] = total;
}
const all = fresh();
for (const result of Object.values(results)) {
for (const key of ['levels', 'raw', 'barriers', 'initialUpper', 'spawnBase', 'spawnUpper', 'gates', 'cycles']) all[key] += result[key];
for (let s = 0; s < 23; s++) for (let c = 0; c < 11; c++) all.matrix[s][c] += result.matrix[s][c];
}
results.all = all;
for (const result of [...Object.values(results), ...levelDetails]) {
result.colorTotals = colors.map((_, c) => result.matrix.reduce((n, row) => n + row[c], 0));
result.shapeTotals = result.matrix.map(row => row.reduce((a, b) => a + b, 0));
result.blocks = result.colorTotals.reduce((a, b) => a + b, 0);
result.includingBarriers = result.blocks + result.barriers;
assert.equal(result.blocks, result.shapeTotals.reduce((a, b) => a + b, 0));
assert.equal(result.includingBarriers, result.raw + result.initialUpper + result.spawnBase + result.spawnUpper);
}
const table = (heads, rows) => ['| ' + heads.join(' | ') + ' |', '| ' + heads.map(() => '---').join(' | ') + ' |', ...rows.map(row => '| ' + row.join(' | ') + ' |')].join('\n');
const n = value => value.toLocaleString('en-US');
const labels = { Json: '主关卡 Json', Json2: '附加数据 Json2', all: '全部文件合计' };
let md = '# 所有关卡方块统计\n\n统计日期:2026-09-15。数据为本地工作区当前文件。\n\n';
md += '## 统计口径\n\n- 主关卡来自 `assets/custom/Json/level*.json`;当前游戏加载路径为 `custom` 分包下的 `Json/levelN`。`Json2` 的 50 个文件单列,全部文件合计包含两套数据,不按相同关卡编号去重。\n- 数量单位是“块”:每个形状整体算一块,叠加块上下层各算一块。包含特殊块、灰色障碍块及生成门队列里的全部方块(假定队列全部生成)。不按占用格数累加。\n- 变色块按初始颜色计一次;问号块按配置底色计数,揭示前的问号外壳不另算一块。\n- `block=23` 由 Barrier 组件创建,其 `color` 字段不用于设置颜色,因此单列为障碍物,不归入彩色方块。门、地板覆盖层、花瓣等附属物不另计为方块。\n- 颜色名称依据实际贴图外观,并附图案便于核对;`BlockColor` 枚举名称与当前贴图不符,未使用该枚举命名。灰色 `color=11` 使用 `0color` 贴图。\n- 规格为宽×高(格),不同朝向按 `block` 编号分开;形状示意中的 `/` 表示换行,`■` 为占用格,`·` 为空格。\n\n';
md += '## 总量核对\n\n' + table(['数据集', '关卡文件', '配置记录', '叠加上层', '生成队列记录', '队列叠加上层', '方块(0–22)', '障碍物(23)', '含障碍物总量'], Object.entries(results).map(([key, r]) => [labels[key], ...[r.levels, r.raw, r.initialUpper, r.spawnBase, r.spawnUpper, r.blocks, r.barriers, r.includingBarriers].map(n)])) + '\n\n';
md += `主关卡编号范围 1–1957,缺少 ${results.Json.missing.join('、')};仅统计实际存在的 ${results.Json.levels} 个文件。\n\n`;
md += '## 按颜色汇总(含叠加层和生成队列,不含 block=23)\n\n' + table(['color', '贴图颜色/图案', '主关卡 Json', 'Json2', '全部文件合计'], colors.map((color, c) => [c + 1, color, ...Object.values(results).map(r => n(r.colorTotals[c]))])) + '\n\n';
for (const key of ['all', 'Json', 'Json2']) {
const r = results[key];
md += `## ${labels[key]}:颜色 × 规格\n\n`;
md += table(['block', '规格', '占格', '形状', ...colors.map((c, i) => `${i + 1} ${c.split('(')[0]}`), '合计'], shapes.map(s => [s.id, `${s.width}×${s.height}`, s.cells, '`' + s.diagram + '`', ...r.matrix[s.id].map(n), n(r.shapeTotals[s.id])]).concat([['合计', '', '', '', ...r.colorTotals.map(n), n(r.blocks)]])) + `\n\n另有 block=23 障碍物 **${n(r.barriers)}** 个。\n\n`;
}
md += '## 核对依据\n\n- `assets/Script/Map.ts`:`blockInit` 的叠加层创建、`createSpawnGateBatch` 的队列创建。\n- `assets/Script/Block.ts`:`initColor` 的初始颜色、`initBlocks` 的 23 种形状占格。\n- `assets/Script/Barrier.ts`:block=23 不按颜色字段换图。\n- `assets/Block/0color0.png` 至 `10color0.png`:颜色及图案。\n- 校验结果:全部 JSON 成功解析;所有颜色、规格编号通过检查;每关以及每套数据的行列合计一致;配置记录 + 叠加上层 + 生成队列记录 + 队列上层 = 含障碍物总量。\n';
fs.mkdirSync(out, { recursive: true });
fs.writeFileSync(path.join(out, '所有关卡方块统计.md'), md);
fs.writeFileSync(path.join(out, 'counts.json'), JSON.stringify({ colors, shapes, results, levelDetails }, null, 2));
console.log(JSON.stringify(Object.fromEntries(Object.entries(results).map(([k, { matrix, ...rest }]) => [k, rest])), null, 2));