在一个 n × m 的环形网格上均匀地放置 x 个物品的算法

31
我正在创建一个基于网格的简单浏览器游戏,希望能够等距地放置玩家和目标单元格(类似于王牌对决)。理想情况下,每个玩家与最近的目标单元格之间的距离也应该相等。
以下是要求:
- 游戏需要支持2到20名玩家。 - n * m 网格可以是任何大小,但越“正方形”越好。 (“正方形”的原则是减少横跨网格所需的最大距离-使事物更易访问) - 目标单元格的数量是灵活的。 - 每个玩家应具有同等数量的目标的平等访问权。 - 任何玩家或目标与任何其他玩家或目标之间的最小距离为4。
请注意,每个单元格都有8个即时邻居(对角线也计算为距离1),并且边缘会包装。 这意味着底部的单元格在逻辑上与顶部的单元格相邻,左/右也是如此。
我一直在思考一个好的算法来在不需要为每个玩家数量创建特定预定网格的情况下,将玩家和目标放置在不同的分布中。我发现了 k-means clusteringLloyd's Algorithm,但我对它们不是很熟悉,也不知道如何将它们应用到这个特定案例中,特别是因为目标单元格的数量是灵活的,我认为这应该会简化解决方案。
以下是一个大大简化的代码片段,创建一个预定的 6 个玩家网格,只是为了展示我要实现的目标的本质:

var cellSize = 20;
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
document.body.appendChild(canvas);

function Cell(x, y) {
  this.x = x * cellSize + cellSize / 2;
  this.y = y * cellSize + cellSize / 2;
  this.id = x + '-' + y;
  this.neighbors = [];
  this.type = null;
}

Cell.prototype.draw = function() {
  var color = '#ffffff';
  if (this.type === 'base') {
    color = '#0000ff';
  } else if (this.type === 'target') {
    color = '#ff0000';
  }
  var d = cellSize / 2;
  ctx.fillStyle = color;
  ctx.fillRect(this.x - d, this.y - d, this.x + d, this.y + d);
  ctx.rect(this.x - d, this.y - d, this.x + d, this.y + d);
  ctx.strokeStyle = '#000';
  ctx.lineWidth = 3;
  ctx.stroke();
};

// Pre-set player and target cells for 6 players as an example
var playerCells = ['0-0', '8-0', '16-0', '0-8', '8-8', '16-8'];
var targetCells = ['4-4', '12-4', '20-4', '4-12', '12-12', '20-12'];
var n = 24;
var m = 16;
canvas.width = n * cellSize + 6;
canvas.height = m * cellSize + 6;

var cellList = [];

for (var i = 0; i < n; i++) {
  for (var j = 0; j < m; j++) {
    var cell = new Cell(i, j);
    if (playerCells.indexOf(cell.id) > -1) {
      cell.type = 'base';
    } else if (targetCells.indexOf(cell.id) > -1) {
      cell.type = 'target';
    }
    cellList.push(cell);
  }
}

// Give each cell a list of it's neighbors so we know where things can move
for (var i = 0; i < cellList.length; i++) {
  var cell = cellList[i];
  var neighbors = [];

  // Get the cell indices around the current cell
  var cx = [cell.x - 1, cell.x, cell.x + 1];
  var cy = [cell.y - 1, cell.y, cell.y + 1];
  var ci, cj;

  for (ci = 0; ci < 3; ci++) {
    if (cx[ci] < 0) {
      cx[ci] = n - 1;
    }
    if (cx[ci] >= n) {
      cx[ci] = 0;
    }
    if (cy[ci] < 0) {
      cy[ci] = m - 1;
    }
    if (cy[ci] >= m) {
      cy[ci] = 0;
    }
  }
  for (ci = 0; ci < 3; ci++) {
    for (cj = 0; cj < 3; cj++) {
      // Skip the current node since we don't need to link it to itself
      if (cellList[n * ci + cj] === cell) {
        continue;
      }
      neighbors.push(cellList[n * ci + cj]);
    }
  }
}

drawGrid();

function drawGrid() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  for (var i = 0; i < cellList.length; i++) {
    cellList[i].draw();
  }
}

