mirror of
https://github.com/unanmed/ginka-generator.git
synced 2026-07-16 22:11:15 +08:00
feat: 机关门检测
This commit is contained in:
parent
32227d2c44
commit
d406b97c90
@ -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],
|
||||
|
||||
@ -13,6 +13,7 @@ export class MapTileConverter implements IMapTileConverter {
|
||||
|
||||
private readonly emptyTiles = new Set<number>([0]);
|
||||
private readonly doorTiles = new Set<number>();
|
||||
private readonly specialDoorTiles = new Set<number>();
|
||||
private readonly enemyTiles = new Set<number>();
|
||||
private readonly resourceTiles = new Set<number>();
|
||||
private readonly keyTiles = new Set<number>();
|
||||
@ -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);
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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<MapGraphNode>
|
||||
): 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<MapGraphNode>): {
|
||||
* @param branchNodes 当前楼层里所有分支节点的去重集合
|
||||
* @returns 闲置门/怪数量,以及该楼层是否存在闲置分支
|
||||
*/
|
||||
function computeIdleBranchStats(branchNodes: Iterable<MapGraphNode>): {
|
||||
function computeIdleBranchStats(
|
||||
branchNodes: Iterable<MapGraphNode>,
|
||||
specialDoorLinkedEnemies?: Set<MapGraphNode>
|
||||
): {
|
||||
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<MapGraphNode>): {
|
||||
|
||||
if (node.branch === BranchType.Door) {
|
||||
idleDoorBranchCount++;
|
||||
} else if (
|
||||
specialDoorLinkedEnemies &&
|
||||
specialDoorLinkedEnemies.has(node)
|
||||
) {
|
||||
ignoredIdleEnemyBySpecialDoorCount++;
|
||||
} else {
|
||||
idleEnemyBranchCount++;
|
||||
}
|
||||
@ -621,6 +641,7 @@ function computeIdleBranchStats(branchNodes: Iterable<MapGraphNode>): {
|
||||
return {
|
||||
idleDoorBranchCount,
|
||||
idleEnemyBranchCount,
|
||||
ignoredIdleEnemyBySpecialDoorCount,
|
||||
hasIdleBranch: idleDoorBranchCount + idleEnemyBranchCount > 0
|
||||
};
|
||||
}
|
||||
@ -704,6 +725,59 @@ function buildMergedNonBranchAreas(graph: IMapGraph): {
|
||||
return { areas, areaMap };
|
||||
}
|
||||
|
||||
function findSpecialDoorLinkedEnemyNodes(
|
||||
branchNodes: Iterable<MapGraphNode>,
|
||||
areaMap: Map<MapGraphNode, IMergedNonBranchArea>
|
||||
): Set<MapGraphNode> {
|
||||
const specialDoorNodes = new Set<MapGraphNode>();
|
||||
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<IMergedNonBranchArea>();
|
||||
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<MapGraphNode>();
|
||||
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<MapGraphNode>,
|
||||
areaMap: Map<MapGraphNode, IMergedNonBranchArea>
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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,7 +154,7 @@ 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()
|
||||
|
||||
# 阶段二:生成怪物、门,同时也允许生成入口以适配结构
|
||||
@ -164,7 +165,7 @@ class GinkaSeperatedDataset(Dataset):
|
||||
|
||||
# 阶段三:生成资源
|
||||
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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
BIN
tiles/6.png
BIN
tiles/6.png
Binary file not shown.
|
Before Width: | Height: | Size: 414 B After Width: | Height: | Size: 995 B |
BIN
tiles/7.png
Normal file
BIN
tiles/7.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 184 B |
Loading…
Reference in New Issue
Block a user