Both early-outs return without touching the generator: p <= 0 false, p >= 1 true, otherwise exactly one word compared with a strict <. The zero-cost cases decide stream alignment wherever a caller's probability climbs -- the spy counter-mission adds 0.2f per failed turn and stops drawing entirely from the fifth. A model without the early-outs drifts one word from there on, for ever. NaN takes neither early-out in the original, so it draws and returns false; reproduced rather than smoothed over.
292 lines
13 KiB
C++
292 lines
13 KiB
C++
// mars::rng tests — reference vectors + state (de)serialization.
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <limits>
|
|
|
|
#include "mars/rng/mt19937.h"
|
|
|
|
using mars::rng::MT19937;
|
|
|
|
static int fails = 0;
|
|
#define CHECK(cond) \
|
|
do { \
|
|
if (!(cond)) { \
|
|
std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
|
|
++fails; \
|
|
} \
|
|
} while (0)
|
|
|
|
int main() {
|
|
// --- standard MT19937 outputs for init_genrand(5489) ------------------------
|
|
{
|
|
MT19937 r(5489u);
|
|
static const uint32_t expect[10] = {3499211612u, 581869302u, 3890346734u, 3586334585u, 545404204u,
|
|
4161255391u, 3922919429u, 949333985u, 2715962298u, 1323567403u};
|
|
for (uint32_t e : expect) CHECK(r.next_u32() == e);
|
|
// the 10000th output of the 5489 stream (well-known check value)
|
|
MT19937 r2(5489u);
|
|
uint32_t v = 0;
|
|
for (int i = 0; i < 10000; ++i) v = r2.next_u32();
|
|
CHECK(v == 4123659995u);
|
|
}
|
|
// --- a fresh generator has twisted once: left == N ------------------------
|
|
{
|
|
MT19937 r(1u);
|
|
CHECK(r.left() == MT19937::N);
|
|
CHECK(r.index() == 0);
|
|
r.next_u32();
|
|
CHECK(r.left() == MT19937::N - 1);
|
|
for (int i = 1; i < MT19937::N; ++i) r.next_u32();
|
|
CHECK(r.left() == 0); // block exhausted; the next draw twists
|
|
r.next_u32();
|
|
CHECK(r.left() == MT19937::N - 1);
|
|
}
|
|
// --- float mapping: y / (2^32 - 1), narrowed to float -----------------------
|
|
// The divisor is 2^32 - 1, so 0xffffffff maps to exactly 1.0 and the range is
|
|
// closed at both ends. The unit value is formed in double and the consumer
|
|
// narrows it to float32.
|
|
{
|
|
CHECK(MT19937::kUnitScale == 1.0 / 4294967295.0);
|
|
CHECK(MT19937::kUnitScale != 1.0 / 4294967296.0);
|
|
CHECK(MT19937::unit_from(0u) == 0.0);
|
|
CHECK(MT19937::unit_from(0xffffffffu) == 1.0);
|
|
CHECK(MT19937::float_from(0xffffffffu) == 1.0f);
|
|
CHECK(MT19937::unit_from(1u) == 1.0 / 4294967295.0);
|
|
// A word with the high bit set exercises the unsigned fix-up path.
|
|
CHECK(MT19937::unit_from(0x80000000u) == 2147483648.0 / 4294967295.0);
|
|
|
|
MT19937 r(5489u);
|
|
float f = r.next_float();
|
|
float expect = static_cast<float>(3499211612.0 / 4294967295.0);
|
|
CHECK(f == expect);
|
|
CHECK(f >= 0.f && f <= 1.f);
|
|
|
|
// The 2^-32 mapping is wrong but only barely: the two differ by 2^-32 relative,
|
|
// which is far below a float32 ulp, so they disagree for roughly one word in a
|
|
// hundred. Pin that the difference is real but rare, so nobody reads a passing
|
|
// behavioural compare as proof that either divisor would do.
|
|
int differ = 0;
|
|
MT19937 q(17u);
|
|
for (int i = 0; i < 20000; ++i) {
|
|
const uint32_t y = q.next_u32();
|
|
if (static_cast<float>(static_cast<double>(y) * (1.0 / 4294967296.0)) !=
|
|
MT19937::float_from(y))
|
|
++differ;
|
|
}
|
|
CHECK(differ > 0);
|
|
CHECK(differ < 20000 / 10);
|
|
|
|
// next_unit / next_float draw from the same stream position.
|
|
MT19937 a(11u), b(11u);
|
|
CHECK(static_cast<float>(a.next_unit()) == b.next_float());
|
|
}
|
|
// --- the 24-bit-precision variant: same for most words, and never far off ---
|
|
{
|
|
int differ = 0;
|
|
MT19937 r(31u);
|
|
for (int i = 0; i < 20000; ++i) {
|
|
const uint32_t y = r.next_u32();
|
|
const float a = MT19937::float_from(y);
|
|
const float b = MT19937::float_from_pc24(y);
|
|
if (a != b) ++differ;
|
|
const double d = static_cast<double>(a) - static_cast<double>(b);
|
|
CHECK(d < 1e-6 && d > -1e-6);
|
|
}
|
|
// The two agree on the vast majority of words; the point of the check is that
|
|
// the choice of x87 precision mode can only move the last bit.
|
|
CHECK(differ < 20000 / 2);
|
|
}
|
|
// --- next_int_inclusive: mask covers n, the bound is INCLUSIVE --------------
|
|
{
|
|
CHECK(MT19937::cover_mask(0u) == 0u);
|
|
CHECK(MT19937::cover_mask(1u) == 1u);
|
|
CHECK(MT19937::cover_mask(100u) == 127u);
|
|
CHECK(MT19937::cover_mask(255u) == 255u);
|
|
CHECK(MT19937::cover_mask(256u) == 511u);
|
|
|
|
MT19937 r(7u);
|
|
bool saw_top = false;
|
|
for (int i = 0; i < 5000; ++i) {
|
|
uint32_t v = r.next_int_inclusive(10);
|
|
CHECK(v <= 10);
|
|
if (v == 10) saw_top = true;
|
|
}
|
|
CHECK(saw_top); // 10 itself is reachable: the bound is inclusive
|
|
|
|
// mask == n means no rejection ever, so exactly one word is consumed
|
|
MT19937 a(9u), b(9u);
|
|
CHECK(a.next_int_inclusive(255u) == (b.next_u32() & 255u));
|
|
CHECK(a.left() == b.left());
|
|
|
|
// n == 0 still consumes a word and always yields 0
|
|
MT19937 c(13u), d(13u);
|
|
CHECK(c.next_int_inclusive(0u) == 0u);
|
|
d.next_u32();
|
|
CHECK(c.left() == d.left());
|
|
}
|
|
// --- the compare design: a generator seeded from a pre-call snapshot reads the
|
|
// same stream, and ends in the same state after the same number of draws ---------
|
|
{
|
|
MT19937 live(0xc0ffeeu);
|
|
for (int i = 0; i < 1500; ++i) live.next_u32(); // wherever the game happens to be
|
|
|
|
// snapshot "before"
|
|
uint8_t before[MT19937::kStateBytes];
|
|
live.save_state(before);
|
|
|
|
// the original consumes some words...
|
|
float a1 = live.next_float();
|
|
float a2 = live.next_float();
|
|
uint8_t after[MT19937::kStateBytes];
|
|
live.save_state(after);
|
|
|
|
// ...and ours, seeded from the snapshot, must produce the same values and land on
|
|
// the same state. Matching post-states is the evidence that the draw counts agree.
|
|
MT19937 ours(1u);
|
|
CHECK(ours.load_state(before, sizeof before));
|
|
CHECK(ours.next_float() == a1);
|
|
CHECK(ours.next_float() == a2);
|
|
uint8_t ours_after[MT19937::kStateBytes];
|
|
ours.save_state(ours_after);
|
|
CHECK(std::memcmp(after, ours_after, sizeof after) == 0);
|
|
|
|
// one draw too few leaves a different state, so the check has teeth
|
|
MT19937 short_(1u);
|
|
CHECK(short_.load_state(before, sizeof before));
|
|
short_.next_float();
|
|
uint8_t short_after[MT19937::kStateBytes];
|
|
short_.save_state(short_after);
|
|
CHECK(std::memcmp(after, short_after, sizeof after) != 0);
|
|
}
|
|
// --- save_state / load_state round trip, blob layout mt[624] + left --------
|
|
{
|
|
MT19937 r(123456u);
|
|
for (int i = 0; i < 700; ++i) r.next_u32(); // past one twist
|
|
uint8_t blob[MT19937::kStateBytes];
|
|
r.save_state(blob);
|
|
CHECK(MT19937::kStateBytes == 0x9c4);
|
|
uint32_t left_in_blob = uint32_t(blob[2496]) | (uint32_t(blob[2497]) << 8) | (uint32_t(blob[2498]) << 16) |
|
|
(uint32_t(blob[2499]) << 24);
|
|
CHECK(int(left_in_blob) == r.left());
|
|
CHECK(std::memcmp(blob, r.state(), 4) == 0 || true); // first word is mt[0] little-endian
|
|
uint32_t w0 = uint32_t(blob[0]) | (uint32_t(blob[1]) << 8) | (uint32_t(blob[2]) << 16) | (uint32_t(blob[3]) << 24);
|
|
CHECK(w0 == r.state()[0]);
|
|
|
|
MT19937 s(1u);
|
|
CHECK(s.load_state(blob, sizeof blob));
|
|
CHECK(s.left() == r.left());
|
|
for (int i = 0; i < 2000; ++i) CHECK(s.next_u32() == r.next_u32());
|
|
|
|
// truncated / out-of-range blobs are rejected
|
|
CHECK(!s.load_state(blob, sizeof blob - 1));
|
|
uint8_t bad[MT19937::kStateBytes];
|
|
std::memcpy(bad, blob, sizeof bad);
|
|
bad[2496] = 0x71; // left = 625 > N
|
|
bad[2497] = 0x02;
|
|
CHECK(!s.load_state(bad, sizeof bad));
|
|
}
|
|
// --- load_state(mt, left) positions the next word at mt[N - left] -----------
|
|
{
|
|
MT19937 r(42u);
|
|
uint32_t st[MT19937::N];
|
|
std::memcpy(st, r.state(), sizeof st);
|
|
MT19937 s(0u);
|
|
s.load_state(st, 5);
|
|
for (int i = 0; i < MT19937::N - 5; ++i) r.next_u32();
|
|
for (int i = 0; i < 100; ++i) CHECK(s.next_u32() == r.next_u32());
|
|
}
|
|
// --- range helpers: word cost is the point, not just the value --------------
|
|
// The original exposes these as separate entry points and they cost different
|
|
// numbers of words. A ledger that counts "draws" without knowing which entry
|
|
// point was called is short by exactly the difference.
|
|
{
|
|
// float_range: EXACTLY ONE word, and it is the same word next_float() would
|
|
// have taken -- so a caller that swaps one for the other keeps the stream.
|
|
MT19937 a(5489u), b(5489u);
|
|
const uint32_t y0 = b.next_u32();
|
|
const float got = a.float_range(-1.0f, 3.0f);
|
|
CHECK(a.left() == MT19937::N - 1); // one word, not two
|
|
CHECK(got == MT19937::range_from(y0, -1.0f, 3.0f));
|
|
// degenerate span consumes a word all the same
|
|
MT19937 c(7u);
|
|
CHECK(c.float_range(2.5f, 2.5f) == 2.5f);
|
|
CHECK(c.left() == MT19937::N - 1);
|
|
// endpoints: unit is closed at both ends, so both endpoints are reachable
|
|
CHECK(MT19937::range_from(0u, -1.0f, 3.0f) == -1.0f);
|
|
CHECK(MT19937::range_from(0xffffffffu, -1.0f, 3.0f) == 3.0f);
|
|
// the double narrowing is observable: rounding the product to float first is
|
|
// not the same as evaluating the whole expression in double.
|
|
int differs = 0;
|
|
MT19937 d(11u);
|
|
for (int i = 0; i < 20000; ++i) {
|
|
const uint32_t y = d.next_u32();
|
|
const double span = 3.0 - (-1.0);
|
|
const float one_rounding = static_cast<float>(-1.0 + span * MT19937::unit_from(y));
|
|
if (MT19937::range_from(y, -1.0f, 3.0f) != one_rounding) ++differs;
|
|
}
|
|
CHECK(differs > 0); // if this ever reads 0 the two models are not distinguishable
|
|
}
|
|
{
|
|
// int_range_bell: AT LEAST TWO words, and the split is (h/2, h - h/2) in that
|
|
// order. h = 7 -> half = 3, so the draws are inclusive [0,3] then [0,4].
|
|
MT19937 a(5489u), b(5489u);
|
|
const int32_t got = a.int_range_bell(10, 17);
|
|
const uint32_t d0 = b.next_int_inclusive(3u);
|
|
const uint32_t d1 = b.next_int_inclusive(4u);
|
|
CHECK(got == int32_t(10u + d0 + d1));
|
|
CHECK(a.index() == b.index()); // same words consumed, same order
|
|
CHECK(a.index() >= 2); // never fewer than two
|
|
CHECK(got >= 10 && got <= 17);
|
|
// an empty span still costs two words: half = 0 masks to 0, accepted at once.
|
|
MT19937 c(3u);
|
|
CHECK(c.int_range_bell(4, 4) == 4);
|
|
CHECK(c.index() == 2);
|
|
// and it is triangular, not uniform: the middle of [0,10] must beat the ends.
|
|
int hist[11] = {0};
|
|
MT19937 e(2024u);
|
|
for (int i = 0; i < 40000; ++i) ++hist[e.int_range_bell(0, 10)];
|
|
CHECK(hist[5] > hist[0] * 2);
|
|
CHECK(hist[5] > hist[10] * 2);
|
|
}
|
|
{
|
|
// chance(): the only entry point that can cost ZERO words, and the zero
|
|
// cases are the whole point -- a caller whose probability climbs past 1
|
|
// stops perturbing the stream, and a model without the early-outs drifts
|
|
// one word per such call, permanently.
|
|
MT19937 a(5489u);
|
|
CHECK(a.chance(0.0f) == false);
|
|
CHECK(a.chance(-1.0f) == false);
|
|
CHECK(a.chance(1.0f) == true);
|
|
CHECK(a.chance(2.0f) == true);
|
|
CHECK(a.index() == 0); // four calls, not one word spent
|
|
|
|
// in (0,1) it is exactly one word, and the comparison is STRICT `<`
|
|
// against the same float the generator would hand any other consumer.
|
|
MT19937 b(5489u), c(5489u);
|
|
const float u = c.next_float();
|
|
CHECK(b.chance(0.5f) == (u < 0.5f));
|
|
CHECK(b.index() == 1);
|
|
CHECK(b.index() == c.index());
|
|
|
|
// the accumulating case the spy counter-mission actually runs: 0.2f,
|
|
// 0.4f, 0.6f, 0.8f each draw, then p >= 1 costs nothing for ever after.
|
|
MT19937 d(1234u);
|
|
float p = 0.2f;
|
|
for (int i = 0; i < 4; ++i, p += 0.2f) (void)d.chance(p);
|
|
const int spent = d.index();
|
|
CHECK(spent == 4);
|
|
for (int i = 0; i < 10; ++i) CHECK(d.chance(p) == true);
|
|
CHECK(d.index() == spent); // ten more calls, still four words
|
|
|
|
// NaN takes neither early-out in the original -- each is a pair of ordered
|
|
// comparisons a NaN makes false -- so it DRAWS and returns false. The word
|
|
// is reproduced deliberately: hiding it would hide a real divergence.
|
|
MT19937 e(77u);
|
|
const float nan = std::numeric_limits<float>::quiet_NaN();
|
|
CHECK(e.chance(nan) == false);
|
|
CHECK(e.index() == 1);
|
|
}
|
|
std::printf("test_rng: %s\n", fails ? "FAILED" : "ok");
|
|
return fails ? 1 : 0;
|
|
}
|