feat: 单独对 Apeiria 塔提供支持

This commit is contained in:
unanmed 2025-03-16 18:56:32 +08:00
parent c34756016d
commit 98f7a9cdcf
5 changed files with 220 additions and 12 deletions

View File

@ -14,12 +14,14 @@
"license": "ISC", "license": "ISC",
"packageManager": "pnpm@10.5.2", "packageManager": "pnpm@10.5.2",
"devDependencies": { "devDependencies": {
"@types/cli-progress": "^3.11.6",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@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"
}, },
"dependencies": { "dependencies": {
"cli-progress": "^3.12.0",
"fs-extra": "^11.3.0" "fs-extra": "^11.3.0"
} }
} }

View File

@ -8,10 +8,16 @@ importers:
.: .:
dependencies: dependencies:
cli-progress:
specifier: ^3.12.0
version: 3.12.0
fs-extra: fs-extra:
specifier: ^11.3.0 specifier: ^11.3.0
version: 11.3.0 version: 11.3.0
devDependencies: devDependencies:
'@types/cli-progress':
specifier: ^3.11.6
version: 3.11.6
'@types/fs-extra': '@types/fs-extra':
specifier: ^11.0.4 specifier: ^11.0.4
version: 11.0.4 version: 11.0.4
@ -285,6 +291,9 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@types/cli-progress@3.11.6':
resolution: {integrity: sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA==}
'@types/estree@1.0.6': '@types/estree@1.0.6':
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
@ -326,6 +335,10 @@ packages:
'@vitest/utils@3.0.8': '@vitest/utils@3.0.8':
resolution: {integrity: sha512-nkBC3aEhfX2PdtQI/QwAWp8qZWwzASsU4Npbcd5RdMPBSSLCpkZp52P3xku3s3uA0HIEhGvEcF8rNkBsz9dQ4Q==} resolution: {integrity: sha512-nkBC3aEhfX2PdtQI/QwAWp8qZWwzASsU4Npbcd5RdMPBSSLCpkZp52P3xku3s3uA0HIEhGvEcF8rNkBsz9dQ4Q==}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
assertion-error@2.0.1: assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -342,6 +355,10 @@ packages:
resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
engines: {node: '>= 16'} engines: {node: '>= 16'}
cli-progress@3.12.0:
resolution: {integrity: sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==}
engines: {node: '>=4'}
debug@4.4.0: debug@4.4.0:
resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
@ -355,6 +372,9 @@ packages:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'} engines: {node: '>=6'}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
es-module-lexer@1.6.0: es-module-lexer@1.6.0:
resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==}
@ -385,6 +405,10 @@ packages:
graceful-fs@4.2.11: graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 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: jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
@ -437,6 +461,14 @@ packages:
std-env@3.8.1: std-env@3.8.1:
resolution: {integrity: sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==} 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: tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@ -681,6 +713,10 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.35.0': '@rollup/rollup-win32-x64-msvc@4.35.0':
optional: true optional: true
'@types/cli-progress@3.11.6':
dependencies:
'@types/node': 22.13.10
'@types/estree@1.0.6': {} '@types/estree@1.0.6': {}
'@types/fs-extra@11.0.4': '@types/fs-extra@11.0.4':
@ -736,6 +772,8 @@ snapshots:
loupe: 3.1.3 loupe: 3.1.3
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
ansi-regex@5.0.1: {}
assertion-error@2.0.1: {} assertion-error@2.0.1: {}
cac@6.7.14: {} cac@6.7.14: {}
@ -750,12 +788,18 @@ snapshots:
check-error@2.1.1: {} check-error@2.1.1: {}
cli-progress@3.12.0:
dependencies:
string-width: 4.2.3
debug@4.4.0: debug@4.4.0:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
deep-eql@5.0.2: {} deep-eql@5.0.2: {}
emoji-regex@8.0.0: {}
es-module-lexer@1.6.0: {} es-module-lexer@1.6.0: {}
esbuild@0.25.1: esbuild@0.25.1:
@ -807,6 +851,8 @@ snapshots:
graceful-fs@4.2.11: {} graceful-fs@4.2.11: {}
is-fullwidth-code-point@3.0.0: {}
jsonfile@6.1.0: jsonfile@6.1.0:
dependencies: dependencies:
universalify: 2.0.1 universalify: 2.0.1
@ -870,6 +916,16 @@ snapshots:
std-env@3.8.1: {} 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: {} tinybench@2.9.0: {}
tinyexec@0.3.2: {} tinyexec@0.3.2: {}

View File

