31 votes

Algorithme pour placer x éléments de manière équidistante sur une grille d'emballage de n par m

Je suis en train de créer un simple jeu par navigateur basé sur une grille où je voudrais placer les joueurs et les cellules cibles (pensez au roi de la colline) de manière équidistante. L'idéal serait de faire en sorte que chaque joueur soit à égale distance de la cellule cible la plus proche.

Voici les conditions requises :

  • Le jeu doit prendre en charge 2 à 20 joueurs .
  • El n por m La grille peut être toute taille mais plus elle est carrée, mieux c'est. (Le principe du "carré" est de réduire la distance maximale à parcourir à travers la grille - pour rendre les choses plus accessibles).
  • Le nombre de cellules cibles est flexible.
  • Chaque joueur doit avoir un accès égal au même nombre de cibles.
  • El minimum la distance entre un joueur ou une cible et tout autre joueur ou cible est 4 .

Notez que chaque cellule a 8 voisins immédiats (oui les diagonales comptent comme une distance de 1 ), et enveloppe des bords . Ce qui signifie que ceux du bas sont logiquement adjacents à ceux du haut, et de même pour la gauche et la droite.

J'ai essayé de penser à un bon algorithme pour placer les joueurs et les cibles dans des distributions variables sans avoir à créer une grille spécifique prédéterminée pour chaque nombre de joueurs. J'ai découvert clustering k-means y Algorithme de Lloyd's mais je ne les connais pas très bien et je ne sais pas vraiment comment les appliquer à ce cas précis, d'autant que le nombre de cellules cibles est flexible, ce qui devrait simplifier un peu la solution.

Voici un extrait de code très simplifié créant une grille prédéterminée de 6 joueurs, juste pour montrer l'essence de ce que je vise :

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

Il crée une grille qui ressemble à ceci : Grid Example

Où les cellules bleues sont les joueurs et les cellules rouges les cibles.

Quelqu'un a-t-il des suggestions sur la manière de procéder ?

Des liens vers des documents utiles seraient très appréciés.

Existe-t-il un gourou capable de créer un algorithme de placement génial qui satisfait à toutes les conditions susmentionnées ?

Ce serait AMAZING si la solution permet également de configurer le nombre de cellules cibles et/ou la distance minimale pour cualquier nombre de joueurs tout en satisfaisant à toutes les conditions, bien que cela ne soit pas strictement nécessaire.

EDIT

Après quelques autres considérations de game design, j'ai changé la distance minimale entre le joueur et la cible en 4 au lieu de 2 . Le texte, le code et l'image ci-dessus ont été modifiés en conséquence. Au moment de cette modification, aucune solution n'était contrainte par cette exigence, donc cela ne devrait rien changer.

EDIT 2

Si vous proposez une solution, veuillez fournir un code JavaScript (ou au moins un pseudo-code) décrivant les étapes détaillées de votre solution. Veuillez également expliquer comment la solution répond aux exigences. Nous vous remercions.

10voto

adilapapaya Points 1463

Êtes-vous contraint à un plan plat ? Si vous pouvez vous déplacer en 3D, alors vous pouvez utiliser la fonction Spirale de Fibonacci pour générer un nombre arbitraire de points équidistants sur une sphère. Il y a un très beau croquis de ce processus à l'adresse suivante http://www.openprocessing.org/sketch/41142 (avec le code qui va avec). L'image ci-dessous montre à quoi cela ressemble. L'un des avantages est que l'"emballage" est automatiquement inclus.

Fibonacci Sphere

Si vous devez vous en tenir à la 2D, vous pouvez essayer ce qui précède suivi d'un projection sphérique vers planaire qui préserve la correspondance. Mais c'est peut-être un peu plus compliqué que ce que vous recherchez...

5voto

גלעד ברקן Points 3044

Une solution intuitive qui me vient à l'esprit est de diviser le plan symétriquement en fonction du nombre de joueurs, de placer un joueur et sa ou ses cibles de façon aléatoire, puis de refléter le placement symétriquement dans les autres sections. Liez théoriquement la grille en un cercle (ou vice versa), puis divisez et réfléchissez.

