Files
MatchMaster/tools/test-spawn-gate-rectangle.cjs
T
2026-10-21 10:13:21 +08:00

138 lines
5.6 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const { loadMembers } = require('./new-player-ab-test-support.cjs');
const MapController = loadMembers('assets/Script/Map.ts', [
'validateSpawnGateConfigs', 'validateCreatedSpawnGateBatch', 'isSpawnGateAreaEmpty',
]);
function fixture(width = 12, height = 12) {
return Object.assign(new MapController(), {
mapWidth: width + 2,
mapHeight: height + 2,
Block_Array: Array.from({ length: 23 }, () => ({})),
mapBlocksWall: Array.from({ length: width + 2 }, () =>
Array.from({ length: height + 2 }, () => {
const cell = { block_Id: '' };
return { getComponent: () => cell };
})),
});
}
function gate(width, height, x = 1, y = 1) {
return {
id: 'rectangle', x, y, width, height,
spawnQueue: [{ members: [{ block: 0, type: 0, position: { x: 0, y: 0 } }] }],
};
}
function block(x, y, offsets = [[0, 0]]) {
return {
getComponent: () => ({
posX: x, posY: y,
allBlocks: offsets.map(([dx, dy]) => ({ x: dx, y: dy })),
}),
};
}
test('spawn config accepts old squares and arbitrary rectangles that fit the map', () => {
const map = fixture();
for (const [width, height] of [[2, 2], [3, 3], [1, 1], [1, 10], [10, 1], [2, 5], [6, 3], [10, 10], [11, 2]]) {
const config = gate(width, height);
const before = JSON.stringify(config);
assert.doesNotThrow(() => map.validateSpawnGateConfigs([config]), `${width}x${height}`);
assert.equal(JSON.stringify(config), before, 'validation must preserve queue and config data');
}
});
test('both spawn dimensions must be positive integers', () => {
const map = fixture();
for (const dimension of ['width', 'height']) {
for (const value of [0, -1, 1.5, NaN, Infinity, '2', undefined]) {
const config = gate(2, 5);
config[dimension] = value;
assert.throws(() => map.validateSpawnGateConfigs([config]), /positive integers/,
`${dimension}=${String(value)}`);
}
}
});
test('rectangles may reach the playable edge but cannot cross it', () => {
const map = fixture(12, 8);
assert.doesNotThrow(() => map.validateSpawnGateConfigs([gate(11, 7, 2, 2)]));
for (const config of [
gate(11, 7, 3, 2), gate(11, 7, 2, 3),
gate(2, 5, 0, 1), gate(2, 5, 1, 0), gate(2, 5, 1.5, 1),
]) {
assert.throws(() => map.validateSpawnGateConfigs([config]), /outside the playable map/);
}
});
test('rectangular regions may share an edge but may not share a cell', () => {
const map = fixture();
const tall = gate(2, 5);
assert.doesNotThrow(() => map.validateSpawnGateConfigs([
tall, gate(6, 3, 3, 1), gate(2, 1, 1, 6),
]));
assert.throws(() => map.validateSpawnGateConfigs([tall, gate(6, 3, 2, 3)]), /overlap/);
});
test('rectangular size support preserves existing batch validation', () => {
const map = fixture();
const cases = [
[config => { delete config.spawnQueue; }, /has no spawnQueue/],
[config => { config.spawnQueue[0].members = []; }, /empty batch/],
[config => { config.spawnQueue[0].members[0].block = 23; }, /invalid block prefab index/],
[config => { config.spawnQueue[0].members[0].position.x = NaN; }, /invalid block position/],
[config => { config.spawnQueue[0].members[0].type = 1; }, /stacking block has no stacking color/],
[config => { config.spawnQueue[0].members[0].type = 9; }, /adhesive blocks must be paired/],
];
for (const [mutate, error] of cases) {
const config = gate(2, 5);
mutate(config);
assert.throws(() => map.validateSpawnGateConfigs([config]), error);
}
});
test('created members use the complete rectangular region and full shape footprint', () => {
const map = fixture();
const tall = gate(2, 5, 2, 3);
assert.doesNotThrow(() => map.validateCreatedSpawnGateBatch([
block(2, 3, [[0, 0], [1, 0]]), block(2, 7, [[0, 0], [1, 0]]),
], tall, 0));
const wide = gate(6, 3, 2, 3);
assert.doesNotThrow(() => map.validateCreatedSpawnGateBatch([
block(2, 3), block(7, 5, [[0, 0], [-1, 0], [0, -1]]),
], wide, 0));
for (const outside of [
block(2, 3, [[0, 0], [-1, 0]]),
block(3, 3, [[0, 0], [1, 0]]),
block(2, 3, [[0, 0], [0, -1]]),
block(2, 7, [[0, 0], [0, 1]]),
]) {
assert.throws(() => map.validateCreatedSpawnGateBatch([outside], tall, 0), /member exceeds gate area/);
}
assert.throws(() => map.validateCreatedSpawnGateBatch([
block(2, 7, [[0, 0], [1, 0]]), block(3, 7),
], tall, 0), /members overlap/);
});
test('a rectangular gate waits for every cell to clear and ignores occupied cells outside', () => {
const map = fixture();
const config = gate(6, 3, 2, 4);
map.mapBlocksWall[1][4].getComponent().block_Id = 'outside';
map.mapBlocksWall[8][4].getComponent().block_Id = 'outside';
map.mapBlocksWall[2][3].getComponent().block_Id = 'outside';
map.mapBlocksWall[2][7].getComponent().block_Id = 'outside';
assert.equal(map.isSpawnGateAreaEmpty(config), true);
for (let x = 2; x < 8; x++) {
for (let y = 4; y < 7; y++) {
const cell = map.mapBlocksWall[x][y].getComponent();
cell.block_Id = 'moving-block';
assert.equal(map.isSpawnGateAreaEmpty(config), false, `${x},${y} still occupied`);
cell.block_Id = '';
assert.equal(map.isSpawnGateAreaEmpty(config), true, `${x},${y} cleared`);
}
}
});