它创建了一个类似于这样的网格: Grid Example 其中蓝色单元格代表玩家,红色单元格代表目标。
有没有人对如何解决此问题有什么建议?
欢迎提供有用的材料链接。
是否有大师可以设计出符合所有条件的精彩放置算法?
如果该解决方案还允许配置任意数量的玩家的目标单元格和/或最小距离,并且仍然满足所有条件,那将是非常棒的,但这不是必须的。
编辑:在考虑其他游戏设计因素后,我将玩家和目标之间的最小距离从2改为4。上述文本、代码和图像已相应更改。在进行此次编辑时,没有任何解决方案受到该要求的限制,因此不会影响任何内容。
编辑2:
如果您提出了解决方案,请提供JavaScript代码(或至少是伪代码),详细说明解决方案的步骤。同时请解释该解决方案如何满足要求。谢谢!

请详细说明网格大小的确定方式 - 是通过游戏结果、玩家设置还是由程序员任意决定?例如,如果网格大小很大,一个看似简单的解决方案是让玩家彼此之间距离较远但靠近他们的目标。共享目标是否很重要? - גלעד ברקן
网格大小应该是最适合玩家放置的。它不是预定义的,而是算法的输出。制作一个巨大的网格,让所有玩家都靠近一个单一的目标,可能看起来像是一个简单的解决方案,但当你意识到即使在那种情况下,你仍然必须确保每个玩家都有同样数量的目标可以接近,即使这些目标很远,这并不容易。 - NanoWizard
@CedricReichenbach,我的要求中没有提到性能。我从未说过这些地图必须在运行时生成。如果您有不严格符合要求(如平等访问)的解决方案,我仍然想看看它。当然,满足所有条件的解决方案会更好,但如果提供的解决方案都未能满足所有条件,那么接受次优的解决方案也是可以的。 - NanoWizard
符合您提出的要点的最简单解决方案是拥有两个循环排列(可以互换)的玩家和目标(即每个玩家平均都能够接触到相同数量的目标)- 除了您提出的要点外,请定义您更喜欢玩家分布还是目标分布更加均匀。可以添加一个由3个玩家和2或4个目标的示例。您是否有兴趣使用“#targets”被限制为“#players”的(可参数化的)函数的解决方案? - BeyelerStudios
你实际上是在要求一个复杂代码的完整实现,而不是这个网站的本意——帮助你学习。因此,我将限制自己对如何解决这个问题发表评论。解决这个问题的一个好方法是放宽条件,只寻找“足够好”的解决方案,然后使用模拟退火算法来生成解决方案。请参见http://ai-maker.com/the-simulated-annealing-algorithm/,了解基本介绍和JavaScript代码示例。 - btilly
显示剩余6条评论
5个回答

10

你是否受限于平面环境?如果你能够转向三维,那么你可以使用斐波那契螺旋在球体上生成任意数量的等间距点。这里有一个非常好的处理程序示例在 http://www.openprocessing.org/sketch/41142 (还有代码)。下图显示了它的样子。一个好处是你自动得到了“包裹”的效果。

Fibonacci Sphere

如果你必须保持二维,则可以尝试以上方法,然后使用球面到平面投影来保留映射关系。不过这可能比你想要的更复杂一些...


是的,我受限于一个二维平面。此外,任何从球形到平面的投影中都存在畸变,因此点之间的距离将无法得到正确保留。即使我可以使用一个球体,这也无法解决玩家到目标可达性的限制问题。 - NanoWizard
这是一个非常有创意的想法 +1 - Dan Kanze
3
一个球体的拓扑结构不正确,他很清楚它等同于一个环面。 - bmm6o

5

一个直观的解决方案是根据玩家数量对平面进行对称划分,随机放置一个玩家及其目标,然后在其他部分对称地反射放置。将网格理论上绑定成一个圆圈(或者反过来),然后进行分割和反射。

在一个(理论上)无限分辨率的网格中,以其中心为极坐标系的中心,我们可以首先放置一个玩家和它的目标(顺便说一句,这些可以放置在网格的任何位置,对称性仍然保持不变),然后为了放置其他 n - 1 个玩家和目标,每次增加初始角度 360° / n,保持相同的半径。然而,由于您的网格将有一个实际的大小限制,您需要通过限制初始生成和/或修改网格大小/奇偶性等方式来确保反射单元存在于网格上。

大致如下:

var numPlayers = 6;
var ts = 2;
var r = 8

function convertFromPolar(cs) {
  return [Math.round(cs[0] * Math.cos(cs[1] * Math.PI / 180)) + r
         ,Math.round(cs[0] * Math.sin(cs[1] * Math.PI / 180)) + r];
}

var first = [r,0];

