rand () gibt für einen kleinen Bereich wieder die gleichen Zahlen an

9

Ich versuche eine Art Spiel zu machen, in dem ich ein Raster von 20x20 habe und einen Spieler (P), ein Ziel (T) und drei Feinde (X) anzeige. Alle diese haben eine X- und eine Y-Koordinate, die mit zugewiesen werden rand(). Das Problem ist, dass wenn ich versuche, mehr Punkte im Spiel zu bekommen (Energie nachfüllen usw.), diese sich mit einem oder mehreren der anderen Punkte überschneiden, weil die Reichweite klein ist (1 bis einschließlich 20).

Dies sind meine Variablen und wie ich ihnen Werte zuweise: (das COORDist ein structmit nur einem X und einem Y)

const int gridSize = 20;
COORD player;
COORD target;
COORD enemy1;
COORD enemy2;
COORD enemy3;

//generate player
srand ( time ( NULL ) );
spawn(&player);
//generate target
spawn(&target);
//generate enemies
spawn(&enemy1);
spawn(&enemy2);
spawn(&enemy3);

void spawn(COORD *point)
{
    //allot X and Y coordinate to a point
    point->X = randNum();
    point->Y = randNum();
}

int randNum()
{
    //generate a random number between 1 and gridSize
    return (rand() % gridSize) + 1;
}

Ich möchte dem Spiel mehr Dinge hinzufügen, aber die Wahrscheinlichkeit einer Überlappung steigt, wenn ich das tue. Gibt es eine Möglichkeit, dies zu beheben?

Rabeez Riaz
quelle
8
rand () ist ein schlechter RNG
Ratschenfreak
3
rand()ist ein mitleidiges RNG, und bei einer so geringen Reichweite muss man nicht nur mit Kollisionen rechnen , sie sind fast garantiert.
Deduplikator
1
Es ist zwar wahr, dass rand()es sich um ein mieses RNG handelt, aber es ist wahrscheinlich für ein Einzelspielerspiel geeignet, und die RNG-Qualität ist hier nicht das Problem.
Gort the Robot
13
Über die Qualität von rand()zu sprechen, scheint hier irrelevant zu sein. Es ist keine Kryptographie beteiligt, und jedes RNG wird sehr wahrscheinlich Kollisionen in einer so kleinen Karte verursachen.
Tom Cornebize
2
Was Sie sehen, ist als Geburtstagsproblem bekannt. Wenn Ihre Zufallszahlen in einen Bereich konvertiert werden, der kleiner als der natürliche Bereich des PRNG ist, ist die Wahrscheinlichkeit, zwei Instanzen derselben Zahl zu erhalten, viel höher als Sie vielleicht denken. Vor einiger Zeit habe ich hier
ConcernedOfTunbridgeWells

Antworten:

40

Während die Benutzer, die sich über rand()bessere RNGs beschweren und diese empfehlen, hinsichtlich der Qualität der Zufallszahlen Recht haben, fehlt ihnen auch das Gesamtbild. Duplikate in Zufallszahlenströmen können nicht vermieden werden, sie sind eine Tatsache des Lebens. Dies ist die Lehre aus dem Geburtstagsproblem .

Bei einem Raster von 20 * 20 = 400 möglichen Spawnpositionen ist ein doppelter Spawnpunkt zu erwarten (50% Wahrscheinlichkeit), selbst wenn nur 24 Entitäten erzeugt werden. Bei 50 Entitäten (immer noch nur 12,5% des gesamten Rasters) liegt die Wahrscheinlichkeit eines Duplikats bei über 95%. Sie müssen mit Kollisionen umgehen.

Manchmal können Sie alle Samples gleichzeitig zeichnen, dann können Sie einen Shuffle-Algorithmus verwenden, um ngarantiert unterschiedliche Elemente zu zeichnen . Sie müssen nur die Liste aller Möglichkeiten erstellen. Wenn die vollständige Liste der Möglichkeiten zu groß ist, um sie zu speichern, können Sie wie jetzt nacheinander Spawn-Positionen generieren (nur mit einem besseren RNG) und bei einer Kollision einfach neu generieren. Obwohl einige Kollisionen wahrscheinlich sind, sind viele Kollisionen in einer Reihe exponentiell unwahrscheinlich, selbst wenn der größte Teil des Gitters gefüllt ist.


