File: hyper_strategy.lua

package info (click to toggle)
crawl 2%3A0.33.1-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 95,264 kB
  • sloc: cpp: 358,145; ansic: 27,203; javascript: 9,491; python: 8,359; perl: 3,327; java: 2,667; xml: 2,191; makefile: 1,830; sh: 611; objc: 250; cs: 15; sed: 9; lisp: 3
file content (343 lines) | stat: -rw-r--r-- 15,052 bytes parent folder | download | duplicates (7)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
------------------------------------------------------------------------------
-- hyper_strategy.lua:
--
-- Defines different strategies for placing rooms (well, chunks of map) onto
-- the layout, using a system of various different callbacks for different
-- parts of the process.
--
-- By dividing things up in this way it's easier to write the logic for how different
-- types of rooms are placed and how they veto, and to reuse chunks of the
-- logic in different contexts. There are several available callbacks:
--
--   * pick_room    -- picks a room to attach to or place within
--   * pick_place   -- picks the coordinate on that room where we will place
--   * pick_anchor  -- picks an anchor and orientation (usually an edge cell) for the room that is being placed
--   * veto_place   -- allows the position to be vetoed before cells are inspected
--   * veto_cell    -- called as each cell is inspected to see what grid cells the map cells are overlapping
--   * veto_after   -- called after all cells are inspected; we can collate data during cell
--                     inspection and act at the end, e.g. "this shouldn't overlap more than two rooms"
--   * apply_cell   -- actually applies the cell to the usage grid
--   * post_place   -- called after placement to perform any post-processing
--   * sort_room    -- handles sorting the room into pick tables after placement

-- TODO: Allow a flow where the place is picked first *then* the room? This works best if we *definitely* know
--       that we're going to be able to place the room there, since room generation is a slow operation.
-- TODO: Actually support all the callbacks

------------------------------------------------------------------------------
-- Place picking callbacks
--

-- Pick a random coord from the whole grid; ignore usage. Mainly useful for placing a
-- primary room but could have some edge cases like slapping decor onto a layout or
-- making crazy abyss layouts, or a layout where square rooms all overlap each other
-- as long as we can make connectivity work...
function hyper.place.pick_place_random(room,build,usage_grid,options)

  -- TODO: If rotation is allowed, decide rotation first so we know how much padding to leave

  -- Work out our boundaries
  local gxm,gym = dgn.max_bounds()
  local bounds = room.generator_used.bounds or { x1 = 0, y1 = 0, x2 = usage_grid.width, y2 = usage_grid.height}
  local padding = room.generator_used.place_padding or 0
  local xmin,xmax,ymin,ymax = bounds.x1+padding, bounds.x2-1-room.size.x-padding, bounds.y1+padding, bounds.y2-1-room.size.y-padding

  local x,y = crawl.random_range(xmin,xmax),crawl.random_range(ymin,ymax)
  return { pos = { x = x, y = y }, usage = hyper.usage.get_usage(usage_grid,x,y) }
end

