rand () memberikan angka yang sama lagi untuk rentang kecil

9

Saya mencoba membuat semacam permainan di mana saya memiliki kotak 20x20 dan saya menampilkan pemain (P), target (T) dan tiga musuh (X). Semua ini memiliki koordinat X dan Y yang ditugaskan menggunakan rand(). Masalahnya adalah jika saya mencoba untuk mendapatkan lebih banyak poin dalam permainan (isi ulang untuk energi dll) mereka tumpang tindih dengan satu atau lebih poin lainnya karena kisarannya kecil (1 hingga 20 inklusif).

Ini adalah variabel saya dan bagaimana saya menetapkan nilai-nilai kepada mereka: ( COORDadalah structdengan hanya X dan 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;
}

Saya ingin menambahkan lebih banyak hal ke permainan tetapi kemungkinan tumpang tindih meningkat ketika saya melakukan itu. Apakah ada cara untuk memperbaikinya?

Rabeez Riaz
sumber
8
rand () adalah RNG yang buruk
orang aneh ratchet
3
rand()adalah RNG yang sangat disayangkan, dan lagi pula dengan kisaran sekecil itu, Anda tidak hanya harus mengharapkan tabrakan, mereka hampir dijamin.
Deduplicator
1
Meskipun memang benar itu rand()adalah RNG yang buruk, mungkin cocok untuk permainan pemain tunggal, dan kualitas RNG bukan masalah di sini.
Gort the Robot
13
Berbicara tentang kualitas rand()sepertinya tidak relevan di sini. Tidak ada kriptografi yang terlibat, dan RNG mana pun akan sangat mungkin memberikan tabrakan dalam peta sekecil itu.
Tom Cornebize
2
Apa yang Anda lihat dikenal sebagai Masalah Ulang Tahun. Jika angka acak Anda sedang dikonversi ke rentang yang lebih kecil dari kisaran alami PRNG maka kemungkinan mendapatkan dua contoh dari angka yang sama jauh lebih tinggi daripada yang Anda kira. Beberapa waktu lalu saya menulis uraian tentang hal ini di Stackoverflow di sini.
ConcernedOfTunbridgeWells

Jawaban:

40

Sementara pengguna yang mengeluhkan rand()dan merekomendasikan RNG yang lebih baik benar tentang kualitas angka acak, mereka juga kehilangan gambaran yang lebih besar. Duplikat dalam aliran angka acak tidak dapat dihindari, mereka adalah fakta kehidupan. Ini adalah pelajaran dari masalah ulang tahun .

Pada kisi 20 * 20 = 400 kemungkinan posisi spawn, duplikasi titik spawn diharapkan (probabilitas 50%) bahkan ketika pemijahan hanya 24 entitas. Dengan 50 entitas (masih hanya 12,5% dari seluruh grid), probabilitas duplikat lebih dari 95%. Anda harus berurusan dengan tabrakan.

Terkadang Anda bisa menggambar semua sampel sekaligus, lalu Anda bisa menggunakan algoritma acak untuk menggambar nitem yang dijamin berbeda. Anda hanya perlu membuat daftar semua kemungkinan. Jika daftar lengkap kemungkinan terlalu besar untuk disimpan, Anda dapat menghasilkan posisi spawn satu per satu seperti yang Anda lakukan sekarang (hanya dengan RNG yang lebih baik) dan hanya menghasilkan ulang ketika terjadi tabrakan. Meskipun memiliki beberapa tabrakan mungkin terjadi, banyak tabrakan berturut-turut secara eksponensial tidak mungkin bahkan jika sebagian besar grid dihuni.


