'use strict'; const test = require('node:test'); const assert = require('node:assert'); const { ExpansionPlanner } = require('../src/core/expansionPlanner'); const { TerritoryModel } = require('../src/core/territoryModel'); const { defaultConfig } = require('../src/config/expansionConfig'); const chunkPos = require('../src/core/chunkPos'); /** * 构造领土模型。 * @param {Record>} states 邦国名 → 区块坐标列表 * @param {string} [selfName] 己方邦国名 */ function makeModel(states, selfName) { const map = new Map(); for (const [name, coords] of Object.entries(states)) { const chunks = new Set(coords.map(([x, z]) => chunkPos.key(x, z))); map.set(name, { name, capital: { x: coords[0][0], z: coords[0][1] }, chunks, color: 0x75bd20 }); } const model = new TerritoryModel(map); if (selfName) model.setSelfStateName(selfName); return model; } const planner = new ExpansionPlanner(); test('策略 1:只返回与己方领土相邻的未占领区块', () => { const model = makeModel({ A: [[0, 0]] }, 'A'); const config = Object.assign(defaultConfig(), { strategy: 1 }); const result = planner.generate(model, config, { x: 0, z: 0 }); // 接壤只算四个正方向,因此单个己方区块只有 4 个合法邻居(对角线不算) assert.strictEqual(result.length, 4, '单个己方区块应产生 4 个正方向邻居候选'); for (const chunk of result) { assert.strictEqual( Math.abs(chunk.x) + Math.abs(chunk.z), 1, `候选 ${chunk.x},${chunk.z} 不是 (0,0) 的正方向邻居` ); } }); test('策略 1:绝不选中「只与其他邦国相邻」的区块(历史 bug 回归)', () => { // 己方只有 (0,0);其他邦国占据 (1,0) 与 (1,1) const model = makeModel({ A: [[0, 0]], B: [[1, 0], [1, 1]] }, 'A'); const config = Object.assign(defaultConfig(), { strategy: 1 }); const result = planner.generate(model, config, { x: 0, z: 0 }); const keys = result.map((c) => chunkPos.key(c.x, c.z)); assert.ok(!keys.includes(chunkPos.key(1, 0)), '不能选中已被 B 占领的区块'); assert.ok(!keys.includes(chunkPos.key(1, 1)), '不能选中已被 B 占领的区块'); // (2,0) 只与 B 的 (1,0) 相邻,与 A 不相邻,必须被排除 assert.ok(!keys.includes(chunkPos.key(2, 0)), '不能选中只与他国相邻的区块'); for (const chunk of result) { const adjacentToOwn = chunkPos.cardinalNeighbors(chunk.x, chunk.z) .some((n) => n.x === 0 && n.z === 0); assert.ok(adjacentToOwn, `候选 ${chunk.x},${chunk.z} 必须与己方领土接壤`); } }); test('策略 1:矩形内部的空洞优先于外部边界', () => { // 己方占 3×3 外圈,中心 (1,1) 是空洞 const ring = []; for (let x = 0; x <= 2; x++) { for (let z = 0; z <= 2; z++) { if (x === 1 && z === 1) continue; ring.push([x, z]); } } const model = makeModel({ A: ring }, 'A'); const config = Object.assign(defaultConfig(), { strategy: 1 }); const result = planner.generate(model, config, { x: 1, z: 1 }); assert.deepStrictEqual(result[0], { x: 1, z: 1 }, '内部空洞应排在最前'); }); test('策略 1:maxRings 影响搜索圈数(扩张轮数为 0 时只看第一圈)', () => { const model = makeModel({ A: [[0, 0]] }, 'A'); const config = Object.assign(defaultConfig(), { strategy: 1, maxRings: 1 }); const result = planner.generate(model, config, { x: 0, z: 0 }); // maxRings=1 → maxExpansions=2,第一圈就能找到候选,结果仍是 4 个正方向邻居 assert.strictEqual(result.length, 4); }); test('策略 2:矩形扩地严格按 S 形蛇形扫描', () => { const model = makeModel({ A: [[0, 0]] }, 'A'); const config = Object.assign(defaultConfig(), { strategy: 2, regionMinX: 0, regionMaxX: 3, regionMinZ: 0, regionMaxZ: 3 }); const result = planner.generate(model, config, { x: 0, z: 0 }); assert.strictEqual(result.length, 15, '4×4 区域去掉 1 个己方区块应为 15 个'); // 王城在 (0,0):起始边为 z=0(北),行内从 x 小到大 assert.deepStrictEqual(result[0], { x: 1, z: 0 }); assert.deepStrictEqual(result[1], { x: 2, z: 0 }); assert.deepStrictEqual(result[2], { x: 3, z: 0 }); // 第二行反向(蛇形) assert.deepStrictEqual(result[3], { x: 3, z: 1 }); assert.deepStrictEqual(result[4], { x: 2, z: 1 }); assert.deepStrictEqual(result[5], { x: 1, z: 1 }); assert.deepStrictEqual(result[6], { x: 0, z: 1 }); // 第三行再次反向 assert.deepStrictEqual(result[7], { x: 0, z: 2 }); }); test('策略 2:每个入选区块都与己方领土(含本次已选)接壤', () => { const model = makeModel({ A: [[0, 0]] }, 'A'); const config = Object.assign(defaultConfig(), { strategy: 2, regionMinX: -2, regionMaxX: 4, regionMinZ: -2, regionMaxZ: 4 }); const result = planner.generate(model, config, { x: 0, z: 0 }); const claimed = new Set([chunkPos.key(0, 0)]); for (const chunk of result) { const adjacent = chunkPos.cardinalNeighbors(chunk.x, chunk.z) .some((n) => claimed.has(chunkPos.key(n.x, n.z))); assert.ok(adjacent, `区块 ${chunk.x},${chunk.z} 必须与己方领土接壤`); claimed.add(chunkPos.key(chunk.x, chunk.z)); } }); test('策略 2:跳过他国区块但不打乱 S 形顺序', () => { // 己方 (0,0)(1,0),他国占 (1,1),区域 0..3 × 0..1 const model = makeModel({ A: [[0, 0], [1, 0]], B: [[1, 1]] }, 'A'); const config = Object.assign(defaultConfig(), { strategy: 2, regionMinX: 0, regionMaxX: 3, regionMinZ: 0, regionMaxZ: 1 }); const result = planner.generate(model, config, { x: 0, z: 0 }); const keys = result.map((c) => `${c.x},${c.z}`); assert.ok(!keys.includes('1,1'), '他国区块必须跳过'); // S 形顺序保持不变:第一行向右扫,第二行向左扫,(1,1) 被跳过但顺序不被打乱; // 末尾的 (0,1) 因为与己方 (0,0) 正方向接壤而被选中 assert.deepStrictEqual(keys, ['2,0', '3,0', '3,1', '2,1', '0,1']); }); test('策略 2:他国区块会切断连通,其后不可达区块不入选', () => { // 己方只有 (0,0),他国占 (2,0):x=3 一列与他国相邻但离己方太远,必须排除 const model = makeModel({ A: [[0, 0]], B: [[2, 0]] }, 'A'); const config = Object.assign(defaultConfig(), { strategy: 2, regionMinX: 0, regionMaxX: 3, regionMinZ: 0, regionMaxZ: 0 }); const result = planner.generate(model, config, { x: 0, z: 0 }); assert.deepStrictEqual(result.map((c) => `${c.x},${c.z}`), ['1,0']); }); test('策略 3:按与玩家的距离由近到远排序', () => { const model = makeModel({ A: [[0, 0]] }, 'A'); const config = Object.assign(defaultConfig(), { strategy: 3 }); const playerChunk = { x: 5, z: 5 }; const result = planner.generate(model, config, playerChunk); assert.strictEqual(result.length, 4); const distances = result.map((c) => chunkPos.manhattan(c, playerChunk)); for (let i = 1; i < distances.length; i++) { assert.ok(distances[i] >= distances[i - 1], '距离必须非递减'); } assert.strictEqual(distances[0], 9, '最近的邻居距离应为 9'); }); test('策略 4:绕国一圈按方位角均匀排序', () => { const model = makeModel({ A: [[0, 0], [1, 0], [0, 1], [1, 1]] }, 'A'); const config = Object.assign(defaultConfig(), { strategy: 4, maxRings: 1 }); const result = planner.generate(model, config, { x: 0, z: 0 }); assert.strictEqual(result.length, 8, '2×2 领土外圈应有 8 个候选'); // 质心 (0,0):方位角从 -π/2 起 assert.deepStrictEqual(result[0], { x: 0, z: -1 }); assert.deepStrictEqual(result[1], { x: 1, z: -1 }); }); test('策略 4:多圈扩张时逐圈向外推进', () => { const model = makeModel({ A: [[0, 0]] }, 'A'); const config = Object.assign(defaultConfig(), { strategy: 4, maxRings: 2 }); const result = planner.generate(model, config, { x: 0, z: 0 }); assert.ok(result.length > 4, '两圈应超过第一圈的 4 个候选'); const firstRing = result.slice(0, 4).every((c) => Math.abs(c.x) + Math.abs(c.z) === 1); assert.ok(firstRing, '前 4 个应是第一圈的四个邻居'); }); test('prefixFlyingChunks:飞地优先且去重、受目标数量限制', () => { const ring = []; for (let x = 0; x <= 2; x++) { for (let z = 0; z <= 2; z++) { if (x === 1 && z === 1) continue; ring.push([x, z]); } } const model = makeModel({ A: ring }, 'A'); const config = Object.assign(defaultConfig(), { strategy: 3, targetCount: 3 }); const planned = [{ x: 3, z: 0 }, { x: 3, z: 1 }, { x: 3, z: 2 }]; const result = planner.prefixFlyingChunks(model, config, { x: 0, z: 0 }, planned); assert.deepStrictEqual(result[0], { x: 1, z: 1 }, '飞地必须排在最前'); assert.strictEqual(result.length, 3, '不得超过 targetCount'); const keys = result.map((c) => `${c.x},${c.z}`); assert.strictEqual(new Set(keys).size, keys.length, '结果不得重复'); }); test('prefixFlyingChunks:区域外的飞地不被优先填入', () => { const ring = []; for (let x = 0; x <= 2; x++) { for (let z = 0; z <= 2; z++) { if (x === 1 && z === 1) continue; ring.push([x, z]); } } const model = makeModel({ A: ring }, 'A'); const config = Object.assign(defaultConfig(), { strategy: 2, regionMinX: 10, regionMaxX: 12, regionMinZ: 10, regionMaxZ: 12 }); const planned = [{ x: 10, z: 10 }]; const result = planner.prefixFlyingChunks(model, config, { x: 0, z: 0 }, planned); assert.deepStrictEqual(result, [{ x: 10, z: 10 }], '区域外的飞地应被过滤掉'); }); test('isInRegion:非策略 2 恒为 true,策略 2 支持矩形与中心+半径', () => { const base = defaultConfig(); assert.strictEqual(planner.isInRegion({ x: 999, z: -999 }, Object.assign(base, { strategy: 1 })), true); const rect = Object.assign(defaultConfig(), { strategy: 2, regionMinX: 0, regionMaxX: 2, regionMinZ: 0, regionMaxZ: 2 }); assert.strictEqual(planner.isInRegion({ x: 2, z: 2 }, rect), true); assert.strictEqual(planner.isInRegion({ x: 3, z: 2 }, rect), false); const circle = Object.assign(defaultConfig(), { strategy: 2, regionMinX: 0, regionMaxX: -1, regionCenterX: 0, regionCenterZ: 0, regionRadius: 2 }); assert.strictEqual(planner.isInRegion({ x: 2, z: -2 }, circle), true); assert.strictEqual(planner.isInRegion({ x: 3, z: 0 }, circle), false); }); test('己方领土为空时不做任何规划', () => { const model = makeModel({ B: [[5, 5]] }, 'A'); const config = defaultConfig(); for (const strategy of [1, 2, 3, 4]) { config.strategy = strategy; const result = planner.generate(model, config, { x: 0, z: 0 }); assert.deepStrictEqual(result, [], `策略 ${strategy} 在己方无领土时应返回空`); } }); test('规划结果始终在世界边界内', () => { const model = makeModel({ A: [[1279, 1279]] }, 'A'); const config = Object.assign(defaultConfig(), { strategy: 1 }); const result = planner.generate(model, config, { x: 1279, z: 1279 }); for (const chunk of result) { assert.ok(chunkPos.withinWorldBorder(chunk.x, chunk.z), `${chunk.x},${chunk.z} 越界`); } });