mirror of
https://github.com/unanmed/ginka-generator.git
synced 2026-05-14 04:41:12 +08:00
feat: 自动标注过滤
This commit is contained in:
parent
5586ea1039
commit
224005b44b
2
.gitignore
vendored
2
.gitignore
vendored
@ -9,3 +9,5 @@ datasets
|
|||||||
*.log
|
*.log
|
||||||
app/model/*
|
app/model/*
|
||||||
!app/model/.gitkeep
|
!app/model/.gitkeep
|
||||||
|
map_images
|
||||||
|
data/result.json
|
||||||
@ -9,6 +9,7 @@
|
|||||||
"merge": "tsx ./src/merge.ts",
|
"merge": "tsx ./src/merge.ts",
|
||||||
"review": "tsx ./src/review.ts",
|
"review": "tsx ./src/review.ts",
|
||||||
"eval": "tsx ./src/eval.ts",
|
"eval": "tsx ./src/eval.ts",
|
||||||
|
"auto": "tsx ./src/auto.ts",
|
||||||
"test:topo": "tsx ./src/topology/test.ts",
|
"test:topo": "tsx ./src/topology/test.ts",
|
||||||
"test:vision": "tsx ./src/vision/test.ts"
|
"test:vision": "tsx ./src/vision/test.ts"
|
||||||
},
|
},
|
||||||
@ -19,6 +20,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cli-progress": "^3.11.6",
|
"@types/cli-progress": "^3.11.6",
|
||||||
"@types/fs-extra": "^11.0.4",
|
"@types/fs-extra": "^11.0.4",
|
||||||
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^22.13.10",
|
"@types/node": "^22.13.10",
|
||||||
"tsx": "^4.19.3",
|
"tsx": "^4.19.3",
|
||||||
"vitest": "^3.0.8"
|
"vitest": "^3.0.8"
|
||||||
@ -26,6 +28,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
"fs-extra": "^11.3.0",
|
"fs-extra": "^11.3.0",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"vm2": "^3.10.0",
|
||||||
"why-is-node-running": "^3.2.2"
|
"why-is-node-running": "^3.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,12 @@ importers:
|
|||||||
fs-extra:
|
fs-extra:
|
||||||
specifier: ^11.3.0
|
specifier: ^11.3.0
|
||||||
version: 11.3.0
|
version: 11.3.0
|
||||||
|
lodash-es:
|
||||||
|
specifier: ^4.17.21
|
||||||
|
version: 4.17.21
|
||||||
|
vm2:
|
||||||
|
specifier: ^3.10.0
|
||||||
|
version: 3.10.0
|
||||||
why-is-node-running:
|
why-is-node-running:
|
||||||
specifier: ^3.2.2
|
specifier: ^3.2.2
|
||||||
version: 3.2.2
|
version: 3.2.2
|
||||||
@ -24,6 +30,9 @@ importers:
|
|||||||
'@types/fs-extra':
|
'@types/fs-extra':
|
||||||
specifier: ^11.0.4
|
specifier: ^11.0.4
|
||||||
version: 11.0.4
|
version: 11.0.4
|
||||||
|
'@types/lodash-es':
|
||||||
|
specifier: ^4.17.12
|
||||||
|
version: 4.17.12
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.13.10
|
specifier: ^22.13.10
|
||||||
version: 22.13.10
|
version: 22.13.10
|
||||||
@ -306,6 +315,12 @@ packages:
|
|||||||
'@types/jsonfile@6.1.4':
|
'@types/jsonfile@6.1.4':
|
||||||
resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==}
|
resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==}
|
||||||
|
|
||||||
|
'@types/lodash-es@4.17.12':
|
||||||
|
resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
|
||||||
|
|
||||||
|
'@types/lodash@4.17.21':
|
||||||
|
resolution: {integrity: sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==}
|
||||||
|
|
||||||
'@types/node@22.13.10':
|
'@types/node@22.13.10':
|
||||||
resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==}
|
resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==}
|
||||||
|
|
||||||
@ -338,6 +353,15 @@ packages:
|
|||||||
'@vitest/utils@3.0.8':
|
'@vitest/utils@3.0.8':
|
||||||
resolution: {integrity: sha512-nkBC3aEhfX2PdtQI/QwAWp8qZWwzASsU4Npbcd5RdMPBSSLCpkZp52P3xku3s3uA0HIEhGvEcF8rNkBsz9dQ4Q==}
|
resolution: {integrity: sha512-nkBC3aEhfX2PdtQI/QwAWp8qZWwzASsU4Npbcd5RdMPBSSLCpkZp52P3xku3s3uA0HIEhGvEcF8rNkBsz9dQ4Q==}
|
||||||
|
|
||||||
|
acorn-walk@8.3.4:
|
||||||
|
resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
||||||
|
acorn@8.15.0:
|
||||||
|
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
ansi-regex@5.0.1:
|
ansi-regex@5.0.1:
|
||||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -415,6 +439,9 @@ packages:
|
|||||||
jsonfile@6.1.0:
|
jsonfile@6.1.0:
|
||||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||||
|
|
||||||
|
lodash-es@4.17.21:
|
||||||
|
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
|
||||||
|
|
||||||
loupe@3.1.3:
|
loupe@3.1.3:
|
||||||
resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==}
|
resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==}
|
||||||
|
|
||||||
@ -575,6 +602,11 @@ packages:
|
|||||||
jsdom:
|
jsdom:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
vm2@3.10.0:
|
||||||
|
resolution: {integrity: sha512-3ggF4Bs0cw4M7Rxn19/Cv3nJi04xrgHwt4uLto+zkcZocaKwP/nKP9wPx6ggN2X0DSXxOOIc63BV1jvES19wXQ==}
|
||||||
|
engines: {node: '>=6.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
why-is-node-running@2.3.0:
|
why-is-node-running@2.3.0:
|
||||||
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
|
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -736,6 +768,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.13.10
|
'@types/node': 22.13.10
|
||||||
|
|
||||||
|
'@types/lodash-es@4.17.12':
|
||||||
|
dependencies:
|
||||||
|
'@types/lodash': 4.17.21
|
||||||
|
|
||||||
|
'@types/lodash@4.17.21': {}
|
||||||
|
|
||||||
'@types/node@22.13.10':
|
'@types/node@22.13.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.20.0
|
undici-types: 6.20.0
|
||||||
@ -780,6 +818,12 @@ snapshots:
|
|||||||
loupe: 3.1.3
|
loupe: 3.1.3
|
||||||
tinyrainbow: 2.0.0
|
tinyrainbow: 2.0.0
|
||||||
|
|
||||||
|
acorn-walk@8.3.4:
|
||||||
|
dependencies:
|
||||||
|
acorn: 8.15.0
|
||||||
|
|
||||||
|
acorn@8.15.0: {}
|
||||||
|
|
||||||
ansi-regex@5.0.1: {}
|
ansi-regex@5.0.1: {}
|
||||||
|
|
||||||
assertion-error@2.0.1: {}
|
assertion-error@2.0.1: {}
|
||||||
@ -867,6 +911,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
|
|
||||||
|
lodash-es@4.17.21: {}
|
||||||
|
|
||||||
loupe@3.1.3: {}
|
loupe@3.1.3: {}
|
||||||
|
|
||||||
magic-string@0.30.17:
|
magic-string@0.30.17:
|
||||||
@ -1024,6 +1070,11 @@ snapshots:
|
|||||||
- tsx
|
- tsx
|
||||||
- yaml
|
- yaml
|
||||||
|
|
||||||
|
vm2@3.10.0:
|
||||||
|
dependencies:
|
||||||
|
acorn: 8.15.0
|
||||||
|
acorn-walk: 8.3.4
|
||||||
|
|
||||||
why-is-node-running@2.3.0:
|
why-is-node-running@2.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
siginfo: 2.0.0
|
siginfo: 2.0.0
|
||||||
|
|||||||
131
data/src/auto.ts
Normal file
131
data/src/auto.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { writeFile } from 'fs/promises';
|
||||||
|
import { autoLabelTowers } from './auto/auto';
|
||||||
|
import { IAutoLabelConfig, TowerColor } from './auto/types';
|
||||||
|
import { GinkaDataset, GinkaTrainData } from './types';
|
||||||
|
|
||||||
|
const [, , output, towerInfo, ...folders] = process.argv;
|
||||||
|
|
||||||
|
// 根据结果手动屏蔽一些满足条件但是不适合的塔
|
||||||
|
const ignoredTower: string[] = [
|
||||||
|
'blossom',
|
||||||
|
'cxzl_imprisoned',
|
||||||
|
'hpxwzwy',
|
||||||
|
'jianchuan1',
|
||||||
|
'MC2D',
|
||||||
|
'minecraft',
|
||||||
|
'waterfultowerx',
|
||||||
|
'xieyuhouxu'
|
||||||
|
];
|
||||||
|
|
||||||
|
const ignoredFloor: Record<string, string[]> = {
|
||||||
|
cxzl2: ['MN5', 'MN7', 'MN8', 'MN49', 'MN57', 'MQ77', 'MT85', 'MZ75'],
|
||||||
|
cxzl: ['MT63', 'MT186', 'MT256', 'MT304', 'MT313', 'MT314', 'MT334'],
|
||||||
|
gl: ['MH2'],
|
||||||
|
gooTower2: ['MT96'],
|
||||||
|
levelstower: ['LV0c'],
|
||||||
|
lzlltete: ['MT0'],
|
||||||
|
myslmdrydtwo: ['MT49'],
|
||||||
|
OneStepToHell: ['MT02', 'MT05', 'MT07', 'MT09', 'MT44'],
|
||||||
|
ousha: ['MT152', 'MT158', 'MT160'],
|
||||||
|
Pacificwar: ['MT102', 'MT131'],
|
||||||
|
tiandijinghz: ['MT54', 'MT41', 'A21', 'MT50', 'MT110'],
|
||||||
|
wdg3: ['MT37'],
|
||||||
|
xiangyaochumo: ['A4', 'A8', 'A9', 'MT62'],
|
||||||
|
xinxin2: ['B02'],
|
||||||
|
yishizhishen: ['MT8'],
|
||||||
|
zd1: ['MH9', 'MT10'],
|
||||||
|
zhenshishenghuo: ['MT52', 'MT131', 'MT132']
|
||||||
|
};
|
||||||
|
|
||||||
|
const labelConfig: IAutoLabelConfig = {
|
||||||
|
allowedSize: [[13, 13]],
|
||||||
|
allowUselessBranch: false,
|
||||||
|
maxWallDensityStd: 0.25,
|
||||||
|
minEnemyRatio: 0.02,
|
||||||
|
maxEnemyRatio: 0.3,
|
||||||
|
minWallRatio: 0.1,
|
||||||
|
maxWallRatio: 0.6,
|
||||||
|
minResourceRatio: 0.02,
|
||||||
|
maxResourceRatio: 0.3,
|
||||||
|
minDoorRatio: 0,
|
||||||
|
maxDoorRatio: 0.2,
|
||||||
|
minFishCount: 0,
|
||||||
|
maxFishCount: 2,
|
||||||
|
minEntryCount: 1,
|
||||||
|
maxEntryCount: 4,
|
||||||
|
ignoreIssues: true,
|
||||||
|
customTowerFilter: info => {
|
||||||
|
if (info.color !== TowerColor.Blue && info.color !== TowerColor.Green) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (info.people < 1000) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (info.name.startsWith('51') && info.name.length > 2) {
|
||||||
|
return Math.random() > 0.98;
|
||||||
|
}
|
||||||
|
if (ignoredTower.includes(info.name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
customFloorFilter: floor => {
|
||||||
|
if (floor.info.topo.graphs.length > 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (floor.data.hasCannotInOut) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (floor.info.topo.unreachable.size > 5) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ignoredFloor[floor.tower.name]?.includes(floor.mapId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const result = await autoLabelTowers(towerInfo, folders, labelConfig);
|
||||||
|
// 转换格式并写入文件
|
||||||
|
const dataset: GinkaDataset = {
|
||||||
|
datasetId: Math.floor(Math.random() * 1e12),
|
||||||
|
data: {}
|
||||||
|
};
|
||||||
|
result.forEach(tower => {
|
||||||
|
tower.maps.forEach(floor => {
|
||||||
|
const id = `${tower.tower.name}::${floor.mapId}`;
|
||||||
|
const width = floor.data.map[0].length;
|
||||||
|
const height = floor.data.map.length;
|
||||||
|
const info = floor.info;
|
||||||
|
const data: GinkaTrainData = {
|
||||||
|
map: floor.data.map,
|
||||||
|
size: [width, height],
|
||||||
|
tag: Array(64).fill(0),
|
||||||
|
val: [
|
||||||
|
info.globalDensity,
|
||||||
|
info.wallDensity,
|
||||||
|
0,
|
||||||
|
info.doorDensity,
|
||||||
|
info.enemyDensity,
|
||||||
|
info.resourceDensity,
|
||||||
|
info.gemDensity,
|
||||||
|
info.potionDensity,
|
||||||
|
info.keyDensity,
|
||||||
|
info.itemDensity,
|
||||||
|
info.entryCount,
|
||||||
|
info.specialDoorCount,
|
||||||
|
info.fishCount,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
};
|
||||||
|
dataset.data[id] = data;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await writeFile(output, JSON.stringify(dataset), 'utf-8');
|
||||||
|
console.log(`结果已写入 ${output}`);
|
||||||
|
})();
|
||||||
205
data/src/auto/auto.ts
Normal file
205
data/src/auto/auto.ts
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
import { readdir, readFile } from 'fs/promises';
|
||||||
|
import { parseFloorInfo, parseTowerInfo } from './info';
|
||||||
|
import { IAutoLabelConfig, IConvertedMapInfo, ITowerInfo } from './types';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { Presets, SingleBar } from 'cli-progress';
|
||||||
|
import { convertTowerMap, runTowerCode } from './tower';
|
||||||
|
|
||||||
|
export interface ILabelResult {
|
||||||
|
/** 塔信息列表 */
|
||||||
|
readonly tower: ITowerInfo;
|
||||||
|
/** 转换后的楼层列表 */
|
||||||
|
readonly maps: IConvertedMapInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function addIssuePrefix(maxLength: number, path: string, content: string) {
|
||||||
|
return `${path}: ${' '.repeat(maxLength - path.length)}${content}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动标注塔地图
|
||||||
|
* @param towerInfo 所有塔的信息路径,文件包括颜色、标签等
|
||||||
|
* @param pathList 塔文件路径列表
|
||||||
|
* @param config 自动标记配置
|
||||||
|
*/
|
||||||
|
export async function autoLabelTowers(
|
||||||
|
towerInfo: string,
|
||||||
|
pathList: string[],
|
||||||
|
config: IAutoLabelConfig
|
||||||
|
) {
|
||||||
|
const labelResult: ILabelResult[] = [];
|
||||||
|
|
||||||
|
// 统计被不同规则过滤掉的楼层
|
||||||
|
let ignoredFloorsSize = 0;
|
||||||
|
let ignoredFloorsEnemy = 0;
|
||||||
|
let ignoredFloorsWall = 0;
|
||||||
|
let ignoredFloorsResource = 0;
|
||||||
|
let ignoredFloorsDoor = 0;
|
||||||
|
let ignoredFloorsFish = 0;
|
||||||
|
let ignoredFloorsEntry = 0;
|
||||||
|
let ignoredFloorsCustom = 0;
|
||||||
|
let ignoredFloorsUseless = 0;
|
||||||
|
let ignoredFloorsStd = 0;
|
||||||
|
|
||||||
|
const towers = await parseTowerInfo(towerInfo);
|
||||||
|
const paths: string[] = [];
|
||||||
|
await Promise.all(
|
||||||
|
pathList.map(async path => {
|
||||||
|
const dir = await readdir(path);
|
||||||
|
paths.push(...dir.map(v => join(path, v)));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const issues: [string, string][] = [];
|
||||||
|
const progress = new SingleBar({}, Presets.shades_classic);
|
||||||
|
progress.start(paths.length, 0);
|
||||||
|
let i = 0;
|
||||||
|
for (const path of paths) {
|
||||||
|
progress.update(++i);
|
||||||
|
let project: string;
|
||||||
|
let floors: string;
|
||||||
|
try {
|
||||||
|
project = await readFile(
|
||||||
|
join(path, 'project', 'project.min.js'),
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
floors = await readFile(
|
||||||
|
join(path, 'project', 'floors.min.js'),
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
issues.push([path, '读取塔信息失败']);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const result = runTowerCode(project, floors);
|
||||||
|
if (result.issue.length > 0) {
|
||||||
|
issues.push(...result.issue.map<[string, string]>(v => [path, v]));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = towers.get(result.data.firstData.name);
|
||||||
|
if (!info) continue;
|
||||||
|
const customPass = config.customTowerFilter?.(info) ?? true;
|
||||||
|
if (!customPass) continue;
|
||||||
|
const convertedMaps: IConvertedMapInfo[] = [];
|
||||||
|
// 处理每个塔的每个楼层
|
||||||
|
for (const [name, floor] of Object.entries(result.main.floors)) {
|
||||||
|
const width = floor.map[0].length;
|
||||||
|
const height = floor.map.length;
|
||||||
|
// 尺寸不匹配
|
||||||
|
const sizePass = config.allowedSize.some(
|
||||||
|
([w, h]) => w === width && h === height
|
||||||
|
);
|
||||||
|
if (!sizePass) {
|
||||||
|
ignoredFloorsSize++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 转换楼层
|
||||||
|
const converted = convertTowerMap(result, floor);
|
||||||
|
const floorInfo = parseFloorInfo(info, converted.map);
|
||||||
|
const floorData: IConvertedMapInfo = {
|
||||||
|
data: converted,
|
||||||
|
tower: info,
|
||||||
|
mapId: name,
|
||||||
|
info: floorInfo
|
||||||
|
};
|
||||||
|
// 配置过滤楼层
|
||||||
|
if (
|
||||||
|
floorInfo.enemyDensity < config.minEnemyRatio ||
|
||||||
|
floorInfo.enemyDensity > config.maxEnemyRatio
|
||||||
|
) {
|
||||||
|
ignoredFloorsEnemy++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
floorInfo.wallDensity < config.minWallRatio ||
|
||||||
|
floorInfo.wallDensity > config.maxWallRatio
|
||||||
|
) {
|
||||||
|
ignoredFloorsWall++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
floorInfo.resourceDensity < config.minResourceRatio ||
|
||||||
|
floorInfo.resourceDensity > config.maxResourceRatio
|
||||||
|
) {
|
||||||
|
ignoredFloorsResource++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
floorInfo.doorDensity < config.minDoorRatio ||
|
||||||
|
floorInfo.doorDensity > config.maxDoorRatio
|
||||||
|
) {
|
||||||
|
ignoredFloorsDoor++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
floorInfo.fishCount < config.minFishCount ||
|
||||||
|
floorInfo.fishCount > config.maxFishCount
|
||||||
|
) {
|
||||||
|
ignoredFloorsFish++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
floorInfo.entryCount < config.minEntryCount ||
|
||||||
|
floorInfo.entryCount > config.maxEntryCount
|
||||||
|
) {
|
||||||
|
ignoredFloorsEntry++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!config.allowUselessBranch && floorInfo.hasUselessBranch) {
|
||||||
|
ignoredFloorsUseless++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (floorInfo.wallDensityStd > config.maxWallDensityStd) {
|
||||||
|
ignoredFloorsStd++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 自定义过滤楼层
|
||||||
|
const customPass = config.customFloorFilter?.(floorData) ?? true;
|
||||||
|
if (!customPass) {
|
||||||
|
ignoredFloorsCustom++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 楼层过滤通过
|
||||||
|
convertedMaps.push(floorData);
|
||||||
|
}
|
||||||
|
labelResult.push({ tower: info, maps: convertedMaps });
|
||||||
|
}
|
||||||
|
progress.stop();
|
||||||
|
|
||||||
|
if (!config.ignoreIssues) {
|
||||||
|
const maxLength = Math.max(...issues.map(v => v[0].length));
|
||||||
|
issues.forEach(v => {
|
||||||
|
console.log(addIssuePrefix(maxLength, v[0], v[1]));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalFilted =
|
||||||
|
ignoredFloorsSize +
|
||||||
|
ignoredFloorsEnemy +
|
||||||
|
ignoredFloorsWall +
|
||||||
|
ignoredFloorsResource +
|
||||||
|
ignoredFloorsDoor +
|
||||||
|
ignoredFloorsFish +
|
||||||
|
ignoredFloorsEntry +
|
||||||
|
ignoredFloorsUseless +
|
||||||
|
ignoredFloorsCustom;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`已处理 ${labelResult.length} 个塔,共 ${labelResult.reduce(
|
||||||
|
(prev, curr) => prev + curr.maps.length,
|
||||||
|
0
|
||||||
|
)} 层,过滤掉 ${totalFilted} 层:`
|
||||||
|
);
|
||||||
|
console.log(`尺寸过滤:${ignoredFloorsSize} 层`);
|
||||||
|
console.log(`怪物过滤:${ignoredFloorsEnemy} 层`);
|
||||||
|
console.log(`墙壁过滤:${ignoredFloorsWall} 层`);
|
||||||
|
console.log(`资源过滤:${ignoredFloorsResource} 层`);
|
||||||
|
console.log(`门过滤:${ignoredFloorsDoor} 层`);
|
||||||
|
console.log(`咸鱼过滤:${ignoredFloorsFish} 层`);
|
||||||
|
console.log(`入口过滤:${ignoredFloorsEntry} 层`);
|
||||||
|
console.log(`无用节点过滤:${ignoredFloorsUseless} 层`);
|
||||||
|
console.log(`标准差过滤:${ignoredFloorsStd} 层`);
|
||||||
|
console.log(`自定义过滤:${ignoredFloorsCustom} 层`);
|
||||||
|
|
||||||
|
return labelResult;
|
||||||
|
}
|
||||||
234
data/src/auto/info.ts
Normal file
234
data/src/auto/info.ts
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
import { readFile } from 'fs/promises';
|
||||||
|
import { IFloorInfo, ITowerInfo, TowerColor } from './types';
|
||||||
|
import { buildTopologicalGraph } from 'src/topology/graph';
|
||||||
|
import {
|
||||||
|
commonDoorTiles,
|
||||||
|
doorTiles,
|
||||||
|
enemyTiles,
|
||||||
|
entryTiles,
|
||||||
|
gemTiles,
|
||||||
|
itemTiles,
|
||||||
|
keyTiles,
|
||||||
|
nonEmptyTiles,
|
||||||
|
potionTiles,
|
||||||
|
resourceTiles,
|
||||||
|
specialDoorTiles,
|
||||||
|
wallTiles
|
||||||
|
} from 'src/shared';
|
||||||
|
import { NodeType } from 'src/topology/interface';
|
||||||
|
|
||||||
|
interface IRawTowerInfo {
|
||||||
|
/** 作者 id */
|
||||||
|
readonly authorId: string;
|
||||||
|
/** 塔颜色 */
|
||||||
|
readonly color: string;
|
||||||
|
/** 评论数量 */
|
||||||
|
readonly comment: string;
|
||||||
|
/** 是否是比赛塔 */
|
||||||
|
readonly competition: string;
|
||||||
|
/** 楼层数量 */
|
||||||
|
readonly floors: string;
|
||||||
|
/** 塔的数字 id */
|
||||||
|
readonly id: string;
|
||||||
|
/** 塔的英文名 */
|
||||||
|
readonly name: string;
|
||||||
|
/** 塔的游玩量 */
|
||||||
|
readonly people: string;
|
||||||
|
/** 塔标签,每一项是对应的标签名 */
|
||||||
|
readonly tag: string;
|
||||||
|
/** 塔的名称 */
|
||||||
|
readonly title: string;
|
||||||
|
/** 测试员列表 */
|
||||||
|
readonly topuser: string;
|
||||||
|
/** 通关人数 */
|
||||||
|
readonly win: string;
|
||||||
|
/** 精美评分,第一项是评分结果,后面五项是选择每个评分的人数 */
|
||||||
|
readonly designrate: number[];
|
||||||
|
/** 难度评分,第一项是评分结果,后面五项是选择每个评分的人数 */
|
||||||
|
readonly hardrate: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析出塔信息
|
||||||
|
* @param path 塔信息文件路径
|
||||||
|
* @returns 塔英文名到塔信息的映射
|
||||||
|
*/
|
||||||
|
export async function parseTowerInfo(
|
||||||
|
path: string
|
||||||
|
): Promise<Map<string, ITowerInfo>> {
|
||||||
|
const file = await readFile(path, 'utf-8');
|
||||||
|
const data = JSON.parse(file) as IRawTowerInfo[];
|
||||||
|
const result: ITowerInfo[] = data.map(v => {
|
||||||
|
return {
|
||||||
|
authorId: parseInt(v.authorId),
|
||||||
|
color: parseInt(v.color) as TowerColor,
|
||||||
|
comment: parseInt(v.comment),
|
||||||
|
competition: parseInt(v.competition) === 1,
|
||||||
|
floors: parseInt(v.floors),
|
||||||
|
id: parseInt(v.id),
|
||||||
|
name: v.name,
|
||||||
|
people: parseInt(v.people),
|
||||||
|
tag: v.tag.split('|').slice(0, -1),
|
||||||
|
title: v.title,
|
||||||
|
topuser: JSON.parse(v.topuser),
|
||||||
|
win: parseInt(v.win),
|
||||||
|
designrate: v.designrate.slice(),
|
||||||
|
hardrate: v.hardrate.slice()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const map = new Map<string, ITowerInfo>();
|
||||||
|
result.forEach(v => {
|
||||||
|
map.set(v.name, v);
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function count(map: number[], set: Set<number>) {
|
||||||
|
let count = 0;
|
||||||
|
map.forEach(v => {
|
||||||
|
if (set.has(v)) count++;
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算地图中墙壁密度的标准差
|
||||||
|
* @param map 地图矩阵 number[][]
|
||||||
|
* @param wallSet 墙壁的 tile ID 集合
|
||||||
|
* @param kernel 卷积核大小(如 5 则是 5x5)
|
||||||
|
*/
|
||||||
|
export function computeWallDensityStd(
|
||||||
|
map: number[][],
|
||||||
|
wallSet: Set<number>,
|
||||||
|
kernel: number,
|
||||||
|
stride: number = Math.floor(kernel / 2)
|
||||||
|
): number {
|
||||||
|
const rows = map.length;
|
||||||
|
const cols = map[0]?.length ?? 0;
|
||||||
|
|
||||||
|
if (rows === 0 || cols === 0) return 0;
|
||||||
|
|
||||||
|
const densities: number[] = [];
|
||||||
|
|
||||||
|
// 遍历 kernel window 左上角坐标
|
||||||
|
for (let sy = 0; sy < rows; sy += stride) {
|
||||||
|
for (let sx = 0; sx < cols; sx += stride) {
|
||||||
|
let countWall = 0;
|
||||||
|
let countTotal = 0;
|
||||||
|
|
||||||
|
// 扫描 kernel 范围内的实际格子
|
||||||
|
for (let y = sy; y < sy + kernel && y < rows; y++) {
|
||||||
|
for (let x = sx; x < sx + kernel && x < cols; x++) {
|
||||||
|
countTotal++;
|
||||||
|
if (wallSet.has(map[y][x])) {
|
||||||
|
countWall++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 避免除零
|
||||||
|
if (countTotal > 0) {
|
||||||
|
const density = countWall / countTotal;
|
||||||
|
densities.push(density);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (densities.length === 0) return 0;
|
||||||
|
|
||||||
|
// 求均值
|
||||||
|
const mean = densities.reduce((a, b) => a + b, 0) / densities.length;
|
||||||
|
|
||||||
|
// 求方差 (总体方差)
|
||||||
|
const variance =
|
||||||
|
densities.reduce((a, b) => a + (b - mean) ** 2, 0) / densities.length;
|
||||||
|
|
||||||
|
// 标准差
|
||||||
|
return Math.sqrt(variance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据地图矩阵解析出地图数据
|
||||||
|
* @param tower 地图所属塔信息
|
||||||
|
* @param map 地图矩阵
|
||||||
|
*/
|
||||||
|
export function parseFloorInfo(tower: ITowerInfo, map: number[][]): IFloorInfo {
|
||||||
|
const topo = buildTopologicalGraph(map);
|
||||||
|
const flattened = map.flat();
|
||||||
|
const area = flattened.length;
|
||||||
|
|
||||||
|
let hasUselessBranch = false;
|
||||||
|
|
||||||
|
// 统计咸鱼门数量
|
||||||
|
let fishCount = 0;
|
||||||
|
topo.graphs.forEach(graph => {
|
||||||
|
// 其实就是判断纯血瓶钥匙的资源节点的邻居是不是全都是门,是的话就判定为咸鱼门
|
||||||
|
// 这么做虽然会有一定的误差,但是也大差不差了
|
||||||
|
// 两个门对一个也判定为一个咸鱼门
|
||||||
|
graph.areaMap.forEach(v => {
|
||||||
|
const res = [...v.resources.entries()];
|
||||||
|
const onlyPotion = res.every(([tile, value]) => {
|
||||||
|
if (!potionTiles.has(tile) && !keyTiles.has(tile)) {
|
||||||
|
return value <= 0;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (!onlyPotion) {
|
||||||
|
// 包含血瓶钥匙之外的不考虑
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let branchCount = 0;
|
||||||
|
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) {
|
||||||
|
fishCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.graph.forEach(v => {
|
||||||
|
if (v.type === NodeType.Branch) {
|
||||||
|
if (v.neighbor.size === 1) {
|
||||||
|
hasUselessBranch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const floorInfo: IFloorInfo = {
|
||||||
|
tower,
|
||||||
|
topo,
|
||||||
|
map,
|
||||||
|
globalDensity: count(flattened, nonEmptyTiles) / area,
|
||||||
|
wallDensity: count(flattened, wallTiles) / area,
|
||||||
|
doorDensity: count(flattened, doorTiles) / area,
|
||||||
|
enemyDensity: count(flattened, enemyTiles) / area,
|
||||||
|
resourceDensity: count(flattened, resourceTiles) / area,
|
||||||
|
gemDensity: count(flattened, gemTiles) / area,
|
||||||
|
potionDensity: count(flattened, potionTiles) / area,
|
||||||
|
keyDensity: count(flattened, keyTiles) / area,
|
||||||
|
itemDensity: count(flattened, itemTiles) / area,
|
||||||
|
entryCount: count(flattened, entryTiles),
|
||||||
|
specialDoorCount: count(flattened, specialDoorTiles),
|
||||||
|
fishCount,
|
||||||
|
hasUselessBranch,
|
||||||
|
wallDensityStd: computeWallDensityStd(map, wallTiles, 5)
|
||||||
|
};
|
||||||
|
|
||||||
|
return floorInfo;
|
||||||
|
}
|
||||||
427
data/src/auto/tower.ts
Normal file
427
data/src/auto/tower.ts
Normal file
@ -0,0 +1,427 @@
|
|||||||
|
import { ICodeRunResult, IConvertedMap, INeededFloorData } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行塔的代码
|
||||||
|
* @param project project.min.js 内容
|
||||||
|
* @param floors floors.min.js 内容
|
||||||
|
*/
|
||||||
|
export function runTowerCode(project: string, floors: string): ICodeRunResult {
|
||||||
|
const result: Partial<ICodeRunResult> = {
|
||||||
|
issue: [],
|
||||||
|
main: {
|
||||||
|
floors: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const projectCode =
|
||||||
|
project +
|
||||||
|
`
|
||||||
|
result.data = data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d;
|
||||||
|
result.enemy = enemys_fcae963b_31c9_42b4_b48c_bb48d09f3f80;
|
||||||
|
result.map = maps_90f36752_8815_4be8_b32b_d7fad1d0542e;
|
||||||
|
result.item = items_296f5d02_12fd_4166_a7c1_b5e830c9ee3a;
|
||||||
|
`;
|
||||||
|
const main = result.main!;
|
||||||
|
try {
|
||||||
|
eval(projectCode);
|
||||||
|
eval(floors);
|
||||||
|
if (Object.keys(main.floors).length === 0) {
|
||||||
|
result.issue?.push(`楼层信息为空`);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
result.issue?.push(`代码运行错误`);
|
||||||
|
}
|
||||||
|
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(
|
||||||
|
result: ICodeRunResult,
|
||||||
|
floor: INeededFloorData
|
||||||
|
): IConvertedMap {
|
||||||
|
const width = floor.map[0].length;
|
||||||
|
const height = floor.map.length;
|
||||||
|
|
||||||
|
let hasCannotInOut = false;
|
||||||
|
const converted: number[][] = Array.from({ length: height }, () =>
|
||||||
|
Array.from<number>({ length: width }).fill(0)
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 键表示怪物位置,值表示怪物的生命乘以攻加防 */
|
||||||
|
const enemyMap = new Map<number, number>();
|
||||||
|
|
||||||
|
/** 键表示道具位置,值表示道具增加的血、攻、防、盾属性 */
|
||||||
|
const itemMap = new Map<number, [number, number, number, number]>();
|
||||||
|
// 这些是为了区分宝石,一个地图只有两种宝石的话当然没必要把三种宝石都用上
|
||||||
|
const itemHpSet = new Set<number>();
|
||||||
|
const itemAtkSet = new Set<number>();
|
||||||
|
const itemDefSet = new Set<number>();
|
||||||
|
const itemMdefSet = new Set<number>();
|
||||||
|
|
||||||
|
const heroStatus: Record<string, number> = {
|
||||||
|
hp: 0,
|
||||||
|
atk: 0,
|
||||||
|
def: 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;
|
||||||
|
|
||||||
|
for (let nx = 0; nx < width; nx++) {
|
||||||
|
for (let ny = 0; ny < height; ny++) {
|
||||||
|
const num = floor.map[ny][nx];
|
||||||
|
if (num === 0) {
|
||||||
|
converted[ny][nx] = 0;
|
||||||
|
continue;
|
||||||
|
} else if (num === 17) {
|
||||||
|
hasCannotInOut = true;
|
||||||
|
converted[ny][nx] = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const loc = `${nx},${ny}`;
|
||||||
|
if (floor.changeFloor[loc]) {
|
||||||
|
converted[ny][nx] = 29;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const block = result.map[num];
|
||||||
|
if (!block) {
|
||||||
|
// 图块不存在说明是额外素材中的内容,默认不可通行,视为墙壁
|
||||||
|
converted[ny][nx] = 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 怪物处理
|
||||||
|
if (block.cls === 'enemys' || block.cls === 'enemy48') {
|
||||||
|
const enemy = result.enemy[block.id];
|
||||||
|
if (!enemy) {
|
||||||
|
converted[ny][nx] = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const value = enemy.hp * (enemy.atk + enemy.def);
|
||||||
|
enemyMap.set(ny * width + nx, value);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 道具处理
|
||||||
|
if (block.cls === 'items') {
|
||||||
|
const item = result.item[block.id];
|
||||||
|
if (!item) {
|
||||||
|
converted[ny][nx] = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 先清空内容
|
||||||
|
heroStatus.hp = 0;
|
||||||
|
heroStatus.atk = 0;
|
||||||
|
heroStatus.def = 0;
|
||||||
|
heroStatus.mdef = 0;
|
||||||
|
if (block.id === 'pickaxe') {
|
||||||
|
converted[ny][nx] = 24;
|
||||||
|
continue;
|
||||||
|
} else if (block.id === 'bomb') {
|
||||||
|
converted[ny][nx] = 24;
|
||||||
|
continue;
|
||||||
|
} else if (block.id === 'centerFly') {
|
||||||
|
converted[ny][nx] = 23;
|
||||||
|
continue;
|
||||||
|
} else if (block.id === 'yellowKey') {
|
||||||
|
converted[ny][nx] = 7;
|
||||||
|
continue;
|
||||||
|
} else if (block.id === 'blueKey') {
|
||||||
|
converted[ny][nx] = 8;
|
||||||
|
continue;
|
||||||
|
} else if (block.id === 'redKey') {
|
||||||
|
converted[ny][nx] = 9;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 执行道具效果
|
||||||
|
if (item.cls === 'items' && item.itemEffect) {
|
||||||
|
try {
|
||||||
|
eval(item.itemEffect);
|
||||||
|
} catch {
|
||||||
|
// 执行失败就清空一下防止被误识别为宝石血瓶
|
||||||
|
heroStatus.hp = 0;
|
||||||
|
heroStatus.atk = 0;
|
||||||
|
heroStatus.def = 0;
|
||||||
|
heroStatus.mdef = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const arr: [number, number, number, number] = [
|
||||||
|
heroStatus.hp,
|
||||||
|
heroStatus.atk,
|
||||||
|
heroStatus.def,
|
||||||
|
heroStatus.mdef
|
||||||
|
];
|
||||||
|
let isResouce = false;
|
||||||
|
// 对每个属性进行判断
|
||||||
|
if (heroStatus.hp > 0) {
|
||||||
|
isResouce = true;
|
||||||
|
itemHpSet.add(heroStatus.hp);
|
||||||
|
}
|
||||||
|
if (heroStatus.atk > 0) {
|
||||||
|
isResouce = true;
|
||||||
|
itemAtkSet.add(heroStatus.atk);
|
||||||
|
}
|
||||||
|
if (heroStatus.def > 0) {
|
||||||
|
isResouce = true;
|
||||||
|
itemDefSet.add(heroStatus.def);
|
||||||
|
}
|
||||||
|
if (heroStatus.mdef > 0) {
|
||||||
|
isResouce = true;
|
||||||
|
itemMdefSet.add(heroStatus.mdef);
|
||||||
|
}
|
||||||
|
if (isResouce) {
|
||||||
|
itemMap.set(ny * width + nx, arr);
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
converted[ny][nx] = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 门信息,这种处理方式只能处理 2.7+ 的塔,老塔估计处理不了,不过老塔占比也不大,忽略就好了
|
||||||
|
if (block.doorInfo && Object.keys(block.doorInfo.keys).length > 0) {
|
||||||
|
if (block.id === 'specialDoor') {
|
||||||
|
converted[ny][nx] = 6;
|
||||||
|
continue;
|
||||||
|
} else if (
|
||||||
|
'redKey' in block.doorInfo.keys ||
|
||||||
|
'greenKey' in block.doorInfo.keys
|
||||||
|
) {
|
||||||
|
converted[ny][nx] = 5;
|
||||||
|
continue;
|
||||||
|
} else if ('blueKey' in block.doorInfo.keys) {
|
||||||
|
converted[ny][nx] = 4;
|
||||||
|
continue;
|
||||||
|
} else if ('yellowKey' in block.doorInfo.keys) {
|
||||||
|
converted[ny][nx] = 3;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
// 其他门一律视为黄门
|
||||||
|
converted[ny][nx] = 3;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 不可入和不可出现在还没办法处理,这两个还需要同时考虑背景前景
|
||||||
|
const bgNum = floor.bgmap?.[ny]?.[nx] ?? 0;
|
||||||
|
const bg2Num = floor.bg2map?.[ny]?.[nx] ?? 0;
|
||||||
|
const fgNum = floor.fgmap?.[ny]?.[nx] ?? 0;
|
||||||
|
const fg2Num = floor.fg2map?.[ny]?.[nx] ?? 0;
|
||||||
|
const bgBlock = result.map[bgNum];
|
||||||
|
const bg2Block = result.map[bg2Num];
|
||||||
|
const fgBlock = result.map[fgNum];
|
||||||
|
const fg2Block = result.map[fg2Num];
|
||||||
|
if (
|
||||||
|
block.cannotIn ||
|
||||||
|
block.cannotOut ||
|
||||||
|
bgBlock?.cannotIn ||
|
||||||
|
bgBlock?.cannotOut ||
|
||||||
|
bg2Block?.cannotIn ||
|
||||||
|
bg2Block?.cannotOut ||
|
||||||
|
fgBlock?.cannotIn ||
|
||||||
|
fgBlock?.cannotOut ||
|
||||||
|
fg2Block?.cannotIn ||
|
||||||
|
fg2Block?.cannotOut
|
||||||
|
) {
|
||||||
|
converted[ny][nx] = 0;
|
||||||
|
hasCannotInOut = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 墙壁处理
|
||||||
|
if (block.canPass) {
|
||||||
|
converted[ny][nx] = 0;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
converted[ny][nx] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理怪物
|
||||||
|
const minEnemyValue = Math.min(...enemyMap.values());
|
||||||
|
const maxEnemyValue = Math.max(...enemyMap.values());
|
||||||
|
const enemyValueDelta = maxEnemyValue - minEnemyValue;
|
||||||
|
|
||||||
|
if (enemyValueDelta <= 0) {
|
||||||
|
// 如果怪物战斗力都一样的话...
|
||||||
|
enemyMap.forEach((value, pos) => {
|
||||||
|
const nx = pos % width;
|
||||||
|
const ny = Math.floor(pos / width);
|
||||||
|
converted[ny][nx] = 26;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
enemyMap.forEach((value, pos) => {
|
||||||
|
const nx = pos % width;
|
||||||
|
const ny = Math.floor(pos / width);
|
||||||
|
const ratio = (value - minEnemyValue) / enemyValueDelta;
|
||||||
|
const n = Math.min(Math.floor(ratio * 3), 2);
|
||||||
|
converted[ny][nx] = 26 + n;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理宝石血瓶
|
||||||
|
const minHpValue = Math.min(...itemHpSet);
|
||||||
|
const maxHpValue = Math.max(...itemHpSet);
|
||||||
|
const minAtkValue = Math.min(...itemAtkSet);
|
||||||
|
const maxAtkValue = Math.max(...itemAtkSet);
|
||||||
|
const minDefValue = Math.min(...itemDefSet);
|
||||||
|
const maxDefValue = Math.max(...itemDefSet);
|
||||||
|
const minMdefValue = Math.min(...itemMdefSet);
|
||||||
|
const maxMdefValue = Math.max(...itemMdefSet);
|
||||||
|
const hpValueDelta = maxHpValue - minHpValue;
|
||||||
|
const atkValueDelta = maxAtkValue - minAtkValue;
|
||||||
|
const defValueDelta = maxDefValue - minDefValue;
|
||||||
|
const mdefValueDelta = maxMdefValue - minMdefValue;
|
||||||
|
|
||||||
|
itemMap.forEach(([hp, atk, def, mdef], pos) => {
|
||||||
|
const nx = pos % width;
|
||||||
|
const ny = Math.floor(pos / width);
|
||||||
|
// 资源判定为占比最大的那个
|
||||||
|
// 如果只有一种资源且道具包含这种属性,全部使用最低的资源种类
|
||||||
|
if (minHpValue === maxHpValue && hp > 0) {
|
||||||
|
converted[ny][nx] = 19;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (minAtkValue === maxAtkValue && atk > 0) {
|
||||||
|
converted[ny][nx] = 10;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (minDefValue === maxDefValue && def > 0) {
|
||||||
|
converted[ny][nx] = 13;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (minMdefValue === maxMdefValue && mdef > 0) {
|
||||||
|
converted[ny][nx] = 16;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hpRatio = (hp - minHpValue) / hpValueDelta;
|
||||||
|
const atkRatio = (atk - minAtkValue) / atkValueDelta;
|
||||||
|
const defRatio = (def - minDefValue) / defValueDelta;
|
||||||
|
const mdefRatio = (mdef - minMdefValue) / mdefValueDelta;
|
||||||
|
|
||||||
|
// 判断资源种类
|
||||||
|
const arr = [hpRatio, atkRatio, defRatio, mdefRatio];
|
||||||
|
let maxIndex = 0;
|
||||||
|
let maxRatio = 0;
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
if (arr[i] > maxRatio) {
|
||||||
|
maxRatio = arr[i];
|
||||||
|
maxIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 转换图块,对于宝石来说,一共有三个级别的宝石,如果数值只有两种,那么需要额外判断下使用哪两个级别的宝石
|
||||||
|
// 倍数差距大于 3 的就使用一级和三级宝石,小于等于 3 的就使用一级和二级宝石
|
||||||
|
// 血瓶不做这个处理
|
||||||
|
switch (maxIndex) {
|
||||||
|
case 0: {
|
||||||
|
// 血瓶
|
||||||
|
const n = Math.min(Math.floor(hpRatio * 4), 3);
|
||||||
|
converted[ny][nx] = 19 + n;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1: {
|
||||||
|
// 红宝石,这里不可能只有一种数值了,不需要判断
|
||||||
|
if (itemAtkSet.size === 2) {
|
||||||
|
if (atkRatio <= 0.5) {
|
||||||
|
// 小宝石
|
||||||
|
converted[ny][nx] = 10;
|
||||||
|
} else {
|
||||||
|
// 大宝石
|
||||||
|
if (maxAtkValue / minAtkValue > 3) {
|
||||||
|
converted[nx][nx] = 12;
|
||||||
|
} else {
|
||||||
|
converted[ny][nx] = 11;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const n = Math.min(Math.floor(atkRatio * 3), 2);
|
||||||
|
converted[ny][nx] = 10 + n;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
// 蓝宝石,这里不可能只有一种数值了,不需要判断
|
||||||
|
if (itemDefSet.size === 2) {
|
||||||
|
if (defRatio <= 0.5) {
|
||||||
|
// 小宝石
|
||||||
|
converted[ny][nx] = 13;
|
||||||
|
} else {
|
||||||
|
// 大宝石
|
||||||
|
if (maxDefValue / minDefValue > 3) {
|
||||||
|
converted[nx][nx] = 15;
|
||||||
|
} else {
|
||||||
|
converted[ny][nx] = 14;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const n = Math.min(Math.floor(defRatio * 3), 2);
|
||||||
|
converted[ny][nx] = 13 + n;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
// 绿宝石,这里不可能只有一种数值了,不需要判断
|
||||||
|
if (itemMdefSet.size === 2) {
|
||||||
|
if (mdefRatio <= 0.5) {
|
||||||
|
// 小宝石
|
||||||
|
converted[ny][nx] = 16;
|
||||||
|
} else {
|
||||||
|
// 大宝石
|
||||||
|
if (maxMdefValue / minMdefValue > 3) {
|
||||||
|
converted[nx][nx] = 18;
|
||||||
|
} else {
|
||||||
|
converted[ny][nx] = 17;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const n = Math.min(Math.floor(mdefRatio * 3), 2);
|
||||||
|
converted[ny][nx] = 16 + n;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
map: converted,
|
||||||
|
hasCannotInOut
|
||||||
|
};
|
||||||
|
}
|
||||||
214
data/src/auto/types.ts
Normal file
214
data/src/auto/types.ts
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
import { GinkaTopologicalGraphs } from 'src/topology/interface';
|
||||||
|
|
||||||
|
export const enum TowerColor {
|
||||||
|
White,
|
||||||
|
Orange,
|
||||||
|
Blue,
|
||||||
|
Green,
|
||||||
|
Red,
|
||||||
|
Purple
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IConvertedMap {
|
||||||
|
/** 地图矩阵 */
|
||||||
|
readonly map: number[][];
|
||||||
|
/** 是否包含不可入不可出图块 */
|
||||||
|
readonly hasCannotInOut: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IConvertedMapInfo {
|
||||||
|
/** 转换后的地图 */
|
||||||
|
readonly data: IConvertedMap;
|
||||||
|
/** 地图信息 */
|
||||||
|
readonly info: IFloorInfo;
|
||||||
|
/** 地图所属塔信息 */
|
||||||
|
readonly tower: ITowerInfo;
|
||||||
|
/** 地图 id */
|
||||||
|
readonly mapId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITowerInfo {
|
||||||
|
/** 作者 id */
|
||||||
|
readonly authorId: number;
|
||||||
|
/** 塔颜色 */
|
||||||
|
readonly color: TowerColor;
|
||||||
|
/** 评论数量 */
|
||||||
|
readonly comment: number;
|
||||||
|
/** 是否是比赛塔 */
|
||||||
|
readonly competition: boolean;
|
||||||
|
/** 楼层数量 */
|
||||||
|
readonly floors: number;
|
||||||
|
/** 塔的数字 id */
|
||||||
|
readonly id: number;
|
||||||
|
/** 塔的英文名 */
|
||||||
|
readonly name: string;
|
||||||
|
/** 塔的游玩量 */
|
||||||
|
readonly people: number;
|
||||||
|
/** 塔标签,每一项是对应的标签名 */
|
||||||
|
readonly tag: string[];
|
||||||
|
/** 塔的名称 */
|
||||||
|
readonly title: string;
|
||||||
|
/** 测试员列表 */
|
||||||
|
readonly topuser: number[];
|
||||||
|
/** 通关人数 */
|
||||||
|
readonly win: number;
|
||||||
|
/** 精美评分,第一项是评分结果,后面五项是选择每个评分的人数 */
|
||||||
|
readonly designrate: number[];
|
||||||
|
/** 难度评分,第一项是评分结果,后面五项是选择每个评分的人数 */
|
||||||
|
readonly hardrate: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFloorInfo {
|
||||||
|
/** 楼层所属的塔信息 */
|
||||||
|
readonly tower: ITowerInfo;
|
||||||
|
/** 楼层拓扑图 */
|
||||||
|
readonly topo: GinkaTopologicalGraphs;
|
||||||
|
/** 地图矩阵 */
|
||||||
|
readonly map: number[][];
|
||||||
|
/** 地图整体密度,非空白图块/地图面积 */
|
||||||
|
readonly globalDensity: number;
|
||||||
|
/** 墙壁密度,墙壁数量/地图面积 */
|
||||||
|
readonly wallDensity: number;
|
||||||
|
/** 门密度,门数量/地图面积 */
|
||||||
|
readonly doorDensity: number;
|
||||||
|
/** 怪物密度,怪物数量/地图面积 */
|
||||||
|
readonly enemyDensity: number;
|
||||||
|
/** 资源密度,资源数量/地图面积,资源包括宝石、血瓶、道具、钥匙 */
|
||||||
|
readonly resourceDensity: number;
|
||||||
|
/** 宝石密度,宝石数量/地图面积 */
|
||||||
|
readonly gemDensity: number;
|
||||||
|
/** 血瓶密度,血瓶数量/地图面积 */
|
||||||
|
readonly potionDensity: number;
|
||||||
|
/** 钥匙密度,钥匙数量/地图面积 */
|
||||||
|
readonly keyDensity: number;
|
||||||
|
/** 道具密度,道具数量/地图面积,道具指破炸飞这些内容 */
|
||||||
|
readonly itemDensity: number;
|
||||||
|
/** 入口数量 */
|
||||||
|
readonly entryCount: number;
|
||||||
|
/** 机关门数量 */
|
||||||
|
readonly specialDoorCount: number;
|
||||||
|
/** 咸鱼门数量,多层咸鱼门算一个 */
|
||||||
|
readonly fishCount: number;
|
||||||
|
/** 是否包含只连接了一个节点的分支节点。这种节点相当于门或怪物后面什么都不加,多数是无用的。 */
|
||||||
|
readonly hasUselessBranch: boolean;
|
||||||
|
/** 墙壁密度标准差 */
|
||||||
|
readonly wallDensityStd: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAutoLabelConfig {
|
||||||
|
/** 地图允许大小 */
|
||||||
|
readonly allowedSize: [number, number][];
|
||||||
|
/** 是否允许无用节点 */
|
||||||
|
readonly allowUselessBranch: boolean;
|
||||||
|
/** 最小怪物占比 */
|
||||||
|
readonly minEnemyRatio: number;
|
||||||
|
/** 最大怪物占比 */
|
||||||
|
readonly maxEnemyRatio: number;
|
||||||
|
/** 最小墙壁占比 */
|
||||||
|
readonly minWallRatio: number;
|
||||||
|
/** 最大墙壁占比 */
|
||||||
|
readonly maxWallRatio: number;
|
||||||
|
/** 血瓶+宝石+道具+钥匙之和最小占比 */
|
||||||
|
readonly minResourceRatio: number;
|
||||||
|
/** 血瓶+宝石+道具+钥匙之和最小占比 */
|
||||||
|
readonly maxResourceRatio: number;
|
||||||
|
/** 最小门占比 */
|
||||||
|
readonly minDoorRatio: number;
|
||||||
|
/** 最大门占比 */
|
||||||
|
readonly maxDoorRatio: number;
|
||||||
|
/** 最小咸鱼门数量,多层咸鱼门算一个 */
|
||||||
|
readonly minFishCount: number;
|
||||||
|
/** 最大咸鱼门数量,多层咸鱼门算一个 */
|
||||||
|
readonly maxFishCount: number;
|
||||||
|
/** 最小入口数量 */
|
||||||
|
readonly minEntryCount: number;
|
||||||
|
/** 最大入口数量 */
|
||||||
|
readonly maxEntryCount: number;
|
||||||
|
|
||||||
|
/** 最大墙壁密度标准差,用于描述一个地图墙壁分布是否均匀的,较大的时候可能是特殊地图,不符合要求 */
|
||||||
|
readonly maxWallDensityStd: number;
|
||||||
|
|
||||||
|
/** 是否忽略问题 */
|
||||||
|
readonly ignoreIssues: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义塔过滤器
|
||||||
|
* @param info 塔信息
|
||||||
|
*/
|
||||||
|
customTowerFilter?: (info: ITowerInfo) => boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义楼层过滤器
|
||||||
|
* @param floor 楼层信息
|
||||||
|
*/
|
||||||
|
customFloorFilter?: (floor: IConvertedMapInfo) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INeededCoreData {
|
||||||
|
readonly main: {
|
||||||
|
readonly floorIds: readonly string[];
|
||||||
|
};
|
||||||
|
readonly firstData: {
|
||||||
|
readonly name: string;
|
||||||
|
};
|
||||||
|
readonly values: {
|
||||||
|
readonly redGem: number;
|
||||||
|
readonly blueGem: number;
|
||||||
|
readonly greenGem: number;
|
||||||
|
readonly redPotion: number;
|
||||||
|
readonly bluePotion: number;
|
||||||
|
readonly greenPotion: number;
|
||||||
|
readonly yellowPotion: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INeededEnemyData {
|
||||||
|
readonly hp: number;
|
||||||
|
readonly atk: number;
|
||||||
|
readonly def: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INeededMapData {
|
||||||
|
readonly cls: string;
|
||||||
|
readonly id: string;
|
||||||
|
readonly canPass?: boolean;
|
||||||
|
readonly trigger?: string;
|
||||||
|
readonly script?: string;
|
||||||
|
readonly cannotOut?: string[];
|
||||||
|
readonly cannotIn?: string[];
|
||||||
|
readonly doorInfo?: {
|
||||||
|
readonly keys: Record<string, number>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INeededItemData {
|
||||||
|
readonly cls: string;
|
||||||
|
readonly useItemEvent?: any;
|
||||||
|
readonly itemEffect?: string;
|
||||||
|
readonly canUseItemEffect?: string;
|
||||||
|
readonly equip?: {
|
||||||
|
readonly value?: Record<string, number>;
|
||||||
|
readonly percentage?: Record<string, number>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INeededFloorData {
|
||||||
|
readonly floorId: string;
|
||||||
|
readonly map: number[][];
|
||||||
|
readonly bgmap?: number[][];
|
||||||
|
readonly bg2map?: number[][];
|
||||||
|
readonly fgmap?: number[][];
|
||||||
|
readonly fg2map?: number[][];
|
||||||
|
readonly changeFloor: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICodeRunResult {
|
||||||
|
issue: string[];
|
||||||
|
data: INeededCoreData;
|
||||||
|
enemy: Record<string, INeededEnemyData>;
|
||||||
|
map: Record<number, INeededMapData>;
|
||||||
|
item: Record<string, INeededItemData>;
|
||||||
|
main: {
|
||||||
|
floors: Record<string, INeededFloorData>;
|
||||||
|
};
|
||||||
|
}
|
||||||
27
data/src/shared.ts
Normal file
27
data/src/shared.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// 基本图块定义
|
||||||
|
export const emptyTiles = new Set([0]);
|
||||||
|
export const wallTiles = new Set([1]);
|
||||||
|
export const decorationTiles = new Set([2]);
|
||||||
|
export const commonDoorTiles = new Set([3, 4, 5]);
|
||||||
|
export const specialDoorTiles = new Set([6]);
|
||||||
|
export const keyTiles = new Set([7, 8, 9]);
|
||||||
|
export const redGemTiles = new Set([10, 11, 12]);
|
||||||
|
export const blueGemTiles = new Set([13, 14, 15]);
|
||||||
|
export const greenGemTiles = new Set([16, 17, 18]);
|
||||||
|
export const potionTiles = new Set([19, 20, 21, 22]);
|
||||||
|
export const itemTiles = new Set([23, 24, 25]);
|
||||||
|
export const enemyTiles = new Set([26, 27, 28]);
|
||||||
|
export const entryTiles = new Set([29]);
|
||||||
|
|
||||||
|
// 组合图块定义
|
||||||
|
export const doorTiles = commonDoorTiles.union(specialDoorTiles);
|
||||||
|
export const gemTiles = redGemTiles.union(blueGemTiles).union(greenGemTiles);
|
||||||
|
export const resourceTiles = keyTiles
|
||||||
|
.union(gemTiles)
|
||||||
|
.union(potionTiles)
|
||||||
|
.union(itemTiles);
|
||||||
|
export const nonEmptyTiles = wallTiles
|
||||||
|
.union(doorTiles)
|
||||||
|
.union(resourceTiles)
|
||||||
|
.union(enemyTiles)
|
||||||
|
.union(enemyTiles);
|
||||||
@ -3,7 +3,8 @@ import {
|
|||||||
GinkaGraph,
|
GinkaGraph,
|
||||||
BranchNode,
|
BranchNode,
|
||||||
GinkaTopologicalGraphs,
|
GinkaTopologicalGraphs,
|
||||||
ResourceNode
|
ResourceNode,
|
||||||
|
NodeType
|
||||||
} from './interface';
|
} from './interface';
|
||||||
|
|
||||||
export const tileType = new Set(
|
export const tileType = new Set(
|
||||||
@ -76,7 +77,7 @@ function buildGraphFromEntrance(
|
|||||||
const ny = Math.floor(v / width);
|
const ny = Math.floor(v / width);
|
||||||
if (!graph.get(v)) {
|
if (!graph.get(v)) {
|
||||||
graph.set(v, {
|
graph.set(v, {
|
||||||
type: 'branch',
|
type: NodeType.Branch,
|
||||||
neighbor: new Set(),
|
neighbor: new Set(),
|
||||||
tile: map[ny][nx]
|
tile: map[ny][nx]
|
||||||
});
|
});
|
||||||
@ -115,7 +116,7 @@ function buildGraphFromEntrance(
|
|||||||
const tile = map[ny][nx];
|
const tile = map[ny][nx];
|
||||||
if (tile === 0) return;
|
if (tile === 0) return;
|
||||||
const node: ResourceNode = {
|
const node: ResourceNode = {
|
||||||
type: 'resource',
|
type: NodeType.Resource,
|
||||||
resourceType: tile,
|
resourceType: tile,
|
||||||
neighbor: v.neighbor,
|
neighbor: v.neighbor,
|
||||||
resourceArea: v
|
resourceArea: v
|
||||||
@ -145,7 +146,7 @@ function findResourceNodes(map: number[][]) {
|
|||||||
const queue: [number, number][] = [];
|
const queue: [number, number][] = [];
|
||||||
queue.push([nx, ny]);
|
queue.push([nx, ny]);
|
||||||
const area: ResourceArea = {
|
const area: ResourceArea = {
|
||||||
type: 'resource',
|
type: NodeType.Resource,
|
||||||
resources: new Map([[tile, 1]]),
|
resources: new Map([[tile, 1]]),
|
||||||
members: new Set([index]),
|
members: new Set([index]),
|
||||||
neighbor: new Set()
|
neighbor: new Set()
|
||||||
|
|||||||
@ -1,43 +1,59 @@
|
|||||||
|
export const enum NodeType {
|
||||||
|
Branch,
|
||||||
|
Resource
|
||||||
|
}
|
||||||
|
|
||||||
export interface ResourceArea {
|
export interface ResourceArea {
|
||||||
type: 'resource';
|
/** 节点类型 */
|
||||||
resources: Map<number, number>;
|
readonly type: NodeType.Resource;
|
||||||
members: Set<number>;
|
/** 每种资源对应的数量 */
|
||||||
neighbor: Set<number>;
|
readonly resources: Map<number, number>;
|
||||||
|
/** 资源区域包含的所有资源图块坐标索引 */
|
||||||
|
readonly members: Set<number>;
|
||||||
|
/** 资源区域的邻居节点 */
|
||||||
|
readonly neighbor: Set<number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BranchNode {
|
export interface BranchNode {
|
||||||
type: 'branch';
|
/** 节点类型 */
|
||||||
neighbor: Set<number>;
|
readonly type: NodeType.Branch;
|
||||||
tile: number;
|
/** 分支节点的邻居节点 */
|
||||||
|
readonly neighbor: Set<number>;
|
||||||
|
/** 分支节点图块 */
|
||||||
|
readonly tile: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResourceNode {
|
export interface ResourceNode {
|
||||||
type: 'resource';
|
/** 节点类型 */
|
||||||
resourceType: number;
|
readonly type: NodeType.Resource;
|
||||||
neighbor: Set<number>;
|
/** 资源类型 */
|
||||||
resourceArea: ResourceArea;
|
readonly resourceType: number;
|
||||||
|
/** 邻居节点 */
|
||||||
|
readonly neighbor: Set<number>;
|
||||||
|
/** 资源节点所属的资源区域 */
|
||||||
|
readonly resourceArea: ResourceArea;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GinkaNode = BranchNode | ResourceNode;
|
export type GinkaNode = BranchNode | ResourceNode;
|
||||||
|
|
||||||
export interface GinkaGraph {
|
export interface GinkaGraph {
|
||||||
/** 拓扑图内容,键表示位置,值表示这一点的节点 */
|
/** 拓扑图内容,键表示位置,值表示这一点的节点 */
|
||||||
graph: Map<number, GinkaNode>;
|
readonly graph: Map<number, GinkaNode>;
|
||||||
/** 资源指针,键表示位置,值表示这一点对应的资源节点在 areaMap 的索引 */
|
/** 资源指针,键表示位置,值表示这一点对应的资源节点在 areaMap 的索引 */
|
||||||
resourceMap: Map<number, number>;
|
readonly resourceMap: Map<number, number>;
|
||||||
/** 资源区域列表 */
|
/** 资源区域列表 */
|
||||||
areaMap: ResourceArea[];
|
readonly areaMap: ResourceArea[];
|
||||||
/** 这个拓扑图包含的入口位置 */
|
/** 这个拓扑图包含的入口位置 */
|
||||||
visitedEntrance: Set<number>;
|
readonly visitedEntrance: Set<number>;
|
||||||
/** 这个拓扑图能够造访的所有位置 */
|
/** 这个拓扑图能够造访的所有位置 */
|
||||||
visited: Set<number>;
|
readonly visited: Set<number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GinkaTopologicalGraphs {
|
export interface GinkaTopologicalGraphs {
|
||||||
/** 这个地图包含的所有独立的图 */
|
/** 这个地图包含的所有独立的图 */
|
||||||
graphs: GinkaGraph[];
|
readonly graphs: GinkaGraph[];
|
||||||
/** 每个入口对应哪个图 */
|
/** 每个入口对应哪个图 */
|
||||||
entranceMap: Map<number, GinkaGraph>;
|
readonly entranceMap: Map<number, GinkaGraph>;
|
||||||
/** 这个图从入口开始的不可到达区域 */
|
/** 这个图从入口开始的不可到达区域 */
|
||||||
unreachable: Set<number>;
|
readonly unreachable: Set<number>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { cosineSimilarity } from 'src/utils';
|
import { cosineSimilarity } from 'src/utils';
|
||||||
import { GinkaGraph, GinkaTopologicalGraphs } from './interface';
|
import { GinkaGraph, GinkaTopologicalGraphs, NodeType } from './interface';
|
||||||
|
|
||||||
interface WLNode {
|
interface WLNode {
|
||||||
originalPos: number;
|
originalPos: number;
|
||||||
@ -16,7 +16,7 @@ function encodeNodeLabels(graph: GinkaGraph) {
|
|||||||
let label: string;
|
let label: string;
|
||||||
|
|
||||||
// 编码为唯一哈希值(用字符串就行,V8 会自动帮你算哈希)
|
// 编码为唯一哈希值(用字符串就行,V8 会自动帮你算哈希)
|
||||||
if (node.type === 'branch') {
|
if (node.type === NodeType.Branch) {
|
||||||
label = `B:${node.tile}`;
|
label = `B:${node.tile}`;
|
||||||
} else {
|
} else {
|
||||||
label = `R:${node.resourceType}`;
|
label = `R:${node.resourceType}`;
|
||||||
|
|||||||
@ -15,12 +15,16 @@ from .generator.loss import WGANGinkaLoss
|
|||||||
from .critic.model import MinamoModel2
|
from .critic.model import MinamoModel2
|
||||||
from shared.image import matrix_to_image_cv
|
from shared.image import matrix_to_image_cv
|
||||||
|
|
||||||
# 标签定义:
|
# 手工标注标签定义:
|
||||||
# 0. 蓝海, 1. 红海, 2: 室内, 3. 野外, 4. 左右对称, 5. 上下对称, 6. 伪对称, 7. 咸鱼层,
|
# 0. 蓝海, 1. 红海, 2: 室内, 3. 野外, 4. 左右对称, 5. 上下对称, 6. 伪对称, 7. 咸鱼层,
|
||||||
# 8. 剧情层, 9. 水层, 10. 爽塔, 11. Boss层, 12. 纯Boss层, 13. 多房间, 14. 多走廊, 15. 道具风
|
# 8. 剧情层, 9. 水层, 10. 爽塔, 11. Boss层, 12. 纯Boss层, 13. 多房间, 14. 多走廊, 15. 道具风
|
||||||
# 16. 区域入口, 17. 区域连接, 18. 有机关门, 19. 道具层, 20. 斜向对称, 21. 左右通道, 22. 上下通道, 23. 多机关门
|
# 16. 区域入口, 17. 区域连接, 18. 有机关门, 19. 道具层, 20. 斜向对称, 21. 左右通道, 22. 上下通道, 23. 多机关门
|
||||||
# 24. 中心对称, 25. 部分对称, 26. 鱼骨
|
# 24. 中心对称, 25. 部分对称, 26. 鱼骨
|
||||||
|
|
||||||
|
# 自动标注标签定义:
|
||||||
|
# 0. 左右对称, 1. 上下对称, 2. 中心对称, 3. 斜向对称, 4. 伪对称, 5. 多房间, 6. 多走廊
|
||||||
|
# 32. 平面塔, 33. 转换塔, 34. 道具塔
|
||||||
|
|
||||||
# 标量值定义:
|
# 标量值定义:
|
||||||
# 0. 整体密度,非空白图块/地图面积,空白图块还包括装饰图块
|
# 0. 整体密度,非空白图块/地图面积,空白图块还包括装饰图块
|
||||||
# 1. 墙体密度,墙壁/地图面积
|
# 1. 墙体密度,墙壁/地图面积
|
||||||
@ -31,8 +35,10 @@ from shared.image import matrix_to_image_cv
|
|||||||
# 6. 宝石密度,宝石数量/地图面积
|
# 6. 宝石密度,宝石数量/地图面积
|
||||||
# 7. 血瓶密度,血瓶数量/地图面积
|
# 7. 血瓶密度,血瓶数量/地图面积
|
||||||
# 8. 钥匙密度,钥匙数量/地图面积
|
# 8. 钥匙密度,钥匙数量/地图面积
|
||||||
# 9. 道具数量
|
# 9. 道具密度,道具数量/地图面积
|
||||||
# 10. 入口数量
|
# 10. 入口数量
|
||||||
|
# 11. 机关门数量
|
||||||
|
# 12. 咸鱼门数量(多层咸鱼门只算一个)
|
||||||
|
|
||||||
# 图块定义:
|
# 图块定义:
|
||||||
# 0. 空地, 1. 墙壁, 2. 装饰(用于野外装饰,视为空地),
|
# 0. 空地, 1. 墙壁, 2. 装饰(用于野外装饰,视为空地),
|
||||||
@ -44,8 +50,7 @@ from shared.image import matrix_to_image_cv
|
|||||||
# 19-22. 四种等级的血瓶
|
# 19-22. 四种等级的血瓶
|
||||||
# 23-25. 三种等级的道具
|
# 23-25. 三种等级的道具
|
||||||
# 26-28. 三种等级的怪物
|
# 26-28. 三种等级的怪物
|
||||||
# 29. 楼梯入口
|
# 29. 入口,不区分楼梯和箭头
|
||||||
# 30. 箭头入口
|
|
||||||
|
|
||||||
BATCH_SIZE = 6
|
BATCH_SIZE = 6
|
||||||
|
|
||||||
|
|||||||
69
shared/visual.py
Normal file
69
shared/visual.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import os
|
||||||
|
import cv2
|
||||||
|
import json
|
||||||
|
import numpy as np
|
||||||
|
from tqdm import tqdm
|
||||||
|
from .image import matrix_to_image_cv
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# 加载 tile 图块
|
||||||
|
# -------------------------
|
||||||
|
def load_tiles(tile_folder):
|
||||||
|
tile_dict = {}
|
||||||
|
for file in os.listdir(tile_folder):
|
||||||
|
name, _ = os.path.splitext(file)
|
||||||
|
img = cv2.imread(os.path.join(tile_folder, file), cv2.IMREAD_UNCHANGED)
|
||||||
|
if img is None:
|
||||||
|
print(f"[WARN] Tile image {file} 读取失败,跳过")
|
||||||
|
continue
|
||||||
|
tile_dict[name] = img
|
||||||
|
print(f"加载了 {len(tile_dict)} 个图块")
|
||||||
|
return tile_dict
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# 主处理逻辑
|
||||||
|
# -------------------------
|
||||||
|
def convert_dataset_to_images(
|
||||||
|
json_path,
|
||||||
|
tile_folder,
|
||||||
|
output_folder,
|
||||||
|
tile_size=32
|
||||||
|
):
|
||||||
|
# 输出路径
|
||||||
|
os.makedirs(output_folder, exist_ok=True)
|
||||||
|
|
||||||
|
# 加载 tiles
|
||||||
|
tile_dict = load_tiles(tile_folder)
|
||||||
|
|
||||||
|
# 读取 json
|
||||||
|
with open(json_path, "r", encoding="utf-8") as f:
|
||||||
|
dataset = json.load(f)
|
||||||
|
|
||||||
|
data = dataset["data"]
|
||||||
|
|
||||||
|
for map_id, train_data in tqdm(data.items()):
|
||||||
|
map_matrix = np.array(train_data["map"])
|
||||||
|
|
||||||
|
try:
|
||||||
|
img = matrix_to_image_cv(map_matrix, tile_dict, tile_size)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] 地图 {map_id} 转换失败: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
out_path = os.path.join(output_folder, f"{map_id.replace('::', '-')}.png")
|
||||||
|
cv2.imwrite(out_path, img)
|
||||||
|
|
||||||
|
print('地图处理完毕!')
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# 执行
|
||||||
|
# -------------------------
|
||||||
|
if __name__ == "__main__":
|
||||||
|
convert_dataset_to_images(
|
||||||
|
json_path="data/result.json", # 数据集文件
|
||||||
|
tile_folder="tiles", # 贴图文件夹
|
||||||
|
output_folder="map_images", # 输出文件夹
|
||||||
|
tile_size=32 # tile 尺寸
|
||||||
|
)
|
||||||
Loading…
Reference in New Issue
Block a user