rand () restituisce gli stessi numeri per un piccolo intervallo


9

Sto cercando di creare una specie di gioco in cui ho una griglia di 20x20 e visualizzo un giocatore (P), un bersaglio (T) e tre nemici (X). Tutti questi hanno una coordinata X e una Y assegnate usando rand(). Il problema è che se provo a ottenere più punti nel gioco (ricariche di energia, ecc.) Si sovrappongono con uno o più degli altri punti perché l'intervallo è piccolo (da 1 a 20 inclusi).

Queste sono le mie variabili e come le sto assegnando valori: (la COORDè una structcon solo una X e 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;
}

Voglio aggiungere più cose al gioco, ma la probabilità di sovrapposizione aumenta quando lo faccio. Esiste un modo per risolvere questo problema?


8
rand () è un cattivo RNG
maniaco del cricchetto,

3
rand()è un RNG pietoso, e comunque con una gamma così piccola, non devi solo aspettarti delle collisioni, sono quasi garantite.
Deduplicatore

1
Mentre è vero che rand()è un RNG scadente, è probabilmente appropriato per un gioco single player, e la qualità del RNG non è il problema qui.
Gort the Robot,

13
Parlare della qualità di rand()sembra essere irrilevante qui. Non è coinvolta la crittografia e qualsiasi RNG molto probabilmente causerà collisioni in una mappa così piccola.
Tom Cornebize,

2
Quello che stai vedendo è noto come il problema del compleanno. Se i tuoi numeri casuali vengono convertiti in un intervallo inferiore all'intervallo naturale del PRNG, la probabilità di ottenere due istanze dello stesso numero è molto più alta di quanto potresti pensare. Qualche tempo fa ho scritto un blurb su questo argomento su Stackoverflow qui.
ConcernedOfTunbridgeWells

Risposte:


40

Mentre gli utenti che si lamentano rand()e raccomandano migliori RNG hanno ragione sulla qualità dei numeri casuali, mancano anche il quadro più ampio. I duplicati nei flussi di numeri casuali non possono essere evitati, sono un dato di fatto. Questa è la lezione del problema del compleanno .

