Подтвердить что ты не робот

Алгоритм размещения x элементов равноудаленно на сетке переноса n на m

Я создаю простую сетчатую браузерную игру, в которой я бы хотел разместить игроков и целевые ячейки (подумайте, что на вершине холма) на одинаковом расстоянии. В идеале это было бы сделано таким образом, чтобы каждый игрок также был одинаково удален от ближайшей целевой ячейки.

Вот требования:

  • Игра должна поддерживать от 2 до 20 игроков.
  • Сетка n через m может быть любого размера, но чем больше "квадрат", тем лучше. (Принцип "квадратный" заключается в уменьшении максимального требуемого расстояния для перемещения по сетке - держите вещи более доступными).
  • Количество целевых ячеек является гибким.
  • Каждый игрок должен иметь равный доступ к одному и тому же числу целей.
  • Расстояние минимальное между любым игроком или целью и любым другим игроком или мишенью 4.

Обратите внимание, что каждая ячейка имеет 8 ближайших соседей (да, диагонали считаются расстоянием 1) и обрезает края. Значения тех, что внизу, логически смежны с теми, что расположены вверху, и то же самое для левого/правого.

Я пытался придумать хороший алгоритм для размещения игроков и целей в разных дистрибутивах, не создавая определенную предварительно определенную сетку для каждого количества игроков. Я обнаружил k-означает кластеризацию и алгоритм Ллойда, но я не очень хорошо знаком с ними и не знаю, как применять их к этому конкретному случаю, особенно потому, что количество целевых ячеек является гибким, что, по моему мнению, должно немного упростить решение.

Вот фрагмент значительно упрощенного кода, создающего предварительно определенную сетку из 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 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();
  }
}
4b9b3361

Ответ 1

Как уже было сказано, вероятно, нет идеального решения, отвечающего всем вашим требованиям, без каких-либо непомерных расходов на расчёты. 1

Подход

Мой подход заключался бы в замене того же расстояния на все целевые требования более гибким условием.

В следующем примере я ввел свойство heat для каждой ячейки, которое должно интуитивно представлять наличие/близость целей. Он рассчитывается путем суммирования тепла по отношению к каждой цели на карте. Тепло по отношению к цели просто 1 делится на расстояние (manhattan в моем примере) между ними. Возможно, вы захотите использовать разные реализации для функций heat и distance.

Позиционирование игрока

Для распространения игроков мы делаем следующее:

  • Сохраните список со всех ячеек, отсортированный по теплу.
  • Начните с некоторой ячейки (в настоящее время выбрано пользователем), найдите ее в отсортированном списке и используйте ее соседи (с одинаковыми значениями тепла) для позиций игроков.

Это гарантирует, что значения тепла для игровых ячеек всегда как можно ближе. Еще лучшим решением было бы поиск последовательности одинаковых значений тепла в отсортированном списке и их использование.

Пример кода

Обновить, чтобы иметь разные целевые позиции

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>

Ответ 2

Вы ограничены плоской плоскостью? Если вы можете переместиться в 3D, вы можете использовать Fibonacci Spiral для создания произвольного количества эквидистантных точек на сфере. Там действительно хороший эскиз обработки этого на работе в http://www.openprocessing.org/sketch/41142 (с кодом, чтобы пойти с ним). На изображении ниже показано, как он выглядит. Одно из преимуществ заключается в том, что вы автоматически получаете включенную "упаковку".

Область Фибоначчи

Если вам нужно придерживаться 2D, вы можете попробовать выше, а затем сферическую к планарной проекции, которая сохраняет отображение. Это может быть немного сложнее, чем вы ищете, хотя...

Ответ 3

Одно интуитивное решение, которое приходит в голову, состоит в том, чтобы разделить плоскость симметрично в соответствии с количеством игроков, случайным образом разместить одного игрока и его цель/с, а затем отразить расположение симметрично в других разделах. Привяжите сетку теоретически по кругу (или наоборот), затем разделите и отразите.

В (теоретической) сетке с бесконечным разрешением, центром которой является центр полярной системы координат, мы могли бы сначала разместить одного игрока и цели (кстати, они могут быть размещены в любом месте сетки и симметрии все равно удерживаются), затем для размещения других 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 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();
  }
}

Ответ 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 the final shape you want), 
  and observe minimum distance from other players 

Поскольку каждый прямоугольник точно такой же, каждый игрок имеет равный доступ к одному и тому же числу целей.

Ответ 5

Я думаю, что расстояние не может быть одинаковым для каждого игрока в каждой комбинации, поэтому вы хотите создать конфигурацию, которая минимизирует несправедливость среди игроков.

Знаете ли вы закон Гука для строк? Я представляю ситуацию, в которой все игроки и цели связаны со сжатыми строками, которые продвигаются пропорционально текущему расстоянию (с обертками). Пусть система эволюционирует из определенной начальной конфигурации, даже если это не самая справедливая, а просто первоначальная догадка. Преимущество состоит в том, что вам не нужно будет перетаскивать его, вы просто оставите его для настройки.

Чтобы повысить шансы конвергенции, вам необходимо реализовать трение/перетаскивание. Я работал с физическим симуляцией, поэтому я написал этот ответ.

Недостаток: возможно, он требует слишком много исследований до реализации, чего вы пытались избежать, поскольку вы упомянули, что не знакомы с указанными алгоритмами.