-- Pick an open spot from the list of eligible floors in usage_grid.
function hyper.place.pick_place_open(room,build,usage_grid,options)
  -- Select an eligible spot from the list in usage_grid
  local available_spots = #(usage_grid.eligibles.open)
  if available_spots == 0 then return hyper.place.pick_place_random(room,build,usage_grid,options) end

  local usage = util.random_from(usage_grid.eligibles.open)
  return { pos = usage.spot, usage = usage }

  -- TODO: Following shouldn't be necessary any more since we can intentionally find a room to attach to rather
  -- than being completely random about it ...

  -- Scan a 5x5 grid around an open spot, see if we find an eligible_open wall to attach to. This makes us much more likely to
  -- join up rooms in open areas, and reduces the amount of 1-tile-width chokepoints between rooms. It's still fairly simplistic
  -- though and maybe we could do this around the whole room area later on instead?
  --local near_eligibles = {}
  --for p in iter.rect_iterator(dgn.point(spot.x-2,spot.y-2),dgn.point(spot.x+2,spot.y+2)) do
  --  local near_usage = hyper.usage.get_usage(usage_grid,p.x,p.y)
  --  if near_usage ~= nil and near_usage.carvable and near_usage.room ~= usage.room then  -- We check it's not from the same room so we don't stop placing rooms within rooms
    --  table.insert(near_eligibles, { spot = p, usage = near_usage })
  --  end
  --end

  -- Randomly pick one of the new spots; maybe_place_vault will at least attempt to attach the room here instead, if possible
  --if #near_eligibles > 0 then
  --  local picked = near_eligibles[crawl.random2(#near_eligibles)+1]
  --  spot = picked.spot
  --  usage = picked.usage
  --end
end

function hyper.place.pick_place_closed(room,build,usage_grid,options)

  local available_spots = #(usage_grid.eligibles.closed)
  if available_spots == 0 then return hyper.place.pick_place_random(room,build,usage_grid,options) end

  local usage = util.random_from(usage_grid.eligibles.open)
  return { pos = usage.spot, usage = usage }
end

------------------------------------------------------------------------------
-- Anchor picking callbacks
--

-- Anchors the room by the top-left corner, no rotation
function hyper.place.anchor_origin()
  return { dir = 0, origin = { x = 0, y = 0 } }
end

-- Rotates the room to a random direction
-- TODO: Pick a random edge square for the origin instead of the corner
function hyper.place.anchor_random()
  return { dir = crawl.random2(4), origin = { x = 0, y = 0 } }
end

-- Anchor to a random (connectable) wall
function hyper.place.anchor_wall(place,room,build,usage_grid,options)
  if #(room.grid.anchors)==0 then return false end

  local anchor = util.random_from(room.grid.anchors)

  if not place.usage.solid or #(place.usage.anchors) == 0 then
    -- Pick a random rotation for the door wall
    v_normal_dir = util.random_from(vector.normals).dir
  else
    -- If placing a room in an existing wall we have the normal stored already in the usage data
    local anchor = util.random_from(place.usage.anchors)
    v_normal_dir = anchor.normal.dir
  end

  local final_orient = (v_normal_dir - anchor.normal.dir + 2) % 4

  return { origin = { x = anchor.origin.x + anchor.pos.x, y = anchor.origin.y + anchor.pos.y }, dir = final_orient }
end

-- Picks a connectable wall spot to anchor to
-- TODO: This is the version that picked a wall then an anchor. It was nice because it gave
-- each potential orientation an equal chance on e.g. long thin rooms. Right now however we
-- don't group the anchors by wall but depending on how things turn out it might be good to reinstate this.
function hyper.place.anchor_wall_old(place,room,build,usage_grid,options)
  -- Choose which wall of the room to attach to this spot (walls were determined in room analysis)
  local orients = { }
  for n = 0, 3, 1 do
    if room.walls[n].eligible then
      table.insert(orients,{ dir = n, wall = room.walls[n] })
    end
  end
  local count = #orients

  if count == 0 then return false end

  -- Now select which of the room's connectable squares we're going to use to attach to the eligible cell
  local v_normal, v_wall, v_normal_dir, wall

  local chosen = orients[crawl.random2(count) + 1]
  wall = chosen.wall
  -- Pick a random open cell on the wall. This is where we'll attach the room to the door.
  if #wall == 0 then return false end -- Shouldn't happen, eligible should mean cells available
  local chosen_wall = wall[crawl.random2(#wall)+1]
  local pos = chosen_wall.pos
  -- Get wall's normal and calculate the door vector (i.e. attaching wall)
  -- In open space we can pick a random normal for the door
  if not usage.solid and #(usage.anchors) == 0 then
    -- Pick a random rotation for the door wall
    v_normal = vector.normals[crawl.random_range(1,4)]
    v_normal_dir = v_normal.dir

  -- If placing a room in an existing wall we have the normal stored already in the usage data
  else
    -- Pick a random anchor
    local anchor = usage.anchors[crawl.random2(#(usage.anchors))+1]
    spot = anchor.grid_pos
    v_normal = anchor.normal
    v_normal_dir = v_normal.dir
  end

  -- Calculate how much the room will have to rotate to match the new orientation
  local final_orient = (v_normal_dir - chosen_wall.normal.dir + 2) % 4
  return { dir = final_orient, origin = spot }

end

------------------------------------------------------------------------------
-- Cell veto callbacks
--

-- Standard check that most or all vetos should observe. Prevents a
-- room placing off the map edge or overlapping a vault. If ignoring these
-- check, special handling should be in place elsewhere so you don't for
-- instance draw floor tiles at the map border. (TODO: Actually we prevent
-- overwriting vaults in any fashion, maybe should also automatically
-- prevent map border floor)
function hyper.place.cell_veto_standard(coord,state)
  local target_usage,usage = coord.grid_usage,state.usage

  -- Should handle these nil values in the loop instead?
  if target_usage == nil and coord.room_cell.buffer then return true end
  if target_usage == nil then
    if coord.room_cell.space then return false else return true end
  end

  -- Don't ever overwrite vaults
  if target_usage.vault then return true end

  return false
end


-- Forces placement (dangerous!)
function hyper.place.cell_veto_none(coord,state)
  return false
end

-- Default check - allows placement in open area, carving into rock,
-- rooms within rooms, and attaching rooms onto to each other.
function hyper.place.cell_veto_normal(coord,state)
  if hyper.place.cell_veto_standard(coord,state) then return true end

  local target_usage,usage = coord.grid_usage,state.usage
  if target_usage == nil then return false end

  -- TODO: We can do interesting stuff by allowing rooms to overlap a bit
  -- Open placement
  if not usage.solid then
    -- Only paint on the same room
    if (target_usage.room ~= usage.room)
      -- Avoid walls
      or (coord.room_cell.buffer and (target_usage.solid or target_usage.wall)) then return true end
  -- Open attached placement
  elseif usage.open_area then
    -- Prevent overlap with internal floor of room we're attaching to
    if not coord.room_cell.space and (target_usage.room == usage.room) and not target_usage.wall then return true end
    -- Prevent overlap with incidental rooms
    if (target_usage.room ~= usage.room) and not coord.room_cell.space and not coord.room_cell.buffer and (target_usage.solid or target_usage.restricted) then return true end
  else
    -- Enclosed placement, we can only burrow into more rock, or overlap our walls with incidental walls, or floor edges with carvable space
    if not coord.room_cell.space and not target_usage.solid then return true end
  end
  return false

end

function hyper.place.cell_veto_open(coord,state)
  if hyper.place.cell_veto_standard(coord,state) then return true end

  local target_usage,usage = coord.grid_usage,state.usage
  if target_usage == nil then return false end

  -- Open placement
  if not usage.solid then
    -- Only paint on the same room
    if (target_usage.room ~= usage.room)
      -- Avoid walls
      or (coord.room_cell.buffer and (target_usage.solid or target_usage.wall)) then return true end
  -- Open placement anchored to an existing room
  else
    -- Prevent overlap with internal floor of room we're attaching to
    if not coord.room_cell.space and (target_usage.room == usage.room) and not target_usage.wall then return true end
    -- Prevent overlap with incidental rooms
    if (target_usage.room ~= usage.room) and not coord.room_cell.space and not coord.room_cell.buffer and (target_usage.solid or target_usage.restricted) then return true end
  end
  return false

end

------------------------------------------------------------------------------
-- Placement strategies
--
-- Some very common groupings of callbacks. Some build passes might want to
-- compose custom strategies instead based on these.

-- For placing an initial large room typically encompassing the level.
-- We don't veto because we may want to underlay an existing orient vault on the level.
-- TODO: For odd shaped layouts (e.g. large cross) the orient vault isn't guaranteed to get connected.
-- There are a number of ways we could enforce a connection; the most obvious is checking at the end
-- of build to see whether it connected and if not use e.g. spotty layout connector to join it up
-- to the nearest available external anchor.
hyper.place.strategy_primary = {
  pick_place = hyper.place.pick_place_random,
  pick_anchor = hyper.place.anchor_origin,
  veto_cell = hyper.place.cell_veto_none
}

-- Places rooms randomly within a large open area.
-- TODO: Doesn't work properly
hyper.place.strategy_random = {
  pick_place = hyper.place.pick_place_random,
  pick_anchor = hyper.place.anchor_random,
  veto_cell = hyper.place.cell_veto_standard
}

-- Places in open areas using internal/open anchors only.
hyper.place.strategy_open = {
  pick_place = hyper.place.pick_place_open,
  pick_anchor = hyper.place.anchor_wall,
  veto_cell = hyper.place.cell_veto_open
}

-- Places rooms only inside other rooms
-- Looks for class="{inner_class}" on the room's generators
hyper.place.strategy_inner = {
  pick_place = hyper.place.pick_place_open,
  pick_anchor = hyper.place.anchor_wall,
  veto_place = function(state,usage_grid)
    if state.usage.room == nil or not state.usage.room.class == state.room.generator_used.inner_class then return true end
  end,
  veto_cell = hyper.place.cell_veto_open
}

-- Carves into rock using only external anchors
hyper.place.strategy_carve = {
  pick_place = hyper.place.pick_place_closed,
  pick_anchor = hyper.place.anchor_wall,
  veto_place = function(state,usage_grid)

  end,
  veto_cell = function(coord,state)  --hyper.place.cell_veto_normal
    if hyper.place.standard_veto(coord,state) then return true end
    -- Enclosed placement, we can only burrow into more rock, or overlap our walls with incidental walls, or floor edges with carvable space
    if not coord.room_cell.space and not coord.grid_usage.solid and not coord.grid_usage.restricted then return true end
  end
}

hyper.place.strategy_inner_or_carve = {
  pick_place = function(room,build,usage_grid,options)
    if crawl.coinflip() then
      return hyper.place.strategy_inner.pick_place(room,build,usage_grid,options)
    else
      return hyper.place.strategy_carve.pick_place(room,build,usage_grid,options)
    end
  end,
  veto_place = function(state,usage_grid)
    if state.usage.open then
      return hyper.place.strategy_inner.veto_place(state,usage_grid)
    else
      return hyper.place.strategy_carve.veto_place(state,usage_grid)
    end
  end,
  pick_anchor = function(place,room,build,usage_grid,options)
    if place.usage.open then
      return hyper.place.strategy_inner.pick_anchor(room,build,usage_grid,options)
    else
      return hyper.place.strategy_carve.pick_anchor(room,build,usage_grid,options)
    end
  end,
  veto_cell = function(coord,state)
    if hyper.place.standard_veto(coord,state) then return true end

    if state.usage.open then
      return hyper.place.strategy_inner.veto_cell(coord,state)
    else
      return hyper.place.strategy_carve.veto_cell(coord,state)
    end
  end
}