Su una griglia di 20 * 20 = 400 possibili posizioni di spawn, ci si deve aspettare un doppio punto di spawn (probabilità del 50%) anche quando si generano solo 24 entità. Con 50 entità (ancora solo il 12,5% dell'intera griglia), la probabilità di un duplicato è superiore al 95%. Devi affrontare le collisioni.

A volte è possibile disegnare tutti i campioni contemporaneamente, quindi utilizzare un algoritmo shuffle per disegnare nelementi distinti garantiti. Hai solo bisogno di generare l'elenco di tutte le possibilità. Se l'elenco completo delle possibilità è troppo grande per essere memorizzato, puoi generare le posizioni di spawn una alla volta come fai ora (solo con un RNG migliore) e semplicemente rigenerare quando si verifica una collisione. Anche se è probabile che si verifichino alcune collisioni, molte collisioni di fila sono imponenzialmente improbabili anche se la maggior parte della griglia è popolata.


Ho pensato di rinascere in caso di collisione, ma se avessi più oggetti, come ho intenzione di fare, la ricerca di una collisione sarebbe complicata. Dovrei anche modificare i controlli in caso di aggiunta o rimozione di un punto dal gioco. Sono abbastanza inesperto, quindi se c'è una soluzione a questo, non potrei vederlo.
Rabeez Riaz,

7
Se hai una scacchiera 20x20, al contrario di un piano XY continuo (reale) 20x20, allora hai una tabella di ricerca di 400 celle per verificare le collisioni. Questo è TRIVIAL.
John R. Strohm,

@RabeezRiaz Se hai una mappa più grande, avrai una struttura di dati basata sulla griglia (una griglia costituita da un'area di celle e ogni elemento all'interno di quella cella è memorizzato in un elenco). Se la tua mappa è ancora più grande, implementerai rect-tree.
rwong,

2
@RabeezRiaz: se la ricerca è troppo complicata, usa il suo primo suggerimento: genera un elenco di tutte e 400 le possibili posizioni di partenza, mescolale in modo che siano in un ordine casuale (cerca l'algoritmo), quindi inizia a utilizzare le posizioni dalla parte anteriore quando necessario per generare materiale (tenere traccia di quanti ne hai già utilizzati). Nessuna collisione
RemcoGerlich,

2
@RabeezRiaz Non è necessario mescolare l'intero elenco, se hai solo bisogno di un piccolo numero di valori casuali, mescola semplicemente la parte che ti serve (come in, prendi un valore casuale dall'elenco di 1..400, rimuovilo e ripeti fino a hai abbastanza elementi). In effetti è così che funziona un algoritmo di shuffle.
Dorus,

3

Se vuoi sempre evitare di giocare una nuova entità in una posizione che è già stata allocata a qualcos'altro, puoi cambiare leggermente il processo. Ciò garantirebbe posizioni uniche, ma richiede un po 'più di sovraccarico. Ecco i passaggi:

  1. Imposta una raccolta di riferimenti a tutte le possibili posizioni sulla mappa (per la mappa 20x20, sarebbero 400 posizioni)
  2. Scegli una posizione a caso da questa raccolta di 400 (rand () funzionerebbe bene per questo)
  3. Rimuovi questa possibilità dalla raccolta di posizioni possibili (quindi ora ha 399 possibilità)
  4. Ripetere l'operazione fino a quando tutte le entità non hanno una posizione specificata

Finché stai rimuovendo la posizione dal set da cui stai scegliendo, non dovrebbe esserci alcuna possibilità che una seconda entità riceva la stessa posizione (a meno che tu non stia selezionando le posizioni da più di un thread alla volta).

Un analogo al mondo reale sarebbe pescare una carta da un mazzo di carte. Attualmente, stai mescolando il mazzo, pescando una carta e segnandola, rimettendo la carta pescata nel mazzo, rimescolando e pescando di nuovo. L'approccio sopra salta rimettendo la carta nel mazzo.


1

Appartenente rand() % nall'essere meno che ideale

Fare rand() % nha una distribuzione non uniforme. Otterrai un numero sproporzionato di determinati valori perché il numero di valori non è un multiplo di 20

Successivamente, in rand()genere è un generatore congruenziale lineare (ce ne sono molti altri , proprio questo è il più probabilmente implementato - e con parametri meno che ideali (ci sono molti modi per selezionare i parametri)). Il problema più grande con questo è che spesso i bit bassi (quelli che ottieni con % 20un'espressione di tipo) non sono così casuali. Ne ricordo uno rand()di anni fa in cui il bit più basso si alternava 1a 0ogni chiamata a rand()- non era molto casuale.

Dalla pagina man rand (3):

Le versioni di rand () e srand () nella libreria C di Linux usano lo stesso
generatore di numeri casuali come random () e srandom (), quindi di ordine inferiore
i bit devono essere casuali come i bit di ordine superiore. Tuttavia, su vecchi
rand () implementazioni e su implementazioni attuali su differenti
sistemi, i bit di ordine inferiore sono molto meno casuali di quelli di livello superiore
bit di ordine. Non utilizzare questa funzione in applicazioni destinate ad essere
portatile quando è necessaria una buona casualità.

Questo potrebbe essere ora relegato nella storia, ma è del tutto possibile che tu abbia ancora un'implementazione rand () scadente nascosta da qualche parte nello stack. Nel qual caso, è ancora abbastanza applicabile.

La cosa da fare è effettivamente utilizzare una buona libreria di numeri casuali (che fornisca buoni numeri casuali) e quindi chiedere numeri casuali all'interno dell'intervallo desiderato.

Un esempio di un buon numero di bit casuale di codice (dalle 13:00 nel video collegato)

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

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

Esegui entrambi questi programmi e confronta la frequenza con cui determinati numeri compaiono (o non compaiono) in quell'output.

Video correlato: rand () considerato dannoso

Alcuni aspetti storici di rand () che causano bug in Nethack che si dovrebbero guardare e considerare nelle proprie implementazioni:

  • Problema di Nethack RNG

    Rand () è una funzione fondamentale per la generazione di numeri casuali di Nethack. Il modo in cui Nethack lo utilizza è difettoso o si potrebbe sostenere che lrand48 () produce numeri pseudo-casuali scadenti. (Tuttavia, lrand48 () è una funzione di libreria che utilizza un metodo PRNG definito e qualsiasi programma che lo utilizza dovrebbe tenere conto dei punti deboli di tale metodo.)

    Il bug è che Nethack si affida (a volte esclusivamente come nel caso di rn (2)) sui bit inferiori dei risultati di lrand48 (). Per questo motivo, l'RNG in tutto il gioco funziona male. Ciò è particolarmente evidente prima che le azioni dell'utente introducano ulteriore casualità, vale a dire nella generazione del personaggio e nella creazione di primo livello.

Sebbene quanto sopra fosse del 2003, dovrebbe essere tenuto presente, poiché potrebbe non essere il caso che tutti i sistemi che eseguono il gioco previsto siano un sistema Linux aggiornato con una buona funzione rand ().

Se lo stai facendo da solo, puoi testare quanto è buono il tuo generatore di numeri casuali scrivendo del codice e testando l'output con ent .


Sulle proprietà di numeri casuali

Esistono altre interpretazioni di "random" che non sono esattamente casuali. In un flusso casuale di dati, è del tutto possibile ottenere lo stesso numero due volte. Se lanci una moneta (casuale), è del tutto possibile ottenere due teste di fila. Oppure lancia due volte un dado e ottieni lo stesso numero due volte di seguito. O girare una ruota della roulette e ottenere lo stesso numero due volte lì.

La distribuzione dei numeri

Durante la riproduzione di un elenco di brani, le persone si aspettano che 'casuale' significhi che lo stesso brano o artista non verrà riprodotto una seconda volta di seguito. La riproduzione di una playlist The Beatles due volte di seguito è considerata "non casuale" (sebbene sia casuale). La percezione che per una playlist di quattro brani suonati per un totale di otto volte:

1 3 2 4 1 2 4 3

è più "casuale" di:

1 3 3 2 1 4 4 2

Altro su questo per il "mescolamento" delle canzoni: come mescolare le canzoni?

Su valori ripetuti

Se non si desidera ripetere i valori, è necessario prendere in considerazione un approccio diverso. Genera tutti i valori possibili e mescolali.

Se stai chiamando rand()(o qualsiasi altro generatore di numeri casuali), lo stai chiamando con la sostituzione. Puoi sempre ottenere lo stesso numero due volte. Un'opzione è quella di lanciare i valori ancora e ancora fino a quando non si seleziona uno che soddisfa i requisiti. Sottolineerò che questo ha un tempo di esecuzione non deterministico ed è possibile che tu possa trovarti in una situazione in cui esiste un ciclo infinito a meno che non inizi a fare una traccia indietro più complessa.

Elenca e scegli

Un'altra opzione è quella di generare un elenco di tutti i possibili stati validi e quindi selezionare un elemento casuale da tale elenco. Trova tutti i punti vuoti (che soddisfano alcune regole) nella stanza, quindi scegline uno casuale da quell'elenco. E poi fallo ancora e ancora finché non hai finito.

rimescolare

L'altro approccio è mescolare come se fosse un mazzo di carte. Inizia con tutti i punti vuoti nella stanza e poi inizia ad assegnarli distribuendo i punti vuoti, uno alla volta, a ciascuna regola / processo che richiede un punto vuoto. Hai finito quando finisci le carte o le cose smettono di chiederle.


3
Next, rand() is typically a linear congruential generatorQuesto non è vero su molte piattaforme ora. Dalla pagina man rand (3) di linux: "Le versioni di rand () e srand () nella libreria C di Linux usano lo stesso generatore di numeri casuali di random (3) e srandom (3), quindi i bit di ordine inferiore dovrebbe essere casuale come i bit di ordine superiore. " Inoltre, come sottolinea @delnan, la qualità del PRNG non è il vero problema qui.
Charles E. Grant,

4
Sto effettuando il downgrade di questo perché non risolve il problema reale.
user253751

@immibis Quindi l'altra risposta non "risolve" il problema reale e dovrebbe essere sottoposta a downgrade. Penso che la domanda non sia "aggiustare il mio codice", è "perché ricevo numeri casuali duplicati?" Alla seconda domanda, credo che la risposta alla domanda.
Neil,

4
Anche con il valore più piccolo di RAND_MAX32767 la differenza è 1638 possibili modi per ottenere alcuni numeri contro 1639 per altri. Sembra improbabile che faccia molta differenza pratica all'OP.
Martin Smith,

@Neil "Correggi il mio codice" non è una domanda.
Razze di leggerezza in orbita,

0

La soluzione più semplice a questo problema è stata citata nelle risposte precedenti: è creare un elenco di valori casuali accanto a ciascuna delle 400 celle e quindi ordinare questo elenco casuale. Il tuo elenco di celle verrà ordinato come elenco casuale e in questo modo verrà mischiato.

Questo metodo ha il vantaggio di evitare totalmente la sovrapposizione di celle selezionate casualmente.

Lo svantaggio è che devi calcolare un valore casuale in un elenco separato per ciascuna delle tue celle. Quindi, preferiresti non farlo mentre il gioco è iniziato.

Ecco un esempio di come puoi farlo:

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

Risultato:

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)

Basta modificare NUMBER_OF_SPAWNS per ottenere celle più o meno casuali, questo non cambierà il tempo di calcolo richiesto per l'attività.


"e poi, per ordinarli tutti" - Credo che tu intenda "shuffle"

Ho completato un po 'la mia spiegazione. Dovrebbe essere più chiaro ora.
KwentRell,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.