quelle
Ich dachte darüber nach, im Falle einer Kollision wieder aufzutauchen, aber wenn ich mehr Gegenstände habe, als ich beabsichtige, würde die Suche nach einer Kollision kompliziert werden. Ich müsste auch die Schecks bearbeiten, falls ein Punkt zum Spiel hinzugefügt oder daraus entfernt wird. Ich bin ziemlich unerfahren. Wenn es also eine Problemumgehung gibt, konnte ich sie nicht sehen.
Rabeez Riaz
7
Wenn Sie ein 20x20-Schachbrett haben, im Gegensatz zu einer 20x20-kontinuierlichen (realen) XY-Ebene, haben Sie eine Nachschlagetabelle mit 400 Zellen, um nach Kollisionen zu suchen. Das ist TRIVIAL.
John R. Strohm
@RabeezRiaz Wenn Sie eine größere Karte haben, haben Sie eine gitterbasierte Datenstruktur (ein Raster, das aus einem Bereich von Zellen besteht, und jedes Element in dieser Zelle wird in einer Liste gespeichert). Wenn Ihre Karte noch größer ist, implementieren Sie den Rect-Tree.
Rwong
2
@RabeezRiaz: Wenn die Suche zu kompliziert ist, verwenden Sie seinen ersten Vorschlag: Erstellen Sie eine Liste aller 400 möglichen Startpositionen, mischen Sie sie so, dass sie in zufälliger Reihenfolge vorliegen (suchen Sie den Algorithmus), und verwenden Sie dann bei Bedarf Positionen von vorne um Dinge zu generieren (verfolgen Sie, wie viele Sie bereits verwendet haben). Keine Kollisionen.
RemcoGerlich
2
@RabeezRiaz Sie müssen nicht die gesamte Liste mischen. Wenn Sie nur eine kleine Anzahl von Zufallswerten benötigen, mischen Sie einfach den Teil, den Sie benötigen (wie in, nehmen Sie einen zufälligen Wert aus der Liste von 1..400, entfernen Sie ihn und wiederholen Sie bis Sie haben genug Elemente). Tatsächlich funktioniert ein Shuffling-Algorithmus sowieso so.
Dorus
3

Wenn Sie immer vermeiden möchten, eine neue Entität an einem Ort abzuspielen, der bereits einem anderen zugeordnet wurde, können Sie Ihren Prozess geringfügig ändern. Dies würde eindeutige Standorte garantieren, erfordert jedoch etwas mehr Overhead. Hier sind die Schritte:

  1. Richten Sie eine Sammlung von Verweisen auf alle möglichen Orte auf der Karte ein (für die 20x20-Karte wären dies 400 Orte).
  2. Wählen Sie einen zufälligen Ort aus dieser Sammlung von 400 (rand () würde dafür gut funktionieren)
  3. Entfernen Sie diese Möglichkeit aus der Sammlung möglicher Standorte (es stehen jetzt 399 Möglichkeiten zur Verfügung).
  4. Wiederholen, bis alle Entitäten einen bestimmten Speicherort haben

Solange Sie den Speicherort aus dem Satz entfernen, aus dem Sie auswählen, sollte es keine Chance geben, dass eine zweite Entität denselben Speicherort erhält (es sei denn, Sie wählen die Speicherorte aus mehr als einem Thread gleichzeitig aus).

Ein reales Analogon dazu wäre das Ziehen einer Karte aus einem Kartenspiel. Derzeit mischen Sie das Deck, ziehen eine Karte und markieren sie, legen die gezogene Karte wieder in das Deck, mischen neu und ziehen erneut. Der obige Ansatz überspringt das Zurücklegen der Karte in das Deck.

Lyise
quelle
1

In Bezug darauf rand() % n, weniger als ideal zu sein

Tun rand() % nhat eine ungleichmäßige Verteilung. Sie erhalten eine unverhältnismäßige Anzahl bestimmter Werte, da die Anzahl der Werte kein Vielfaches von 20 ist