@ -18,11 +18,48 @@ const numMap: Record<number, number> = {
53: 12 // 道具 53: 12 // 道具
}; };
export function convertFloor( const apeiriaMap: Record<number, number> = {
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[][], map: number[][],
[x, y, w, h]: [number, number, number, number], [x, y, w, h]: [number, number, number, number],
name: string, name: string,
floorId: string floorId: string,
numMap: Record<number, number>
) { ) {
const clipped: number[][] = []; const clipped: number[][] = [];
@ -42,3 +79,73 @@ export function convertFloor(
return clipped; return clipped;
} }
function convertApeiriaEnemy(
map: number[][],
enemyMap: Record<number, ApeiriaEnemy>
) {
const width = map[0].length;
const height = map.length;
const enemy = new Set<ApeiriaEnemy>();
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<number, ApeiriaEnemy>
) {
return convert(
convertApeiriaEnemy(map, enemyMap),
clip,
name,
floorId,
apeiriaMap
);
}

View File

@ -5,6 +5,7 @@ import { mirrorMapX, mirrorMapY, rotateMap } from './topology/transform';
import { directions, tileType } from './topology/graph'; import { directions, tileType } from './topology/graph';
import { calculateVisualSimilarity } from './vision/similarity'; import { calculateVisualSimilarity } from './vision/similarity';
import { BaseConfig } from './types'; import { BaseConfig } from './types';
import { Presets, SingleBar } from 'cli-progress';
interface MinamoConfig extends BaseConfig {} interface MinamoConfig extends BaseConfig {}
@ -32,9 +33,9 @@ function chooseFrom<T>(arr: T[], n: number): T[] {
return copy.slice(0, n); 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 totalCount = Math.round((n * (n - 1)) / 2);
const count = Math.min(totalCount, 1000); const count = Math.min(totalCount, max);
const pairs: number[] = []; const pairs: number[] = [];
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) { 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 return trans
.map(([rot, flip]) => { .map(([rot, flip]) => {
const com1 = `${id1}.${rot}.${flip}:${id1}`; const com1 = `${id1}.${rot}.${flip}:${id1}`;
@ -153,7 +154,7 @@ function generateSimilarData(id: string, map: number[][]) {
// 生成最多五个微调地图 // 生成最多五个微调地图
const width = map[0].length; const width = map[0].length;
const height = map.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][] = []; const res: [id: string, data: MinamoTrainData][] = [];
for (let i = 0; i < num; i++) { for (let i = 0; i < num; i++) {
@ -210,7 +211,11 @@ function generateDataset(
): Record<string, MinamoTrainData> { ): Record<string, MinamoTrainData> {
const data: Record<string, MinamoTrainData> = {}; const data: Record<string, MinamoTrainData> = {};
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 num1 = Math.floor(v / floorIds.length);
const num2 = v % floorIds.length; const num2 = v % floorIds.length;
const id1 = floorIds[num1]; const id1 = floorIds[num1];
@ -268,8 +273,11 @@ function generateDataset(
// 地图微调训练集 // 地图微调训练集
Object.assign(data, Object.fromEntries(generateSimilarData(id1, map1))); Object.assign(data, Object.fromEntries(generateSimilarData(id1, map1)));
// Object.assign(data, Object.fromEntries(generateSimilarData(id2, map2))); // Object.assign(data, Object.fromEntries(generateSimilarData(id2, map2)));
progress.update(i + 1);
}); });
progress.stop();
return data; return data;
} }
@ -277,7 +285,7 @@ function parseAllData(data: Map<string, FloorData>): MinamoDataset {
const length = data.size; const length = data.size;
const totalCount = Math.round((length * (length - 1)) / 2); const totalCount = Math.round((length * (length - 1)) / 2);
const pairs = choosePair(length); const pairs = choosePair(length, 10000);
console.log( console.log(
`✅ 共发现 ${length} 个楼层,共 ${totalCount} 种组合,选取 ${pairs.length} 个组合` `✅ 共发现 ${length} 个楼层,共 ${totalCount} 种组合,选取 ${pairs.length} 个组合`

View File

@ -1,7 +1,7 @@
import { readFile } from 'fs-extra'; import { readFile } from 'fs-extra';
import { join } from 'path'; import { join } from 'path';
import { BaseConfig, TowerInfo } from './types'; import { BaseConfig, TowerInfo } from './types';
import { convertFloor } from './floor'; import { convertApeiriaMap, convertFloor } from './floor';
interface DatasetMergable<T> { interface DatasetMergable<T> {
datasetId: number; datasetId: number;
@ -67,7 +67,30 @@ export async function parseTowerInfo(
export async function getAllFloors(...info: TowerInfo[]) { export async function getAllFloors(...info: TowerInfo[]) {
const floorData = await Promise.all( 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<string, any>;
const mapData = JSON.parse(
mapFile.split('\n').slice(1).join('\n')
) as Record<number, any>;
const enemyNumMap: Record<number, any> = {};
// 将怪物转化为数字映射
for (const [key, value] of Object.entries(mapData)) {
if (value.cls === 'enemys') {
enemyNumMap[parseInt(key)] = enemyMap[value.id];
}
}
return Promise.all( return Promise.all(
tower.floorIds.map(async id => { tower.floorIds.map(async id => {
const floorFile = await readFile( const floorFile = await readFile(
@ -75,13 +98,25 @@ export async function getAllFloors(...info: TowerInfo[]) {
'utf-8' 'utf-8'
); );
const data = JSON.parse( const data = JSON.parse(
floorFile.split('\n').slice(1).join('\n') floorFile
.replaceAll("'", '"')
.slice(floorFile.indexOf('=') + 1)
); );
const map = data.map as number[][]; const map = data.map as number[][];
// 裁剪地图 // 裁剪地图
const { clip } = tower.config; const { clip } = tower.config;
const area = clip.special[id] ?? clip.defaults; 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);
}
}) })
); );
}) })