rand () знову дає однакові номери для невеликого діапазону


9

Я намагаюся зробити якусь гру, де у мене є сітка 20x20, і я показую гравця (P), ціль (T) та трьох ворогів (X). Усі вони мають координати X і Y, які призначаються за допомогою rand(). Проблема полягає в тому, що якщо я спробую отримати більше балів у грі (поповнення енергії тощо), вони перетинаються з одним або кількома іншими пунктами, оскільки діапазон невеликий (від 1 до 20 включно).

Це мої змінні, і як я присвоюю їм значення: ( COORDa structз просто X і 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;
}

Я хочу додати в гру більше речей, але ймовірність перекриття збільшується, коли я це роблю. Чи можна це виправити?


8
rand () - поганий RNG
щурцовий вирод

3
rand()це шкода RNG, і все одно при такому невеликому діапазоні, вам не доведеться просто очікувати зіткнень, вони майже гарантовані.
Дедуплікатор

1
Хоча це правда, що rand()це паршивий RNG, він, ймовірно, підходить для однієї гри гравця, і якість RNG тут не проблема.
Gort the Robot

13
Якщо говорити про якість, то тут, rand()мабуть, не має значення. Криптографії не задіяно, і будь-яка RNG дуже ймовірно спричинить зіткнення на такій невеликій карті.
Том Корнебіз

2
Те, що ви бачите, відоме як Проблема дня народження. Якщо ваші випадкові числа перетворюються на діапазон, менший за природний діапазон PRNG, то ймовірність отримати два екземпляри одного і того ж числа набагато вища, ніж ви могли подумати. Деякий час тому я написав розмиття на цю тему на Stackoverflow тут.
ЗанепокоєнийOfTunbridgeWells

Відповіді:


40

Незважаючи на те, що користувачі, які скаржаться rand()і рекомендують кращі RNG, мають рацію щодо якості випадкових чисел, вони також не вистачають більшої картини. Дублікатів у потоках випадкових чисел не уникнути, вони є фактом життя. Це урок проблеми з днем народження .

На сітці з 20 * 20 = 400 можливих нерестових позицій слід очікувати повторної точки нересту (50% ймовірності), навіть якщо нерестується лише 24 особи. З 50 об'єктів (все ще лише 12,5% всієї сітки) ймовірність виникнення дубліката перевищує 95%. Ви повинні мати справу зі зіткненнями.

Іноді ви можете намалювати всі зразки одночасно, тоді ви можете використовувати алгоритм перетасовки, щоб намалювати nчітко визначені елементи. Вам просто потрібно сформувати список усіх можливостей. Якщо повний список можливостей занадто великий для зберігання, ви можете генерувати нерестові позиції по черзі, як це робите зараз (лише з кращою RNG) та просто знову генерувати, коли відбувається зіткнення. Незважаючи на те, що можливі певні зіткнення, багато зіткнень поспіль є експоненціально малоймовірними, навіть якщо більша частина сітки заповнена.


Я думав про відродження в разі зіткнення, але якщо у мене буде більше предметів, як я маю намір мати, тоді пошук зіткнення ускладниться. Я також повинен був би відредагувати чеки в разі додавання або видалення точки з гри. Я досить недосвідчений, тож якщо є вирішення цього питання, я не міг цього побачити.
Rabeez Riaz

7
Якщо у вас є шашка 20х20, на відміну від безперервної (реальної) площини XY 20x20, то у вас є таблиця пошуку 400 комірок для перевірки на зіткнення. Це ТРІВАЛЬНО.
Джон Р. Стром

@RabeezRiaz Якщо у вас є більша карта, ви матимете структуру даних на основі сітки (сітка, що складається з деякої області комірок, і кожен елемент всередині цієї комірки зберігається у списку). Якщо ваша карта ще більша, ви будете реалізовувати rect-дерево.
rwong

2
@RabeezRiaz: якщо пошук занадто складний, скористайтеся його першою пропозицією: створіть список усіх 400 можливих початкових місць, перетасуйте їх так, щоб вони знаходилися у випадковому порядку (пошук алгоритму), а потім почніть використовувати місця з фронту, коли вам потрібно створювати речі (слідкуйте за тим, скільки ви вже використовували). Жодних зіткнень.
RemcoGerlich

2
@RabeezRiaz Не потрібно переміщувати весь список, якщо вам потрібна лише невелика кількість випадкових значень, просто перетасуйте потрібну частину (як у, візьміть випадкове значення зі списку 1..400, вийміть його та повторіть, доки у вас достатньо елементів). Насправді так працює алгоритм перетасування.
Дорус

