Skip to content

Commit d702f30

Browse files
authored
Preserve shared RNG state when copying Perlin and Simplex noise (#195)
Preserve shared random streams when copying ordinary noise
1 parent 51e4832 commit d702f30

3 files changed

Lines changed: 30 additions & 2 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version
88

99
### Fixed
1010

11+
- Copying or pickling Perlin and Simplex noise no longer advances a shared random generator while preparing an unused wavelet tile.
1112
- `tcod.image.Image.rotate90` updates the width and height of rectangular images, keeping NumPy array conversions consistent with the rotated pixels.
1213
- Adding a root to an already resolved `tcod.path.Pathfinder` resets its predecessor only when its value improves the current distance. This prevents cyclic paths while preserving routes to cheaper roots.
1314
- Lazy pathfinder queries reconsider previously visited goals after adding a root or lowering a root's distance.

‎tcod/noise.py‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,9 +357,18 @@ def __getstate__(self) -> dict[str, Any]:
357357
if self.dimensions < 4 and self.noise_c.waveletTileData == ffi.NULL: # noqa: PLR2004
358358
# Trigger a side effect of wavelet, so that copies will be synced.
359359
saved_algo = self.algorithm
360+
saved_random = self.noise_c.rand
361+
wavelet_random = saved_random
362+
if saved_algo != tcod.constants.NOISE_WAVELET:
363+
# Preparing an unused algorithm must not advance a shared RNG.
364+
wavelet_random = ffi.gc(lib.TCOD_random_save(saved_random), lib.TCOD_random_delete)
365+
self.noise_c.rand = wavelet_random
360366
self.algorithm = tcod.constants.NOISE_WAVELET
361-
self.get_point()
362-
self.algorithm = saved_algo
367+
try:
368+
self.get_point()
369+
finally:
370+
self.algorithm = saved_algo
371+
self.noise_c.rand = saved_random
363372

364373
waveletTileData = None # noqa: N806
365374
if self.noise_c.waveletTileData != ffi.NULL:

‎tests/test_noise.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import copy
44
import pickle
5+
from collections.abc import Callable
56

67
import numpy as np
78
import pytest
@@ -103,3 +104,20 @@ def test_noise_copy() -> None:
103104

104105
def test_noise_grid() -> None:
105106
tcod.noise.grid((2, 2), scale=2) # Check int scale
107+
108+
109+
@pytest.mark.parametrize("algorithm", [tcod.noise.Algorithm.PERLIN, tcod.noise.Algorithm.SIMPLEX])
110+
@pytest.mark.parametrize("rng_algorithm", [tcod.random.MERSENNE_TWISTER, tcod.random.MULTIPLY_WITH_CARRY])
111+
@pytest.mark.parametrize("snapshot", [copy.copy, copy.deepcopy, lambda n: pickle.loads(pickle.dumps(n))])
112+
def test_noise_snapshot_preserves_shared_random(
113+
algorithm: int, rng_algorithm: int, snapshot: Callable[[tcod.noise.Noise], tcod.noise.Noise]
114+
) -> None:
115+
rng = tcod.random.Random(rng_algorithm, seed=42)
116+
noise = tcod.noise.Noise(2, algorithm=algorithm, seed=rng)
117+
expected = copy.deepcopy(rng)
118+
noise_copy = snapshot(noise)
119+
assert [rng.randint(0, 100) for _ in range(8)] == [expected.randint(0, 100) for _ in range(8)]
120+
assert noise.algorithm == algorithm
121+
# The lazy wavelet tile remains synchronized if this algorithm is selected later.
122+
noise.algorithm = noise_copy.algorithm = tcod.noise.Algorithm.WAVELET
123+
np.testing.assert_array_equal(noise.sample_ogrid(np.ogrid[:3, :4]), noise_copy.sample_ogrid(np.ogrid[:3, :4]))

0 commit comments

Comments
 (0)