Dans une grille (théorique) de résolution infinie, dont le centre est le centre d'un système de coordonnées polaires, nous pourrions d'abord placer un joueur et ses cibles (en passant, celles-ci peuvent être placées n'importe où sur la grille et la symétrie sera toujours respectée), puis placer l'autre joueur. n - 1 joueurs et cible/s, incrémenter le degré initial de 360° / n à chaque fois, en gardant le même rayon. Cependant, comme votre grille aura une limite de taille pratique, vous devrez garantir d'une manière ou d'une autre que les cellules réfléchies existent sur la grille, peut-être par une combinaison de restriction de la génération initiale et/ou de modification de la taille/parité de la grille.

Quelque chose du genre :

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

5voto

Cedric Reichenbach Points 1838

Comme nous l'avons déjà dit, il n'existe probablement pas de solution parfaite répondant exactement à toutes vos exigences sans frais de calcul exorbitants. 1

Approche

Mon approche consisterait à remplacer le même distance pour toutes les cibles par une condition plus souple.

Dans l'exemple suivant, j'ai introduit un chaleur pour chaque cellule, qui devrait intuitivement représenter la disponibilité/proximité des cibles. Elle est calculée en additionnant la chaleur par rapport à chaque cible sur la carte. La chaleur par rapport à une cible est simplement 1 divisé par la distance (Manhattan dans mon exemple) entre eux. Peut-être voudrez-vous utiliser des implémentations différentes pour les fonctions suivantes heat y distance .

Positionnement des joueurs

Pour distribuer les joueurs, nous procédons comme suit :

  1. Conservez une liste de toutes les cellules, triées par chaleur.
  2. Commencez à une certaine cellule (actuellement sélectionnée par l'utilisateur), trouvez-la dans la liste triée et utilisez ses voisins (avec des valeurs de chaleur similaires) pour les positions des joueurs.

Cela permet de garantir que les valeurs thermiques des cellules des joueurs sont toujours aussi proches que possible. Une solution encore meilleure serait de rechercher une séquence de valeurs thermiques similaires dans la liste triée et de les utiliser.

Exemple de code

Rechargez pour avoir différentes positions de cible

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>

Idem dans JSFiddle pour jouer avec les variables

Extrémités ouvertes

Cet exemple a été cousu assez rapidement. Vous remarquerez qu'il y a plusieurs extrémités ouvertes et des coins non couverts. Je l'ai juste fait pour exposer mon idée.

Non couvert :

  • Emballage sur les bords : Cela n'affecte probablement que les dist fonction
  • Distance entre les cibles : Ils sont juste jetés au hasard quelque part sur la carte.
  • La sélection des postes ne fait que chercher vers le bas le heat liste : Cela pourrait être fait de manière plus intelligente, par exemple en utilisant la distance la plus courte vers la cellule sélectionnée d'origine.
  • Distribution automatique des joueurs (sans cliquer) : Vous pouvez prendre un joueur au hasard pour commencer.

1 Je veux dire, théoriquement, vous pourriez simplement essayer toutes les variations possibles et vérifier si elles sont correctes (si vous avez un grand cluster dans votre jardin).

4voto

גלעד ברקן Points 3044

Peut-être que j'ai raté quelque chose, mais ne pouvez-vous pas simplement faire la grille en tant que N copies d'un placement aléatoire dans les limites ( N étant le nombre de joueurs) ?

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 

Comme chaque rectangle est exactement le même, chaque joueur a un accès égal au même nombre de cibles.

2voto

Eduardo Poço Points 56

Je pense que la distance ne peut pas être exactement la même pour tous les joueurs dans toutes les combinaisons, donc vous voulez créer une configuration qui minimise les injustices entre les joueurs.

Connaissez-vous la loi de Hooke pour les cordes ? J'imagine une situation dans laquelle tous les joueurs et les cibles sont reliés par des cordes comprimées qui poussent proportionnellement à la distance actuelle (avec des enroulements). Laissez le système évoluer à partir d'une configuration initiale particulière, même si ce n'est pas la plus juste, mais juste une supposition initiale. L'avantage est que vous n'aurez pas besoin de recourir à la force brute, vous le laissez simplement s'ajuster de lui-même.

Pour augmenter les chances de convergence, vous devrez mettre en place une friction/traînée. J'ai travaillé avec des simulations de physique, c'est pourquoi j'ai écrit cette réponse.

L'inconvénient est qu'il demande peut-être trop d'efforts de recherche avant la mise en œuvre, ce que vous essayiez d'éviter puisque vous avez mentionné ne pas être familier avec les algorithmes en question.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X