mirror of
https://github.com/unanmed/ginka-generator.git
synced 2026-05-23 20:41:12 +08:00
refactor: 新的筛选算法
This commit is contained in:
parent
c64a783d5e
commit
ef79dd4d31
@ -254,14 +254,16 @@ const labelConfig: IAutoLabelConfig = {
|
|||||||
entry: 10
|
entry: 10
|
||||||
},
|
},
|
||||||
allowedSize: [[13, 13]],
|
allowedSize: [[13, 13]],
|
||||||
allowUselessBranch: true,
|
allowUselessBranch: false,
|
||||||
maxWallDensityStd: 0.23,
|
maxWallDensityStd: 1,
|
||||||
|
maxEmptyArea: 8,
|
||||||
|
maxResourceArea: 8,
|
||||||
minEnemyRatio: 0.02,
|
minEnemyRatio: 0.02,
|
||||||
maxEnemyRatio: 0.3,
|
maxEnemyRatio: 0.3,
|
||||||
minWallRatio: 0.2,
|
minWallRatio: 0.2,
|
||||||
maxWallRatio: 0.6,
|
maxWallRatio: 0.6,
|
||||||
minResourceRatio: 0.02,
|
minResourceRatio: 0.05,
|
||||||
maxResourceRatio: 0.3,
|
maxResourceRatio: 0.25,
|
||||||
minDoorRatio: 0,
|
minDoorRatio: 0,
|
||||||
maxDoorRatio: 0.12,
|
maxDoorRatio: 0.12,
|
||||||
minFishCount: 0,
|
minFishCount: 0,
|
||||||
@ -269,15 +271,15 @@ const labelConfig: IAutoLabelConfig = {
|
|||||||
minEntryCount: 1,
|
minEntryCount: 1,
|
||||||
maxEntryCount: 4,
|
maxEntryCount: 4,
|
||||||
guassainRadius: 0,
|
guassainRadius: 0,
|
||||||
heatmapKernel: 0,
|
heatmapKernel: 1,
|
||||||
ignoreIssues: true,
|
ignoreIssues: true,
|
||||||
customTowerFilter: info => {
|
customTowerFilter: info => {
|
||||||
// if (info.name !== 'Apeiria') {
|
// if (info.name !== 'Apeiria') {
|
||||||
// return false;
|
// return false;
|
||||||
// }
|
// }
|
||||||
if (info.color !== TowerColor.Blue && info.color !== TowerColor.Green) {
|
// if (info.color !== TowerColor.Blue && info.color !== TowerColor.Green) {
|
||||||
return false;
|
// return false;
|
||||||
}
|
// }
|
||||||
if (info.people < 1000) {
|
if (info.people < 1000) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -290,26 +292,26 @@ const labelConfig: IAutoLabelConfig = {
|
|||||||
if (info.name.startsWith('24') && info.name.length > 2) {
|
if (info.name.startsWith('24') && info.name.length > 2) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (ignoredTower.includes(info.name)) {
|
// if (ignoredTower.includes(info.name)) {
|
||||||
return false;
|
// return false;
|
||||||
}
|
// }
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
customFloorFilter: floor => {
|
customFloorFilter: floor => {
|
||||||
if (floor.info.topo.graphs.length > 1) {
|
if (floor.info.topo.graph.areas.size > 1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (floor.data.hasCannotInOut) {
|
if (floor.data.hasCannotInOut) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (floor.info.topo.unreachable.size > 0) {
|
if (floor.info.topo.graph.unreachableArea.size > 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (ignoredFloor[floor.tower.name]?.includes(floor.mapId)) {
|
if (ignoredFloor[floor.tower.name]?.includes(floor.mapId)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (floor.tower.name === 'Apeiria') {
|
if (floor.tower.name === 'Apeiria') {
|
||||||
return Math.random() < 0.2;
|
return Math.random() < 0.1;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { IAutoLabelConfig, IConvertedMapInfo, ITowerInfo } from './types';
|
|||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { Presets, SingleBar } from 'cli-progress';
|
import { Presets, SingleBar } from 'cli-progress';
|
||||||
import { convertTowerMap, runTowerCode } from './tower';
|
import { convertTowerMap, runTowerCode } from './tower';
|
||||||
|
import { MapTileConverter } from './converter';
|
||||||
|
|
||||||
export interface ILabelResult {
|
export interface ILabelResult {
|
||||||
/** 塔信息列表 */
|
/** 塔信息列表 */
|
||||||
@ -31,6 +32,8 @@ export async function autoLabelTowers(
|
|||||||
|
|
||||||
// 统计被不同规则过滤掉的楼层
|
// 统计被不同规则过滤掉的楼层
|
||||||
let ignoredFloorsSize = 0;
|
let ignoredFloorsSize = 0;
|
||||||
|
let ignoredMaxEmptyArea = 0;
|
||||||
|
let ignoredMaxResourceArea = 0;
|
||||||
let ignoredFloorsEnemy = 0;
|
let ignoredFloorsEnemy = 0;
|
||||||
let ignoredFloorsWall = 0;
|
let ignoredFloorsWall = 0;
|
||||||
let ignoredFloorsResource = 0;
|
let ignoredFloorsResource = 0;
|
||||||
@ -76,6 +79,8 @@ export async function autoLabelTowers(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const converter = new MapTileConverter(result, config);
|
||||||
|
|
||||||
const info = towers.get(result.data.firstData.name);
|
const info = towers.get(result.data.firstData.name);
|
||||||
if (!info) continue;
|
if (!info) continue;
|
||||||
const customPass = config.customTowerFilter?.(info) ?? true;
|
const customPass = config.customTowerFilter?.(info) ?? true;
|
||||||
@ -94,8 +99,29 @@ export async function autoLabelTowers(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// 转换楼层
|
// 转换楼层
|
||||||
const converted = convertTowerMap(result, floor, config);
|
const converted = convertTowerMap(result, floor, config, converter);
|
||||||
const floorInfo = parseFloorInfo(info, converted.map, config);
|
const otherLayers = [];
|
||||||
|
if (floor.bgmap) {
|
||||||
|
otherLayers.push(floor.bgmap);
|
||||||
|
}
|
||||||
|
if (floor.bg2map) {
|
||||||
|
otherLayers.push(floor.bg2map);
|
||||||
|
}
|
||||||
|
if (floor.fgmap) {
|
||||||
|
otherLayers.push(floor.fgmap);
|
||||||
|
}
|
||||||
|
if (floor.fg2map) {
|
||||||
|
otherLayers.push(floor.fg2map);
|
||||||
|
}
|
||||||
|
const floorInfo = parseFloorInfo(
|
||||||
|
info,
|
||||||
|
floor.map,
|
||||||
|
converted.map,
|
||||||
|
otherLayers,
|
||||||
|
config,
|
||||||
|
converter,
|
||||||
|
name
|
||||||
|
);
|
||||||
const floorData: IConvertedMapInfo = {
|
const floorData: IConvertedMapInfo = {
|
||||||
data: converted,
|
data: converted,
|
||||||
tower: info,
|
tower: info,
|
||||||
@ -103,6 +129,14 @@ export async function autoLabelTowers(
|
|||||||
info: floorInfo
|
info: floorInfo
|
||||||
};
|
};
|
||||||
// 配置过滤楼层
|
// 配置过滤楼层
|
||||||
|
if (floorInfo.maxEmptyArea > config.maxEmptyArea) {
|
||||||
|
ignoredMaxEmptyArea++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (floorInfo.maxResourceArea > config.maxResourceArea) {
|
||||||
|
ignoredMaxResourceArea++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
floorInfo.enemyDensity < config.minEnemyRatio ||
|
floorInfo.enemyDensity < config.minEnemyRatio ||
|
||||||
floorInfo.enemyDensity > config.maxEnemyRatio
|
floorInfo.enemyDensity > config.maxEnemyRatio
|
||||||
@ -131,13 +165,6 @@ export async function autoLabelTowers(
|
|||||||
ignoredFloorsDoor++;
|
ignoredFloorsDoor++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
floorInfo.fishCount < config.minFishCount ||
|
|
||||||
floorInfo.fishCount > config.maxFishCount
|
|
||||||
) {
|
|
||||||
ignoredFloorsFish++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
floorInfo.entryCount < config.minEntryCount ||
|
floorInfo.entryCount < config.minEntryCount ||
|
||||||
floorInfo.entryCount > config.maxEntryCount
|
floorInfo.entryCount > config.maxEntryCount
|
||||||
@ -191,6 +218,8 @@ export async function autoLabelTowers(
|
|||||||
)} 层,过滤掉 ${totalFilted} 层:`
|
)} 层,过滤掉 ${totalFilted} 层:`
|
||||||
);
|
);
|
||||||
console.log(`尺寸过滤:${ignoredFloorsSize} 层`);
|
console.log(`尺寸过滤:${ignoredFloorsSize} 层`);
|
||||||
|
console.log(`空地过滤:${ignoredMaxEmptyArea} 层`);
|
||||||
|
console.log(`资源区域过滤:${ignoredMaxResourceArea} 层`);
|
||||||
console.log(`怪物过滤:${ignoredFloorsEnemy} 层`);
|
console.log(`怪物过滤:${ignoredFloorsEnemy} 层`);
|
||||||
console.log(`墙壁过滤:${ignoredFloorsWall} 层`);
|
console.log(`墙壁过滤:${ignoredFloorsWall} 层`);
|
||||||
console.log(`资源过滤:${ignoredFloorsResource} 层`);
|
console.log(`资源过滤:${ignoredFloorsResource} 层`);
|
||||||
|
|||||||
343
data/src/auto/converter.ts
Normal file
343
data/src/auto/converter.ts
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
import { sum } from 'lodash-es';
|
||||||
|
import { IAutoLabelConfig, ICodeRunResult } from './types';
|
||||||
|
import { CannotInOut, IMapTileConverter, ResourceType } from './types';
|
||||||
|
|
||||||
|
export class MapTileConverter implements IMapTileConverter {
|
||||||
|
private readonly tower: ICodeRunResult;
|
||||||
|
private readonly config: IAutoLabelConfig;
|
||||||
|
private readonly noPassMap = new Map<number, boolean>();
|
||||||
|
private readonly cannotInMap = new Map<number, number>();
|
||||||
|
private readonly cannotOutMap = new Map<number, number>();
|
||||||
|
private readonly labelMap = new Map<number, number>();
|
||||||
|
private readonly resourceMap = new Map<number, Map<ResourceType, number>>();
|
||||||
|
|
||||||
|
private readonly emptyTiles = new Set<number>([0]);
|
||||||
|
private readonly doorTiles = new Set<number>();
|
||||||
|
private readonly enemyTiles = new Set<number>();
|
||||||
|
private readonly resourceTiles = new Set<number>();
|
||||||
|
private readonly keyTiles = new Set<number>();
|
||||||
|
private readonly itemTiles = new Set<number>();
|
||||||
|
|
||||||
|
constructor(tower: ICodeRunResult, config: IAutoLabelConfig) {
|
||||||
|
this.tower = tower;
|
||||||
|
this.config = config;
|
||||||
|
|
||||||
|
// 基于 maps 字典预计算各 tile 的分类与属性,避免重复解析。
|
||||||
|
const tileMap = tower.map ?? {};
|
||||||
|
for (const key of Object.keys(tileMap)) {
|
||||||
|
const tile = Number(key);
|
||||||
|
if (!Number.isFinite(tile)) continue;
|
||||||
|
this.precomputeTile(tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static parseDirectionFlag(d: string | undefined): number {
|
||||||
|
if (!d) return 0;
|
||||||
|
switch (d) {
|
||||||
|
case 'left':
|
||||||
|
return CannotInOut.Left;
|
||||||
|
case 'right':
|
||||||
|
return CannotInOut.Right;
|
||||||
|
case 'up':
|
||||||
|
case 'top':
|
||||||
|
return CannotInOut.Top;
|
||||||
|
case 'down':
|
||||||
|
case 'bottom':
|
||||||
|
return CannotInOut.Bottom;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseFlags(arr?: string[]): number {
|
||||||
|
if (!arr || arr.length === 0) return 0;
|
||||||
|
let result = 0;
|
||||||
|
for (const d of arr) {
|
||||||
|
result |= MapTileConverter.parseDirectionFlag(d);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeResource(values: Partial<Record<ResourceType, number>>) {
|
||||||
|
const out = new Map<ResourceType, number>();
|
||||||
|
for (const [k, v] of Object.entries(values)) {
|
||||||
|
const n = Number(v);
|
||||||
|
if (!Number.isFinite(n) || n <= 0) continue;
|
||||||
|
out.set(Number(k) as ResourceType, n);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private evalItemEffectResource(
|
||||||
|
itemEffect?: string
|
||||||
|
): Map<ResourceType, number> {
|
||||||
|
if (!itemEffect) return new Map();
|
||||||
|
|
||||||
|
const heroStatus = {
|
||||||
|
hp: 0,
|
||||||
|
atk: 0,
|
||||||
|
def: 0,
|
||||||
|
mdef: 0
|
||||||
|
};
|
||||||
|
const thisMap = { ratio: 1 };
|
||||||
|
const values = this.tower.data?.values;
|
||||||
|
if (!values) return new Map();
|
||||||
|
|
||||||
|
const core = {
|
||||||
|
values: new Proxy(values, {
|
||||||
|
set() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
status: {
|
||||||
|
hero: new Proxy(heroStatus, {
|
||||||
|
set(target, p: string, newValue) {
|
||||||
|
if (typeof newValue !== 'number') return true;
|
||||||
|
if (
|
||||||
|
p !== 'hp' &&
|
||||||
|
p !== 'atk' &&
|
||||||
|
p !== 'def' &&
|
||||||
|
p !== 'mdef'
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
target[p] = newValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
thisMap: new Proxy(thisMap, {
|
||||||
|
set() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const log = console.log;
|
||||||
|
console.log = () => {};
|
||||||
|
try {
|
||||||
|
eval(itemEffect);
|
||||||
|
} catch (e) {
|
||||||
|
console.log = log;
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
console.log = log;
|
||||||
|
|
||||||
|
return this.normalizeResource({
|
||||||
|
[ResourceType.Hp]: heroStatus.hp,
|
||||||
|
[ResourceType.Atk]: heroStatus.atk,
|
||||||
|
[ResourceType.Def]: heroStatus.def,
|
||||||
|
[ResourceType.Mdef]: heroStatus.mdef
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private precomputeTile(tile: number): void {
|
||||||
|
if (this.labelMap.has(tile)) return;
|
||||||
|
|
||||||
|
const labels = this.config.classes;
|
||||||
|
const block = this.tower.map?.[tile];
|
||||||
|
|
||||||
|
if (this.emptyTiles.has(tile)) {
|
||||||
|
this.noPassMap.set(tile, false);
|
||||||
|
this.cannotInMap.set(tile, 0);
|
||||||
|
this.cannotOutMap.set(tile, 0);
|
||||||
|
this.labelMap.set(tile, labels.empty);
|
||||||
|
this.resourceMap.set(tile, new Map());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!block || tile === 17) {
|
||||||
|
// 未知 tile 默认按墙处理
|
||||||
|
this.noPassMap.set(tile, true);
|
||||||
|
this.cannotInMap.set(tile, 0b1111);
|
||||||
|
this.cannotOutMap.set(tile, 0b1111);
|
||||||
|
this.labelMap.set(tile, labels.wall);
|
||||||
|
this.resourceMap.set(tile, new Map());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cannotIn = this.parseFlags(block.cannotIn);
|
||||||
|
const cannotOut = this.parseFlags(block.cannotOut);
|
||||||
|
this.cannotInMap.set(tile, cannotIn);
|
||||||
|
this.cannotOutMap.set(tile, cannotOut);
|
||||||
|
|
||||||
|
const blockId = block.id;
|
||||||
|
const cls = block.cls;
|
||||||
|
|
||||||
|
// 1. 钥匙资源识别(doorInfo.keys)
|
||||||
|
let isKey = false;
|
||||||
|
let keyCount = 0;
|
||||||
|
if (block.doorInfo && block.doorInfo.keys) {
|
||||||
|
const keys = block.doorInfo.keys;
|
||||||
|
keyCount = sum(Object.values(keys));
|
||||||
|
if (keyCount > 0) {
|
||||||
|
isKey = true;
|
||||||
|
this.keyTiles.add(tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 道具资源识别(tools 且不是钥匙)
|
||||||
|
let isItem = false;
|
||||||
|
const item = this.tower.item?.[blockId];
|
||||||
|
if (item?.cls === 'tools') {
|
||||||
|
// 不是钥匙的 tools
|
||||||
|
if (!isKey) {
|
||||||
|
isItem = true;
|
||||||
|
this.itemTiles.add(tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDoor =
|
||||||
|
block.doorInfo ||
|
||||||
|
blockId.toLowerCase().endsWith('door') ||
|
||||||
|
blockId === 'specialDoor';
|
||||||
|
const isEnemy = cls === 'enemys' || cls === 'enemy48';
|
||||||
|
|
||||||
|
let isResource = false;
|
||||||
|
let resources = new Map<ResourceType, number>();
|
||||||
|
if (isKey) {
|
||||||
|
resources.set(ResourceType.Key, keyCount > 0 ? keyCount : 1);
|
||||||
|
isResource = true;
|
||||||
|
} else if (isItem) {
|
||||||
|
resources.set(ResourceType.Item, 1);
|
||||||
|
isResource = true;
|
||||||
|
} else if (cls === 'items') {
|
||||||
|
if (item?.cls === 'items') {
|
||||||
|
resources = this.evalItemEffectResource(item?.itemEffect);
|
||||||
|
isResource = resources.size > 0;
|
||||||
|
} else if (item?.cls === 'equip') {
|
||||||
|
if (item?.equip?.value) {
|
||||||
|
resources = this.normalizeResource({
|
||||||
|
[ResourceType.Hp]: item.equip.value.hp,
|
||||||
|
[ResourceType.Atk]: item.equip.value.atk,
|
||||||
|
[ResourceType.Def]: item.equip.value.def,
|
||||||
|
[ResourceType.Mdef]: item.equip.value.mdef
|
||||||
|
});
|
||||||
|
isResource = resources.size > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEmpty =
|
||||||
|
!isDoor && !isEnemy && !isResource && block.canPass === true;
|
||||||
|
|
||||||
|
if (isDoor) {
|
||||||
|
this.doorTiles.add(tile);
|
||||||
|
this.noPassMap.set(tile, false);
|
||||||
|
const label = labels.commonDoors[0];
|
||||||
|
this.labelMap.set(tile, label);
|
||||||
|
} else if (isEnemy) {
|
||||||
|
this.enemyTiles.add(tile);
|
||||||
|
this.noPassMap.set(tile, false);
|
||||||
|
this.labelMap.set(tile, labels.enemies[0]);
|
||||||
|
} else if (isResource) {
|
||||||
|
this.resourceTiles.add(tile);
|
||||||
|
this.noPassMap.set(tile, false);
|
||||||
|
this.resourceMap.set(tile, resources);
|
||||||
|
|
||||||
|
let label = labels.items[0] ?? labels.empty;
|
||||||
|
const hp = resources.get(ResourceType.Hp) ?? 0;
|
||||||
|
const atk = resources.get(ResourceType.Atk) ?? 0;
|
||||||
|
const def = resources.get(ResourceType.Def) ?? 0;
|
||||||
|
const mdef = resources.get(ResourceType.Mdef) ?? 0;
|
||||||
|
const key = resources.get(ResourceType.Key) ?? 0;
|
||||||
|
const item = resources.get(ResourceType.Item) ?? 0;
|
||||||
|
const max = Math.max(hp, atk, def, mdef, key, item);
|
||||||
|
if (max > 0) {
|
||||||
|
if (max === hp) {
|
||||||
|
label = labels.potions[0] ?? label;
|
||||||
|
} else if (max === atk) {
|
||||||
|
label = labels.redGems[0] ?? label;
|
||||||
|
} else if (max === def) {
|
||||||
|
label = labels.blueGems[0] ?? label;
|
||||||
|
} else if (max === mdef) {
|
||||||
|
label = labels.greenGems[0] ?? label;
|
||||||
|
} else if (max === key) {
|
||||||
|
label = labels.keys[0] ?? label;
|
||||||
|
} else if (max === item) {
|
||||||
|
label = labels.items[0] ?? label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.labelMap.set(tile, label);
|
||||||
|
} else if (isEmpty) {
|
||||||
|
this.noPassMap.set(tile, false);
|
||||||
|
this.labelMap.set(tile, labels.empty);
|
||||||
|
} else if (block.canPass) {
|
||||||
|
this.noPassMap.set(tile, false);
|
||||||
|
this.labelMap.set(tile, labels.empty);
|
||||||
|
} else {
|
||||||
|
this.noPassMap.set(tile, true);
|
||||||
|
this.labelMap.set(tile, labels.empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.resourceMap.has(tile)) {
|
||||||
|
this.resourceMap.set(tile, new Map());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getLabeledTile(tile: number): number {
|
||||||
|
this.precomputeTile(tile);
|
||||||
|
return this.labelMap.get(tile) ?? this.config.classes.wall;
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmpty(tile: number): boolean {
|
||||||
|
this.precomputeTile(tile);
|
||||||
|
return this.labelMap.get(tile) === this.config.classes.empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
isEntry(tile: number, x: number, y: number, floorId: string): boolean {
|
||||||
|
const loc = `${x},${y}`;
|
||||||
|
if (this.tower.main.floors[floorId].changeFloor[loc]) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isDoor(tile: number): boolean {
|
||||||
|
this.precomputeTile(tile);
|
||||||
|
return this.doorTiles.has(tile);
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnemy(tile: number): boolean {
|
||||||
|
this.precomputeTile(tile);
|
||||||
|
return this.enemyTiles.has(tile);
|
||||||
|
}
|
||||||
|
|
||||||
|
isResource(tile: number): boolean {
|
||||||
|
this.precomputeTile(tile);
|
||||||
|
return (
|
||||||
|
this.resourceTiles.has(tile) ||
|
||||||
|
this.keyTiles.has(tile) ||
|
||||||
|
this.itemTiles.has(tile)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNoPass(tile: number, x: number, y: number): boolean {
|
||||||
|
void x;
|
||||||
|
void y;
|
||||||
|
this.precomputeTile(tile);
|
||||||
|
return this.noPassMap.get(tile) ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCannotIn(tile: number, x: number, y: number): number {
|
||||||
|
void x;
|
||||||
|
void y;
|
||||||
|
this.precomputeTile(tile);
|
||||||
|
return this.cannotInMap.get(tile) ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCannotOut(tile: number, x: number, y: number): number {
|
||||||
|
void x;
|
||||||
|
void y;
|
||||||
|
this.precomputeTile(tile);
|
||||||
|
return this.cannotOutMap.get(tile) ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getResource(tile: number, x: number, y: number): Map<ResourceType, number> {
|
||||||
|
void x;
|
||||||
|
void y;
|
||||||
|
this.precomputeTile(tile);
|
||||||
|
return new Map(this.resourceMap.get(tile) ?? []);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,7 +8,6 @@ export function generateHeatmap(
|
|||||||
tokens: Set<number>,
|
tokens: Set<number>,
|
||||||
kernel: number = 5
|
kernel: number = 5
|
||||||
): number[][] {
|
): number[][] {
|
||||||
if (kernel === 0) return map.map(v => v.slice());
|
|
||||||
if (kernel % 2 !== 1) {
|
if (kernel % 2 !== 1) {
|
||||||
throw new Error(`Kernal size must be odd.`);
|
throw new Error(`Kernal size must be odd.`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
import { readFile } from 'fs/promises';
|
import { readFile } from 'fs/promises';
|
||||||
import { IAutoLabelConfig, IFloorInfo, ITowerInfo, TowerColor } from './types';
|
|
||||||
import { buildTopologicalGraph } from '../topology/graph';
|
|
||||||
import {
|
import {
|
||||||
commonDoorTiles,
|
GraphNodeType,
|
||||||
|
IAutoLabelConfig,
|
||||||
|
IFloorInfo,
|
||||||
|
IMapTileConverter,
|
||||||
|
ITowerInfo,
|
||||||
|
TowerColor
|
||||||
|
} from './types';
|
||||||
|
import {
|
||||||
doorTiles,
|
doorTiles,
|
||||||
enemyTiles,
|
enemyTiles,
|
||||||
entryTiles,
|
entryTiles,
|
||||||
@ -15,8 +20,8 @@ import {
|
|||||||
specialDoorTiles,
|
specialDoorTiles,
|
||||||
wallTiles
|
wallTiles
|
||||||
} from '../shared';
|
} from '../shared';
|
||||||
import { NodeType } from '../topology/interface';
|
|
||||||
import { gaussainHeatmap, generateHeatmap } from './heatmap';
|
import { gaussainHeatmap, generateHeatmap } from './heatmap';
|
||||||
|
import { MapTopology } from './topo';
|
||||||
|
|
||||||
interface IRawTowerInfo {
|
interface IRawTowerInfo {
|
||||||
/** 作者 id */
|
/** 作者 id */
|
||||||
@ -155,62 +160,71 @@ export function computeWallDensityStd(
|
|||||||
*/
|
*/
|
||||||
export function parseFloorInfo(
|
export function parseFloorInfo(
|
||||||
tower: ITowerInfo,
|
tower: ITowerInfo,
|
||||||
|
originMap: number[][],
|
||||||
map: number[][],
|
map: number[][],
|
||||||
config: IAutoLabelConfig
|
otherLayers: number[][][],
|
||||||
|
config: IAutoLabelConfig,
|
||||||
|
converter: IMapTileConverter,
|
||||||
|
floorId: string
|
||||||
): IFloorInfo {
|
): IFloorInfo {
|
||||||
const topo = buildTopologicalGraph(map);
|
const topo = new MapTopology(
|
||||||
|
floorId,
|
||||||
|
originMap,
|
||||||
|
map,
|
||||||
|
otherLayers,
|
||||||
|
converter,
|
||||||
|
config.classes
|
||||||
|
);
|
||||||
const flattened = map.flat();
|
const flattened = map.flat();
|
||||||
const area = flattened.length;
|
const area = flattened.length;
|
||||||
|
|
||||||
let hasUselessBranch = false;
|
let hasUselessBranch = false;
|
||||||
|
|
||||||
// 统计咸鱼门数量
|
// 统计拓扑图信息
|
||||||
let fishCount = 0;
|
let maxEmptyArea = 0;
|
||||||
topo.graphs.forEach(graph => {
|
let maxResourceArea = 0;
|
||||||
// 其实就是判断纯血瓶钥匙的资源节点的邻居是不是全都是门,是的话就判定为咸鱼门
|
topo.graph.areas.forEach(area => {
|
||||||
// 这么做虽然会有一定的误差,但是也大差不差了
|
area.nodes.forEach(v => {
|
||||||
// 两个门对一个也判定为一个咸鱼门
|
if (v.type === GraphNodeType.Empty) {
|
||||||
graph.areaMap.forEach(v => {
|
let branchConnection = 0;
|
||||||
const res = [...v.resources.entries()];
|
v.neighbors.forEach(v => {
|
||||||
const onlyPotion = res.every(([tile, value]) => {
|
// 对节点的每个邻居遍历,如果邻居是分支节点,且直接相连的分支节点数小于 2,
|
||||||
if (!potionTiles.has(tile) && !keyTiles.has(tile)) {
|
// 说明这个连接可能会导致无用节点
|
||||||
return value <= 0;
|
// 至于为什么要多一次额外的邻居节点判断:
|
||||||
|
// |---|---|---|---|---|
|
||||||
|
// | W | W | D | W | W |
|
||||||
|
// |---|---|---|---|---|
|
||||||
|
// | W | | E | | W |
|
||||||
|
// |---|---|---|---|---|
|
||||||
|
// | W | W | D | W | W |
|
||||||
|
// |---|---|---|---|---|
|
||||||
|
if (v.type === GraphNodeType.Branch) {
|
||||||
|
let directBranch = 0;
|
||||||
|
for (const n of v.neighbors) {
|
||||||
|
if (n.type === GraphNodeType.Branch) {
|
||||||
|
directBranch++;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
});
|
|
||||||
if (!onlyPotion) {
|
|
||||||
// 包含血瓶钥匙之外的不考虑
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
if (directBranch < 2) {
|
||||||
let branchCount = 0;
|
branchConnection++;
|
||||||
let noneBranchCount = 0;
|
|
||||||
|
|
||||||
v.neighbor.forEach(value => {
|
|
||||||
const node = graph.graph.get(value);
|
|
||||||
if (!node) {
|
|
||||||
noneBranchCount++;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.type === NodeType.Branch) {
|
|
||||||
if (!commonDoorTiles.has(node.tile)) {
|
|
||||||
branchCount++;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
noneBranchCount++;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (noneBranchCount >= 0 && branchCount === 0) {
|
// 如果连接的分支数与邻居数相同,且小于等于 0,说明是门或怪物后面连接了一整片空地,是无用分支
|
||||||
fishCount++;
|
// 如果连接的分支数与邻居数不相同,说明可能连接了资源节点、入口节点等,这些显然不应该算入无用分支
|
||||||
}
|
if (
|
||||||
});
|
branchConnection <= 1 &&
|
||||||
|
v.neighbors.size === branchConnection
|
||||||
graph.graph.forEach(v => {
|
) {
|
||||||
if (v.type === NodeType.Branch) {
|
|
||||||
if (v.neighbor.size === 1) {
|
|
||||||
hasUselessBranch = true;
|
hasUselessBranch = true;
|
||||||
}
|
}
|
||||||
|
if (v.tiles.size > maxEmptyArea) {
|
||||||
|
maxEmptyArea = v.tiles.size;
|
||||||
|
}
|
||||||
|
} else if (v.type === GraphNodeType.Resource) {
|
||||||
|
if (v.tiles.size > maxResourceArea) {
|
||||||
|
maxResourceArea = v.tiles.size;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -219,6 +233,9 @@ export function parseFloorInfo(
|
|||||||
tower,
|
tower,
|
||||||
topo,
|
topo,
|
||||||
map,
|
map,
|
||||||
|
maxEmptyArea,
|
||||||
|
maxResourceArea,
|
||||||
|
hasUselessBranch,
|
||||||
globalDensity: count(flattened, nonEmptyTiles) / area,
|
globalDensity: count(flattened, nonEmptyTiles) / area,
|
||||||
wallDensity: count(flattened, wallTiles) / area,
|
wallDensity: count(flattened, wallTiles) / area,
|
||||||
doorDensity: count(flattened, doorTiles) / area,
|
doorDensity: count(flattened, doorTiles) / area,
|
||||||
@ -230,8 +247,6 @@ export function parseFloorInfo(
|
|||||||
itemDensity: count(flattened, itemTiles) / area,
|
itemDensity: count(flattened, itemTiles) / area,
|
||||||
entryCount: count(flattened, entryTiles),
|
entryCount: count(flattened, entryTiles),
|
||||||
specialDoorCount: count(flattened, specialDoorTiles),
|
specialDoorCount: count(flattened, specialDoorTiles),
|
||||||
fishCount,
|
|
||||||
hasUselessBranch,
|
|
||||||
wallDensityStd: computeWallDensityStd(map, wallTiles, 5),
|
wallDensityStd: computeWallDensityStd(map, wallTiles, 5),
|
||||||
wallHeatmap: gaussainHeatmap(
|
wallHeatmap: gaussainHeatmap(
|
||||||
generateHeatmap(map, wallTiles, config.heatmapKernel),
|
generateHeatmap(map, wallTiles, config.heatmapKernel),
|
||||||
|
|||||||
397
data/src/auto/topo.ts
Normal file
397
data/src/auto/topo.ts
Normal file
@ -0,0 +1,397 @@
|
|||||||
|
import {
|
||||||
|
CannotInOut,
|
||||||
|
GraphNodeType,
|
||||||
|
BranchType,
|
||||||
|
ResourceType,
|
||||||
|
type IMapTopology,
|
||||||
|
type IMapGraph,
|
||||||
|
type IMapGraphArea,
|
||||||
|
type MapGraphNode,
|
||||||
|
type IEntryMapGraphNode,
|
||||||
|
type IMapTileConverter,
|
||||||
|
IMapBlockConfig
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
/** [dx, dy, 离开方向标记, 进入方向标记] */
|
||||||
|
const dirs: [number, number, CannotInOut, CannotInOut][] = [
|
||||||
|
[-1, 0, CannotInOut.Left, CannotInOut.Right],
|
||||||
|
[1, 0, CannotInOut.Right, CannotInOut.Left],
|
||||||
|
[0, -1, CannotInOut.Top, CannotInOut.Bottom],
|
||||||
|
[0, 1, CannotInOut.Bottom, CannotInOut.Top]
|
||||||
|
];
|
||||||
|
|
||||||
|
const ALL_BLOCKED =
|
||||||
|
CannotInOut.Left | CannotInOut.Top | CannotInOut.Right | CannotInOut.Bottom;
|
||||||
|
|
||||||
|
export class MapTopology implements IMapTopology {
|
||||||
|
readonly originMap: number[][];
|
||||||
|
readonly otherLayersMap: number[][][];
|
||||||
|
readonly convertedMap: number[][];
|
||||||
|
readonly noPass: boolean[][];
|
||||||
|
readonly cannotIn: number[][];
|
||||||
|
readonly cannotOut: number[][];
|
||||||
|
readonly graph: IMapGraph;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly floorId: string,
|
||||||
|
map: number[][],
|
||||||
|
convertedMap: number[][],
|
||||||
|
otherLayers: number[][][],
|
||||||
|
converter: IMapTileConverter,
|
||||||
|
readonly config: IMapBlockConfig
|
||||||
|
) {
|
||||||
|
this.originMap = map;
|
||||||
|
this.otherLayersMap = otherLayers;
|
||||||
|
this.convertedMap = convertedMap;
|
||||||
|
|
||||||
|
const height = map.length;
|
||||||
|
const width = height > 0 ? map[0].length : 0;
|
||||||
|
|
||||||
|
this.noPass = map.map((row, y) =>
|
||||||
|
row.map((tile, x) => converter.getNoPass(tile, x, y))
|
||||||
|
);
|
||||||
|
|
||||||
|
this.cannotIn = map.map((row, y) =>
|
||||||
|
row.map((tile, x) => {
|
||||||
|
if (this.noPass[y][x]) return ALL_BLOCKED;
|
||||||
|
let flags = converter.getCannotIn(tile, x, y);
|
||||||
|
for (const layer of otherLayers) {
|
||||||
|
flags |= converter.getCannotIn(layer[y]?.[x] ?? 0, x, y);
|
||||||
|
}
|
||||||
|
return flags;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.cannotOut = map.map((row, y) =>
|
||||||
|
row.map((tile, x) => {
|
||||||
|
if (this.noPass[y][x]) return ALL_BLOCKED;
|
||||||
|
let flags = converter.getCannotOut(tile, x, y);
|
||||||
|
for (const layer of otherLayers) {
|
||||||
|
flags |= converter.getCannotOut(layer[y]?.[x] ?? 0, x, y);
|
||||||
|
}
|
||||||
|
return flags;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.graph = this.buildGraph(width, height, converter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildGraph(
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
converter: IMapTileConverter
|
||||||
|
): IMapGraph {
|
||||||
|
const size = width * height;
|
||||||
|
|
||||||
|
// 1. 使用 converter 对每个图块进行分类
|
||||||
|
const tileType = new Array<GraphNodeType | null>(size).fill(null);
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
const tile = this.convertedMap[y][x];
|
||||||
|
const origin = this.originMap[y][x];
|
||||||
|
const idx = y * width + x;
|
||||||
|
if (tile === this.config.wall) {
|
||||||
|
tileType[idx] = GraphNodeType.Wall;
|
||||||
|
} else if (
|
||||||
|
tile === this.config.entry ||
|
||||||
|
converter.isEntry(origin, x, y, this.floorId)
|
||||||
|
) {
|
||||||
|
tileType[idx] = GraphNodeType.Entry;
|
||||||
|
} else if (
|
||||||
|
this.config.enemies.includes(tile) ||
|
||||||
|
converter.isEnemy(origin)
|
||||||
|
) {
|
||||||
|
tileType[idx] = GraphNodeType.Branch;
|
||||||
|
} else if (
|
||||||
|
this.config.commonDoors.includes(tile) ||
|
||||||
|
this.config.specialDoors.includes(tile) ||
|
||||||
|
converter.isDoor(origin)
|
||||||
|
) {
|
||||||
|
tileType[idx] = GraphNodeType.Branch;
|
||||||
|
} else if (
|
||||||
|
this.config.potions.includes(tile) ||
|
||||||
|
this.config.redGems.includes(tile) ||
|
||||||
|
this.config.blueGems.includes(tile) ||
|
||||||
|
this.config.greenGems.includes(tile) ||
|
||||||
|
this.config.items.includes(tile) ||
|
||||||
|
this.config.keys.includes(tile) ||
|
||||||
|
converter.isResource(origin)
|
||||||
|
) {
|
||||||
|
tileType[idx] = GraphNodeType.Resource;
|
||||||
|
} else if (
|
||||||
|
tile === this.config.empty ||
|
||||||
|
converter.isEmpty(origin)
|
||||||
|
) {
|
||||||
|
tileType[idx] = GraphNodeType.Empty;
|
||||||
|
} else {
|
||||||
|
tileType[idx] = GraphNodeType.Wall;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 通过 BFS 将图块分组为节点
|
||||||
|
// 空白和资源节点:相邻同类型的图块合并为一个节点
|
||||||
|
// 分支和入口节点:每个图块独立为一个节点
|
||||||
|
const nodeMap = new Map<number, MapGraphNode>();
|
||||||
|
const visited = new Set<number>();
|
||||||
|
let nodeIndex = 0;
|
||||||
|
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
const idx = y * width + x;
|
||||||
|
if (visited.has(idx)) continue;
|
||||||
|
visited.add(idx);
|
||||||
|
|
||||||
|
const type = tileType[idx]!;
|
||||||
|
if (type === GraphNodeType.Wall) continue;
|
||||||
|
const tiles = new Set<number>([idx]);
|
||||||
|
const neighbors = new Set<MapGraphNode>();
|
||||||
|
|
||||||
|
// 分支和入口节点不合并,每个图块独立为一个节点
|
||||||
|
if (type === GraphNodeType.Entry) {
|
||||||
|
nodeMap.set(idx, {
|
||||||
|
type: GraphNodeType.Entry,
|
||||||
|
index: nodeIndex++,
|
||||||
|
tiles,
|
||||||
|
neighbors
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === GraphNodeType.Branch) {
|
||||||
|
const tile = this.originMap[y][x];
|
||||||
|
nodeMap.set(idx, {
|
||||||
|
type: GraphNodeType.Branch,
|
||||||
|
index: nodeIndex++,
|
||||||
|
tiles,
|
||||||
|
neighbors,
|
||||||
|
branch: converter.isDoor(tile)
|
||||||
|
? BranchType.Door
|
||||||
|
: BranchType.Enemy
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 空白和资源节点:BFS 合并相邻同类型图块
|
||||||
|
const queue: number[] = [idx];
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const ci = queue.shift()!;
|
||||||
|
const cx = ci % width;
|
||||||
|
const cy = (ci - cx) / width;
|
||||||
|
|
||||||
|
for (const [dx, dy, outFlag, inFlag] of dirs) {
|
||||||
|
const nx = cx + dx;
|
||||||
|
const ny = cy + dy;
|
||||||
|
if (nx < 0 || nx >= width || ny < 0 || ny >= height)
|
||||||
|
continue;
|
||||||
|
const ni = ny * width + nx;
|
||||||
|
if (visited.has(ni) || tileType[ni] !== type) continue;
|
||||||
|
|
||||||
|
// 任一方向可通行即可合并
|
||||||
|
const canGo =
|
||||||
|
!(this.cannotOut[cy][cx] & outFlag) &&
|
||||||
|
!(this.cannotIn[ny][nx] & inFlag);
|
||||||
|
const canCome =
|
||||||
|
!(this.cannotOut[ny][nx] & inFlag) &&
|
||||||
|
!(this.cannotIn[cy][cx] & outFlag);
|
||||||
|
if (false) continue;
|
||||||
|
// if (!canGo && !canCome) continue;
|
||||||
|
|
||||||
|
visited.add(ni);
|
||||||
|
tiles.add(ni);
|
||||||
|
queue.push(ni);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let node: MapGraphNode;
|
||||||
|
if (type === GraphNodeType.Empty) {
|
||||||
|
node = {
|
||||||
|
type: GraphNodeType.Empty,
|
||||||
|
index: nodeIndex++,
|
||||||
|
tiles,
|
||||||
|
neighbors
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const resources = new Map<ResourceType, number>();
|
||||||
|
for (const t of tiles) {
|
||||||
|
const tx = t % width;
|
||||||
|
const ty = (t - tx) / width;
|
||||||
|
const res = converter.getResource(
|
||||||
|
this.originMap[ty][tx],
|
||||||
|
tx,
|
||||||
|
ty
|
||||||
|
);
|
||||||
|
for (const [k, v] of res) {
|
||||||
|
resources.set(k, (resources.get(k) ?? 0) + v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node = {
|
||||||
|
type: GraphNodeType.Resource,
|
||||||
|
index: nodeIndex++,
|
||||||
|
tiles,
|
||||||
|
neighbors,
|
||||||
|
resources
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const t of tiles) {
|
||||||
|
nodeMap.set(t, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 构建节点间的邻接关系
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
const node = nodeMap.get(y * width + x);
|
||||||
|
if (!node) continue;
|
||||||
|
|
||||||
|
for (const [dx, dy, outFlag, inFlag] of dirs) {
|
||||||
|
const nx = x + dx;
|
||||||
|
const ny = y + dy;
|
||||||
|
if (nx < 0 || nx >= width || ny < 0 || ny >= height)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const neighbor = nodeMap.get(ny * width + nx);
|
||||||
|
if (!neighbor || neighbor === node) continue;
|
||||||
|
|
||||||
|
// 至少一个方向可通行则建立邻接关系
|
||||||
|
const canGo =
|
||||||
|
!(this.cannotOut[y][x] & outFlag) &&
|
||||||
|
!(this.cannotIn[ny][nx] & inFlag);
|
||||||
|
const canCome =
|
||||||
|
!(this.cannotOut[ny][nx] & inFlag) &&
|
||||||
|
!(this.cannotIn[y][x] & outFlag);
|
||||||
|
// if (canGo || canCome) {
|
||||||
|
if (true) {
|
||||||
|
node.neighbors.add(neighbor);
|
||||||
|
neighbor.neighbors.add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 通过 BFS 连通分量构建区域
|
||||||
|
const areas = new Set<IMapGraphArea>();
|
||||||
|
const unreachableArea = new Set<IMapGraphArea>();
|
||||||
|
const entries = new Set<IEntryMapGraphNode>();
|
||||||
|
const visitedNodes = new Set<MapGraphNode>();
|
||||||
|
|
||||||
|
for (const node of new Set(nodeMap.values())) {
|
||||||
|
if (visitedNodes.has(node)) continue;
|
||||||
|
visitedNodes.add(node);
|
||||||
|
|
||||||
|
const areaNodes = new Set<MapGraphNode>();
|
||||||
|
const areaEntries: IEntryMapGraphNode[] = [];
|
||||||
|
const queue: MapGraphNode[] = [node];
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const current = queue.shift()!;
|
||||||
|
if (areaNodes.has(current)) continue;
|
||||||
|
areaNodes.add(current);
|
||||||
|
visitedNodes.add(current);
|
||||||
|
|
||||||
|
if (current.type === GraphNodeType.Entry) {
|
||||||
|
areaEntries.push(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const nb of current.neighbors) {
|
||||||
|
if (!visitedNodes.has(nb)) {
|
||||||
|
visitedNodes.add(nb);
|
||||||
|
queue.push(nb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const area: IMapGraphArea = { nodes: areaNodes };
|
||||||
|
areas.add(area);
|
||||||
|
|
||||||
|
if (areaEntries.length > 0) {
|
||||||
|
for (const e of areaEntries) {
|
||||||
|
entries.add(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unreachableArea.add(area);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log(areas.size);
|
||||||
|
|
||||||
|
return { unreachableArea, areas, entries, nodeMap };
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveIgnored(
|
||||||
|
ignoredNode?: (MapGraphNode | number)[]
|
||||||
|
): Set<MapGraphNode> {
|
||||||
|
const ignored = new Set<MapGraphNode>();
|
||||||
|
if (!ignoredNode) return ignored;
|
||||||
|
for (const item of ignoredNode) {
|
||||||
|
if (typeof item === 'number') {
|
||||||
|
const node = this.graph.nodeMap.get(item);
|
||||||
|
if (node) ignored.add(node);
|
||||||
|
} else {
|
||||||
|
ignored.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedToAnyEntry(
|
||||||
|
pos: number,
|
||||||
|
ignoredNode?: (MapGraphNode | number)[]
|
||||||
|
): boolean {
|
||||||
|
const startNode = this.graph.nodeMap.get(pos);
|
||||||
|
if (!startNode) return false;
|
||||||
|
if (startNode.type === GraphNodeType.Entry) return true;
|
||||||
|
|
||||||
|
const ignored = this.resolveIgnored(ignoredNode);
|
||||||
|
if (ignored.has(startNode)) return false;
|
||||||
|
|
||||||
|
const visited = new Set<MapGraphNode>([startNode]);
|
||||||
|
const queue: MapGraphNode[] = [startNode];
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const current = queue.shift()!;
|
||||||
|
for (const nb of current.neighbors) {
|
||||||
|
if (visited.has(nb) || ignored.has(nb)) continue;
|
||||||
|
if (nb.type === GraphNodeType.Entry) return true;
|
||||||
|
visited.add(nb);
|
||||||
|
queue.push(nb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedToSpecificEntry(
|
||||||
|
pos: number,
|
||||||
|
entry: number | IEntryMapGraphNode,
|
||||||
|
ignoredNode?: (MapGraphNode | number)[]
|
||||||
|
): boolean {
|
||||||
|
const startNode = this.graph.nodeMap.get(pos);
|
||||||
|
if (!startNode) return false;
|
||||||
|
|
||||||
|
const targetNode =
|
||||||
|
typeof entry === 'number' ? this.graph.nodeMap.get(entry) : entry;
|
||||||
|
if (!targetNode) return false;
|
||||||
|
if (startNode === targetNode) return true;
|
||||||
|
|
||||||
|
const ignored = this.resolveIgnored(ignoredNode);
|
||||||
|
if (ignored.has(startNode)) return false;
|
||||||
|
|
||||||
|
const visited = new Set<MapGraphNode>([startNode]);
|
||||||
|
const queue: MapGraphNode[] = [startNode];
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const current = queue.shift()!;
|
||||||
|
for (const nb of current.neighbors) {
|
||||||
|
if (visited.has(nb) || ignored.has(nb)) continue;
|
||||||
|
if (nb === targetNode) return true;
|
||||||
|
visited.add(nb);
|
||||||
|
queue.push(nb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { IMapTileConverter, ResourceType } from './types';
|
||||||
import {
|
import {
|
||||||
IAutoLabelConfig,
|
IAutoLabelConfig,
|
||||||
ICodeRunResult,
|
ICodeRunResult,
|
||||||
@ -26,6 +27,8 @@ export function runTowerCode(project: string, floors: string): ICodeRunResult {
|
|||||||
result.item = items_296f5d02_12fd_4166_a7c1_b5e830c9ee3a;
|
result.item = items_296f5d02_12fd_4166_a7c1_b5e830c9ee3a;
|
||||||
`;
|
`;
|
||||||
const main = result.main!;
|
const main = result.main!;
|
||||||
|
const log = console.log;
|
||||||
|
console.log = () => {};
|
||||||
try {
|
try {
|
||||||
eval(projectCode);
|
eval(projectCode);
|
||||||
eval(floors);
|
eval(floors);
|
||||||
@ -35,17 +38,15 @@ export function runTowerCode(project: string, floors: string): ICodeRunResult {
|
|||||||
} catch {
|
} catch {
|
||||||
result.issue?.push(`代码运行错误`);
|
result.issue?.push(`代码运行错误`);
|
||||||
}
|
}
|
||||||
|
console.log = log;
|
||||||
return result as ICodeRunResult;
|
return result as ICodeRunResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
function edge(x: number, y: number, width: number, height: number) {
|
|
||||||
return x === 0 || y === 0 || x === width - 1 || y === height - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function convertTowerMap(
|
export function convertTowerMap(
|
||||||
result: ICodeRunResult,
|
result: ICodeRunResult,
|
||||||
floor: INeededFloorData,
|
floor: INeededFloorData,
|
||||||
config: IAutoLabelConfig
|
config: IAutoLabelConfig,
|
||||||
|
converter: IMapTileConverter
|
||||||
): IConvertedMap {
|
): IConvertedMap {
|
||||||
const width = floor.map[0].length;
|
const width = floor.map[0].length;
|
||||||
const height = floor.map.length;
|
const height = floor.map.length;
|
||||||
@ -73,48 +74,6 @@ export function convertTowerMap(
|
|||||||
mdef: 0
|
mdef: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
const thisMap = {
|
|
||||||
ratio: 1
|
|
||||||
};
|
|
||||||
|
|
||||||
// 给后面的 eval 用的
|
|
||||||
const core = {
|
|
||||||
values: new Proxy(result.data.values, {
|
|
||||||
set() {
|
|
||||||
// 防止被修改
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
status: {
|
|
||||||
hero: new Proxy(heroStatus, {
|
|
||||||
set(target, p: string, newValue) {
|
|
||||||
if (typeof newValue !== 'number') return true;
|
|
||||||
if (
|
|
||||||
p !== 'hp' &&
|
|
||||||
p !== 'atk' &&
|
|
||||||
p !== 'def' &&
|
|
||||||
p !== 'mdef'
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
target[p] = newValue;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
thisMap: new Proxy(thisMap, {
|
|
||||||
set() {
|
|
||||||
// 防止被修改
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
core.status.hero.hp = 0;
|
|
||||||
core.status.hero.atk = 0;
|
|
||||||
core.status.hero.def = 0;
|
|
||||||
core.status.hero.mdef = 0;
|
|
||||||
|
|
||||||
const tiles = config.classes;
|
const tiles = config.classes;
|
||||||
|
|
||||||
for (let nx = 0; nx < width; nx++) {
|
for (let nx = 0; nx < width; nx++) {
|
||||||
@ -188,17 +147,11 @@ export function convertTowerMap(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// 执行道具效果
|
// 执行道具效果
|
||||||
if (item.cls === 'items' && item.itemEffect) {
|
const effect = converter.getResource(num, nx, ny);
|
||||||
try {
|
heroStatus.hp = effect.get(ResourceType.Hp) ?? 0;
|
||||||
eval(item.itemEffect);
|
heroStatus.atk = effect.get(ResourceType.Atk) ?? 0;
|
||||||
} catch {
|
heroStatus.def = effect.get(ResourceType.Def) ?? 0;
|
||||||
// 执行失败就清空一下防止被误识别为宝石血瓶
|
heroStatus.mdef = effect.get(ResourceType.Mdef) ?? 0;
|
||||||
heroStatus.hp = 0;
|
|
||||||
heroStatus.atk = 0;
|
|
||||||
heroStatus.def = 0;
|
|
||||||
heroStatus.mdef = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const arr: [number, number, number, number] = [
|
const arr: [number, number, number, number] = [
|
||||||
heroStatus.hp,
|
heroStatus.hp,
|
||||||
heroStatus.atk,
|
heroStatus.atk,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { GinkaTopologicalGraphs } from 'src/topology/interface';
|
import { GinkaTopologicalGraphs } from '../topology/interface';
|
||||||
|
|
||||||
export const enum TowerColor {
|
export const enum TowerColor {
|
||||||
White,
|
White,
|
||||||
@ -62,7 +62,11 @@ export interface IFloorInfo {
|
|||||||
/** 楼层所属的塔信息 */
|
/** 楼层所属的塔信息 */
|
||||||
readonly tower: ITowerInfo;
|
readonly tower: ITowerInfo;
|
||||||
/** 楼层拓扑图 */
|
/** 楼层拓扑图 */
|
||||||
readonly topo: GinkaTopologicalGraphs;
|
readonly topo: IMapTopology;
|
||||||
|
/** 最大空地面积 */
|
||||||
|
readonly maxEmptyArea: number;
|
||||||
|
/** 最大资源区域面积 */
|
||||||
|
readonly maxResourceArea: number;
|
||||||
/** 地图矩阵 */
|
/** 地图矩阵 */
|
||||||
readonly map: number[][];
|
readonly map: number[][];
|
||||||
/** 地图整体密度,非空白图块/地图面积 */
|
/** 地图整体密度,非空白图块/地图面积 */
|
||||||
@ -87,9 +91,7 @@ export interface IFloorInfo {
|
|||||||
readonly entryCount: number;
|
readonly entryCount: number;
|
||||||
/** 机关门数量 */
|
/** 机关门数量 */
|
||||||
readonly specialDoorCount: number;
|
readonly specialDoorCount: number;
|
||||||
/** 咸鱼门数量,多层咸鱼门算一个 */
|
/** 是否包含只连接了一个节点的空白节点。这种节点相当于门或怪物后面什么都不加,多数是无用的。 */
|
||||||
readonly fishCount: number;
|
|
||||||
/** 是否包含只连接了一个节点的分支节点。这种节点相当于门或怪物后面什么都不加,多数是无用的。 */
|
|
||||||
readonly hasUselessBranch: boolean;
|
readonly hasUselessBranch: boolean;
|
||||||
/** 墙壁密度标准差 */
|
/** 墙壁密度标准差 */
|
||||||
readonly wallDensityStd: number;
|
readonly wallDensityStd: number;
|
||||||
@ -183,6 +185,10 @@ export interface IAutoLabelConfig {
|
|||||||
|
|
||||||
/** 最大墙壁密度标准差,用于描述一个地图墙壁分布是否均匀的,较大的时候可能是特殊地图,不符合要求 */
|
/** 最大墙壁密度标准差,用于描述一个地图墙壁分布是否均匀的,较大的时候可能是特殊地图,不符合要求 */
|
||||||
readonly maxWallDensityStd: number;
|
readonly maxWallDensityStd: number;
|
||||||
|
/** 最大空地区域面积,超过这个的地图会忽略 */
|
||||||
|
readonly maxEmptyArea: number;
|
||||||
|
/** 最大资源区域面积,超过这个的地图会忽略 */
|
||||||
|
readonly maxResourceArea: number;
|
||||||
/** 热力图统计算子 */
|
/** 热力图统计算子 */
|
||||||
readonly heatmapKernel: number;
|
readonly heatmapKernel: number;
|
||||||
/** 热力图高斯模糊的标准差 */
|
/** 热力图高斯模糊的标准差 */
|
||||||
@ -260,15 +266,195 @@ export interface INeededFloorData {
|
|||||||
readonly fgmap?: number[][];
|
readonly fgmap?: number[][];
|
||||||
readonly fg2map?: number[][];
|
readonly fg2map?: number[][];
|
||||||
readonly changeFloor: Record<string, unknown>;
|
readonly changeFloor: Record<string, unknown>;
|
||||||
|
readonly events?: Record<string, any[] | { readonly noPass: boolean }>;
|
||||||
|
readonly cannotMove?: ('left' | 'right' | 'up' | 'down')[];
|
||||||
|
readonly cannotMoveIn?: ('left' | 'right' | 'up' | 'down')[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICodeRunResult {
|
export interface ICodeRunResult {
|
||||||
issue: string[];
|
issue: string[];
|
||||||
data: INeededCoreData;
|
data: INeededCoreData;
|
||||||
enemy: Record<string, INeededEnemyData>;
|
enemy: Record<string, INeededEnemyData>;
|
||||||
|
/** Tile 数字到其内容的映射 */
|
||||||
map: Record<number, INeededMapData>;
|
map: Record<number, INeededMapData>;
|
||||||
item: Record<string, INeededItemData>;
|
item: Record<string, INeededItemData>;
|
||||||
main: {
|
main: {
|
||||||
floors: Record<string, INeededFloorData>;
|
floors: Record<string, INeededFloorData>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const enum CannotInOut {
|
||||||
|
/** 左侧不可入 / 不可出 */
|
||||||
|
Left = 0b0001,
|
||||||
|
/** 上侧不可入 / 不可出 */
|
||||||
|
Top = 0b0010,
|
||||||
|
/** 右侧不可入 / 不可出 */
|
||||||
|
Right = 0b0100,
|
||||||
|
/** 下侧不可入 / 不可出 */
|
||||||
|
Bottom = 0b1000
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum GraphNodeType {
|
||||||
|
Empty,
|
||||||
|
/** 资源节点,由资源组成 */
|
||||||
|
Resource,
|
||||||
|
/** 分支节点,由门或怪物组成 */
|
||||||
|
Branch,
|
||||||
|
/** 入口节点 */
|
||||||
|
Entry,
|
||||||
|
/** 墙 */
|
||||||
|
Wall
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum ResourceType {
|
||||||
|
Hp,
|
||||||
|
Atk,
|
||||||
|
Def,
|
||||||
|
Mdef,
|
||||||
|
Item,
|
||||||
|
Key
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum BranchType {
|
||||||
|
Door,
|
||||||
|
Enemy
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapGraphNodeBase {
|
||||||
|
/** 节点类型 */
|
||||||
|
readonly type: GraphNodeType;
|
||||||
|
/** 当前节点在拓扑图中的索引 */
|
||||||
|
readonly index: number;
|
||||||
|
/** 此节点包含的所有地图坐标 */
|
||||||
|
readonly tiles: Set<number>;
|
||||||
|
/** 当前节点的邻居节点 */
|
||||||
|
readonly neighbors: Set<MapGraphNode>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEmptyMapGraphNode extends IMapGraphNodeBase {
|
||||||
|
readonly type: GraphNodeType.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IResourceMapGraphNode extends IMapGraphNodeBase {
|
||||||
|
readonly type: GraphNodeType.Resource;
|
||||||
|
/** 节点包含的资源数量 */
|
||||||
|
readonly resources: Map<ResourceType, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBranchMapGraphNode extends IMapGraphNodeBase {
|
||||||
|
readonly type: GraphNodeType.Branch;
|
||||||
|
/** 分支节点类型 */
|
||||||
|
readonly branch: BranchType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEntryMapGraphNode extends IMapGraphNodeBase {
|
||||||
|
readonly type: GraphNodeType.Entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MapGraphNode =
|
||||||
|
| IEmptyMapGraphNode
|
||||||
|
| IResourceMapGraphNode
|
||||||
|
| IBranchMapGraphNode
|
||||||
|
| IEntryMapGraphNode;
|
||||||
|
|
||||||
|
export interface IMapGraphArea {
|
||||||
|
/** 当前区域包含的所有节点 */
|
||||||
|
readonly nodes: Set<MapGraphNode>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapGraph {
|
||||||
|
/** 不可到达的区域 */
|
||||||
|
readonly unreachableArea: Set<IMapGraphArea>;
|
||||||
|
/** 当前拓扑图包含的区域信息 */
|
||||||
|
readonly areas: Set<IMapGraphArea>;
|
||||||
|
/** 当前拓扑图的所有入口 */
|
||||||
|
readonly entries: Set<IEntryMapGraphNode>;
|
||||||
|
/** 坐标至其所在节点的映射,可以根据坐标获取其对应的节点 */
|
||||||
|
readonly nodeMap: Map<number, MapGraphNode>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapTopology {
|
||||||
|
/** 原始地图 */
|
||||||
|
readonly originMap: number[][];
|
||||||
|
/** 事件层除外的层的地图 */
|
||||||
|
readonly otherLayersMap: number[][][];
|
||||||
|
/** 经过转换的地图 */
|
||||||
|
readonly convertedMap: number[][];
|
||||||
|
/** 不可通行标记 */
|
||||||
|
readonly noPass: boolean[][];
|
||||||
|
/** 不可入标记 */
|
||||||
|
readonly cannotIn: number[][];
|
||||||
|
/** 不可出标记 */
|
||||||
|
readonly cannotOut: number[][];
|
||||||
|
/** 地图的拓扑图 */
|
||||||
|
readonly graph: IMapGraph;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断一个点是否连接至任意一个入口节点,数字表示 y * width + x
|
||||||
|
* @param pos 需要判断的点
|
||||||
|
* @param ignoredNode 路径中不允许通过的节点
|
||||||
|
*/
|
||||||
|
connectedToAnyEntry(
|
||||||
|
pos: number,
|
||||||
|
ignoredNode?: (MapGraphNode | number)[]
|
||||||
|
): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断一个点是否连接至指定的入口节点,数字表示 y * width + x
|
||||||
|
* @param pos 需要判断的点
|
||||||
|
* @param entry 指定入口
|
||||||
|
* @param ignoredNode 路径中不允许通过的节点
|
||||||
|
*/
|
||||||
|
connectedToSpecificEntry(
|
||||||
|
pos: number,
|
||||||
|
entry: number | IEntryMapGraphNode,
|
||||||
|
ignoredNode?: (MapGraphNode | number)[]
|
||||||
|
): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapTileConverter {
|
||||||
|
/**
|
||||||
|
* 根据地图原始图块,获取对应的标签图块
|
||||||
|
* @param tile 地图原始图块
|
||||||
|
*/
|
||||||
|
getLabeledTile(tile: number): number;
|
||||||
|
|
||||||
|
isEmpty(tile: number): boolean;
|
||||||
|
|
||||||
|
isEntry(tile: number, x: number, y: number, floorId: string): boolean;
|
||||||
|
|
||||||
|
isDoor(tile: number): boolean;
|
||||||
|
|
||||||
|
isEnemy(tile: number): boolean;
|
||||||
|
|
||||||
|
isResource(tile: number): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定原始图块在指定位置的通行信息
|
||||||
|
* @param tile 地图原始图块
|
||||||
|
* @param x 图块所在位置
|
||||||
|
* @param y 图块所在位置
|
||||||
|
*/
|
||||||
|
getNoPass(tile: number, x: number, y: number): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定原始图块在指定位置的不可入信息
|
||||||
|
* @param tile 地图原始图块
|
||||||
|
* @param x 图块所在位置
|
||||||
|
* @param y 图块所在位置
|
||||||
|
*/
|
||||||
|
getCannotIn(tile: number, x: number, y: number): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定原始图块在指定位置的不可出信息
|
||||||
|
* @param tile 地图原始图块
|
||||||
|
* @param x 图块所在位置
|
||||||
|
* @param y 图块所在位置
|
||||||
|
*/
|
||||||
|
getCannotOut(tile: number, x: number, y: number): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定图块所包含的资源
|
||||||
|
*/
|
||||||
|
getResource(tile: number, x: number, y: number): Map<ResourceType, number>;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user