var targets = [];
for (var i = 0; i < ts; i++) {
  var _first = first.slice();
  _first[0] = _first[0] - 4 - Math.round(Math.random() * 3);
  _first[1] = _first[1] + Math.round(Math.random() * 8);
  targets.push(_first);
}

var playerCells = [];
var targetCells = [];

for (var i = 0; i < numPlayers; i++) {
  playerCells.push(convertFromPolar(first).join('-'));
  first[1] = (first[1] + 360 / numPlayers) % 360;
  for (var j = 0; j < ts; j++) {
    targetCells.push(convertFromPolar(targets[j]).join('-'));
    targets[j][1] = (targets[j][1] + 360 / numPlayers) % 360;
  }
}

var cellSize = 20;
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
document.body.appendChild(canvas);

function Cell(x, y) {
  this.x = x * cellSize + cellSize / 2;
  this.y = y * cellSize + cellSize / 2;
  this.id = x + '-' + y;
  this.neighbors = [];
  this.type = null;
}

Cell.prototype.draw = function() {
  var color = '#ffffff';
  if (this.type === 'base') {
    color = '#0000ff';
  } else if (this.type === 'target') {
    color = '#ff0000';
  } else if (this.type === 'outOfBounds') {
    color = '#000000';
  }
  var d = cellSize / 2;
  ctx.fillStyle = color;
  ctx.fillRect(this.x - d, this.y - d, this.x + d, this.y + d);
  ctx.rect(this.x - d, this.y - d, this.x + d, this.y + d);
  ctx.strokeStyle = '#000';
  ctx.lineWidth = 3;
  ctx.stroke();
};

var n = 24;
var m = 16;
canvas.width = n * cellSize + 6;
canvas.height = m * cellSize + 6;

var cellList = [];

for (var i = 0; i < n; i++) {
  for (var j = 0; j < m; j++) {
    var cell = new Cell(i, j);
    if (playerCells.indexOf(cell.id) > -1) {
      cell.type = 'base';
    } else if (targetCells.indexOf(cell.id) > -1) {
      cell.type = 'target';
    } else if (Math.pow(i - r,2) + Math.pow(j - r,2) > (r + 2)*(r + 2) ) {
      cell.type = 'outOfBounds';
    }
    cellList.push(cell);
  }
}

// Give each cell a list of it's neighbors so we know where things can move
for (var i = 0; i < cellList.length; i++) {
  var cell = cellList[i];
  var neighbors = [];

  // Get the cell indices around the current cell
  var cx = [cell.x - 1, cell.x, cell.x + 1];
  var cy = [cell.y - 1, cell.y, cell.y + 1];
  var ci, cj;

  for (ci = 0; ci < 3; ci++) {
    if (cx[ci] < 0) {
      cx[ci] = n - 1;
    }
    if (cx[ci] >= n) {
      cx[ci] = 0;
    }
    if (cy[ci] < 0) {
      cy[ci] = m - 1;
    }
    if (cy[ci] >= m) {
      cy[ci] = 0;
    }
  }
  for (ci = 0; ci < 3; ci++) {
    for (cj = 0; cj < 3; cj++) {
      // Skip the current node since we don't need to link it to itself
      if (cellList[n * ci + cj] === cell) {
        continue;
      }
      neighbors.push(cellList[n * ci + cj]);
    }
  }
}

drawGrid();

function drawGrid() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  for (var i = 0; i < cellList.length; i++) {
    cellList[i].draw();
  }
}


你能提供更多细节吗?从数学上讲,你如何对称地划分并在网格上进行反射?你能保证解决方案适用于每个可能的玩家数量吗? - NanoWizard
@NanoWizard 我在你的代码上添加了反射的粗略组合(请参见我的新片段...没有时间将圆形移动到网格内...)。包裹的边界应该在x ^ 2 + y ^ 2 <=(r + c)^ 2附近。一种变化是将目标设为与玩家相关的内/外圆。 - גלעד ברקן
我将给予这个解决方案额外的声望奖励,因为加上额外的逻辑后,它似乎可以符合问题中所述的所有要求。不过我会将Cedric的答案标记为被接受的答案,因为尽管它并不严格遵循玩家可访问性的要求,但我喜欢它的额外的不可预测性,所以我认为我最终会在实际游戏中使用那种方法的一个变种。 :) - NanoWizard
@NanoWizard 谢谢!也很高兴你找到了一种有趣的游戏方法。 - גלעד ברקן

5

正如已经提到的,可能没有完美的解决方案能够在不超支的计算费用下完全满足您的所有要求。1

方法

