From d406b97c901a3aefa00da7eff6a73d8536e787b6 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Wed, 15 Jul 2026 17:01:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9C=BA=E5=85=B3=E9=97=A8=E6=A3=80?= =?UTF-8?q?=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/src/auto.ts | 2 +- data/src/auto/converter.ts | 10 +++- data/src/auto/info.test.ts | 4 +- data/src/auto/info.ts | 119 ++++++++++++++++++++++++++++++++++--- data/src/auto/topo.ts | 12 +++- data/src/auto/types.ts | 13 ++++ data/src/shared.ts | 4 +- ginka/dataset.py | 21 +++---- ginka/train_seperated.py | 13 ++-- tiles/6.png | Bin 414 -> 995 bytes tiles/7.png | Bin 0 -> 184 bytes 11 files changed, 165 insertions(+), 33 deletions(-) create mode 100644 tiles/7.png diff --git a/data/src/auto.ts b/data/src/auto.ts index a81b33a..318696e 100644 --- a/data/src/auto.ts +++ b/data/src/auto.ts @@ -243,7 +243,7 @@ const labelConfig: IAutoLabelConfig = { wall: 1, decoration: 16, commonDoors: [2], - specialDoors: [2, 2], + specialDoors: [6, 6], keys: [3], redGems: [3], blueGems: [3], diff --git a/data/src/auto/converter.ts b/data/src/auto/converter.ts index bda544e..bff8b85 100644 --- a/data/src/auto/converter.ts +++ b/data/src/auto/converter.ts @@ -13,6 +13,7 @@ export class MapTileConverter implements IMapTileConverter { private readonly emptyTiles = new Set([0]); private readonly doorTiles = new Set(); + private readonly specialDoorTiles = new Set(); private readonly enemyTiles = new Set(); private readonly resourceTiles = new Set(); private readonly keyTiles = new Set(); @@ -224,8 +225,13 @@ export class MapTileConverter implements IMapTileConverter { if (isDoor) { this.doorTiles.add(tile); this.noPassMap.set(tile, false); - const label = labels.commonDoors[0]; - this.labelMap.set(tile, label); + if (blockId === 'specialDoor') { + this.specialDoorTiles.add(tile); + this.labelMap.set(tile, labels.specialDoors[0]); + } else { + const label = labels.commonDoors[0]; + this.labelMap.set(tile, label); + } } else if (isEnemy) { this.enemyTiles.add(tile); this.noPassMap.set(tile, false); diff --git a/data/src/auto/info.test.ts b/data/src/auto/info.test.ts index f6db53b..a6fe3fe 100644 --- a/data/src/auto/info.test.ts +++ b/data/src/auto/info.test.ts @@ -22,7 +22,7 @@ class MockTileConverter implements IMapTileConverter { } isDoor(tile: number): boolean { - return tile === 2; + return tile === 2 || tile === 6; } isEnemy(tile: number): boolean { @@ -76,7 +76,7 @@ const config: IAutoLabelConfig = { wall: 1, decoration: 16, commonDoors: [2], - specialDoors: [6, 7], + specialDoors: [6, 6], keys: [3], redGems: [3], blueGems: [3], diff --git a/data/src/auto/info.ts b/data/src/auto/info.ts index 3f88c7a..76fc660 100644 --- a/data/src/auto/info.ts +++ b/data/src/auto/info.ts @@ -1,6 +1,7 @@ import { readFile } from 'fs/promises'; import { BranchType, + DoorKind, GraphNodeType, IAutoLabelConfig, IFloorInfo, @@ -466,12 +467,21 @@ function collectReachableNodes( */ function isUselessBranchNode( topo: MapTopology, - branchNode: MapGraphNode + branchNode: MapGraphNode, + specialDoorLinkedEnemies?: Set ): boolean { if (branchNode.type !== GraphNodeType.Branch) { return false; } + if ( + branchNode.branch === BranchType.Enemy && + specialDoorLinkedEnemies && + specialDoorLinkedEnemies.has(branchNode) + ) { + return false; + } + const branchTile = getNodeTile(branchNode); if (countGridPassableDirections(topo, branchTile) <= 1) { // 格子层只有一个可通行方向时,直接按死胡同分支处理。 @@ -598,13 +608,18 @@ function computeBranchClusterStats(branchNodes: Iterable): { * @param branchNodes 当前楼层里所有分支节点的去重集合 * @returns 闲置门/怪数量,以及该楼层是否存在闲置分支 */ -function computeIdleBranchStats(branchNodes: Iterable): { +function computeIdleBranchStats( + branchNodes: Iterable, + specialDoorLinkedEnemies?: Set +): { idleDoorBranchCount: number; idleEnemyBranchCount: number; + ignoredIdleEnemyBySpecialDoorCount: number; hasIdleBranch: boolean; } { let idleDoorBranchCount = 0; let idleEnemyBranchCount = 0; + let ignoredIdleEnemyBySpecialDoorCount = 0; for (const node of branchNodes) { if (node.type !== GraphNodeType.Branch || node.neighbors.size !== 1) { @@ -613,6 +628,11 @@ function computeIdleBranchStats(branchNodes: Iterable): { if (node.branch === BranchType.Door) { idleDoorBranchCount++; + } else if ( + specialDoorLinkedEnemies && + specialDoorLinkedEnemies.has(node) + ) { + ignoredIdleEnemyBySpecialDoorCount++; } else { idleEnemyBranchCount++; } @@ -621,6 +641,7 @@ function computeIdleBranchStats(branchNodes: Iterable): { return { idleDoorBranchCount, idleEnemyBranchCount, + ignoredIdleEnemyBySpecialDoorCount, hasIdleBranch: idleDoorBranchCount + idleEnemyBranchCount > 0 }; } @@ -704,6 +725,59 @@ function buildMergedNonBranchAreas(graph: IMapGraph): { return { areas, areaMap }; } +function findSpecialDoorLinkedEnemyNodes( + branchNodes: Iterable, + areaMap: Map +): Set { + const specialDoorNodes = new Set(); + for (const node of branchNodes) { + if ( + node.type === GraphNodeType.Branch && + node.branch === BranchType.Door && + node.doorKind === DoorKind.Special + ) { + specialDoorNodes.add(node); + } + } + + if (specialDoorNodes.size === 0) { + return new Set(); + } + + const specialDoorLinkedAreas = new Set(); + for (const specialDoor of specialDoorNodes) { + for (const neighbor of specialDoor.neighbors) { + const area = areaMap.get(neighbor); + if (area) { + specialDoorLinkedAreas.add(area); + } + } + } + + if (specialDoorLinkedAreas.size === 0) { + return new Set(); + } + + const linkedEnemyNodes = new Set(); + for (const node of branchNodes) { + if ( + node.type !== GraphNodeType.Branch || + node.branch !== BranchType.Enemy + ) { + continue; + } + for (const neighbor of node.neighbors) { + const area = areaMap.get(neighbor); + if (area && specialDoorLinkedAreas.has(area)) { + linkedEnemyNodes.add(node); + break; + } + } + } + + return linkedEnemyNodes; +} + function buildRepeatedGuardCandidates( branchNodes: Iterable, areaMap: Map @@ -936,18 +1010,44 @@ export function parseFloorInfo( hasLargeEnemyCluster } = computeBranchClusterStats(branchNodes); - const { idleDoorBranchCount, idleEnemyBranchCount, hasIdleBranch } = - computeIdleBranchStats(branchNodes); + const mergedAreas = buildMergedNonBranchAreas(topo.graph); + const specialDoorLinkedEnemies = findSpecialDoorLinkedEnemyNodes( + branchNodes, + mergedAreas.areaMap + ); + const specialDoorLinkedEnemyCount = specialDoorLinkedEnemies.size; + + const { + idleDoorBranchCount, + idleEnemyBranchCount, + ignoredIdleEnemyBySpecialDoorCount, + hasIdleBranch + } = computeIdleBranchStats(branchNodes, specialDoorLinkedEnemies); const { repeatedGuardDoorBranchCount, repeatedGuardEnemyBranchCount, hasRepeatedGuardIdleBranch } = computeRepeatedGuardIdleStats(topo.graph, branchNodes, width); - // 无用分支逐点判定:只要任意一个分支命中,整层就带有无用分支标签。 - const hasUselessBranch = [...branchNodes].some(node => - isUselessBranchNode(topo, node) - ); + let hasUselessBranch = false; + let ignoredUselessBranchBySpecialDoorCount = 0; + for (const node of branchNodes) { + const useless = isUselessBranchNode( + topo, + node, + specialDoorLinkedEnemies + ); + if (useless) { + if ( + node.branch === BranchType.Enemy && + specialDoorLinkedEnemies.has(node) + ) { + ignoredUselessBranchBySpecialDoorCount++; + } else { + hasUselessBranch = true; + } + } + } // 统计拓扑图信息 let maxEmptyArea = 0; @@ -984,6 +1084,9 @@ export function parseFloorInfo( itemDensity: count(flattened, itemTiles) / area, entryCount: count(flattened, entryTiles), specialDoorCount: count(flattened, specialDoorTiles), + specialDoorLinkedEnemyCount, + ignoredIdleEnemyBySpecialDoorCount, + ignoredUselessBranchBySpecialDoorCount, maxDoorClusterSize, maxEnemyClusterSize, hasLargeDoorCluster, diff --git a/data/src/auto/topo.ts b/data/src/auto/topo.ts index 7fdd056..21aa7b6 100644 --- a/data/src/auto/topo.ts +++ b/data/src/auto/topo.ts @@ -2,6 +2,7 @@ import { CannotInOut, GraphNodeType, BranchType, + DoorKind, ResourceType, type IMapTopology, type IMapGraph, @@ -160,14 +161,19 @@ export class MapTopology implements IMapTopology { if (type === GraphNodeType.Branch) { const tile = this.originMap[y][x]; + const isDoor = converter.isDoor(tile); + const convertedTile = this.convertedMap[y][x]; nodeMap.set(idx, { type: GraphNodeType.Branch, index: nodeIndex++, tiles, neighbors, - branch: converter.isDoor(tile) - ? BranchType.Door - : BranchType.Enemy + branch: isDoor ? BranchType.Door : BranchType.Enemy, + doorKind: isDoor + ? this.config.specialDoors.includes(convertedTile) + ? DoorKind.Special + : DoorKind.Common + : DoorKind.Common }); continue; } diff --git a/data/src/auto/types.ts b/data/src/auto/types.ts index 29fb657..eac920c 100644 --- a/data/src/auto/types.ts +++ b/data/src/auto/types.ts @@ -89,6 +89,12 @@ export interface IFloorInfo { readonly entryCount: number; /** 机关门数量 */ readonly specialDoorCount: number; + /** 机关门关联怪数量 */ + readonly specialDoorLinkedEnemyCount: number; + /** 因机关门关联而被豁免的闲置怪数量 */ + readonly ignoredIdleEnemyBySpecialDoorCount: number; + /** 因机关门关联而被豁免的无用分支数量 */ + readonly ignoredUselessBranchBySpecialDoorCount: number; /** 同类门分支连通块的最大大小 */ readonly maxDoorClusterSize: number; /** 同类怪分支连通块的最大大小 */ @@ -360,6 +366,11 @@ export const enum BranchType { Enemy } +export const enum DoorKind { + Common, + Special +} + export interface IMapGraphNodeBase { /** 节点类型 */ readonly type: GraphNodeType; @@ -385,6 +396,8 @@ export interface IBranchMapGraphNode extends IMapGraphNodeBase { readonly type: GraphNodeType.Branch; /** 分支节点类型 */ readonly branch: BranchType; + /** 门子类型,仅 Door 分支有效 */ + readonly doorKind: DoorKind; } export interface IEntryMapGraphNode extends IMapGraphNodeBase { diff --git a/data/src/shared.ts b/data/src/shared.ts index 054420a..43623f9 100644 --- a/data/src/shared.ts +++ b/data/src/shared.ts @@ -1,10 +1,10 @@ // 基本图块定义 -// 新方案 ID:0=空地 1=墙壁 2=门 3=资源(all) 4=怪物 5=入口 6=掩码 +// 新方案 ID:0=空地 1=墙壁 2=普通门 3=资源(all) 4=怪物 5=入口 6=机关门 7=掩码 export const emptyTiles = new Set([0]); export const wallTiles = new Set([1]); export const decorationTiles = new Set([16]); export const commonDoorTiles = new Set([2]); -export const specialDoorTiles = new Set([2]); +export const specialDoorTiles = new Set([6]); export const keyTiles = new Set([3]); export const redGemTiles = new Set([3]); export const blueGemTiles = new Set([3]); diff --git a/ginka/dataset.py b/ginka/dataset.py index 86695a9..8140c35 100644 --- a/ginka/dataset.py +++ b/ginka/dataset.py @@ -28,7 +28,8 @@ class GinkaSeperatedDataset(Dataset): RESOURCE = 3 MONSTER = 4 ENTRANCE = 5 - MASK_ID = 6 + SPECIAL_DOOR = 6 + MASK_ID = 7 MAP_SIZE = 13 * 13 def __init__( @@ -51,7 +52,7 @@ class GinkaSeperatedDataset(Dataset): def compute_density_stats(self) -> dict: wall_densities = [self.count_tile(item['map'], self.WALL) / self.MAP_SIZE for item in self.data] - door_densities = [self.count_tile(item['map'], self.DOOR) / self.MAP_SIZE for item in self.data] + door_densities = [self.count_tile(item['map'], self.DOOR) / self.MAP_SIZE + self.count_tile(item['map'], self.SPECIAL_DOOR) / self.MAP_SIZE for item in self.data] monster_densities = [self.count_tile(item['map'], self.MONSTER) / self.MAP_SIZE for item in self.data] entrance_densities = [self.count_tile(item['map'], self.ENTRANCE) / self.MAP_SIZE for item in self.data] resource_densities = [self.count_tile(item['map'], self.RESOURCE) / self.MAP_SIZE for item in self.data] @@ -79,7 +80,7 @@ class GinkaSeperatedDataset(Dataset): def build_target_density(self, map_data: list) -> torch.Tensor: return torch.FloatTensor([ self.count_tile(map_data, self.WALL) / self.MAP_SIZE, - self.count_tile(map_data, self.DOOR) / self.MAP_SIZE, + (self.count_tile(map_data, self.DOOR) + self.count_tile(map_data, self.SPECIAL_DOOR)) / self.MAP_SIZE, self.count_tile(map_data, self.MONSTER) / self.MAP_SIZE, self.count_tile(map_data, self.ENTRANCE) / self.MAP_SIZE, self.count_tile(map_data, self.RESOURCE) / self.MAP_SIZE @@ -153,20 +154,20 @@ class GinkaSeperatedDataset(Dataset): def create_degreaded(self, raw: np.ndarray): # 阶段一:仅生成墙壁骨架 target1 = raw.copy() - self.degrade_tile(target1, [self.DOOR, self.RESOURCE, self.MONSTER, self.ENTRANCE]) + self.degrade_tile(target1, [self.DOOR, self.SPECIAL_DOOR, self.RESOURCE, self.MONSTER, self.ENTRANCE]) inp1 = target1.copy() - + # 阶段二:生成怪物、门,同时也允许生成入口以适配结构 target2 = raw.copy() self.degrade_tile(target2, [self.RESOURCE, self.WALL]) inp2 = raw.copy() self.degrade_tile(inp2, [self.RESOURCE]) - + # 阶段三:生成资源 target3 = raw.copy() - self.degrade_tile(target3, [self.WALL, self.DOOR, self.MONSTER, self.ENTRANCE]) + self.degrade_tile(target3, [self.WALL, self.DOOR, self.SPECIAL_DOOR, self.MONSTER, self.ENTRANCE]) inp3 = raw.copy() - + return target1, inp1, target2, inp2, target3, inp3 def apply_subset1(self, raw: np.ndarray): @@ -182,7 +183,7 @@ class GinkaSeperatedDataset(Dataset): inp1[self.std_mask()] = self.MASK_ID # stage2:对 floor+功能元素区域 std_mask - need_mask = np.isin(inp2, [self.FLOOR, self.DOOR, self.MONSTER, self.ENTRANCE]) + need_mask = np.isin(inp2, [self.FLOOR, self.DOOR, self.SPECIAL_DOOR, self.MONSTER, self.ENTRANCE]) inp2[need_mask & self.std_mask()] = self.MASK_ID # stage3:对 floor+resource 区域 std_mask @@ -201,7 +202,7 @@ class GinkaSeperatedDataset(Dataset): need_mask = np.isin(inp2, [self.FLOOR, self.WALL]) inp1[need_mask & self.std_mask()] = self.MASK_ID - need_mask = np.isin(inp2, [self.FLOOR, self.DOOR, self.MONSTER, self.ENTRANCE]) + need_mask = np.isin(inp2, [self.FLOOR, self.DOOR, self.SPECIAL_DOOR, self.MONSTER, self.ENTRANCE]) inp2[need_mask] = self.MASK_ID need_mask = np.isin(inp3, [self.FLOOR, self.RESOURCE]) inp3[need_mask] = self.MASK_ID diff --git a/ginka/train_seperated.py b/ginka/train_seperated.py index f17c0ac..cc01024 100644 --- a/ginka/train_seperated.py +++ b/ginka/train_seperated.py @@ -33,7 +33,7 @@ from shared.image import matrix_to_image_cv # stage3 → resource(资源点) # 图块 ID 定义: -# 0. 空地 1. 墙壁 2. 门 3. 资源 4. 怪物 5. 入口 6. 掩码(MASK_TOKEN) +# 0. 空地 1. 墙壁 2. 普通门 3. 资源 4. 怪物 5. 入口 6. 机关门 7. 掩码(MASK_TOKEN) # 共用 VQ-VAE 超参 # 三组编码器(vq1/vq2/vq3)共享相同超参,分别对三阶段地图上下文独立编码 @@ -76,8 +76,8 @@ STAGE2_VQ_WEIGHT = 0.5 STAGE3_VQ_WEIGHT = 0.5 # 全局参数 -NUM_CLASSES = 7 # 图块类型数 -MASK_TOKEN = 6 # 掩码图块 +NUM_CLASSES = 8 # 图块类型数 +MASK_TOKEN = 7 # 掩码图块 MAP_W = 13 # 地图宽度 MAP_H = 13 # 地图高度 MAP_SIZE = MAP_W * MAP_H # 地图大小 @@ -276,7 +276,7 @@ def compute_remaining( remain = torch.zeros(current.size(0), DENSITY_DIM, device=current.device) visible_wall = (current == 1).sum(dim=1).float() / MAP_SIZE - visible_door = (current == 2).sum(dim=1).float() / MAP_SIZE + visible_door = ((current == 2) | (current == 6)).sum(dim=1).float() / MAP_SIZE visible_monster = (current == 4).sum(dim=1).float() / MAP_SIZE visible_entrance = (current == 5).sum(dim=1).float() / MAP_SIZE visible_resource = (current == 3).sum(dim=1).float() / MAP_SIZE @@ -419,7 +419,7 @@ def full_generate_specific_z( pred2_np = maskgit_sample( mg2, inp2, z2, struct, target_density, 2, - GENERATE_STEP, target_tiles=[2, 4, 5], keep_fixed=keep_fixed[1] + GENERATE_STEP, target_tiles=[2, 6, 4, 5], keep_fixed=keep_fixed[1] ) merged12 = pred1_np.copy() merged12[pred2_np != 0] = pred2_np[pred2_np != 0] @@ -742,6 +742,9 @@ def validate( true_map = true_map_batch[batch_idx] pred_count = float((pred_map == tile_id).sum().item()) true_count = float((true_map == tile_id).sum().item()) + if tile_id == 2: + pred_count += float((pred_map == 6).sum().item()) + true_count += float((true_map == 6).sum().item()) density_metrics[tile_id]["mae"] += abs(pred_count - true_count) / MAP_SIZE density_metrics[tile_id]["over"] += max(pred_count - true_count, 0.0) / MAP_SIZE density_metrics[tile_id]["count"] += 1 diff --git a/tiles/6.png b/tiles/6.png index eb627852c81f06a4f7aa982b763d620b37d3e28d..7cb1727615b3a021688a98eefd73a2d6a877dbcc 100644 GIT binary patch delta 974 zcmV;<12O!b1LFsfBYy*!NklLaggDVM=l#&`;f@?@ANsvnZ zK^+9S2T0}(I15tNr@sxYxZItc9a5BRUt0fpe*W?At=3E1TkF85)&dgv^z}o#*C3z{ z!@su<9Qf2-;v_-x=eOT&Wne};{(jw;HSke~foG(*cOU_R`+vSOw8t-h+T+KADMoT6 zV~$;aJuocm5NVKZ>1hF)HQ4u=c0T`345nAa>=*NTHxQ)6!J1b*y=01^hu zNuh}}53$^vS1>-1LWLuo$47W7Kz05gF`7WCBf@hMCzhPC$CaPp3C#l3h+r(L{Wuh4 zRgfpHP+i2xfgu<%#F^i+S%6gYj^&OREan+2eLWjueH^2|*pVfZw0Twl91V|R#g4pyAx)W2B( z4qLJ^ix7~;Sehe7)Yqt; z!dI%UNdT*2>mZ4ZO&~@H)#Mqm_~gLAL!4T2Wq;d7c(PdlhiXV?tJsySm}i~%0QH|^ z$`ClN8?Ra6G(Wri z8V-qYm{Z(-MHxG$1ms=qasPt>SsCAY6`ClZ!3Pa|;*`2SP|fMzgZv zmq=CxE|!?_9XtBq!i}Gvm#3%C@5TT=WJfCl6N1rS)sO>=rIy~_0h}aA-ecgY!|-p_ woW;SX?mqwk0RR7h%&qnS000I_L_t&o06j`R^4nO;NdN!<07*qoM6N<$fPx$SV=@dR9HvtmcbE%Fc3vU37oVdTEGih z2`6d9@uDrzkta*QS(xv97aTSrnOw+4M=<~Kck`FRWnM*QeYXPyJMg%$#00c(X`+wDruJ}Nh0kk*->X6YHpndZp2>{!zpA;RREQokxWIm7LCF`R_v24WiK#yLz&hG?pBib;+ljwvmD;= iLr@7e=*|AR1AG8{QWY>=mj!D80000gTe~DWM4fl?6D; literal 0 HcmV?d00001