diff --git a/data/package.json b/data/package.json index d8f90fc..5b428c0 100644 --- a/data/package.json +++ b/data/package.json @@ -14,12 +14,14 @@ "license": "ISC", "packageManager": "pnpm@10.5.2", "devDependencies": { + "@types/cli-progress": "^3.11.6", "@types/fs-extra": "^11.0.4", "@types/node": "^22.13.10", "tsx": "^4.19.3", "vitest": "^3.0.8" }, "dependencies": { + "cli-progress": "^3.12.0", "fs-extra": "^11.3.0" } } diff --git a/data/pnpm-lock.yaml b/data/pnpm-lock.yaml index d0a1d18..86581ad 100644 --- a/data/pnpm-lock.yaml +++ b/data/pnpm-lock.yaml @@ -8,10 +8,16 @@ importers: .: dependencies: + cli-progress: + specifier: ^3.12.0 + version: 3.12.0 fs-extra: specifier: ^11.3.0 version: 11.3.0 devDependencies: + '@types/cli-progress': + specifier: ^3.11.6 + version: 3.11.6 '@types/fs-extra': specifier: ^11.0.4 version: 11.0.4 @@ -285,6 +291,9 @@ packages: cpu: [x64] os: [win32] + '@types/cli-progress@3.11.6': + resolution: {integrity: sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA==} + '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} @@ -326,6 +335,10 @@ packages: '@vitest/utils@3.0.8': resolution: {integrity: sha512-nkBC3aEhfX2PdtQI/QwAWp8qZWwzASsU4Npbcd5RdMPBSSLCpkZp52P3xku3s3uA0HIEhGvEcF8rNkBsz9dQ4Q==} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -342,6 +355,10 @@ packages: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} + cli-progress@3.12.0: + resolution: {integrity: sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==} + engines: {node: '>=4'} + debug@4.4.0: resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} @@ -355,6 +372,9 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + es-module-lexer@1.6.0: resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} @@ -385,6 +405,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} @@ -437,6 +461,14 @@ packages: std-env@3.8.1: resolution: {integrity: sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -681,6 +713,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.35.0': optional: true + '@types/cli-progress@3.11.6': + dependencies: + '@types/node': 22.13.10 + '@types/estree@1.0.6': {} '@types/fs-extra@11.0.4': @@ -736,6 +772,8 @@ snapshots: loupe: 3.1.3 tinyrainbow: 2.0.0 + ansi-regex@5.0.1: {} + assertion-error@2.0.1: {} cac@6.7.14: {} @@ -750,12 +788,18 @@ snapshots: check-error@2.1.1: {} + cli-progress@3.12.0: + dependencies: + string-width: 4.2.3 + debug@4.4.0: dependencies: ms: 2.1.3 deep-eql@5.0.2: {} + emoji-regex@8.0.0: {} + es-module-lexer@1.6.0: {} esbuild@0.25.1: @@ -807,6 +851,8 @@ snapshots: graceful-fs@4.2.11: {} + is-fullwidth-code-point@3.0.0: {} + jsonfile@6.1.0: dependencies: universalify: 2.0.1 @@ -870,6 +916,16 @@ snapshots: std-env@3.8.1: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + tinybench@2.9.0: {} tinyexec@0.3.2: {} diff --git a/data/src/floor.ts b/data/src/floor.ts index d4b802d..b4631b8 100644 --- a/data/src/floor.ts +++ b/data/src/floor.ts @@ -18,11 +18,48 @@ const numMap: Record = { 53: 12 // 道具 }; -export function convertFloor( +const apeiriaMap: Record = { + 0: 0, // 空地 + 1: 1, // 墙壁 + 224: 1, // 吸血鬼,视为墙壁 + 21: 2, // 黄钥匙 + 22: 2, // 蓝钥匙 + 23: 2, // 红钥匙 + 27: 3, // 红宝石 + 28: 4, // 蓝宝石 + 29: 0, // 绿宝石 + 31: 5, // 红血瓶 + 32: 5, // 蓝血瓶 + 33: 5, // 绿血瓶 + 34: 5, // 黄血瓶 + 81: 6, // 门 + 201: 7, // 弱怪 + 202: 8, // 中怪 + 203: 9, // 强怪 + 87: 10, // 楼梯 + 88: 10, // 楼梯 + 161: 11, // 箭头 + 162: 11, // 箭头 + 163: 11, // 箭头 + 164: 11, // 箭头 + 53: 12, // 幸运金币 + 50: 12, // 飞 + 49: 12, // 炸 + 47: 12 // 破 +}; + +export interface ApeiriaEnemy { + hp: number; + atk: number; + def: number; +} + +function convert( map: number[][], [x, y, w, h]: [number, number, number, number], name: string, - floorId: string + floorId: string, + numMap: Record ) { const clipped: number[][] = []; @@ -42,3 +79,73 @@ export function convertFloor( return clipped; } + +function convertApeiriaEnemy( + map: number[][], + enemyMap: Record +) { + const width = map[0].length; + const height = map.length; + const enemy = new Set(); + for (let ny = 0; ny < height; ny++) { + for (let nx = 0; nx < width; nx++) { + const tile = map[ny][nx]; + if (tile > 200 && tile <= 280) { + // 这些是怪物 + if (enemyMap[tile]) enemy.add(enemyMap[tile]); + } + } + } + const attrs = [...enemy].map(v => (v.atk + v.def) * v.hp); + const maxAttr = Math.max(...attrs); + const minAttr = Math.min(...attrs); + const delta = maxAttr - minAttr; + for (let ny = 0; ny < height; ny++) { + for (let nx = 0; nx < width; nx++) { + const tile = map[ny][nx]; + if (tile > 200 && tile <= 280) { + // 这些是怪物 + if (enemyMap[tile]) { + // 替换为弱怪/中怪/强怪 + const enemy = enemyMap[tile]; + const attr = (enemy.atk + enemy.def) * enemy.hp; + const ad = attr - minAttr; + if (ad < delta / 3) { + map[ny][nx] = 201; + } else if (ad < (delta * 2) / 3) { + map[ny][nx] = 202; + } else { + map[ny][nx] = 203; + } + } + } + } + } + + return map; +} + +export function convertFloor( + map: number[][], + clip: [number, number, number, number], + name: string, + floorId: string +) { + return convert(map, clip, name, floorId, numMap); +} + +export function convertApeiriaMap( + map: number[][], + clip: [number, number, number, number], + name: string, + floorId: string, + enemyMap: Record +) { + return convert( + convertApeiriaEnemy(map, enemyMap), + clip, + name, + floorId, + apeiriaMap + ); +} diff --git a/data/src/minamo.ts b/data/src/minamo.ts index d2e53ea..655e115 100644 --- a/data/src/minamo.ts +++ b/data/src/minamo.ts @@ -5,6 +5,7 @@ import { mirrorMapX, mirrorMapY, rotateMap } from './topology/transform'; import { directions, tileType } from './topology/graph'; import { calculateVisualSimilarity } from './vision/similarity'; import { BaseConfig } from './types'; +import { Presets, SingleBar } from 'cli-progress'; interface MinamoConfig extends BaseConfig {} @@ -32,9 +33,9 @@ function chooseFrom(arr: T[], n: number): T[] { return copy.slice(0, n); } -function choosePair(n: number) { +function choosePair(n: number, max: number = 1000) { const totalCount = Math.round((n * (n - 1)) / 2); - const count = Math.min(totalCount, 1000); + const count = Math.min(totalCount, max); const pairs: number[] = []; for (let i = 0; i < n; i++) { for (let j = i + 1; j < n; j++) { @@ -79,7 +80,7 @@ function generateTransformData( } } // 随机抽取最多两个 - const trans = chooseFrom(types, Math.floor(Math.random() * 3)); + const trans = chooseFrom(types, Math.floor(Math.random() * 2)); return trans .map(([rot, flip]) => { const com1 = `${id1}.${rot}.${flip}:${id1}`; @@ -153,7 +154,7 @@ function generateSimilarData(id: string, map: number[][]) { // 生成最多五个微调地图 const width = map[0].length; const height = map.length; - const num = Math.floor(Math.random() * 6); + const num = Math.floor(Math.random() * 3); const res: [id: string, data: MinamoTrainData][] = []; for (let i = 0; i < num; i++) { @@ -210,7 +211,11 @@ function generateDataset( ): Record { const data: Record = {}; - pairs.forEach(v => { + const progress = new SingleBar({}, Presets.shades_classic); + + progress.start(pairs.length, 0); + + pairs.forEach((v, i) => { const num1 = Math.floor(v / floorIds.length); const num2 = v % floorIds.length; const id1 = floorIds[num1]; @@ -268,8 +273,11 @@ function generateDataset( // 地图微调训练集 Object.assign(data, Object.fromEntries(generateSimilarData(id1, map1))); // Object.assign(data, Object.fromEntries(generateSimilarData(id2, map2))); + progress.update(i + 1); }); + progress.stop(); + return data; } @@ -277,7 +285,7 @@ function parseAllData(data: Map): MinamoDataset { const length = data.size; const totalCount = Math.round((length * (length - 1)) / 2); - const pairs = choosePair(length); + const pairs = choosePair(length, 10000); console.log( `✅ 共发现 ${length} 个楼层,共 ${totalCount} 种组合,选取 ${pairs.length} 个组合` diff --git a/data/src/utils.ts b/data/src/utils.ts index 3e30794..593de54 100644 --- a/data/src/utils.ts +++ b/data/src/utils.ts @@ -1,7 +1,7 @@ import { readFile } from 'fs-extra'; import { join } from 'path'; import { BaseConfig, TowerInfo } from './types'; -import { convertFloor } from './floor'; +import { convertApeiriaMap, convertFloor } from './floor'; interface DatasetMergable { datasetId: number; @@ -67,7 +67,30 @@ export async function parseTowerInfo( export async function getAllFloors(...info: TowerInfo[]) { const floorData = await Promise.all( - info.map(tower => { + info.map(async tower => { + // 获取必要信息 + const enemyFile = await readFile( + join(tower.path, 'enemys.js'), + 'utf-8' + ); + const mapFile = await readFile( + join(tower.path, 'maps.js'), + 'utf-8' + ); + const enemyMap = JSON.parse( + enemyFile.split('\n').slice(1).join('\n') + ) as Record; + const mapData = JSON.parse( + mapFile.split('\n').slice(1).join('\n') + ) as Record; + const enemyNumMap: Record = {}; + // 将怪物转化为数字映射 + for (const [key, value] of Object.entries(mapData)) { + if (value.cls === 'enemys') { + enemyNumMap[parseInt(key)] = enemyMap[value.id]; + } + } + return Promise.all( tower.floorIds.map(async id => { const floorFile = await readFile( @@ -75,13 +98,25 @@ export async function getAllFloors(...info: TowerInfo[]) { 'utf-8' ); const data = JSON.parse( - floorFile.split('\n').slice(1).join('\n') + floorFile + .replaceAll("'", '"') + .slice(floorFile.indexOf('=') + 1) ); const map = data.map as number[][]; // 裁剪地图 const { clip } = tower.config; const area = clip.special[id] ?? clip.defaults; - return convertFloor(map, area, tower.name, id); + if (tower.name === 'Apeiria') { + return convertApeiriaMap( + map, + area, + tower.name, + id, + enemyNumMap + ); + } else { + return convertFloor(map, area, tower.name, id); + } }) ); })