sumber
Saya berpikir tentang respawn jika terjadi tabrakan tetapi jika saya memiliki lebih banyak item, seperti yang saya inginkan, maka pencarian tabrakan akan menjadi rumit. Saya juga harus mengedit cek jika ada poin yang ditambahkan atau dihapus dari permainan. Saya cukup berpengalaman sehingga jika ada solusi untuk ini, saya tidak bisa melihatnya.
Rabeez Riaz
7
Jika Anda memiliki kotak-kotak 20x20, yang bertentangan dengan pesawat XY 20x20 kontinu (nyata), maka yang Anda miliki adalah tabel pencarian 400-sel untuk memeriksa tabrakan. Ini TRIVIAL.
John R. Strohm
@RabeezRiaz Jika Anda memiliki peta yang lebih besar, Anda akan memiliki beberapa struktur data berbasis kisi (kisi yang terdiri dari beberapa area sel, dan setiap item di dalam sel itu disimpan dalam daftar). Jika peta Anda lebih besar, Anda akan menerapkan pohon segi empat.
rwong
2
@RabeezRiaz: jika pencarian terlalu rumit, gunakan saran pertamanya: buat daftar semua 400 lokasi awal yang mungkin, kocok mereka sehingga mereka berada dalam urutan acak (mencari algoritma), dan kemudian mulai menggunakan lokasi dari depan ketika Anda membutuhkan untuk menghasilkan barang (melacak berapa banyak yang sudah Anda gunakan). Tidak ada tabrakan.
RemcoGerlich
2
@RabeezRiaz Tidak perlu mengacak seluruh daftar, jika Anda hanya membutuhkan sejumlah kecil nilai acak, cukup kocok bagian yang Anda butuhkan (seperti, ambil nilai acak dari daftar 1..400, hapus, dan ulangi sampai Anda memiliki elemen yang cukup). Sebenarnya begitulah algoritma pengocokan bekerja.
Dorus
3

Jika Anda selalu ingin menghindari memainkan entitas baru di lokasi yang telah dialokasikan untuk sesuatu yang lain, Anda dapat sedikit mengubah proses Anda. Ini akan menjamin lokasi yang unik, tetapi membutuhkan overhead yang lebih sedikit. Berikut langkah-langkahnya:

  1. Siapkan koleksi referensi ke semua lokasi yang mungkin di peta (untuk peta 20x20, ini akan menjadi 400 lokasi)
  2. Pilih lokasi secara acak dari koleksi 400 ini (rand () akan bekerja dengan baik untuk ini)
  3. Hapus kemungkinan ini dari kemungkinan pengumpulan lokasi (jadi sekarang ada 399 kemungkinan)
  4. Ulangi sampai semua entitas memiliki lokasi yang ditentukan

Selama Anda menghapus lokasi dari set yang Anda pilih, seharusnya tidak ada kemungkinan entitas kedua menerima lokasi yang sama (kecuali Anda memilih lokasi dari lebih dari satu utas sekaligus).

Analog dunia nyata dengan ini akan menggambar kartu dari setumpuk kartu. Saat ini, Anda menyeret geladak, menggambar kartu dan menandainya, meletakkan kartu yang ditarik kembali ke geladak, menyeret kembali dan menggambar lagi. Pendekatan di atas tidak memasukkan kartu kembali ke dalam geladak.

Berbohong
sumber
1

Berkaitan dengan rand() % nkurang ideal

Melakukan rand() % nmemiliki distribusi yang tidak seragam. Anda akan mendapatkan jumlah nilai tertentu yang tidak proporsional karena jumlah nilai bukan kelipatan 20

Berikutnya, rand()biasanya merupakan generator kongruensial linier (ada banyak yang lain , hanya ini yang paling mungkin diterapkan - dan dengan parameter yang kurang ideal (ada banyak cara untuk memilih parameter)). Masalah terbesar dengan ini adalah seringnya bit rendah di dalamnya (yang Anda dapatkan dengan % 20ekspresi tipe) tidak acak. Saya ingat satu rand()dari tahun lalu di mana bit terendah berganti dari 1ke 0dengan setiap panggilan ke rand()- itu tidak terlalu acak.

Dari halaman manual rand (3):