Als nächstes rand()wird typischerweise ein linearer Kongruenzgenerator verwendet (es gibt viele andere , nur dies ist der wahrscheinlichste implementierte - und mit weniger als idealen Parametern (es gibt viele Möglichkeiten, die Parameter auszuwählen)). Das größte Problem dabei ist, dass die niedrigen Bits darin (die, die Sie mit einem % 20Typausdruck erhalten) oft nicht so zufällig sind. Ich erinnere mich an eine rand()von Jahren , wo das niedrigste Bit von abgewechselt , 1um 0mit jedem Anruf rand()- es ist nicht sehr zufällig war.

Aus der Manpage von rand (3):

Die Versionen von rand () und srand () in der Linux C-Bibliothek verwenden dasselbe
Zufallszahlengenerator als random () und srandom (), also die niedrigere Ordnung
Bits sollten so zufällig sein wie die Bits höherer Ordnung. Allerdings bei älteren
rand () - Implementierungen und aktuelle Implementierungen auf verschiedenen
Systeme sind die Bits niedrigerer Ordnung viel weniger zufällig als die Bits höherer Ordnung
Bits bestellen. Verwenden Sie diese Funktion nicht in Anwendungen, für die dies vorgesehen ist
tragbar, wenn gute Zufälligkeit benötigt wird.

Dies mag jetzt in die Geschichte verbannt sein, aber es ist durchaus möglich, dass Sie immer noch eine schlechte rand () - Implementierung haben, die sich irgendwo im Stapel versteckt. In diesem Fall ist es immer noch durchaus anwendbar.

Die Sache zu tun ist, tatsächlich eine gute Zufallszahlenbibliothek zu verwenden (die gute Zufallszahlen liefert) und dann nach Zufallszahlen innerhalb des gewünschten Bereichs zu fragen.

Ein Beispiel für ein gutes Zufallszahlen-Codebit (ab 13:00 im verknüpften Video)

#include <iostream>
#include <random>
int main() {
    std::mt19937 mt(1729); // yes, this is a fixed seed
    std::uniform_int_distribution<int> dist(0, 99);
    for (int i = 0; i < 10000; i++) {
        std::cout << dist(mt) << " ";
    }
    std::cout << std::endl;
}

Vergleichen Sie dies mit:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
    srand(time(NULL));
    for (int i = 0; i < 10000; i++) {
        printf("%d ", rand() % 100);
    }
    printf("\n");
}

Führen Sie beide Programme aus und vergleichen Sie, wie oft bestimmte Zahlen in dieser Ausgabe angezeigt werden (oder nicht).

In Verbindung stehendes Video: rand () als schädlich angesehen

Einige historische Aspekte von rand (), die Fehler in Nethack verursachen, die man bei seinen eigenen Implementierungen beobachten und berücksichtigen sollte:

  • Nethack RNG Problem

    Rand () ist eine sehr grundlegende Funktion für die Zufallszahlengenerierung von Nethack. Die Art und Weise, wie Nethack es benutzt, ist fehlerhaft oder es kann argumentiert werden, dass lrand48 () beschissene Pseudozufallszahlen erzeugt. (Lrand48 () ist jedoch eine Bibliotheksfunktion, die eine definierte PRNG-Methode verwendet, und jedes Programm, das sie verwendet, sollte die Schwächen dieser Methode berücksichtigen.)

    Der Fehler ist, dass Nethack (manchmal ausschließlich wie in rn (2)) auf die unteren Bits der Ergebnisse von lrand48 () angewiesen ist. Aus diesem Grund funktioniert RNG im gesamten Spiel schlecht. Dies macht sich insbesondere dann bemerkbar, wenn Benutzeraktionen weitere Zufälligkeiten einführen, dh bei der Charaktererstellung und der Erstellung der ersten Ebene.

Obwohl das oben Genannte aus dem Jahr 2003 stammt, sollte es dennoch beachtet werden, da möglicherweise nicht alle Systeme, auf denen Ihr beabsichtigtes Spiel ausgeführt wird, ein aktuelles Linux-System mit einer guten rand () -Funktion sind.

Wenn Sie dies nur für sich selbst tun, können Sie testen, wie gut Ihr Zufallszahlengenerator ist, indem Sie Code schreiben und die Ausgabe mit ent testen .


Über die Eigenschaften von Zufallszahlen

