rand () da los mismos números nuevamente para un rango pequeño

9

Estoy tratando de hacer una especie de juego donde tengo una cuadrícula de 20x20 y muestro un jugador (P), un objetivo (T) y tres enemigos (X). Todos estos tienen una coordenada X e Y que se asignan usando rand(). El problema es que si trato de obtener más puntos en el juego (recargas de energía, etc.) se superponen con uno o más de los otros puntos porque el rango es pequeño (1 a 20 inclusive).

Estas son mis variables y cómo les estoy asignando valores: ( COORDes una structcon solo una X y una 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;
}

Quiero agregar más cosas al juego, pero la probabilidad de superposición aumenta cuando hago eso. ¿Hay alguna forma de arreglar esto?

Rabeez Riaz
fuente
8
rand () es un RNG malo
fanático del trinquete
3
rand()es un RNG lamentable, y de todos modos con un rango tan pequeño, no solo tiene que esperar colisiones, sino que están casi garantizadas.
Deduplicador
1
Si bien es cierto que rand()es un RNG pésimo, probablemente sea apropiado para un juego de un solo jugador, y la calidad de RNG no es el problema aquí.
Gort the Robot
13
Hablar sobre la calidad de rand()parece ser irrelevante aquí. No hay criptografía involucrada, y cualquier RNG probablemente dará colisiones en un mapa tan pequeño.
Tom Cornebize
2
Lo que estás viendo se conoce como el problema del cumpleaños. Si sus números aleatorios se convierten a un rango más pequeño que el rango natural del PRNG, entonces la probabilidad de obtener dos instancias del mismo número es mucho mayor de lo que podría pensar. Hace algún tiempo escribí una reseña sobre este tema en Stackoverflow aquí.
Preocupado por

Respuestas:

40

Si bien los usuarios que se quejan rand()y recomiendan mejores RNG tienen razón sobre la calidad de los números aleatorios, también les falta el panorama general. Los duplicados en flujos de números aleatorios no se pueden evitar, son una realidad. Esta es la lección del problema del cumpleaños .

En una cuadrícula de 20 * 20 = 400 posibles posiciones de generación, se espera un punto de generación duplicado (50% de probabilidad) incluso cuando se generan solo 24 entidades. Con 50 entidades (todavía solo el 12.5% ​​de toda la cuadrícula), la probabilidad de un duplicado es superior al 95%. Tienes que lidiar con colisiones.

A veces puede dibujar todas las muestras a la vez, luego puede usar un algoritmo aleatorio para dibujar nelementos distintos garantizados. Solo necesita generar la lista de todas las posibilidades. Si la lista completa de posibilidades es demasiado grande para almacenar, puede generar posiciones de generación de una en una como lo hace ahora (solo con un mejor RNG) y simplemente volver a generar cuando se produce una colisión. Aunque es probable que haya algunas colisiones, muchas colisiones seguidas son exponencialmente improbables incluso si la mayor parte de la cuadrícula está poblada.


fuente
Pensé en reaparecer en caso de una colisión, pero si tengo más elementos, como pretendo tener, entonces la búsqueda de una colisión se complicaría. También tendría que editar los controles en caso de que se agregue o elimine un punto del juego. Soy bastante inexperto, así que si hay una solución para esto, no podría verlo.
Rabeez Riaz
77
Si tiene un tablero de ajedrez de 20x20, a diferencia de un plano XY continuo (real) de 20x20, entonces lo que tiene es una tabla de búsqueda de 400 celdas para verificar colisiones. Esto es trivial.
John R. Strohm
@RabeezRiaz Si tiene un mapa más grande, tendrá una estructura de datos basada en cuadrícula (una cuadrícula que consta de un área de celdas y cada elemento dentro de esa celda se almacena en una lista). Si su mapa es aún más grande, implementará rect-tree.
rwong
2
@RabeezRiaz: si la búsqueda es demasiado complicada, use su primera sugerencia: generar una lista de las 400 ubicaciones iniciales posibles, barajarlas para que estén en un orden aleatorio (buscar el algoritmo) y luego comenzar a usar ubicaciones desde el frente cuando lo necesite para generar cosas (realiza un seguimiento de la cantidad que ya has usado). Sin colisiones.
RemcoGerlich
2
@RabeezRiaz No es necesario barajar la lista completa, si solo necesita una pequeña cantidad de valores aleatorios, simplemente baraje la parte que necesita (como en, tome un valor aleatorio de la lista de 1..400, elimínelo y repita hasta tienes suficientes elementos). De hecho, así es como funciona un algoritmo de barajado de todos modos.
Dorus
3

