Files
ArrowBeatTap-Gp/Assets/Editor/ArrowGameValidator.cs
T

507 lines
16 KiB
C#
Raw Normal View History

#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