我的方法是通过更灵活的条件来替换对所有目标“距离相同”的要求。

在以下示例中,我为每个单元格引入了一个热度属性,它应直观地表示目标的可用性/接近程度。 它通过将与地图上每个目标相关的热度相加来计算。 与目标相关的热度仅是它们之间(在我的示例中为曼哈顿距离)的距离的倒数。 也许您想为函数heatdistance使用不同的实现。

玩家定位

为了分配玩家,我们按照以下步骤进行:

  1. 保持所有单元格的列表,按热度排序
  2. 从某个单元格开始(当前为用户选择),在排序列表中找到它并使用其邻居(具有类似热度值的邻居)作为玩家位置

这可以确保玩家单元格的热度值始终尽可能接近。 更好的解决方案是在排序列表中搜索一系列尽可能接近的热度值并使用它们。

示例代码

重新加载以获取不同的目标位置

var numPlayers = 4;
var numTargets = numPlayers;
var gridSize = numPlayers * 4;
var minDistance = 4;

var targetPositions = [];
for (var i = 0; i < numTargets; i++) {
 // TODO: Make sure targets don't get too close
  targetPositions[i] = randomPos();
}

var heatMap = [];
for (var i = 0; i < gridSize; i++) {
  heatMap[i] = [];
  for (var j = 0; j < gridSize; j++) {
    heatMap[i][j] = heat(i, j);
  }
}
printHeat();

function heat(x, y) {
  var result = 0;
  for (var i in targetPositions) {
    var pos = targetPositions[i];
    result += 1 / distance(x - pos.x, y - pos.y); // XXX: What about zero division?
  }
  return result;
}

function distance(l1, l2) {
  // manhattan distance
  return Math.abs(l1) + Math.abs(l2);
}

function randomPos() {
  return {
    x: random(gridSize),
    y: random(gridSize),
    toString: function() {
      return this.x + '/' + this.y
    }
  };

  function random(max) {
    return Math.floor(Math.random() * max);
  }
}

function printHeat() {
  for (var i = 0; i < gridSize; i++) {
    var tr = $('<tr>');
    $('#heat').append(tr);
    for (var j = 0; j < gridSize; j++) {
      var heatVal = heatMap[i][j];
      var td = $('<td> ' + heatVal + ' </td>');
      if (heatVal > numTargets) // hack
        td.addClass('target');
      td.attr('data-x', i).attr('data-y', j);
      td.css('background-color', 'rgb(' + Math.floor(heatVal * 255) + ',160,80)');
      tr.append(td);
    }
  }
}

var cellsSorted = $('td').sort(function(a, b) {
  return numOfCell(a) > numOfCell(b);
}).toArray();
$('td').click(function() {
  $('.player').removeClass('player');
  var index = cellsSorted.indexOf(this);
  // TODO: Don't just search downwards, but in both directions with lowest difference
  for (var k = 0; k < numPlayers; k++) {
    var newIndex = index - k; // XXX Check against outOfBounds
    var cell = cellsSorted[newIndex];
    if (!validPlayerCell(cell)) {
      // skip one
      k--;
      index--;
      continue;
    }
    $(cell).addClass('player');
  }
});

function validPlayerCell(cell) {
  var otherItems = $('.player, .target').toArray();
  for (var i in otherItems) {
    var item = otherItems[i];
    var xa = parseInt($(cell).attr('data-x'));
    var ya = parseInt($(cell).attr('data-y'));
    var xb = parseInt($(item).attr('data-x'));
    var yb = parseInt($(item).attr('data-y'));
    if (distance(xa - xb, ya - yb) < minDistance)
      return false;
  }
  return true;
}

function numOfCell(c) {
  return parseFloat($(c).text());
}
body {
  font-family: sans-serif;
}

h2 {
  margin: 1ex 0;
}

td {
  border: 1px solid #0af;
  padding: 0.5ex;
  font-family: monospace;
  font-size: 10px;
  max-width: 4em;
  height: 4em;
  overflow: hidden;
  text-overflow: ellipsis;
}

td.target {
  border-color: #f80;
}

td.player {
  border-color: black;
}

