Skip to content
Permalink
Browse files

Remove crawl.random_element and replace with something more stable

Because lua iteration order, including at the C level, is not stable,
this was leading to seed variation in parts of the dungeon that heavily
use layout code. I've replaced it with a pure lua implementation of
random_choose_weighted (that parallels the C++ version); in principle
one could call into the C++ version, but it wasn't clear that there was
much of a point to that amount of effort. This version is stable because
it uses an array of pairs for the weight table; `ipairs` gives a stable
iteration order. Notationally it's not any more effortful.
  • Loading branch information...
rawlins committed Jan 31, 2019
1 parent a9077f9 commit 18b65154750a31a5866a4b2ecbe71c581ff3657c
@@ -125,11 +125,24 @@ that are sequenced by `;`s.
### 3.2 choosing from lists

When you are randomly choosing an item from a list (e.g. by randomly picking
an index in the list range), be careful about the underlying list order. This
is a case where the RNG behavior can be fine, but the list order itself could
lead to divergence. For example, when choosing from a list of files provided
by the OS, you cannot assume that all OSs and OS versions will give you these
files in a stable order - sort it yourself.
an index in the list range), be careful about both the underlying list order,
and the iteration order.

**Underlying list order**: This is a case where the RNG behavior can be fine,
but the list order itself could lead to divergence. For example, when choosing
from a list of files provided by the OS, you cannot assume that all OSs and OS
versions will give you these files in a stable order - sort it yourself.

**Iteration order**: If you are using some sort of reservoir-sampling style of
random choice algorithm, you will iterate through the container, stopping the
iteration at some point determined by one or more random draws. If the
iteration order is not guaranteed to be defined, this can lead to different
results from run to run. For example, in lua, iteration via `pairs` or the
C equivalents over a table is not guaranteed to have a stable order, and we
have found that in practice it doesn't -- leading to different choices with
the same random number draws. The `ipairs` iterator is safe, but places some
obvious constraints on the structure you are using. Sorting the underlying
container may be a solution as well.

### 3.3 other sources of randomization

@@ -114,10 +114,10 @@ TAGS: overwritable layout no_primary_vault unrand layout_type_open
obl = 5 + crawl.random2(20)
end

