将x项等距放置在n×m包裹网格

时间:2016-01-01 22:25:05

标签: javascript algorithm

我正在创建一个简单的基于网格的浏览器游戏,我想在此等距离放置玩家和目标单元格(想想山丘之王)。理想情况下,这将以每个玩家与最近的目标单元格相等的方式进行。

以下是要求:

  • 游戏需要支持 2到20名玩家
  • nm网格可以任意大小,但越“正方形”越好。 ('方形'背后的原则是减少穿越网格所需的最大距离 - 让事情更容易接近)
  • 目标细胞的数量是灵活的。
  • 每个玩家应该有相同数量的目标。
  • 任何玩家或目标与任何其他玩家或目标之间的最小距离 4

请注意,每个单元格都有 8 直接邻居(是对角线计为 1 的距离), 边缘包裹 < / strong>即可。这意味着底部的那些逻辑上与顶部的相邻,左/右相同。

我一直在努力想出一个好的算法来将玩家和目标放在不同的发行版中,而不必为每个玩家数量创建一个特定的预定网格。我发现了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

蓝色细胞是玩家,红色细胞是目标。

有没有人对如何解决这个问题有任何建议?

非常感谢有用材料的链接。

那里是否有专家能够制作出满足上述所有条件的精彩放置算法?

如果解决方案还允许任何数量的玩家可以配置目标单元格数和/或最小距离,并且仍然满足所有这些,那么 AMAZING 条件,虽然这不是绝对必要的。

修改

经过其他一些游戏设计考虑,我改变了玩家和玩家之间的最小距离。定位到 4 而不是 2 。上面的文本,代码和图像已相应更改。在编辑时,没有任何解决方案受到该要求的约束,因此它不应该影响任何内容。

编辑2

如果您提出解决方案,请提供JavaScript代码(或至少是伪代码),概述解决方案的详细步骤。另请说明解决方案如何满足要求。谢谢!

5 个答案:

答案 0 :(得分:10)

你被限制在一架平面上吗?如果可以移动到3D,则可以使用Fibonacci Spiral在球体上生成任意数量的等距点。在http://www.openprocessing.org/sketch/41142处有一个非常好的处理草图(使用代码)。下图显示了它的外观。一个好处是你自动获得包装&#39;包括在内。

Fibonacci Sphere

如果你必须坚持2D,那么你可以尝试上面的,然后是保留映射的spherical to planar projection。这可能比你想要的更复杂......

答案 1 :(得分:5)

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

在(理论上的)无限分辨率网格中,以中心为极坐标系的中心,我们可以先放置一个玩家及其目标(顺便说一句,这些可以放在网格和对称性仍将保持),然后放置其他n - 1玩家和目标/ s,每次将初始度数增加360° / n,保持相同的半径。但是,由于您的网格将具有实际的大小限制,您需要以某种方式保证反射的单元格存在于网格上,可能是通过限制初始生成和/或修改网格大小/奇偶校验的组合。

有些事情:

&#13;
&#13;
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();
  }
}
&#13;
&#13;
&#13;

答案 2 :(得分:5)

正如已经说过的那样,可能没有完美的解决方案可以完全满足您的所有要求,而且没有过高的计算费用。 1

方法

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

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

玩家定位

对于分发玩家,我们会执行以下操作:

  1. 列出所有单元格的列表,按热量排序
  2. 从某个单元格(当前是用户选择的)开始,在排序列表中找到它并使用其邻居(具有相似的热值)作为玩家位置
  3. 这确保了玩家单元的热值总是尽可能接近。 更好的解决方案是在排序列表中搜索一系列尽可能相似的热值并使用它们。

    示例代码

    重新加载以具有不同的目标位置

    &#13;
    &#13;
    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());
    }
    &#13;
    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;
    }
    &#13;
    <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>
    &#13;
    &#13;
    &#13;

    Same in JSFiddle to play around with variables

    开放式结束

    这个例子已经很快拼凑在一起了。您会注意到有几个开放的末端和未覆盖的角落情况。我刚刚概述了我的想法。

    未涵盖:

    • 包裹边缘:这可能只会影响dist功能
    • 目标之间的距离:他们只是随机地扔在地图上的某个地方
    • 选择位置只需向下搜索heat列表:这可以更智能地完成,例如与原始选定单元格的最短距离
    • 自动分发玩家(不点击):您可能只想随机选择

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

答案 3 :(得分: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 

由于每个矩形完全相同,每个玩家可以平等地访问相同数量的目标。

答案 4 :(得分:2)

我认为每个组合中的每个玩家的距离都不能完全相同,因此您需要创建一个最小化玩家不公平的配置。

你知道胡克的弦乐法吗?我想象一种情况,其中所有玩家和目标都与压缩字符串连接,压缩字符串与当前距离(包裹)成比例地推动。让系统从特定的初始配置发展,即使它不是最公平的,但只是初步猜测。优点是你不需要暴力破解它,你只需要让它自己调整。

为了增加收敛的机会,你需要实现摩擦/拖拽。我一直在使用物理模拟,这就是我写这个答案的原因。

缺点是:在实施之前可能需要太多的研究工作,你试图避免,因为你提到不熟悉所述算法。