Si siempre desea evitar jugar una nueva entidad en una ubicación que ya ha sido asignada a otra cosa, puede cambiar ligeramente su proceso. Esto garantizaría ubicaciones únicas, pero requiere un poco más de gastos generales. Aquí están los pasos:

  1. Configure una colección de referencias a todas las ubicaciones posibles en el mapa (para el mapa 20x20, esto sería 400 ubicaciones)
  2. Elija una ubicación al azar de esta colección de 400 (rand () funcionaría bien para esto)
  3. Elimine esta posibilidad de la colección de ubicaciones posibles (por lo que ahora tiene 399 posibilidades)
  4. Repita hasta que todas las entidades tengan una ubicación específica

Siempre que elimine la ubicación del conjunto del que está eligiendo, no debería haber ninguna posibilidad de que una segunda entidad reciba la misma ubicación (a menos que elija las ubicaciones de más de un hilo a la vez).

Un análogo del mundo real a esto sería robar una carta de una baraja de cartas. Actualmente, barajas el mazo, robas una carta y la marcas, vuelves a colocar la carta robada en el mazo, barajas y vuelves a dibujar. El enfoque anterior omite volver a colocar la carta en el mazo.

Lyise
fuente
1

Perteneciente a rand() % nser menos que ideal

Hacer rand() % ntiene una distribución no uniforme. Obtendrá un número desproporcionado de ciertos valores porque el número de valores no es un múltiplo de 20

A continuación, rand()suele ser un generador congruencial lineal (hay muchos otros , solo que este es el más implementado, y con parámetros menos que ideales (hay muchas formas de seleccionar los parámetros)). El mayor problema con esto es que a menudo los bits bajos (los que se obtienen con una % 20expresión de tipo) no son tan aleatorios. Recuerdo uno rand()de hace años, donde el bit más bajo alternaba de 1a 0con cada llamada a rand(), no era muy aleatorio.

Desde la página de manual de rand (3):

Las versiones de rand () y srand () en la Biblioteca Linux C usan lo mismo
generador de números aleatorios como random () y srandom (), por lo que el orden inferior
los bits deben ser tan aleatorios como los bits de orden superior. Sin embargo, en mayores
implementaciones rand (), y en implementaciones actuales en diferentes
sistemas, los bits de orden inferior son mucho menos aleatorios que los de nivel superior
orden de bits. No utilice esta función en aplicaciones destinadas a ser
portátil cuando se necesita buena aleatoriedad.

Esto puede relegarse ahora a la historia, pero es muy posible que todavía tenga una implementación pobre de rand () escondida en algún lugar de la pila. En cuyo caso, sigue siendo bastante aplicable.

Lo que hay que hacer es usar una buena biblioteca de números aleatorios (que proporcione buenos números aleatorios) y luego pedir números aleatorios dentro del rango que desee.

Un ejemplo de un buen bit de código de número aleatorio (a partir de las 13:00 en el video vinculado)

#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;
}

Compare esto con:

#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");
}

Ejecute ambos programas y compare con qué frecuencia surgen ciertos números (o no aparecen) en esa salida.

Video relacionado: rand () considerado dañino

Algunos aspectos históricos de rand () que causan errores en Nethack que uno debe observar y considerar en sus propias implementaciones:

  • Problema de Nethack RNG

    Rand () es una función muy fundamental para la generación de números aleatorios de Nethack. La forma en que Nethack lo usa es defectuosa o se puede argumentar que lrand48 () produce números pseudoaleatorios mal. (Sin embargo, lrand48 () es una función de biblioteca que usa un método PRNG definido y cualquier programa que lo use debe tener en cuenta las debilidades de ese método).

    El error es que Nethack se basa (a veces exclusivamente, como es el caso en rn (2)) en los bits más bajos de los resultados de lrand48 (). Debido a esto, RNG en todo el juego funciona mal. Esto es especialmente notable antes de que las acciones del usuario introduzcan más aleatoriedad, es decir, en la generación de personajes y la creación de primer nivel.

Si bien lo anterior fue de 2003, aún debe tenerse en cuenta, ya que puede no ser el caso de que todos los sistemas que ejecutan el juego previsto sean un sistema Linux actualizado con una buena función rand ().

Si solo está haciendo esto por sí mismo, puede probar qué tan bueno es su generador de números aleatorios escribiendo un código y probando la salida con ent .