3

Якщо ви завжди хочете уникати відтворення нового об'єкта в місці, яке вже було виділено для чогось іншого, ви можете трохи змінити процес. Це гарантувало б унікальні місця, але це вимагає трохи більше витрат. Ось такі кроки:

  1. Налаштуйте набір посилань на всі можливі місця на карті (для карти 20x20 це буде 400 місць)
  2. Виберіть місце з цієї колекції з 400 випадково (місце для цього буде rand)
  3. Видаліть цю можливість з колекції можливих локацій (тому вона наразі має 399 можливостей)
  4. Повторіть, поки всі об'єкти не мають вказаного місця

Поки ви видаляєте місце з набору, з якого ви вибираєте, не повинно бути жодного шансу, щоб другий суб'єкт отримав те саме місце розташування (якщо ви не обираєте місця з декількох потоків одночасно).

Реальним аналогом цього світу було б малювання картки з колоди карт. Наразі ви перетасуєте колоду, витягуєте карту і розмічаєте її, кладете намальовану карту назад в колоду, повторно перетасуєте та малюєте знову. Вищеописаний підхід пропускає, повертаючи картку назад в колоду.


1

Що стосується того, щоб rand() % nбути менш ідеальним

Дія rand() % nмає нерівномірний розподіл. Ви отримаєте непропорційну кількість певних значень, оскільки кількість значень не кратна 20

Далі, rand()це типовий генератор лінійного конгруенціїбагато інших , саме це, швидше за все, реалізований - і з меншими за ідеальні параметри (існує безліч способів вибору параметрів)). Найбільша проблема з цим полягає в тому, що часто низькі біти в ньому (ті, які ви отримуєте з % 20виразом типу), не такі випадкові. Я згадую один rand()з років тому , коли молодший біт чергувалися з 1до 0кожним викликом rand()- це було не дуже випадково.

На сторінці чоловіка rand (3):

Версії rand () та srand () у бібліотеці Linux C використовують одне і те ж
генератор випадкових чисел як випадкових (), так і srandom (), тому нижнього порядку
біти повинні бути такими ж випадковими, як і біти вищого порядку. Однак на старших
реалізацій rand () та поточних реалізацій для різних
Системи, біти нижчого порядку набагато менш випадкові, ніж вищі
замовити біти. Не використовуйте цю функцію в програмах, призначених для використання
портативний, коли потрібна хороша випадковість.

Це може бути віднесено до історії, але цілком можливо, що у вас все ще є погана реалізація rand (), яка ховається десь у стеку. У цьому випадку його все ще досить застосовно.

Що потрібно зробити, це насправді використовувати гарну бібліотеку випадкових чисел (яка дає хороші випадкові числа), а потім запитати випадкові числа в межах потрібного діапазону.