Versi rand () dan srand () di Linux C Library menggunakan hal yang sama
generator angka acak sebagai acak () dan acak (), jadi urutan bawah
bit harus acak seperti bit orde tinggi. Namun, pada yang lebih tua
rand () implementasi, dan implementasi saat ini berbeda
sistem, bit orde rendah jauh lebih sedikit acak daripada yang lebih tinggi-
memesan bit. Jangan gunakan fungsi ini dalam aplikasi yang dimaksudkan
portabel saat keacakan dibutuhkan.

Ini mungkin sekarang diturunkan ke sejarah, tetapi sangat mungkin bahwa Anda masih memiliki implementasi rand () yang buruk yang tersembunyi di suatu tempat di stack. Dalam hal ini, masih cukup berlaku.

Yang harus dilakukan adalah benar-benar menggunakan pustaka angka acak yang baik (yang memberikan angka acak yang baik) dan kemudian meminta angka acak dalam rentang yang Anda inginkan.

Contoh bit kode acak yang bagus (mulai pukul 13:00 di video yang ditautkan)

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

Bandingkan ini dengan:

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

Jalankan kedua program ini dan bandingkan seberapa sering angka-angka tertentu muncul (atau tidak muncul) dalam output itu.

Video terkait: rand () dianggap berbahaya

Beberapa aspek historis rand () yang menyebabkan bug di Nethack yang harus diperhatikan dan dipertimbangkan dalam implementasi sendiri:

  • Masalah Nethack RNG

    Rand () adalah fungsi yang sangat mendasar untuk pembuatan angka acak Nethack. Cara Nethack menggunakannya adalah buggy atau dapat dikatakan bahwa lrand48 () menghasilkan angka pseudo-acak yang jelek. (Namun, lrand48 () adalah fungsi pustaka yang menggunakan metode PRNG yang ditentukan dan program apa pun yang menggunakannya harus memperhitungkan kelemahan metode itu.)

    Bugnya adalah bahwa Nethack mengandalkan (kadang-kadang secara eksklusif seperti halnya dalam rn (2)) pada bit yang lebih rendah dari hasil dari lrand48 (). Karena itu, RNG di seluruh permainan bekerja buruk. Ini terutama terlihat sebelum tindakan pengguna memperkenalkan keacakan lebih lanjut, yaitu dalam pembuatan karakter dan pembuatan tingkat pertama.

Walaupun hal di atas berasal dari tahun 2003, hal itu harus tetap diingat karena mungkin tidak semua sistem yang menjalankan game yang Anda tuju akan menjadi sistem Linux terkini dengan fungsi rand () yang bagus.

Jika Anda hanya melakukan ini untuk diri sendiri, Anda dapat menguji seberapa baik generator nomor acak Anda dengan menulis beberapa kode dan menguji output dengan ent .


Pada sifat-sifat angka acak

Ada interpretasi lain 'acak' yang tidak sepenuhnya acak. Dalam aliran data acak, sangat mungkin untuk mendapatkan nomor yang sama dua kali. Jika Anda melempar koin (acak), sangat mungkin untuk mendapatkan dua kepala berturut-turut. Atau melempar dadu dua kali dan mendapatkan nomor yang sama dua kali berturut-turut. Atau memutar roda roulette dan mendapatkan nomor yang sama dua kali di sana.

Distribusi angka

Saat memainkan daftar lagu, orang berharap 'acak' berarti bahwa lagu atau artis yang sama tidak akan diputar untuk kedua kalinya berturut-turut. Memiliki daftar putar bermain The Beatles dua kali berturut-turut dianggap 'tidak acak' (meskipun adalah acak). Persepsi bahwa untuk daftar putar empat lagu diputar total delapan kali:

1 3 2 4 1 2 4 3

lebih 'acak' dari:

1 3 3 2 1 4 4 2

Lebih lanjut tentang ini untuk 'pengacakan' lagu: Bagaimana cara mengacak lagu?

Pada nilai yang diulang

Jika Anda tidak ingin mengulangi nilai, ada pendekatan berbeda yang harus dipertimbangkan. Hasilkan semua nilai yang mungkin, dan kocok.

