507 lines
16 KiB
C#
507 lines
16 KiB
C#
#if UNITY_EDITOR
|
|||
|
|
using System.Collections.Generic;
|
||
|
|
using System.IO;
|
||
|
|
using System.Linq;
|
||
|
|
using ChillConnect;
|
||
|
|
using UnityEditor;
|
||
|
|
using UnityEngine;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 关卡死局检测工具(Unity编辑器下使用)
|
||
|
|
/// </summary>
|
||
|
|
public class ArrowGameValidator
|
||
|
|
{
|
||
|
|
// ========== 方向转换工具 ==========
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 方向字符串转行列偏移量
|
||
|
|
/// </summary>
|
||
|
|
private static (int dRow, int dCol) DirToOffset(string dir)
|
||
|
|
{
|
||
|
|
return dir switch
|
||
|
|
{
|
||
|
|
"up" => (-1, 0),
|
||
|
|
"down" => (1, 0),
|
||
|
|
"left" => (0, -1),
|
||
|
|
"right" => (0, 1),
|
||
|
|
_ => (0, 0)
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// pointId 转 (row, col)
|
||
|
|
/// </summary>
|
||
|
|
private static (int row, int col) PointIdToRowCol(int pointId, int totalCols)
|
||
|
|
{
|
||
|
|
int row = pointId / totalCols;
|
||
|
|
int col = pointId % totalCols;
|
||
|
|
return (row, col);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// (row, col) 转 pointId
|
||
|
|
/// </summary>
|
||
|
|
private static int RowColToPointId(int row, int col, int totalCols)
|
||
|
|
{
|
||
|
|
return row * totalCols + col;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========== 核心检测逻辑 ==========
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 构建所有箭头占用的网格点集合
|
||
|
|
/// 返回: Dictionary<pointId, List<arrowId>> —— 每个网格点被哪些箭头占用
|
||
|
|
/// </summary>
|
||
|
|
private static Dictionary<int, List<int>> BuildGridOccupation(LevelConfig config)
|
||
|
|
{
|
||
|
|
var gridOccupation = new Dictionary<int, List<int>>();
|
||
|
|
int totalCols = config.gridCols;
|
||
|
|
|
||
|
|
foreach (var arrow in config.arrows)
|
||
|
|
{
|
||
|
|
int curPointId = arrow.startPoint;
|
||
|
|
|
||
|
|
// 起点也加入占用
|
||
|
|
if (!gridOccupation.ContainsKey(curPointId))
|
||
|
|
gridOccupation[curPointId] = new List<int>();
|
||
|
|
gridOccupation[curPointId].Add(arrow.id);
|
||
|
|
|
||
|
|
// 沿路径逐步添加占用
|
||
|
|
foreach (string dir in arrow.path)
|
||
|
|
{
|
||
|
|
var (row, col) = PointIdToRowCol(curPointId, totalCols);
|
||
|
|
var (dRow, dCol) = DirToOffset(dir);
|
||
|
|
row += dRow;
|
||
|
|
col += dCol;
|
||
|
|
|
||
|
|
// 边界检查
|
||
|
|
if (row < 0 || row >= config.gridRows || col < 0 || col >= config.gridCols)
|
||
|
|
{
|
||
|
|
Debug.LogWarning($"箭头 {arrow.id} 路径超出网格边界");
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
curPointId = RowColToPointId(row, col, totalCols);
|
||
|
|
|
||
|
|
if (!gridOccupation.ContainsKey(curPointId))
|
||
|
|
gridOccupation[curPointId] = new List<int>();
|
||
|
|
gridOccupation[curPointId].Add(arrow.id);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return gridOccupation;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 获取箭头的最终方向(头部朝向)
|
||
|
|
/// </summary>
|
||
|
|
private static string GetFinalDirection(ArrowConfig arrow)
|
||
|
|
{
|
||
|
|
if (arrow.path != null && arrow.path.Count > 0)
|
||
|
|
return arrow.path[arrow.path.Count - 1];
|
||
|
|
return "up"; // 默认向上
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 获取箭头头部的网格位置 (pointId)
|
||
|
|
/// </summary>
|
||
|
|
private static int GetArrowHeadPointId(ArrowConfig arrow, int totalCols)
|
||
|
|
{
|
||
|
|
int curPointId = arrow.startPoint;
|
||
|
|
foreach (string dir in arrow.path)
|
||
|
|
{
|
||
|
|
var (row, col) = PointIdToRowCol(curPointId, totalCols);
|
||
|
|
var (dRow, dCol) = DirToOffset(dir);
|
||
|
|
curPointId = RowColToPointId(row + dRow, col + dCol, totalCols);
|
||
|
|
}
|
||
|
|
return curPointId;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 构建依赖图:检测每个箭头前方被哪些其他箭头阻挡
|
||
|
|
/// 返回: 邻接表 —— blockedBy[arrowId] = List of arrowIds that block it
|
||
|
|
/// 即:要消除 arrowId,必须先消除 blockedBy[arrowId] 中的箭头
|
||
|
|
/// </summary>
|
||
|
|
private static Dictionary<int, HashSet<int>> BuildDependencyGraph(
|
||
|
|
LevelConfig config,
|
||
|
|
Dictionary<int, List<int>> gridOccupation)
|
||
|
|
{
|
||
|
|
var blockedBy = new Dictionary<int, HashSet<int>>();
|
||
|
|
int totalCols = config.gridCols;
|
||
|
|
int totalRows = config.gridRows;
|
||
|
|
|
||
|
|
// 初始化
|
||
|
|
foreach (var arrow in config.arrows)
|
||
|
|
{
|
||
|
|
blockedBy[arrow.id] = new HashSet<int>();
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (var arrow in config.arrows)
|
||
|
|
{
|
||
|
|
string finalDir = GetFinalDirection(arrow);
|
||
|
|
int headPointId = GetArrowHeadPointId(arrow, totalCols);
|
||
|
|
var (headRow, headCol) = PointIdToRowCol(headPointId, totalCols);
|
||
|
|
var (dRow, dCol) = DirToOffset(finalDir);
|
||
|
|
|
||
|
|
// 沿最终方向逐格扫描
|
||
|
|
int curRow = headRow + dRow;
|
||
|
|
int curCol = headCol + dCol;
|
||
|
|
|
||
|
|
while (curRow >= 0 && curRow < totalRows && curCol >= 0 && curCol < totalCols)
|
||
|
|
{
|
||
|
|
int curPointId = RowColToPointId(curRow, curCol, totalCols);
|
||
|
|
|
||
|
|
// 检查该网格点是否被其他箭头占用
|
||
|
|
if (gridOccupation.TryGetValue(curPointId, out var occupyingArrows))
|
||
|
|
{
|
||
|
|
foreach (int occupyingArrowId in occupyingArrows)
|
||
|
|
{
|
||
|
|
// 不能阻挡自己
|
||
|
|
if (occupyingArrowId != arrow.id)
|
||
|
|
{
|
||
|
|
// occupyingArrow 阻挡了当前箭头
|
||
|
|
// 要消除当前箭头,必须先消除 occupyingArrow
|
||
|
|
blockedBy[arrow.id].Add(occupyingArrowId);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
curRow += dRow;
|
||
|
|
curCol += dCol;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return blockedBy;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 使用拓扑排序检测是否有环(死局)
|
||
|
|
/// 返回: (isSolvable, unsolvableArrowIds)
|
||
|
|
/// </summary>
|
||
|
|
private static (bool isSolvable, List<int> unsolvableArrowIds) DetectDeadlock(
|
||
|
|
LevelConfig config,
|
||
|
|
Dictionary<int, HashSet<int>> blockedBy)
|
||
|
|
{
|
||
|
|
// 计算每个箭头的入度(被多少箭头阻挡)
|
||
|
|
var inDegree = new Dictionary<int, int>();
|
||
|
|
foreach (var arrow in config.arrows)
|
||
|
|
{
|
||
|
|
inDegree[arrow.id] = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 构建反向邻接表:谁依赖于我
|
||
|
|
var dependents = new Dictionary<int, List<int>>();
|
||
|
|
foreach (var arrow in config.arrows)
|
||
|
|
{
|
||
|
|
dependents[arrow.id] = new List<int>();
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (var kvp in blockedBy)
|
||
|
|
{
|
||
|
|
int arrowId = kvp.Key;
|
||
|
|
foreach (int blockerId in kvp.Value)
|
||
|
|
{
|
||
|
|
// arrowId 依赖 blockerId
|
||
|
|
// blockerId -> arrowId
|
||
|
|
dependents[blockerId].Add(arrowId);
|
||
|
|
inDegree[arrowId]++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Kahn 拓扑排序
|
||
|
|
var queue = new Queue<int>();
|
||
|
|
foreach (var arrow in config.arrows)
|
||
|
|
{
|
||
|
|
if (inDegree[arrow.id] == 0)
|
||
|
|
{
|
||
|
|
queue.Enqueue(arrow.id);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
int removedCount = 0;
|
||
|
|
while (queue.Count > 0)
|
||
|
|
{
|
||
|
|
int current = queue.Dequeue();
|
||
|
|
removedCount++;
|
||
|
|
|
||
|
|
foreach (int dependent in dependents[current])
|
||
|
|
{
|
||
|
|
inDegree[dependent]--;
|
||
|
|
if (inDegree[dependent] == 0)
|
||
|
|
{
|
||
|
|
queue.Enqueue(dependent);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 如果所有箭头都能被移除,则可解
|
||
|
|
if (removedCount == config.arrows.Count)
|
||
|
|
{
|
||
|
|
return (true, new List<int>());
|
||
|
|
}
|
||
|
|
|
||
|
|
// 找出无法移除的箭头(入度 > 0 的)
|
||
|
|
var unsolvableArrows = new List<int>();
|
||
|
|
foreach (var arrow in config.arrows)
|
||
|
|
{
|
||
|
|
if (inDegree[arrow.id] > 0)
|
||
|
|
{
|
||
|
|
unsolvableArrows.Add(arrow.id);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return (false, unsolvableArrows);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 从环路中提取所有涉及的箭头ID
|
||
|
|
/// </summary>
|
||
|
|
private static List<int> ExtractCycleArrowIds(
|
||
|
|
LevelConfig config,
|
||
|
|
Dictionary<int, HashSet<int>> blockedBy,
|
||
|
|
List<int> unsolvableArrows)
|
||
|
|
{
|
||
|
|
// 从不可解的箭头出发,DFS找出所有在环路中的箭头
|
||
|
|
var allCycleNodes = new HashSet<int>();
|
||
|
|
|
||
|
|
foreach (int startId in unsolvableArrows)
|
||
|
|
{
|
||
|
|
if (allCycleNodes.Contains(startId))
|
||
|
|
continue;
|
||
|
|
|
||
|
|
// 尝试从 startId 出发找环
|
||
|
|
var visited = new HashSet<int>();
|
||
|
|
var inStack = new HashSet<int>();
|
||
|
|
var path = new List<int>();
|
||
|
|
|
||
|
|
if (FindCycle(startId, blockedBy, visited, inStack, path, out var cycleNodes))
|
||
|
|
{
|
||
|
|
foreach (int node in cycleNodes)
|
||
|
|
{
|
||
|
|
allCycleNodes.Add(node);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return allCycleNodes.ToList();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// DFS寻找环路
|
||
|
|
/// </summary>
|
||
|
|
private static bool FindCycle(
|
||
|
|
int current,
|
||
|
|
Dictionary<int, HashSet<int>> blockedBy,
|
||
|
|
HashSet<int> visited,
|
||
|
|
HashSet<int> inStack,
|
||
|
|
List<int> path,
|
||
|
|
out List<int> cycleNodes)
|
||
|
|
{
|
||
|
|
visited.Add(current);
|
||
|
|
inStack.Add(current);
|
||
|
|
path.Add(current);
|
||
|
|
|
||
|
|
if (blockedBy.TryGetValue(current, out var blockers))
|
||
|
|
{
|
||
|
|
foreach (int blocker in blockers)
|
||
|
|
{
|
||
|
|
if (!visited.Contains(blocker))
|
||
|
|
{
|
||
|
|
if (FindCycle(blocker, blockedBy, visited, inStack, path, out cycleNodes))
|
||
|
|
{
|
||
|
|
inStack.Remove(current);
|
||
|
|
path.RemoveAt(path.Count - 1);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else if (inStack.Contains(blocker))
|
||
|
|
{
|
||
|
|
// 找到环
|
||
|
|
int cycleStartIndex = path.IndexOf(blocker);
|
||
|
|
cycleNodes = path.GetRange(cycleStartIndex, path.Count - cycleStartIndex);
|
||
|
|
inStack.Remove(current);
|
||
|
|
path.RemoveAt(path.Count - 1);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
inStack.Remove(current);
|
||
|
|
path.RemoveAt(path.Count - 1);
|
||
|
|
cycleNodes = null;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========== 公共接口 ==========
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 检测单个关卡是否可解
|
||
|
|
/// </summary>
|
||
|
|
public static ValidationResult ValidateLevel(LevelConfig config)
|
||
|
|
{
|
||
|
|
var result = new ValidationResult
|
||
|
|
{
|
||
|
|
levelId = config.levelId,
|
||
|
|
levelName = config.levelName
|
||
|
|
};
|
||
|
|
|
||
|
|
if (config.arrows == null || config.arrows.Count == 0)
|
||
|
|
{
|
||
|
|
result.isSolvable = true;
|
||
|
|
result.message = "关卡无箭头";
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 1. 构建网格占用
|
||
|
|
var gridOccupation = BuildGridOccupation(config);
|
||
|
|
|
||
|
|
// 2. 构建依赖图
|
||
|
|
var blockedBy = BuildDependencyGraph(config, gridOccupation);
|
||
|
|
|
||
|
|
// 3. 拓扑排序检测
|
||
|
|
var (isSolvable, unsolvableArrows) = DetectDeadlock(config, blockedBy);
|
||
|
|
|
||
|
|
result.isSolvable = isSolvable;
|
||
|
|
result.blockedBy = blockedBy;
|
||
|
|
|
||
|
|
if (isSolvable)
|
||
|
|
{
|
||
|
|
result.message = "✅ 可解";
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
// 找出环路中的具体箭头
|
||
|
|
var cycleArrows = ExtractCycleArrowIds(config, blockedBy, unsolvableArrows);
|
||
|
|
result.unsolvableArrowIds = cycleArrows.OrderBy(x => x).ToList();
|
||
|
|
result.message = $"❌ 死局,无法消除的箭头: [{string.Join(", ", result.unsolvableArrowIds)}]";
|
||
|
|
}
|
||
|
|
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 批量检测关卡目录下所有JSON文件
|
||
|
|
/// </summary>
|
||
|
|
public static void ValidateAllLevels(string folderPath)
|
||
|
|
{
|
||
|
|
if (!Directory.Exists(folderPath))
|
||
|
|
{
|
||
|
|
Debug.LogError($"目录不存在: {folderPath}");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
string[] jsonFiles = Directory.GetFiles(folderPath, "*.json");
|
||
|
|
int totalCount = jsonFiles.Length;
|
||
|
|
int solvableCount = 0;
|
||
|
|
int unsolvableCount = 0;
|
||
|
|
var unsolvableResults = new List<ValidationResult>();
|
||
|
|
|
||
|
|
foreach (string filePath in jsonFiles)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
string json = File.ReadAllText(filePath);
|
||
|
|
LevelConfig config = JsonUtility.FromJson<LevelConfig>(json);
|
||
|
|
|
||
|
|
var result = ValidateLevel(config);
|
||
|
|
|
||
|
|
if (result.isSolvable)
|
||
|
|
solvableCount++;
|
||
|
|
else
|
||
|
|
{
|
||
|
|
unsolvableCount++;
|
||
|
|
unsolvableResults.Add(result);
|
||
|
|
}
|
||
|
|
|
||
|
|
Debug.Log($"[{filePath}] {result.message}");
|
||
|
|
}
|
||
|
|
catch (System.Exception e)
|
||
|
|
{
|
||
|
|
Debug.LogError($"解析文件失败: {filePath}, 错误: {e.Message}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Debug.Log("========== 检测汇总 ==========");
|
||
|
|
Debug.Log($"总关卡数: {totalCount}");
|
||
|
|
Debug.Log($"可解关卡: {solvableCount}");
|
||
|
|
Debug.Log($"死局关卡: {unsolvableCount}");
|
||
|
|
|
||
|
|
if (unsolvableResults.Count > 0)
|
||
|
|
{
|
||
|
|
Debug.Log("========== 死局关卡详情 ==========");
|
||
|
|
foreach (var result in unsolvableResults)
|
||
|
|
{
|
||
|
|
Debug.Log($"关卡 {result.levelId} ({result.levelName}): {result.message}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========== 编辑器菜单 ==========
|
||
|
|
|
||
|
|
[MenuItem("Tools/箭头游戏/检测单个关卡(选择JSON文件)")]
|
||
|
|
public static void ValidateSelectedLevel()
|
||
|
|
{
|
||
|
|
string path = EditorUtility.OpenFilePanel("选择关卡JSON", Application.streamingAssetsPath, "json");
|
||
|
|
if (string.IsNullOrEmpty(path)) return;
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
string json = File.ReadAllText(path);
|
||
|
|
LevelConfig config = JsonUtility.FromJson<LevelConfig>(json);
|
||
|
|
|
||
|
|
var result = ValidateLevel(config);
|
||
|
|
|
||
|
|
Debug.Log($"========== 关卡 {result.levelId} ({result.levelName}) 检测结果 ==========");
|
||
|
|
Debug.Log(result.message);
|
||
|
|
|
||
|
|
if (!result.isSolvable)
|
||
|
|
{
|
||
|
|
Debug.Log($"环路箭头: [{string.Join(", ", result.unsolvableArrowIds)}]");
|
||
|
|
|
||
|
|
// 打印依赖详情
|
||
|
|
Debug.Log("========== 依赖关系 ==========");
|
||
|
|
foreach (int id in result.unsolvableArrowIds)
|
||
|
|
{
|
||
|
|
if (result.blockedBy.TryGetValue(id, out var blockers))
|
||
|
|
{
|
||
|
|
Debug.Log($"箭头 {id} 被阻挡于: [{string.Join(", ", blockers)}]");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
EditorUtility.DisplayDialog("检测完成", result.message, "确定");
|
||
|
|
}
|
||
|
|
catch (System.Exception e)
|
||
|
|
{
|
||
|
|
Debug.LogError($"检测失败: {e.Message}");
|
||
|
|
EditorUtility.DisplayDialog("错误", $"检测失败: {e.Message}", "确定");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
[MenuItem("Tools/箭头游戏/批量检测所有关卡")]
|
||
|
|
public static void ValidateAllLevelsMenu()
|
||
|
|
{
|
||
|
|
string folderPath = EditorUtility.OpenFolderPanel("选择关卡目录", Application.streamingAssetsPath, "");
|
||
|
|
if (string.IsNullOrEmpty(folderPath)) return;
|
||
|
|
|
||
|
|
ValidateAllLevels(folderPath);
|
||
|
|
EditorUtility.DisplayDialog("批量检测完成", "请查看Console输出结果", "确定");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========== 数据结构 ==========
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 检测结果
|
||
|
|
/// </summary>
|
||
|
|
public class ValidationResult
|
||
|
|
{
|
||
|
|
public int levelId;
|
||
|
|
public string levelName;
|
||
|
|
public bool isSolvable;
|
||
|
|
public string message;
|
||
|
|
public List<int> unsolvableArrowIds = new List<int>();
|
||
|
|
public Dictionary<int, HashSet<int>> blockedBy;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#endif
|