随机生长算法没有按预期工作
在我正在做的一个游戏里,我先用一个8×8的整型数组,初始值全部为0。然后我在每一行/每一列随机放置一个非零值,要求每行只有一个非零值、每列也只有一个非零值。就像这样:
0 1 0 0 0 0 0 0
0 0 0 0 2 0 0 0
0 0 0 0 0 0 3 0
4 0 0 0 0 0 0 0
0 0 5 0 0 0 0 0
0 0 0 0 0 0 0 6
0 0 0 0 0 7 0 0
0 0 0 8 0 0 0 0
这一部分工作得很好。每次生成都会得到一个不一样的网格。
但接下来我想把每个数字随机地向周围的0 扩展,这样就会得到8 个区域,每个区域由一个数字填充。下面是我得到的一个例子:
4 1 1 5 2 2 2 3
4 1 1 5 2 2 3 3
4 1 5 5 2 2 3 3
4 4 5 5 5 2 3 6
4 4 5 5 5 2 2 6
4 4 5 5 5 2 2 6
4 4 5 8 5 7 2 6
4 4 4 8 8 7 2 6
请注意,虽然数字1 可以向下扩展,数字8 可以向上扩展,但位于最右边最远列的数字6 从未离开那一列。以及位于左上第一列的数字4,从来不让其他数字进入那一列。这是我用来进行扩展的函数。我会记录有多少个0,然后重复调用这个函数直到它们全部被填满:
在这种情况下,size 是8,而 color 数组在每个维度上从0 增长到9,尽管我实际只使用1 到8。之所以这样做,是为了在查看要检查的相邻单元格时,不需要使用条件判断。
private void ExpandColors()
{
//1 is up, 2 is right, 3 is down, 4 is left
int cell = rand.Next(1, size * size);
string coord = CellToCoords(cell);
string[] c = coord.Split('ÿ');
int row = Convert.ToInt16(c[0]);
int col = Convert.ToInt16(c[1]);
if (color[row, col] > 0)
{
switch (rand.Next(1, 4))
{
case 1:
if (row == 1) return; //can't look up
if (color[row - 1, col] == 0)
{
color[row - 1, col] = color[row, col];
uncolored--;
return;
}
break;
case 2:
if (col == size) return; //can't look right
if (color[row, col + 1] == 0)
{
color[row, col + 1] = color[row, col];
uncolored--;
return;
}
break;
case 3:
if (row == size) return; //can't look down
if (color[row + 1, col] == 0)
{
color[row + 1, col] = color[row, col];
uncolored--;
return;
}
break;
case 4:
if (col == 1) return; //can't look left
if (color[row, col - 1] == 0)
{
color[row, col - 1] = color[row, col];
uncolored--;
return;
}
break;
}
}
}
private string CellToCoords(int cell)
{
int col = cell % size;
if (col == 0) col = size;
int row = (cell - col) / size + 1;
return row.ToString() + "ÿ" + col.ToString();
// so 2, 5 would be 2ÿ5 and can easily be split
}
我已经仔细检查过 CellToCoords 能正确工作。并且在 ExtendColors 上似乎没有任何功能差异会让水平与垂直之间产生差别。然而我仍然没有遇到顶部和底部行的数值正常扩展的问题,反而在最左列和最右列的数值扩展上确实存在问题。我已经运行了几十次,左列和右列同样的异常行为每次都出现。
难道我漏掉了什么明显的地方?
解决方案
这个问题已经在评论中得到解答。rand.Next 使用的是开区间上界。也就是说 rand.Next(1, 4) 只会返回1、2、3,永远不会返回4。请使用 rand.Next(1,5) 或 rand.Next(0,4) 来生成4 个随机值。
但我也想提出一种对更大棋盘更实用的替代算法。你当前的方法在更大棋盘上会很慢,因为随着坐标被填充,随机得到未填充坐标的概率会下降。
其核心思路是维护一个与某个数值相邻的坐标列表。然后你可以从这个列表中迭代地选取值来填充。对于大型棋盘来说,即使需要额外花点时间来维护这个列表,这样会快得多。大致可以这样写:
public readonly record struct CoordinateAndValue(Vec2i Pos, int Value);
public readonly record struct Vec2i(int X, int Y)
{
public static Vec2i operator +(in Vec2i l, in Vec2i r) => new(l.X + r.X, l.Y + r.Y);
};
public static void ExpandColors(int[,] board)
{
var offsets = new Vec2i[]
{
new(-1, 0),
new(1, 0),
new(0, 1),
new(0, -1),
};
var width = board.GetLength(0);
var height = board.GetLength(1);
var candidateSet = new HashSet<CoordinateAndValue>();
// local function that adds all adjacent coordinates that are currently zero to the candidate set
void AddAdjacent(Vec2i pos, int value)
{
foreach (var o in offsets)
{
var p = pos + o;
if (p.X >= 0 && p.Y >= 0 && p.X < width && p.Y < height &&
board[p.X, p.Y] == 0)
{
candidateSet.Add(new CoordinateAndValue(p, value));
}
}
}
// Fill the candidate set from the initial board state
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
var pos = new Vec2i(x, y);
var boardValue = board[x, y];
if (boardValue != 0)
{
AddAdjacent(pos, boardValue);
}
}
}
// Start filling random coordinates from the candidate set
var rand = new Random(42);
while (candidateSet.Count > 0)
{
var index = rand.Next(0, candidateSet.Count);
var c = candidateSet.ElementAt(index);
candidateSet.Remove(c);
var boardValue = board[c.Pos.X, c.Pos.Y];
// We need to check if the value is zero again,
// since the coordinate could have been filled by another value
if (boardValue == 0)
{
board[c.Pos.X, c.Pos.Y] = c.Value;
AddAdjacent(c.Pos, c.Value);
}
}
}
请注意 HashSet 可能并不理想,因为 candidateSet.ElementAt 有点慢。理想情况下你需要一个既具集合语义,又有快速索引的集合。但我会省略关于数据结构选择的深入讨论。
我也对这个思路做了一些快速测试,但问题仍可能存在。代码仅作为表达一般思路的示例。