local fill = crawl.random_element({
["."] = 10,
["w"] = 5,
["l"] = 1,
local fill = util.random_choose_weighted({
{'.', 10},
{'w', 5},
{'l', 1},
})
octa_room{x1=25, y1=25, x2=gxm-25, y2=gym-25,
oblique=obl, replace='x', inside=fill}
@@ -281,15 +281,15 @@ TAGS: uniq_open_layout

-- pillar types and relative weights
local pillar_fill = {
["x"] = 15,
["b"] = 5,
["v"] = 4,
["m"] = 3,
["w"] = 2,
["l"] = 1,
{'x', 15},
{'b', 5},
{'v', 4},
{'m', 3},
{'w', 2},
{'l', 1}
}
if (you.in_branch("lair")) then
pillar_fill["t"] = 15
pillar_fill[#pillar_fill + 1] = {'t', 15}
end

-- Potential pillar drawing routines
@@ -314,7 +314,7 @@ TAGS: uniq_open_layout
1 + crawl.random2((pfunc == make_diamond) and 3 or 2)
circle_radius = circle_radius + pillar_radius * 2 + num / 2
if circle_radius >= (gym-oblique)/2 then break end
local fill = crawl.random_element(pillar_fill)
local fill = util.random_choose_weighted(pillar_fill)

-- Finally, make the pillars
local make_pillar = function(x, y)
@@ -591,7 +591,8 @@ TAGS: no_rotate no_vmirror no_hmirror
local rooms = {}
local i, j

local wall_type = crawl.random_element { ['x'] = 3, ['c'] = 3, ['v'] = 2 }
local wall_type = util.random_choose_weighted(
{ {'x', 3}, {'c', 3}, {'v', 2} } )
local percent_thickness_2 = crawl.random_range(0, 8)
* crawl.random_range(0, 8)
local percent_thickness_3 = crawl.random_range(0, 7)
@@ -728,14 +729,14 @@ TAGS: no_rotate no_vmirror no_hmirror
local outer_door = '+'
local inner_door = '+'
if thickness == 2 then
outer_door = crawl.random_element { ['+'] = 1, ['.'] = 2 }
inner_door = crawl.random_element { ['+'] = 1, ['.'] = 3 }
outer_door = util.random_choose_weighted { {'+', 1}, {'.', 2} }
inner_door = util.random_choose_weighted { {'+', 1}, {'.', 3} }
elseif thickness == 3 then
outer_door = crawl.random_element { ['+'] = 2, ['.'] = 1 }
inner_door = crawl.random_element { ['+'] = 1, ['.'] = 2 }
outer_door = util.random_choose_weighted { {'+', 2}, {'.', 1} }
inner_door = util.random_choose_weighted { {'+', 1}, {'.', 2} }
elseif thickness == 4 then
outer_door = crawl.random_element { ['+'] = 4, ['.'] = 1 }
inner_door = crawl.random_element { ['+'] = 1, ['.'] = 1 }
outer_door = util.random_choose_weighted { {'+', 4}, {'.', 1} }
inner_door = util.random_choose_weighted { {'+', 1}, {'.', 1} }
end

if outer_door == '.' and crawl.coinflip() then
@@ -1062,8 +1063,8 @@ TAGS: overwritable layout no_primary_vault allow_dup unrand layout_type_rooms

extend_map { width = gxm, height = gym, fill = 'x' }

local rows = crawl.random_element({[5] = 10, [4] = 5, [3] = 1})
local cols = crawl.random_element({[5] = 10, [4] = 5, [3] = 1})
local rows = util.random_choose_weighted({ {5, 10}, {4, 5}, {3, 1} })
local cols = util.random_choose_weighted({ {5, 10}, {4, 5}, {3, 1} })
local xspace = 13 + 2*(5-cols)
local yspace = 11 + 2*(5-rows)
local xsize = 6 + 2*(5-cols)
@@ -1144,9 +1145,9 @@ TAGS: overwritable layout no_primary_vault allow_dup unrand layout_type_rooms
and (you.in_branch("D") or you.in_branch("Lair"))
and crawl.x_chance_in_y(you.absdepth() - 9, 450)
then
local feat = crawl.random_element { ['v'] = 2,
['c'] = 3,
['x'] = 3 }
local feat = util.random_choose_weighted({ {'v', 2},
{'c', 3},
{'x', 3} })
make_inner_room(room, feat)
end
end
@@ -261,10 +261,10 @@ TAGS: no_rotate no_hmirror no_vmirror
local percent_solid = { 0, 10, 40 }
local in_distance = { 6, 5, 3 }

local base_wall_type = crawl.random_element
{ ['x'] = 7,
['c'] = 2,
['v'] = 1 }
local base_wall_type = util.random_choose_weighted(
{ {'x', 7},
{'c', 2},
{'v', 1} })
if you.in_branch("Dis") then
base_wall_type = 'x'
end
@@ -314,12 +314,12 @@ TAGS: no_rotate no_hmirror no_vmirror
if not blocked then
local door_count = crawl.random_range(1, max_doors[current_stage])
local floor = '.'
local wall = crawl.random_element
{ [base_wall_type] = base_wall_frequency,
['x'] = 50,
['c'] = 30,
['v'] = 20,
['b'] = 1 }
local wall = util.random_choose_weighted(
{ {base_wall_type, base_wall_frequency},
{'x', 50},
{'c', 30},
{'v', 20},
{'b', 1} })

if you.in_branch("Dis") then
wall = 'x'
@@ -202,11 +202,11 @@ TAGS: no_rotate no_vmirror no_hmirror uniq_layout_twisted_delve
local arm_thickness = crawl.random_range(1, 2)


local room_func = crawl.random_element(
{ [make_circle] = 3,
[make_diamond] = 3,
[make_square] = 2,
[make_rounded_square] = 1 } )
local room_func = util.random_choose_weighted(
{ {make_circle, 3},
{make_diamond, 3},
{make_square, 2},
{make_rounded_square, 1} } )
local room_radius = crawl.random_range(4, 7)

if (i == 1) then
@@ -434,10 +434,10 @@ TAGS: overwritable layout allow_dup unrand layout_type_narrow_caves
ngb_min = 3
ngb_max = 4
end
local connchance = crawl.random_element
{ [0] = 2, [5] = 1, [20] = 1, [50] = 1, [100] = 1 }
local top = crawl.random_element
{ [1] = 1, [20] = 1, [125] = 1, [500] = 1, [999999] =1 }
local connchance = util.random_choose_weighted
{ {0, 2}, {5, 1}, {20, 1}, {50, 1}, {100, 1} }
local top = util.random_choose_weighted
{ {1, 1}, {20, 1}, {125, 1}, {500, 1}, {999999, 1} }
crawl.dpr("<lightmagenta>delve(" .. ngb_min .. ", " .. ngb_max .. ", " ..
connchance .. ", -1, " .. top .. ")</lightmagenta>")
delve(ngb_min, ngb_max, connchance, -1, top)
@@ -535,13 +535,13 @@ TAGS: no_rotate no_vmirror no_hmirror

-- Delve some smaller paths
for i = 1, crawl.random_range(1, 3) do
ngb_min = crawl.random_element { [1] = 3, [2] = 1 }
ngb_min = util.random_choose_weighted { {1, 3}, {2, 1} }
ngb_max = crawl.random_range(2, 5)
connchance = crawl.random_element
{ [0] = 1, [2] = 2, [5] = 1, [20] = 1 }
connchance = util.random_choose_weighted
{ {0, 1}, {2, 2}, {5, 1}, {20, 1} }
total_count = crawl.random_range(50, 150)
top = crawl.random_element
{ [1] = 1, [20] = 1, [125] = 1, [500] = 1, [999999] = 1 }
top = util.random_choose_weighted
{ {1, 1}, {20, 1}, {125, 1}, {500, 1}, {999999, 1} }

delve(2, 3, 3, 150, 100)
end
@@ -564,8 +564,8 @@ TAGS: no_rotate no_vmirror no_hmirror
for i = 1, crawl.random_range(15, 25) do
local x = crawl.random_range(8, gxm-9)
local y = crawl.random_range(8, gym-9)
local radius = crawl.random_element
{ [1] = 30, [2] = 50, [3] = 15, [4] = 4, [5] = 1 }
local radius = util.random_choose_weighted
{ {1, 30}, {2, 50}, {3, 15}, {4, 4}, {5, 1} }

octa_room { x1 = x - radius, y1 = y - radius,
x2 = x + radius, y2 = y + radius,
@@ -999,15 +999,16 @@ TAGS: no_rotate no_vmirror no_hmirror
end

-- draw the turrets
local turret_center = crawl.random_element {
[geoelf.glyphs.TREE] = geoelf.glyphs.PLANTLIKE_OPTIONS[geoelf.glyphs.TREE],
[geoelf.glyphs.BUSH] = geoelf.glyphs.PLANTLIKE_OPTIONS[geoelf.glyphs.BUSH],
[geoelf.glyphs.PLANT] = geoelf.glyphs.PLANTLIKE_OPTIONS[geoelf.glyphs.PLANT],
[geoelf.glyphs.FUNGUS] = geoelf.glyphs.PLANTLIKE_OPTIONS[geoelf.glyphs.FUNGUS],
[geoelf.glyphs.STATUE] = geoelf.glyphs.FEATURE_OPTIONS[geoelf.glyphs.STATUE] * 5,
[geoelf.glyphs.FOUNTAIN] = geoelf.glyphs.FEATURE_OPTIONS[geoelf.glyphs.FOUNTAIN] * 5,
[INSIDE_GLYPH] = 15,
}
local turret_center_weights = {}
for i,p in ipairs(geoelf.glyphs.PLANTLIKE_OPTIONS) do
turrent_center_weights[#turret_center_weights + 1] = p
end
for i,p in ipairs(geoelf.glyphs.FEATURE_OPTIONS) do
turrent_center_weights[#turret_center_weights + 1] = {p[0], p[1] * 5}
end
turrent_center_weights[#turret_center_weights + 1] = {INSIDE_GLYPH, 15}

local turret_center = util.random_choose_weighted(turret_center_weights)

for i = geoelf.directions.COUNT_STRAIGHT, geoelf.directions.COUNT - 1 do
if (castle.turret[i].is_placed) then
@@ -634,7 +634,7 @@ function loopsInsertRoom (e, center_x, center_y, radius, wall_glyph)
end
end
else
local shape = crawl.random_element { [0] = 3, [1] = 2, [2] = 1 }
local shape = util.random_choose_weighted { {0, 3}, {1, 2}, {2, 1} }
local center = crawl.random2(6)
if you.in_branch("Elf") then
shape = crawl.random2(4)
@@ -173,7 +173,7 @@ TAGS: no_rotate no_vmirror no_hmirror
if you.in_branch("snake") then
connection = '.'
else
connection = crawl.random_element({['.'] = 1, ['+'] = 2})
connection = util.random_choose_weighted({{'.', 1}, {'+', 2}})
end

add_pools { replace = ".",
@@ -656,15 +656,15 @@ flood(flood_x, flood_y, crawl.random_range(6, 16), function(x, y, distance)
local c = mapgrd[x][y]

local replacements = {
['.'] = { ['.'] = 1, W = 2 },
x = { x = 5, w = 5 },
c = { c = 5, w = 3, W = 2, x = 1 },
['+'] = { ['+'] = 10, W = 5 },
['1'] = { ['2'] = 1 }
['.'] = { {'.', 1}, {'W', 2} },
['x'] = { {'x', 5}, {'w', 5} },
['c'] = { {'c', 5}, {'w', 3}, {'W', 2}, {'x', 1} },
['+'] = { {'+', 10}, {'W', 5} },
['1'] = { {'2', 1} }
}

if replacements[c] then
mapgrd[x][y] = crawl.random_element(replacements[c])
mapgrd[x][y] = util.random_choose_weighted(replacements[c])
end

if c == '+' or c == 'v' then
@@ -1618,19 +1618,19 @@ DEPTH: D:3-4

-- possible hobgoblin bosses and their weights
bossw = {hobgoblin = 1, Ijyb = 10, Robin = 10}
bosses = {hobgoblin = 1}
bosses = { {"hobgoblin", 1}, }

-- Find out which hobgoblin uniques we can use
if not you.uniques("Ijyb") then
bosses["Ijyb"] = bossw["Ijyb"]
bosses[#bosses + 1] = {"Ijyb", bossw["Ijyb"]}
end
if not you.uniques("Robin") then
bosses["Robin"] = bossw["Robin"]
bosses[#bosses + 1] = {"Robin", bossw["Robin"]}
end

-- Choose a boss, and remove the supporting goblins if we select Robin, since
-- she has her own band.
boss = crawl.random_element(bosses)
boss = util.random_choose_weighted(bosses)
if boss == "Robin" then
subst("B = .")
boss = "Robin band"
@@ -1810,11 +1810,11 @@ DEPTH: D, Depths
-- Kind of a complicated version of init_hm_decor_walldepth to make sure that
-- the glass matches the wall feature.
local a = you.absdepth() * 5
local weights = {["x"] = 50, ["c"] = a}
local weights = { {"x", 50}, {"c", a} }
if a - 75 > 0 then
weights["b"] = a - 75
weights[#weights + 1] = {"b", a - 75}
end
local wall = crawl.random_element(weights)
local wall = util.random_choose_weighted(weights)
local glass = 'm'
if wall == 'c' then
glass = 'n'
@@ -155,15 +155,15 @@ geoelf.glyphs.OUTLINE_SUBSTITUTIONS =

-- choosing a random feature
geoelf.glyphs.FEATURE_OPTIONS =
{ [geoelf.glyphs.STATUE] = 2,
[geoelf.glyphs.FOUNTAIN] = 1 }
{ {geoelf.glyphs.STATUE, 2},
{geoelf.glyphs.FOUNTAIN, 1} }

-- choosing a random plantlike thing
geoelf.glyphs.PLANTLIKE_OPTIONS =
{ [geoelf.glyphs.TREE] = 3,
[geoelf.glyphs.BUSH] = 1,
[geoelf.glyphs.PLANT] = 2,
[geoelf.glyphs.FUNGUS] = 4 }
{ {geoelf.glyphs.TREE, 3},
{geoelf.glyphs.BUSH, 1},
{geoelf.glyphs.PLANT, 2},
{geoelf.glyphs.FUNGUS, 4} }

-- making things into glass if possible
geoelf.glyphs.TO_GLASS =
Oops, something went wrong.

0 comments on commit 18b6515

Please sign in to comment.
You can’t perform that action at this time.