- ACT_COUNT/RUN_LENGTH 3→5, ACT_MAPS map01~map05 (반복 런 기반 확장) - MAP_ROWS 7→6 (걷는 행 6 + 보스 = depth 최대 7), 막 배율 0.6→0.45 완화 - 노드 타입 인접 금지를 elite 단독 → rest/shop/elite 3종으로 일반화 (Lua GenerateMap + rogue-map.mjs JS 미러 동시 수정, 테스트 9/9 통과) - 맵 파일 생성기 카운트 11→5, map06~map11 삭제, SectorConfig 정리(stale 제거) - 산출물 재생성(ui/codeblock/map01~05). 검증 헬퍼 tools/verify/count.mjs 추가 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
123 lines
4.3 KiB
JavaScript
123 lines
4.3 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { generateMap, ROWS, COLS } from './rogue-map.mjs';
|
|
import { mulberry32 } from '../balance/sim-balance.mjs';
|
|
|
|
function gen(seed) {
|
|
return generateMap(mulberry32(seed));
|
|
}
|
|
|
|
test('결정성: 동일 시드 동일 맵', () => {
|
|
assert.deepEqual(gen(1), gen(1));
|
|
assert.deepEqual(gen(42), gen(42));
|
|
});
|
|
|
|
test('시작 노드 2개 이상 (경로 1·2 시작열 상이)', () => {
|
|
for (let s = 1; s <= 30; s++) {
|
|
const { start } = gen(s);
|
|
assert.ok(start.length >= 2, `seed ${s}: start ${start.length}개`);
|
|
assert.equal(new Set(start).size, start.length);
|
|
}
|
|
});
|
|
|
|
test('연결성: 모든 노드가 시작점에서 도달 가능하고 boss에 도달함', () => {
|
|
for (let s = 1; s <= 30; s++) {
|
|
const { nodes, start } = gen(s);
|
|
// 시작점에서 전방 BFS
|
|
const fwd = new Set(start);
|
|
const queue = [...start];
|
|
while (queue.length) {
|
|
const id = queue.shift();
|
|
for (const nid of nodes[id].next) {
|
|
if (!fwd.has(nid)) { fwd.add(nid); queue.push(nid); }
|
|
}
|
|
}
|
|
for (const id of Object.keys(nodes)) {
|
|
assert.ok(fwd.has(id), `seed ${s}: ${id} 시작점에서 도달 불가`);
|
|
}
|
|
// boss에서 역방향 BFS
|
|
const back = new Set(['boss']);
|
|
let changed = true;
|
|
while (changed) {
|
|
changed = false;
|
|
for (const [id, n] of Object.entries(nodes)) {
|
|
if (!back.has(id) && n.next.some((x) => back.has(x))) { back.add(id); changed = true; }
|
|
}
|
|
}
|
|
for (const id of Object.keys(nodes)) {
|
|
assert.ok(back.has(id), `seed ${s}: ${id}에서 boss 도달 불가`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('타입 규칙: 1~2행 combat만, elite·treasure는 4행부터, shop·rest는 3행부터', () => {
|
|
for (let s = 1; s <= 50; s++) {
|
|
const { nodes } = gen(s);
|
|
for (const [id, n] of Object.entries(nodes)) {
|
|
if (n.row <= 2) assert.equal(n.type, 'combat', `seed ${s}: ${id} (row ${n.row}) = ${n.type}`);
|
|
if (n.type === 'elite' || n.type === 'treasure') assert.ok(n.row >= 4, `seed ${s}: ${id} ${n.type} row ${n.row}`);
|
|
if (n.type === 'shop' || n.type === 'rest') assert.ok(n.row >= 3, `seed ${s}: ${id} ${n.type} row ${n.row}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('boss: row 7 단일 노드, 마지막 걷는 행 노드는 전부 boss로 연결', () => {
|
|
for (let s = 1; s <= 30; s++) {
|
|
const { nodes } = gen(s);
|
|
const bosses = Object.entries(nodes).filter(([, n]) => n.type === 'boss');
|
|
assert.equal(bosses.length, 1, `seed ${s}`);
|
|
assert.equal(bosses[0][0], 'boss');
|
|
assert.equal(bosses[0][1].row, ROWS + 1);
|
|
for (const [id, n] of Object.entries(nodes)) {
|
|
if (n.row === ROWS) {
|
|
assert.deepEqual(n.next, ['boss'], `seed ${s}: ${id}`);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
test('간선 제약: row+1로만, 열 차이 1 이하 (boss 간선 제외)', () => {
|
|
for (let s = 1; s <= 30; s++) {
|
|
const { nodes } = gen(s);
|
|
for (const [id, n] of Object.entries(nodes)) {
|
|
for (const nid of n.next) {
|
|
if (nid === 'boss') continue;
|
|
const t = nodes[nid];
|
|
assert.equal(t.row, n.row + 1, `seed ${s}: ${id}→${nid}`);
|
|
assert.ok(Math.abs(t.col - n.col) <= 1, `seed ${s}: ${id}→${nid} 열 차이 ${Math.abs(t.col - n.col)}`);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
test('연속 금지: rest/shop/elite 는 부모와 같은 타입을 자식으로 두지 않음', () => {
|
|
const NO_REPEAT = new Set(['rest', 'shop', 'elite']);
|
|
for (let s = 1; s <= 100; s++) {
|
|
const { nodes } = gen(s);
|
|
for (const [id, n] of Object.entries(nodes)) {
|
|
if (!NO_REPEAT.has(n.type)) continue;
|
|
for (const nid of n.next) {
|
|
if (nid === 'boss') continue;
|
|
assert.notEqual(nodes[nid].type, n.type, `seed ${s}: ${id}(${n.type}) → ${nid}(${n.type})`);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
test('그리드 범위: 행 1..6, 열 1..4 (boss 제외)', () => {
|
|
const { nodes } = gen(7);
|
|
for (const [id, n] of Object.entries(nodes)) {
|
|
if (id === 'boss') continue;
|
|
assert.ok(n.row >= 1 && n.row <= ROWS);
|
|
assert.ok(n.col >= 1 && n.col <= COLS);
|
|
assert.equal(id, `r${n.row}c${n.col}`);
|
|
}
|
|
});
|
|
|
|
test('다양성: 서로 다른 시드는 (대체로) 다른 맵', () => {
|
|
const a = JSON.stringify(gen(1));
|
|
let diff = 0;
|
|
for (let s = 2; s <= 11; s++) if (JSON.stringify(gen(s)) !== a) diff++;
|
|
assert.ok(diff >= 9, `10개 중 ${diff}개만 상이`);
|
|
});
|