Приклад хорошого коду випадкового числа (з 13:00 у зв'язаному відео)

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

Порівняйте це з:

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

Запустіть обидві ці програми та порівняйте, як часто певні числа з’являються (або не з’являються) у цьому висновку.

Пов’язане відео: rand () вважається шкідливим

Деякі історичні аспекти rand (), що спричиняють помилки в Nethack, які слід спостерігати та враховувати у власних реалізаціях:

  • Проблема Nethack RNG

    Rand () - дуже фундаментальна функція для генерації випадкових чисел Nethack. Те, яким чином Nethack користується ним, є баггі або може стверджувати, що lrand48 () видає хитрі псевдовипадкові числа. (Однак, lrand48 () - це функція бібліотеки, що використовує визначений метод PRNG, і будь-яка програма, яка використовує її, повинна враховувати слабкі сторони цього методу.)

    Помилка в тому, що Nethack покладається (іноді виключно, як це відбувається у rn (2)) на нижніх бітах результатів lrand48 (). Через це RNG у всій грі працює погано. Це особливо помітно перед тим, як дії користувача вводять подальшу випадковість, тобто у генеруванні персонажів та створенні першого рівня.

Хоча вищезгадане було з 2003 року, все ж слід пам’ятати, оскільки це може бути не так, що всі системи, в яких запущена ваша гра, будуть оновленою системою Linux з хорошою функцією rand ().

Якщо ви це робите для себе, ви можете перевірити, наскільки хороший ваш генератор випадкових чисел, написавши якийсь код і протестувавши вихід з ent .


Про властивості випадкових чисел

Є й інші трактування «випадкових», які не зовсім випадкові. У випадковому потоці даних цілком можливо отримати одне і те ж число двічі. Якщо перевернути монету (випадково), цілком можливо отримати дві голови підряд. Або киньте кістки двічі і отримайте однакове число двічі поспіль. Або крутиться колесо рулетки і отримуєш ту саму кількість двічі.

Розподіл чисел

Під час відтворення списку пісень люди очікують, що "випадковий" означає, що та сама пісня чи виконавець не будуть відтворюватися другий раз поспіль. Проведення відтворення у "Бітлз" двох разів поспіль вважається "не випадковим" (хоча воно є випадковим). Сприйняття, що за список відтворень із чотирьох пісень виконували загалом вісім разів:

1 3 2 4 1 2 4 3

є більш "випадковим", ніж:

1 3 3 2 1 4 4 2

Детальніше про це для "перетасування" пісень: Як перетасувати пісні?

На повторних значеннях

Якщо ви не хочете повторювати значення, слід розглянути інший підхід. Створіть усі можливі значення та перемістіть їх.

Якщо ви телефонуєте rand()(або будь-який інший генератор випадкових чисел), ви викликаєте його із заміною. Ви завжди можете отримати одне і те ж число двічі. Одним із варіантів є викидання значень знову і знову, поки ви не вибрали той, який відповідає вашим вимогам. Я зазначу, що це має недетермінований час виконання, і цілком можливо, що ви могли б опинитися в ситуації, коли існує нескінченний цикл, якщо ви не почнете робити більш складний пошук назад.

Список та вибір

Інший варіант - сформувати список усіх можливих дійсних станів, а потім вибрати випадковий елемент із цього списку. Знайдіть у кімнаті всі порожні плями (які відповідають деяким правилам), а потім виберіть випадковий із цього списку. А потім робіть це знову і знову, поки не закінчите.

Перемішати

Інший підхід - перетасувати, ніби це колоди карт. Почніть з усіх порожніх плям у кімнаті, а потім почніть призначати їх, роздаючи порожні плями, по одному, кожному правилу / процесу, запитуючи про порожнє місце. Ви закінчили, коли у вас закінчилися картки або речі перестали їх просити.


3
Next, rand() is typically a linear congruential generatorЗараз це не так на багатьох платформах. Зі man сторінки сторінки rand (3) linux: "Версії rand () і srand () в бібліотеці Linux C використовують той же генератор випадкових чисел, що і випадкові (3) і srandom (3), тому біти нижчого порядку повинні бути такими ж випадковими, як і біти вищого порядку ". Також, як зазначає @delnan, якість PRNG не є справжньою проблемою.
Чарльз Е. Грант

4
Я спростовую це, оскільки це не вирішує фактичну проблему.
користувач253751

@immibis Тоді жодна інша відповідь не "вирішує" дійсну проблему, і її слід не допускати. Я думаю, що питання не є "виправити свій код", це "чому я отримую дублікати випадкових чисел?" На друге питання, я вважаю, на це питання відповіли.
Ніл

4
Навіть при найменшому значенні RAND_MAX32767 різниця становить 1638 можливих способів отримання одних чисел проти 1639 для інших. Мабуть, навряд чи матиме багато практичних змін для ОП.
Мартін Сміт

@Neil "Виправити код" - це не питання.
Гонки легкості по орбіті

0

Найпростіший варіант вирішення цієї проблеми був цитований у попередніх відповідях: це скласти список випадкових значень поряд з кожною з ваших 400 комірок, а потім сортувати цей випадковий список. Ваш список комірок буде відсортований як випадковий список, і таким чином буде змінено.

Цей спосіб має перевагу в тому, щоб повністю уникати перекриття випадково вибраних клітин.

Недоліком є ​​те, що вам потрібно обчислити випадкову величину в окремому списку для кожної з ваших комірок. Отже, ви б краще не робили цього, поки гра почалася.

Ось приклад того, як це можна зробити:

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

Результат:

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)

Просто змініть NUMBER_OF_SPAWNS, щоб отримати більш-менш випадкові комірки, це не змінить час обчислення, необхідний для виконання завдання.


"і потім, щоб сортувати їх усіх" - я вважаю, ви маєте на увазі "перетасувати"

Я трохи доповнив своє пояснення. Зараз має бути зрозуміліше.
KwentRell
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.