td.player::after {
  font-family: sans-serif;
  content: "player here";
  position: absolute;
  color: white;
  background-color: rgba(0, 0, 0, 0.5);
  font-weight: bold;
  padding: 2px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h2>Click a cell to distribute players</h2>
<table id="heat">

</table>

JSFiddle中有相同的内容,可以用来调整变量

未解决的问题

这个例子是比较快地拼凑起来的。你会注意到有几个未解决的问题和未覆盖的情况。我只是为了概述我的想法而制作的。

未涉及的内容:

  • 在边缘处进行包装:这可能只影响dist函数
  • 目标之间的距离:它们只是随机抛到地图上的某个地方
  • 位置的选择仅搜索heat列表向下的位置:这可以更加智能地完成,例如,选择原始选定单元格的最短距离
  • 玩家的自动分配(无需点击):您可能只想从中随机选择一个开始

1我的意思是,理论上你可以尝试所有可能的变化并检查它们是否正确(如果你在后院有一个大集群)。


关于暴力检查所有可能的变化的观点是一个好的观点。正如你所提到的,你的方法需要一些调整来适应要求,但我喜欢它! - NanoWizard
2
这是一个很棒的想法!建议:要求中说对角线=1,所以不要使用曼哈顿距离,而是使用return Math.hypot(Math.abs(l1), Math.abs(l2));。此外,我认为使用var numTargets = Math.ceil(Math.random() * numPlayers * 1.3);更有趣。 - Josiah
2
@Josiah,由于OP似乎正在使用带有单元格的网格,因此更符合他关于距离是“从单元格a到单元格b需要移动的次数”的评论的公式应该是max(abs(a.x - b.x), abs(a.y - b.y)),其中x,y是网格中的单元格索引,与单元格大小无关(我认为这是不考虑包装的情况)。 - גלעד ברקן
我接受这个答案,因为我真的很喜欢这种方法,而且我认为我会在实际游戏中使用它的变体。尽管如此,我还是将声望奖励授予了גלעד ברקן的答案,因为我相信它实际上可以满足“到目标的距离相同”的要求,而这个答案并不能保证。 - NanoWizard
不幸的是,尽管该算法保证了对所有目标的某种“平均距离”,但它根本不关心平等的访问。考虑两个目标和两个玩家p1、p2。 p1的目标距离为3和15,p2的目标距离为5和5。即使它们的目标距离完全不同(按OP要求使用最大距离在环面上计算),此算法也认为两个玩家都处于最佳位置。 - le_m

4
也许我理解有误,但您是否可以将网格制作为边界内一个随机放置的位置的N个副本(其中N是玩家人数)?
Define `p = (x,y)` as first player location

Make target/s randomly for `p` at least 4 cells away and
  within either a horizontal or vertical rectangular limit

Now define the grid as (N - 1) copies of the rectangle with space added 
  so as to make the regtangles form a square (if that's the final shape you want), 
  and observe minimum distance from other players 

由于每个矩形完全相同,每个玩家都能够平等地接近相同数量的目标。


不幸的是,尽管这很容易实现,但当N具有接近因子时,它只能产生“类似正方形”的结果。例如,在N=12的情况下,我们可以有4行3个复制的网格部分(因为4*3=12)。然而,如果我们有13个玩家(一个质数),这种解决方案符合所有条件的唯一方法是使用1行(或列)13个复制的部分,这并不是非常正方形。我认为它仍然会满足所有其他条件。你提到“添加空间以形成正方形”,但这并不能确保所有玩家都有平等的访问。 - NanoWizard
@NanoWizard 奇怪的配置,如果考虑到环绕可能是可行的(我会再想一下),但当然可以将一行变成一个正方形,只需在初始赋值后向矩形添加适当的空间,这样当它被乘以时就会得到一个正方形。 - גלעד ברקן
那是技术上的真相。方形设计背后的整个原则是为了减少地图上所需行进的最大距离,并减少网格的资源需求,尽管我没有在要求中明确提到这些事情。 - NanoWizard
1
@NanoWizard 你可能需要在问题中澄清,“类似正方形”是指最终放置玩家和目标的配置,而不是网格本身。 - גלעד ברקן

2

我认为在每种组合中对于每个玩家来说距离不能完全相同,因此你要创建一个最小化玩家之间不公平的配置。

你知道弦的胡克定律吗?我想象这样一种情况:所有玩家和目标都连接在被压缩的弦上,这些弦按比例推动当前的距离(带有包裹)。让系统从一个特定的初始配置开始演变,即使它不是最公平的,只是一个初始猜测。优点是你不需要使用蛮力法,你只需让它自己调整。

为了提高收敛的机会,你需要实现摩擦/阻力。我一直在使用物理模拟,这就是我写这篇答案的原因。

缺点是:也许在实现之前需要太多的研究工作,这与你提到的不熟悉所述算法的要求相违背。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接