271 lines
9.2 KiB
C#
271 lines
9.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using Random = UnityEngine.Random;
|
|
using ChillConnect;
|
|
|
|
namespace ChillConnect.Editor
|
|
{
|
|
public static class ArrowLevelGenerator
|
|
{
|
|
private const int MaxGenerateAttempts = 500; // 单关最大尝试次数
|
|
private static readonly string[] Directions = { "up", "down", "left", "right" };
|
|
|
|
/// <summary>
|
|
/// 生成单个关卡
|
|
/// </summary>
|
|
public static EditorLevelConfig GenerateLevel(int levelId, LevelDifficultyPreset preset)
|
|
{
|
|
for (int attempt = 0; attempt < MaxGenerateAttempts; attempt++)
|
|
{
|
|
EditorLevelConfig level = new EditorLevelConfig
|
|
{
|
|
levelId = levelId,
|
|
levelName = $"第{levelId - 1000}关",
|
|
gridRows = preset.gridRows,
|
|
gridCols = preset.gridCols,
|
|
pointSpacing = 60,
|
|
arrows = new List<EditorArrowConfig>()
|
|
};
|
|
|
|
bool[,] occupied = new bool[preset.gridRows, preset.gridCols];
|
|
int arrowId = 1;
|
|
int failedCount = 0;
|
|
|
|
// 逐个生成箭头,直到达到目标数量或多次失败
|
|
while (level.arrows.Count < preset.arrowCount && failedCount < 100)
|
|
{
|
|
if (TryGenerateArrow(preset, occupied, level.gridRows, level.gridCols,
|
|
out EditorArrowConfig arrow))
|
|
{
|
|
arrow.id = arrowId++;
|
|
level.arrows.Add(arrow);
|
|
|
|
// 标记占用
|
|
MarkOccupied(arrow, occupied, level.gridCols);
|
|
}
|
|
else
|
|
{
|
|
failedCount++;
|
|
}
|
|
}
|
|
|
|
// 箭头数太少,放弃本次
|
|
if (level.arrows.Count < preset.minArrowCount)
|
|
continue;
|
|
|
|
// 可解性校验
|
|
if (CheckSolvable(level))
|
|
{
|
|
return level;
|
|
}
|
|
}
|
|
|
|
Debug.LogWarning($"关卡 {levelId} 达到最大尝试次数仍未生成合格关卡");
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 尝试生成单个合法箭头
|
|
/// </summary>
|
|
private static bool TryGenerateArrow(LevelDifficultyPreset preset, bool[,] occupied,
|
|
int rows, int cols, out EditorArrowConfig arrow)
|
|
{
|
|
arrow = null;
|
|
|
|
// 随机起点
|
|
int startRow = Random.Range(0, rows);
|
|
int startCol = Random.Range(0, cols);
|
|
if (occupied[startRow, startCol])
|
|
return false;
|
|
|
|
// 随机路径长度和折数
|
|
int pathLength = Random.Range(preset.minPathLength, preset.maxPathLength + 1);
|
|
int maxTurns = preset.maxTurnCount;
|
|
int turns = 0;
|
|
|
|
List<string> path = new List<string>();
|
|
List<(int r, int c)> pathPoints = new List<(int r, int c)> { (startRow, startCol) };
|
|
|
|
string lastDir = Directions[Random.Range(0, Directions.Length)];
|
|
int curRow = startRow;
|
|
int curCol = startCol;
|
|
|
|
for (int i = 0; i < pathLength; i++)
|
|
{
|
|
string curDir = lastDir;
|
|
|
|
// 有概率转向
|
|
if (i > 0 && turns < maxTurns && Random.value < preset.turnProbability)
|
|
{
|
|
// 随机换一个方向(不能回头)
|
|
List<string> validDirs = new List<string>(Directions);
|
|
validDirs.Remove(GetOppositeDir(lastDir));
|
|
curDir = validDirs[Random.Range(0, validDirs.Count)];
|
|
turns++;
|
|
}
|
|
|
|
// 计算下一点
|
|
(int nr, int nc) = MoveDir(curRow, curCol, curDir);
|
|
|
|
// 越界或已占用,终止
|
|
if (nr < 0 || nr >= rows || nc < 0 || nc >= cols || occupied[nr, nc])
|
|
break;
|
|
|
|
path.Add(curDir);
|
|
pathPoints.Add((nr, nc));
|
|
curRow = nr;
|
|
curCol = nc;
|
|
lastDir = curDir;
|
|
}
|
|
|
|
// 路径太短,无效
|
|
if (path.Count < 1)
|
|
return false;
|
|
|
|
// 构造箭头
|
|
arrow = new EditorArrowConfig
|
|
{
|
|
startPoint = startRow * cols + startCol,
|
|
path = path
|
|
};
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 标记箭头占用的网格点
|
|
/// </summary>
|
|
private static void MarkOccupied(EditorArrowConfig arrow, bool[,] occupied, int totalCols)
|
|
{
|
|
int pointId = arrow.startPoint;
|
|
int row = pointId / totalCols;
|
|
int col = pointId % totalCols;
|
|
occupied[row, col] = true;
|
|
|
|
foreach (string dir in arrow.path)
|
|
{
|
|
(row, col) = MoveDir(row, col, dir);
|
|
occupied[row, col] = true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 可解性校验:贪心模拟消除法
|
|
/// </summary>
|
|
public static bool CheckSolvable(EditorLevelConfig level)
|
|
{
|
|
int rows = level.gridRows;
|
|
int cols = level.gridCols;
|
|
|
|
// 复制占用矩阵
|
|
bool[,] occupied = new bool[rows, cols];
|
|
foreach (var arrow in level.arrows)
|
|
{
|
|
MarkOccupied(arrow, occupied, cols);
|
|
}
|
|
|
|
List<EditorArrowConfig> remaining = new List<EditorArrowConfig>(level.arrows);
|
|
int roundCount = 0;
|
|
const int maxRounds = 200;
|
|
|
|
while (remaining.Count > 0 && roundCount < maxRounds)
|
|
{
|
|
roundCount++;
|
|
List<EditorArrowConfig> canRemove = new List<EditorArrowConfig>();
|
|
|
|
foreach (var arrow in remaining)
|
|
{
|
|
if (CanArrowFlyOut(arrow, occupied, rows, cols))
|
|
{
|
|
canRemove.Add(arrow);
|
|
}
|
|
}
|
|
|
|
if (canRemove.Count == 0)
|
|
return false; // 死局
|
|
|
|
// 消除第一个可消除的箭头,释放占用
|
|
var toRemove = canRemove[0];
|
|
ReleaseOccupied(toRemove, occupied, cols);
|
|
remaining.Remove(toRemove);
|
|
}
|
|
|
|
return remaining.Count == 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 判断箭头是否能直接飞出(前方无阻挡)
|
|
/// </summary>
|
|
private static bool CanArrowFlyOut(EditorArrowConfig arrow, bool[,] occupied, int rows, int cols)
|
|
{
|
|
// 获取最终射出方向
|
|
string finalDir = arrow.path[arrow.path.Count - 1];
|
|
|
|
// 获取箭头头部位置(路径终点)
|
|
int pointId = arrow.startPoint;
|
|
int row = pointId / cols;
|
|
int col = pointId % cols;
|
|
|
|
foreach (string dir in arrow.path)
|
|
{
|
|
(row, col) = MoveDir(row, col, dir);
|
|
}
|
|
|
|
// 沿射出方向一路检查到边界
|
|
while (true)
|
|
{
|
|
(row, col) = MoveDir(row, col, finalDir);
|
|
|
|
if (row < 0 || row >= rows || col < 0 || col >= cols)
|
|
return true; // 飞出边界
|
|
|
|
if (occupied[row, col])
|
|
return false; // 前方有阻挡
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 释放箭头占用
|
|
/// </summary>
|
|
private static void ReleaseOccupied(EditorArrowConfig arrow, bool[,] occupied, int totalCols)
|
|
{
|
|
int pointId = arrow.startPoint;
|
|
int row = pointId / totalCols;
|
|
int col = pointId % totalCols;
|
|
occupied[row, col] = false;
|
|
|
|
foreach (string dir in arrow.path)
|
|
{
|
|
(row, col) = MoveDir(row, col, dir);
|
|
occupied[row, col] = false;
|
|
}
|
|
}
|
|
|
|
#region 工具方法
|
|
private static (int r, int c) MoveDir(int row, int col, string dir)
|
|
{
|
|
switch (dir)
|
|
{
|
|
case "up": return (row - 1, col);
|
|
case "down": return (row + 1, col);
|
|
case "left": return (row, col - 1);
|
|
case "right": return (row, col + 1);
|
|
default: return (row, col);
|
|
}
|
|
}
|
|
|
|
private static string GetOppositeDir(string dir)
|
|
{
|
|
switch (dir)
|
|
{
|
|
case "up": return "down";
|
|
case "down": return "up";
|
|
case "left": return "right";
|
|
case "right": return "left";
|
|
default: return dir;
|
|
}
|
|
}
|
|
#endregion
|
|
}
|
|
} |