Jika Anda menelepon rand()(atau generator nomor acak lainnya), Anda memanggilnya dengan pengganti. Anda selalu bisa mendapatkan nomor yang sama dua kali. Satu opsi adalah membuang nilai berulang-ulang sampai Anda memilih satu yang memenuhi persyaratan Anda. Saya akan menunjukkan bahwa ini memiliki runtime non-determistic dan ada kemungkinan bahwa Anda dapat menemukan diri Anda dalam situasi di mana ada infinite loop kecuali Anda mulai melakukan penelusuran balik yang lebih kompleks.

Daftar dan Pilih

Pilihan lain adalah membuat daftar semua status yang mungkin valid dan kemudian memilih elemen acak dari daftar itu. Temukan semua tempat kosong (yang memenuhi beberapa aturan) di ruangan dan kemudian pilih satu yang acak dari daftar itu. Dan kemudian lakukan lagi dan lagi sampai selesai.

Acak

Pendekatan lain adalah mengocok seolah-olah setumpuk kartu. Mulailah dengan semua tempat kosong di ruangan itu dan kemudian mulai menugaskan mereka dengan membagikan tempat kosong, satu per satu, untuk setiap aturan / proses meminta tempat kosong. Anda selesai ketika Anda kehabisan kartu atau hal-hal berhenti meminta mereka.

Komunitas
sumber
3
Next, rand() is typically a linear congruential generatorIni tidak benar pada banyak platform sekarang. Dari halaman manual rand (3) dari linux: "Versi rand () dan srand () di Linux C Library menggunakan generator nomor acak yang sama seperti acak (3) dan srandom (3), sehingga bit urutan rendah harus acak seperti bit orde tinggi. " Juga, seperti yang ditunjukkan @delnan, kualitas PRNG bukanlah masalah sebenarnya di sini.
Charles E. Grant
4
Saya downvoting ini karena tidak menyelesaikan masalah yang sebenarnya.
user253751
@immibis Maka tidak ada jawaban lain "memecahkan" masalah yang sebenarnya dan harus diturunkan. Saya pikir pertanyaannya bukan "memperbaiki kode saya", itu "mengapa saya mendapatkan nomor acak rangkap?" Untuk pertanyaan kedua, saya yakin pertanyaan itu dijawab.
Neil
4
Bahkan dengan nilai terkecil RAND_MAX32767 perbedaannya adalah 1638 cara yang mungkin untuk mendapatkan beberapa angka vs 1639 untuk orang lain. Tampaknya tidak membuat banyak perbedaan praktis untuk OP.
Martin Smith
@Neil "Perbaiki kode saya" bukan pertanyaan.
Lightness Races in Orbit
0

Solusi paling sederhana untuk masalah ini telah dikutip dalam jawaban sebelumnya: itu adalah membuat daftar nilai acak bersama setiap 400 sel Anda, dan kemudian, untuk mengurutkan daftar acak ini. Daftar sel Anda akan diurutkan sebagai daftar acak, dan dengan cara ini, akan diacak.

Metode ini memiliki keuntungan untuk benar-benar menghindari tumpang tindih sel yang dipilih secara acak.

Kerugiannya adalah Anda harus menghitung nilai acak pada daftar terpisah untuk masing - masing sel Anda. Jadi, Anda lebih suka tidak melakukannya saat gim telah dimulai.

Berikut ini adalah contoh bagaimana Anda dapat melakukannya:

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

Hasil:

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)

Ubah saja NUMBER_OF_SPAWNS untuk mendapatkan lebih atau kurang sel acak, ini tidak akan mengubah waktu perhitungan yang diperlukan untuk tugas tersebut.

KwentRell
sumber
"lalu, untuk memilah semuanya" - Saya yakin maksud Anda "shuffle"
Saya telah menyelesaikan penjelasan saya sedikit. Seharusnya lebih jelas sekarang.
KwentRell