Es gibt andere Interpretationen von "zufällig", die nicht genau zufällig sind. In einem zufälligen Datenstrom ist es durchaus möglich, dieselbe Zahl zweimal zu erhalten. Wenn Sie eine Münze werfen (zufällig), ist es durchaus möglich, zwei Köpfe hintereinander zu bekommen. Oder wirf zweimal einen Würfel und erhalte zweimal hintereinander dieselbe Zahl. Oder ein Roulette-Rad drehen und dort zweimal die gleiche Nummer bekommen.

Die Verteilung von Zahlen

Beim Abspielen einer Songliste erwarten die Leute, dass "zufällig" bedeutet, dass derselbe Song oder Künstler nicht ein zweites Mal hintereinander gespielt wird. Eine Wiedergabeliste mit den Beatles zweimal hintereinander spielen wird als ‚nicht zufällig‘ zu haben (obwohl es ist zufällig). Die Wahrnehmung, dass für eine Wiedergabeliste von vier Songs insgesamt acht Mal gespielt wurde:

1 3 2 4 1 2 4 3

ist eher 'zufällig' als:

1 3 3 2 1 4 4 2

Mehr dazu zum 'Mischen' von Songs: Wie mische ich Songs?

Bei wiederholten Werten

Wenn Sie Werte nicht wiederholen möchten, sollten Sie einen anderen Ansatz in Betracht ziehen. Generieren Sie alle möglichen Werte und mischen Sie sie.

Wenn Sie anrufen rand()(oder einen anderen Zufallszahlengenerator), rufen Sie ihn mit Ersatz an. Sie können immer zweimal dieselbe Nummer erhalten. Eine Möglichkeit besteht darin, die Werte immer wieder herauszuwerfen, bis Sie einen auswählen, der Ihren Anforderungen entspricht. Ich werde darauf hinweisen, dass dies eine nicht deterministische Laufzeit hat und es möglich ist, dass Sie sich in einer Situation befinden, in der es eine Endlosschleife gibt, wenn Sie nicht mit einer komplexeren Rückverfolgung beginnen.

Liste und Auswahl

Eine andere Möglichkeit besteht darin, eine Liste aller möglichen gültigen Zustände zu erstellen und dann ein zufälliges Element aus dieser Liste auszuwählen. Finde alle leeren Stellen (die einigen Regeln entsprechen) im Raum und wähle dann eine zufällige aus dieser Liste aus. Und dann mach es immer wieder, bis du fertig bist.

Mischen

Der andere Ansatz besteht darin, zu mischen, als wäre es ein Kartenspiel. Beginnen Sie mit allen leeren Stellen im Raum und weisen Sie sie dann zu, indem Sie die leeren Stellen nacheinander jeder Regel / jedem Prozess zuordnen und nach einer leeren Stelle fragen. Sie sind fertig, wenn Ihnen die Karten ausgehen oder die Dinge nicht mehr nach ihnen fragen.

Gemeinschaft
quelle
3
Next, rand() is typically a linear congruential generatorDies gilt derzeit nicht auf vielen Plattformen. Aus der rand (3) Manpage von Linux: "Die Versionen von rand () und srand () in der Linux C Library verwenden denselben Zufallszahlengenerator wie random (3) und srandom (3), also die Bits niedrigerer Ordnung sollte so zufällig sein wie die höherwertigen Bits. " Wie @delnan betont, ist die Qualität des PRNG hier nicht das eigentliche Problem.
Charles E. Grant
4
Ich stimme dem zu, weil es das eigentliche Problem nicht löst.
user253751
@immibis Dann "löst" auch die andere Antwort das eigentliche Problem nicht und sollte herabgestimmt werden. Ich denke, die Frage lautet nicht "Meinen Code reparieren", sondern "Warum bekomme ich doppelte Zufallszahlen?" Auf die zweite Frage glaube ich, dass die Frage beantwortet ist.
Neil
4
Selbst mit dem kleinsten Wert von RAND_MAX32767 beträgt der Unterschied 1638 Möglichkeiten, einige Zahlen zu erhalten, gegenüber 1639 für andere. Es scheint unwahrscheinlich, dass das OP einen großen praktischen Unterschied macht.
Martin Smith
@Neil "Fix my code" ist keine Frage.
Leichtigkeitsrennen im Orbit
0