Sobre las propiedades de números aleatorios

Hay otras interpretaciones de 'aleatorio' que no son exactamente aleatorias. En una secuencia de datos aleatoria, es bastante posible obtener el mismo número dos veces. Si lanzas una moneda (al azar), es muy posible obtener dos caras seguidas. O tira un dado dos veces y obtén el mismo número dos veces seguidas. O girando una ruleta y obteniendo el mismo número dos veces allí.

La distribución de números.

Al reproducir una lista de canciones, las personas esperan que "aleatorio" signifique que la misma canción o artista no se reproducirá por segunda vez consecutiva. Tener una lista de reproducción en The Beatles dos veces seguidas se considera 'no aleatorio' (aunque es aleatorio). La percepción de que para una lista de reproducción de cuatro canciones tocó un total de ocho veces:

1 3 2 4 1 2 4 3

es más 'aleatorio' que:

1 3 3 2 1 4 4 2

Más sobre esto para la 'mezcla' de canciones: ¿Cómo mezclar canciones?

En valores repetidos

Si no desea repetir valores, hay que considerar un enfoque diferente. Genere todos los valores posibles y barajelos.

Si está llamando rand()(o cualquier otro generador de números aleatorios), lo está llamando con reemplazo. Siempre puedes obtener el mismo número dos veces. Una opción es desechar los valores una y otra vez hasta que seleccione uno que cumpla con sus requisitos. Señalaré que esto tiene un tiempo de ejecución no determinista y es posible que te encuentres en una situación en la que hay un bucle infinito a menos que comiences a hacer un rastreo más complejo.

Lista y selección

Otra opción es generar una lista de todos los estados válidos posibles y luego seleccionar un elemento aleatorio de esa lista. Encuentra todos los lugares vacíos (que cumplen con algunas reglas) en la habitación y luego elige uno aleatorio de esa lista. Y luego hazlo una y otra vez hasta que hayas terminado.

Barajar

El otro enfoque es barajar como si fuera una baraja de cartas. Comience con todos los espacios vacíos en la sala y luego comience a asignarlos repartiendo los espacios vacíos, uno a la vez, a cada regla / proceso que solicite un espacio vacío. Has terminado cuando te quedas sin cartas o las cosas dejan de pedirlas.

Comunidad
fuente
3
Next, rand() is typically a linear congruential generatorEsto no es cierto en muchas plataformas ahora. Desde la página de comando man rand (3) de linux: "Las versiones de rand () y srand () en la Biblioteca Linux C usan el mismo generador de números aleatorios que random (3) y srandom (3), por lo que los bits de orden inferior debe ser tan aleatorio como los bits de orden superior ". Además, como señala @delnan, la calidad del PRNG no es el verdadero problema aquí.
Charles E. Grant
44
Estoy rechazando esto porque no resuelve el problema real.
user253751
@immibis Entonces la otra respuesta tampoco "resuelve" el problema real y debe ser rechazado. Creo que la pregunta no es "arreglar mi código", es "¿por qué obtengo números aleatorios duplicados?" A la segunda pregunta, creo que la pregunta está respondida.
Neil
44
Incluso con el valor más pequeño de RAND_MAX32767, la diferencia es 1638 formas posibles de obtener algunos números frente a 1639 para otros. Parece poco probable que haga mucha diferencia práctica para el OP.
Martin Smith
@Neil "Arreglar mi código" no es una pregunta.
ligereza corre en órbita el
0

La solución más simple a este problema se ha citado en respuestas anteriores: es hacer una lista de valores aleatorios junto con cada una de sus 400 celdas, y luego, ordenar esta lista aleatoria. Su lista de celdas se ordenará como lista aleatoria y, de esta manera, se barajará.

Este método tiene la ventaja de evitar totalmente la superposición de celdas seleccionadas al azar.

La desventaja es que debe calcular un valor aleatorio en una lista separada para cada una de sus celdas. Entonces, prefieres no hacerlo mientras el juego ha comenzado.

Aquí hay un ejemplo de cómo puedes hacerlo:

#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;
}

Resultado:

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)

Simplemente cambie NUMBER_OF_SPAWNS para obtener celdas más o menos aleatorias, esto no cambiará el tiempo de cálculo requerido para la tarea.

KwentRell
fuente
"y luego, para ordenarlos todos" - Creo que te refieres a "barajar"
He completado mi explicación un poco. Debería ser más claro ahora.
KwentRell