mirror of
https://github.com/unanmed/ginka-generator.git
synced 2026-05-14 04:41:12 +08:00
refactor: 删除旧版拓扑算法
This commit is contained in:
parent
ef79dd4d31
commit
01c9e1972e
@ -1,29 +0,0 @@
|
||||
import { buildTopologicalGraph } from './graph';
|
||||
import { GinkaTopologicalGraphs } from './interface';
|
||||
import { overallSimilarity } from './similarity';
|
||||
|
||||
const cache = new Map<string, GinkaTopologicalGraphs>();
|
||||
|
||||
export function getTopologicalGraph(
|
||||
floorId: string,
|
||||
map: number[][]
|
||||
): GinkaTopologicalGraphs {
|
||||
if (cache.has(floorId)) return cache.get(floorId)!;
|
||||
const graphs = buildTopologicalGraph(map);
|
||||
cache.set(floorId, graphs);
|
||||
return graphs;
|
||||
}
|
||||
|
||||
export function compareMap(
|
||||
floorId1: string,
|
||||
floorId2: string,
|
||||
map1: number[][],
|
||||
map2: number[][]
|
||||
) {
|
||||
const graph1 = getTopologicalGraph(floorId1, map1);
|
||||
const graph2 = getTopologicalGraph(floorId2, map2);
|
||||
|
||||
const kernel = overallSimilarity(graph1, graph2);
|
||||
|
||||
return kernel;
|
||||
}
|
||||
@ -1,255 +0,0 @@
|
||||
import {
|
||||
ResourceArea,
|
||||
GinkaGraph,
|
||||
BranchNode,
|
||||
GinkaTopologicalGraphs,
|
||||
ResourceNode,
|
||||
NodeType
|
||||
} from './interface';
|
||||
|
||||
export const tileType = new Set(
|
||||
Array(13)
|
||||
.fill(0)
|
||||
.map((_, i) => i)
|
||||
);
|
||||
const branchType = new Set([6, 7, 8, 9]);
|
||||
const entranceType = new Set([10, 11]);
|
||||
const resourceType = new Set([0, 2, 3, 4, 5, 10, 11, 12, 13]);
|
||||
|
||||
export const directions: [number, number][] = [
|
||||
[-1, 0],
|
||||
[1, 0],
|
||||
[0, -1],
|
||||
[0, 1]
|
||||
];
|
||||
|
||||
function buildGraphFromEntrance(
|
||||
map: number[][],
|
||||
entrance: number,
|
||||
resourceMap: Map<number, number>,
|
||||
areaMap: ResourceArea[]
|
||||
): GinkaGraph {
|
||||
const width = map[0].length;
|
||||
const height = map[1].length;
|
||||
|
||||
const visitedEntrance = new Set<number>([entrance]);
|
||||
const visited = new Set<number>();
|
||||
const queue: [number, number][] = [];
|
||||
queue.push([entrance % width, Math.floor(entrance / width)]);
|
||||
|
||||
const branchNodes = new Set<number>();
|
||||
|
||||
// 1. BFS 检测所有分支节点
|
||||
while (queue.length > 0) {
|
||||
const item = queue.shift();
|
||||
if (!item) continue;
|
||||
const [nx, ny] = item;
|
||||
const index = ny * width + nx;
|
||||
if (visited.has(index)) continue;
|
||||
const tile = map[ny][nx];
|
||||
|
||||
if (entranceType.has(tile)) {
|
||||
visitedEntrance.add(index);
|
||||
}
|
||||
if (branchType.has(tile)) {
|
||||
branchNodes.add(index);
|
||||
}
|
||||
visited.add(index);
|
||||
|
||||
for (const [dx, dy] of directions) {
|
||||
const px = dx + nx;
|
||||
const py = dy + ny;
|
||||
if (px < 0 || px >= width || py < 0 || py >= height) {
|
||||
continue;
|
||||
}
|
||||
const tile = map[py][px];
|
||||
if (tile !== 1) {
|
||||
// 非墙区域可通行
|
||||
queue.push([px, py]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 从分支节点构建拓扑图
|
||||
const graph = new Map<number, BranchNode | ResourceNode>();
|
||||
branchNodes.forEach(v => {
|
||||
const nx = v % width;
|
||||
const ny = Math.floor(v / width);
|
||||
if (!graph.get(v)) {
|
||||
graph.set(v, {
|
||||
type: NodeType.Branch,
|
||||
neighbor: new Set(),
|
||||
tile: map[ny][nx]
|
||||
});
|
||||
}
|
||||
const node = graph.get(v)!;
|
||||
for (const [dx, dy] of directions) {
|
||||
const px = nx + dx;
|
||||
const py = ny + dy;
|
||||
if (px < 0 || px >= width || py < 0 || py >= height) {
|
||||
continue;
|
||||
}
|
||||
const index = py * width + px;
|
||||
|
||||
// 先检查临近节点是不是分支节点,是的话链接到自己
|
||||
if (branchNodes.has(index)) {
|
||||
node.neighbor.add(index);
|
||||
} else {
|
||||
// 检查是不是资源节点
|
||||
const pointer = resourceMap.get(index);
|
||||
if (pointer === void 0) continue;
|
||||
const area = areaMap[pointer];
|
||||
if (!area) continue;
|
||||
area.neighbor.add(v);
|
||||
area.members.forEach(v => {
|
||||
node.neighbor.add(v);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 把资源节点拆分成并排,并放入拓扑图
|
||||
areaMap.forEach(v => {
|
||||
v.members.forEach(index => {
|
||||
const nx = index % width;
|
||||
const ny = Math.floor(index / width);
|
||||
const tile = map[ny][nx];
|
||||
if (tile === 0) return;
|
||||
const node: ResourceNode = {
|
||||
type: NodeType.Resource,
|
||||
resourceType: tile,
|
||||
neighbor: v.neighbor,
|
||||
resourceArea: v
|
||||
};
|
||||
graph.set(index, node);
|
||||
});
|
||||
});
|
||||
|
||||
return { graph, resourceMap, areaMap, visitedEntrance, visited };
|
||||
}
|
||||
|
||||
function findResourceNodes(map: number[][]) {
|
||||
const width = map[0].length;
|
||||
const height = map[1].length;
|
||||
|
||||
const visited = new Set<number>();
|
||||
const areas: ResourceArea[] = [];
|
||||
const resourcesMap: Map<number, number> = new Map();
|
||||
|
||||
for (let ny = 0; ny < height; ny++) {
|
||||
for (let nx = 0; nx < width; nx++) {
|
||||
const tile = map[ny][nx];
|
||||
const index = ny * width + nx;
|
||||
if (visited.has(index) || !resourceType.has(tile)) {
|
||||
continue;
|
||||
}
|
||||
const queue: [number, number][] = [];
|
||||
queue.push([nx, ny]);
|
||||
const area: ResourceArea = {
|
||||
type: NodeType.Resource,
|
||||
resources: new Map([[tile, 1]]),
|
||||
members: new Set([index]),
|
||||
neighbor: new Set()
|
||||
};
|
||||
|
||||
while (queue.length > 0) {
|
||||
const item = queue.shift();
|
||||
if (!item) continue;
|
||||
const [nx, ny] = item;
|
||||
const index = ny * width + nx;
|
||||
if (visited.has(index)) {
|
||||
continue;
|
||||
}
|
||||
const tile = map[ny][nx];
|
||||
if (!resourceType.has(tile)) {
|
||||
continue;
|
||||
}
|
||||
visited.add(index);
|
||||
|
||||
const exists = area.resources.get(tile);
|
||||
if (!exists) {
|
||||
area.resources.set(tile, 1);
|
||||
} else {
|
||||
area.resources.set(tile, exists + 1);
|
||||
}
|
||||
area.members.add(index);
|
||||
resourcesMap.set(index, areas.length);
|
||||
|
||||
for (const [dx, dy] of directions) {
|
||||
const px = nx + dx;
|
||||
const py = ny + dy;
|
||||
if (px < 0 || px >= width || py < 0 || py >= height) {
|
||||
continue;
|
||||
}
|
||||
queue.push([px, py]);
|
||||
}
|
||||
}
|
||||
|
||||
areas.push(area);
|
||||
}
|
||||
}
|
||||
|
||||
return { areaMap: areas, resourcesMap };
|
||||
}
|
||||
|
||||
export function buildTopologicalGraph(map: number[][]): GinkaTopologicalGraphs {
|
||||
const width = map[0].length;
|
||||
const height = map[1].length;
|
||||
|
||||
// 1. 找到所有入口
|
||||
const entrances = new Set<number>();
|
||||
for (let ny = 0; ny < height; ny++) {
|
||||
for (let nx = 0; nx < width; nx++) {
|
||||
const tile = map[ny][nx];
|
||||
if (entranceType.has(tile)) {
|
||||
entrances.add(ny * width + nx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 找到所有的资源节点
|
||||
const { areaMap, resourcesMap } = findResourceNodes(map);
|
||||
|
||||
// 3. 对每个入口计算拓扑图
|
||||
const graphs: GinkaGraph[] = [];
|
||||
const usedEntrance = new Set<number>();
|
||||
const totalVisited = new Set<number>();
|
||||
/** 入口位置到拓扑图的映射 */
|
||||
const entranceMap = new Map<number, GinkaGraph>();
|
||||
entrances.forEach(v => {
|
||||
if (usedEntrance.has(v)) {
|
||||
return;
|
||||
}
|
||||
const nx = v % width;
|
||||
const ny = Math.floor(v / width);
|
||||
const entranceGraph = buildGraphFromEntrance(
|
||||
map,
|
||||
v,
|
||||
resourcesMap,
|
||||
areaMap
|
||||
);
|
||||
const { graph, visited, visitedEntrance } = entranceGraph;
|
||||
graphs.push(entranceGraph);
|
||||
// 标记已经探索到的入口,并标记这个入口对应了哪个图
|
||||
visitedEntrance.forEach(v => {
|
||||
usedEntrance.add(v);
|
||||
entranceMap.set(v, entranceGraph);
|
||||
});
|
||||
visited.forEach(v => {
|
||||
totalVisited.add(v);
|
||||
});
|
||||
});
|
||||
|
||||
// 3. 计算不可到达区域
|
||||
const unreachable = new Set<number>();
|
||||
for (let ny = 0; ny < height; ny++) {
|
||||
for (let nx = 0; nx < width; nx++) {
|
||||
const index = ny * width + nx;
|
||||
if (!totalVisited.has(index) && map[ny][nx] !== 1) {
|
||||
unreachable.add(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { graphs, entranceMap, unreachable };
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
export const enum NodeType {
|
||||
Branch,
|
||||
Resource
|
||||
}
|
||||
|
||||
export interface ResourceArea {
|
||||
/** 节点类型 */
|
||||
readonly type: NodeType.Resource;
|
||||
/** 每种资源对应的数量 */
|
||||
readonly resources: Map<number, number>;
|
||||
/** 资源区域包含的所有资源图块坐标索引 */
|
||||
readonly members: Set<number>;
|
||||
/** 资源区域的邻居节点 */
|
||||
readonly neighbor: Set<number>;
|
||||
}
|
||||
|
||||
export interface BranchNode {
|
||||
/** 节点类型 */
|
||||
readonly type: NodeType.Branch;
|
||||
/** 分支节点的邻居节点 */
|
||||
readonly neighbor: Set<number>;
|
||||
/** 分支节点图块 */
|
||||
readonly tile: number;
|
||||
}
|
||||
|
||||
export interface ResourceNode {
|
||||
/** 节点类型 */
|
||||
readonly type: NodeType.Resource;
|
||||
/** 资源类型 */
|
||||
readonly resourceType: number;
|
||||
/** 邻居节点 */
|
||||
readonly neighbor: Set<number>;
|
||||
/** 资源节点所属的资源区域 */
|
||||
readonly resourceArea: ResourceArea;
|
||||
}
|
||||
|
||||
export type GinkaNode = BranchNode | ResourceNode;
|
||||
|
||||
export interface GinkaGraph {
|
||||
/** 拓扑图内容,键表示位置,值表示这一点的节点 */
|
||||
readonly graph: Map<number, GinkaNode>;
|
||||
/** 资源指针,键表示位置,值表示这一点对应的资源节点在 areaMap 的索引 */
|
||||
readonly resourceMap: Map<number, number>;
|
||||
/** 资源区域列表 */
|
||||
readonly areaMap: ResourceArea[];
|
||||
/** 这个拓扑图包含的入口位置 */
|
||||
readonly visitedEntrance: Set<number>;
|
||||
/** 这个拓扑图能够造访的所有位置 */
|
||||
readonly visited: Set<number>;
|
||||
}
|
||||
|
||||
export interface GinkaTopologicalGraphs {
|
||||
/** 这个地图包含的所有独立的图 */
|
||||
readonly graphs: GinkaGraph[];
|
||||
/** 每个入口对应哪个图 */
|
||||
readonly entranceMap: Map<number, GinkaGraph>;
|
||||
/** 这个图从入口开始的不可到达区域 */
|
||||
readonly unreachable: Set<number>;
|
||||
}
|
||||
@ -1,179 +0,0 @@
|
||||
import { cosineSimilarity } from 'src/utils';
|
||||
import { GinkaGraph, GinkaTopologicalGraphs, NodeType } from './interface';
|
||||
|
||||
interface WLNode {
|
||||
originalPos: number;
|
||||
originalLabel: string;
|
||||
currentLabel: string;
|
||||
neighbors: WLNode[];
|
||||
}
|
||||
|
||||
function encodeNodeLabels(graph: GinkaGraph) {
|
||||
const nodes: WLNode[] = [];
|
||||
const nodeMap = new Map<number, WLNode>();
|
||||
|
||||
graph.graph.forEach((node, pos) => {
|
||||
let label: string;
|
||||
|
||||
// 编码为唯一哈希值(用字符串就行,V8 会自动帮你算哈希)
|
||||
if (node.type === NodeType.Branch) {
|
||||
label = `B:${node.tile}`;
|
||||
} else {
|
||||
label = `R:${node.resourceType}`;
|
||||
}
|
||||
|
||||
const wlNode: WLNode = {
|
||||
originalPos: pos,
|
||||
originalLabel: label,
|
||||
currentLabel: label,
|
||||
neighbors: []
|
||||
};
|
||||
nodeMap.set(pos, wlNode);
|
||||
nodes.push(wlNode);
|
||||
});
|
||||
|
||||
// 映射邻居节点
|
||||
nodes.forEach(node => {
|
||||
const ginkaNode = graph.graph.get(node.originalPos);
|
||||
ginkaNode?.neighbor.forEach(v => {
|
||||
const wl = nodeMap.get(v);
|
||||
if (wl) node.neighbors.push(wl);
|
||||
});
|
||||
});
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function weisfeilerLehmanIteration(
|
||||
nodes: WLNode[],
|
||||
iterations: number,
|
||||
decay: number = 0.6 // 衰减权重,减小长距离图的权重
|
||||
) {
|
||||
const labelHistory: string[][] = [];
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const newLabels: string[] = [];
|
||||
|
||||
// 生成新标签
|
||||
nodes.forEach(node => {
|
||||
const neighborLabels = node.neighbors
|
||||
.map(n => n.currentLabel)
|
||||
.sort();
|
||||
|
||||
const compositeLabel = `${node.currentLabel}|${neighborLabels.join(
|
||||
','
|
||||
)}`.slice(0, 8192);
|
||||
|
||||
newLabels.push(compositeLabel);
|
||||
});
|
||||
|
||||
// 更新节点标签并记录
|
||||
nodes.forEach((node, idx) => {
|
||||
node.currentLabel = newLabels[idx];
|
||||
});
|
||||
labelHistory.push([...newLabels]);
|
||||
}
|
||||
|
||||
// 统计每个节点的数量
|
||||
let weight = 1;
|
||||
const numMap = new Map<string, number>();
|
||||
labelHistory.forEach(iter => {
|
||||
iter.forEach(v => {
|
||||
if (!numMap.has(v)) {
|
||||
numMap.set(v, weight);
|
||||
} else {
|
||||
numMap.set(v, numMap.get(v)! + weight);
|
||||
}
|
||||
});
|
||||
weight *= decay;
|
||||
});
|
||||
// 把每个节点的原始标签也加上,权重使用最远权重,可以认为是资源重复率
|
||||
nodes.forEach(node => {
|
||||
if (!numMap.has(node.originalLabel)) {
|
||||
numMap.set(node.originalLabel, weight);
|
||||
} else {
|
||||
numMap.set(
|
||||
node.originalLabel,
|
||||
numMap.get(node.originalLabel)! + weight
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return numMap;
|
||||
}
|
||||
|
||||
function vectorizeFeatures(features: Map<string, number>, vocab: string[]) {
|
||||
const vec: number[] = new Array(vocab.length).fill(0);
|
||||
|
||||
features.forEach((count, label) => {
|
||||
const index = vocab.indexOf(label);
|
||||
if (index !== -1) {
|
||||
vec[index] += count;
|
||||
}
|
||||
});
|
||||
|
||||
return vec;
|
||||
}
|
||||
|
||||
function wlKernel(
|
||||
graphA: GinkaGraph,
|
||||
graphB: GinkaGraph,
|
||||
iterations = 3
|
||||
): number {
|
||||
// 编码节点
|
||||
const nodesA = encodeNodeLabels(graphA);
|
||||
const nodesB = encodeNodeLabels(graphB);
|
||||
|
||||
// 迭代生成标签
|
||||
const featuresA = weisfeilerLehmanIteration(nodesA, iterations);
|
||||
const featuresB = weisfeilerLehmanIteration(nodesB, iterations);
|
||||
|
||||
// 构建特征向量
|
||||
const vocab = [...new Set([...featuresA.keys(), ...featuresB.keys()])];
|
||||
const vecA = vectorizeFeatures(featuresA, vocab);
|
||||
const vecB = vectorizeFeatures(featuresB, vocab);
|
||||
|
||||
// 计算余弦相似度
|
||||
return cosineSimilarity(vecA, vecB);
|
||||
}
|
||||
|
||||
export function overallSimilarity(
|
||||
a: GinkaTopologicalGraphs,
|
||||
b: GinkaTopologicalGraphs
|
||||
) {
|
||||
// 使用 Weisfeiler-Lehman Kernel 方式计算拓扑图相似度
|
||||
const graphsA = a.graphs;
|
||||
const graphsB = b.graphs;
|
||||
|
||||
let totalSimilarity = 0;
|
||||
const comparedGraph = new Set<GinkaGraph>();
|
||||
graphsA.forEach(ga => {
|
||||
let maxSimilarity = 0;
|
||||
let maxGraph: GinkaGraph | null = null;
|
||||
// 图之间两两比较,找到最接近的作为相似度
|
||||
for (const gb of graphsB) {
|
||||
if (comparedGraph.has(gb)) continue;
|
||||
// 计算迭代次数
|
||||
const min = Math.min(ga.graph.size, gb.graph.size);
|
||||
const iterations = Math.ceil(Math.max(1, Math.log(min)));
|
||||
const similarity = wlKernel(ga, gb, iterations);
|
||||
if (similarity > maxSimilarity && !isNaN(similarity)) {
|
||||
maxSimilarity = similarity;
|
||||
maxGraph = gb;
|
||||
}
|
||||
if (similarity === 1) break;
|
||||
}
|
||||
totalSimilarity += maxSimilarity;
|
||||
if (maxGraph) comparedGraph.add(maxGraph);
|
||||
});
|
||||
|
||||
// 不可达区域惩罚
|
||||
const reduction =
|
||||
1 / (1 + Math.abs(a.unreachable.size - b.unreachable.size));
|
||||
// 取根号使结果更接近线性
|
||||
if (graphsA.length === 0) {
|
||||
return 0;
|
||||
} else {
|
||||
return Math.sqrt(totalSimilarity / graphsA.length) * reduction;
|
||||
}
|
||||
}
|
||||
@ -43,70 +43,6 @@ export function mergeDataset<T>(
|
||||
return dataset;
|
||||
}
|
||||
|
||||
export function cosineSimilarity(vecA: number[], vecB: number[]): number {
|
||||
if (vecA.length !== vecB.length) {
|
||||
throw new Error('Vectors must have same dimension');
|
||||
}
|
||||
|
||||
let dot = 0,
|
||||
normA = 0,
|
||||
normB = 0;
|
||||
for (let i = 0; i < vecA.length; i++) {
|
||||
dot += vecA[i] * vecB[i];
|
||||
normA += vecA[i] ** 2;
|
||||
normB += vecB[i] ** 2;
|
||||
}
|
||||
|
||||
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
||||
}
|
||||
|
||||
export async function parseTowerInfo(
|
||||
path: string,
|
||||
configName: string
|
||||
): Promise<TowerInfo> {
|
||||
const dataFile = await readFile(join(path, 'data.js'), 'utf-8');
|
||||
const data: any = JSON.parse(dataFile.split('\n').slice(1).join('\n'));
|
||||
const configFile = await readFile(join(path, configName), 'utf-8');
|
||||
|
||||
return {
|
||||
path: path,
|
||||
name: data.firstData.name as string,
|
||||
floorIds: data.main.floorIds as string[],
|
||||
config: JSON.parse(configFile) as BaseConfig
|
||||
};
|
||||
}
|
||||
|
||||
export function mergeFloorIds(...info: TowerInfo[]) {
|
||||
const ids: string[] = [];
|
||||
info.forEach(v => {
|
||||
ids.push(...v.floorIds.map(id => `${v.name}:${id}`));
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
|
||||
export async function fromJSON(path: string) {
|
||||
const file = await readFile(path, 'utf-8');
|
||||
const data = JSON.parse(file) as Record<string, number[][]>;
|
||||
const clip: Record<string, [number, number, number, number]> = {};
|
||||
const config: BaseConfig = {
|
||||
clip: {
|
||||
defaults: [0, 0, 0, 0],
|
||||
special: clip
|
||||
}
|
||||
};
|
||||
const name = (Math.random() * 12).toFixed(0);
|
||||
const floorMap = new Map<string, FloorData>();
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
const floorData: FloorData = {
|
||||
map: value,
|
||||
id: key,
|
||||
config
|
||||
};
|
||||
floorMap.set(`${name}:${key}`, floorData);
|
||||
}
|
||||
return floorMap;
|
||||
}
|
||||
|
||||
export function chooseFrom<T>(arr: T[], n: number): T[] {
|
||||
const copy = arr.slice();
|
||||
for (let i = copy.length - 1; i > 0; i--) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user