Die einfachste Lösung für dieses Problem wurde in früheren Antworten zitiert: Es besteht darin, neben jeder Ihrer 400 Zellen eine Liste mit Zufallswerten zu erstellen und diese Zufallsliste dann zu sortieren. Ihre Liste der Zellen wird als Zufallsliste sortiert und auf diese Weise gemischt.

Dieses Verfahren hat den Vorteil, dass die Überlappung zufällig ausgewählter Zellen vollständig vermieden wird.

Der Nachteil ist, dass Sie für jede Ihrer Zellen einen zufälligen Wert in einer separaten Liste berechnen müssen . Also machst du es lieber nicht, während das Spiel gestartet ist.

Hier ist ein Beispiel, wie Sie es tun können:

#include <algorithm>
#include <iostream>
#include <vector>

#define NUMBER_OF_SPAWNS 20
#define WIDTH 20
#define HEIGHT 20

typedef struct _COORD
{
  int x;
  int y;
  _COORD() : x(0), y(0) {}
  _COORD(int xp, int yp) : x(xp), y(yp) {}
} COORD;

typedef struct _spawnCOORD
{
  float rndValue;
  COORD*coord;
  _spawnCOORD() : rndValue(0.) {}
} spawnCOORD;

struct byRndValue {
  bool operator()(spawnCOORD const &a, spawnCOORD const &b) {
    return a.rndValue < b.rndValue;
  }
};

int main(int argc, char** argv)
{
  COORD map[WIDTH][HEIGHT];
  std::vector<spawnCOORD>       rndSpawns(WIDTH * HEIGHT);

  for (int x = 0; x < WIDTH; ++x)
    for (int y = 0; y < HEIGHT; ++y)
      {
        map[x][y].x = x;
        map[x][y].y = y;
        rndSpawns[x + y * WIDTH].coord = &(map[x][y]);
        rndSpawns[x + y * WIDTH].rndValue = rand();
      }

  std::sort(rndSpawns.begin(), rndSpawns.end(), byRndValue());

  for (int i = 0; i < NUMBER_OF_SPAWNS; ++i)
    std::cout << "Case selected for spawn : " << rndSpawns[i].coord->x << "x"
              << rndSpawns[i].coord->y << " (rnd=" << rndSpawns[i].rndValue << ")\n";
  return 0;
}

Ergebnis:

root@debian6:/home/eh/testa# ./exe 
Case selected for spawn : 11x15 (rnd=6.93951e+06)
Case selected for spawn : 14x1 (rnd=7.68493e+06)
Case selected for spawn : 8x12 (rnd=8.93699e+06)
Case selected for spawn : 18x13 (rnd=1.16148e+07)
Case selected for spawn : 1x0 (rnd=3.50052e+07)
Case selected for spawn : 2x17 (rnd=4.29992e+07)
Case selected for spawn : 9x14 (rnd=7.60658e+07)
Case selected for spawn : 3x11 (rnd=8.43539e+07)
Case selected for spawn : 12x7 (rnd=8.77554e+07)
Case selected for spawn : 19x0 (rnd=1.05576e+08)
Case selected for spawn : 19x14 (rnd=1.10613e+08)
Case selected for spawn : 8x2 (rnd=1.11538e+08)
Case selected for spawn : 7x2 (rnd=1.12806e+08)
Case selected for spawn : 19x15 (rnd=1.14724e+08)
Case selected for spawn : 8x9 (rnd=1.16088e+08)
Case selected for spawn : 2x19 (rnd=1.35497e+08)
Case selected for spawn : 2x16 (rnd=1.37807e+08)
Case selected for spawn : 2x8 (rnd=1.49798e+08)
Case selected for spawn : 7x16 (rnd=1.50123e+08)
Case selected for spawn : 8x11 (rnd=1.55325e+08)

Ändern Sie einfach NUMBER_OF_SPAWNS, um mehr oder weniger zufällige Zellen zu erhalten. Dadurch wird die für die Aufgabe erforderliche Rechenzeit nicht geändert.

KwentRell
quelle
"und dann, um sie alle zu sortieren" - ich glaube du meinst "mischen"
Ich habe meine Erklärung etwas abgeschlossen. Es sollte jetzt klarer sein.
KwentRell