Scale Ziggurat monsters per floor very harsly to number of completed zigs
[crawl.git] / crawl-ref / source / dat / dlua / ziggurat.lua
1 ------------------------------------------------------------------------------
2 -- ziggurat.lua:
3 --
4 -- Code for ziggurats.
5 --
6 -- Important notes:
7 -- ----------------
8 -- Functions that are attached to Lua markers' onclimb properties
9 -- cannot be closures, because Lua markers must be saved and closure
10 -- upvalues cannot (yet) be saved.
11 ------------------------------------------------------------------------------
12
13 function zig()
14   if not dgn.persist.ziggurat then
15     dgn.persist.ziggurat = { }
16     -- Initialise here to handle ziggurats accessed directly by &P.
17     initialise_ziggurat(dgn.persist.ziggurat)
18   end
19   return dgn.persist.ziggurat
20 end
21
22 local wall_colours = {
23   "blue", "red", "lightblue", "magenta", "green", "white"
24 }
25
26 function ziggurat_wall_colour()
27   return util.random_from(wall_colours)
28 end
29
30 function initialise_ziggurat(z, portal)
31   -- Any given ziggurat will use the same builder for all its levels,
32   -- and the same colours for outer walls. Before choosing the builder,
33   -- we specify a global excentricity. If zig_exc=0, then the ellipses
34   -- will be circles etc. It is not the actual excentricity but some
35   -- value between 0 and 100. For deformed ellipses and rectangles, make
36   -- sure that the map is wider than it is high for the sake of ASCII.
37
38   z.zig_exc = crawl.random2(101)
39   z.builder = ziggurat_choose_builder()
40   z.colour = ziggurat_wall_colour()
41 end
42
43 function callback.ziggurat_initialiser(portal)
44   dgn.persist.ziggurat = { }
45   initialise_ziggurat(dgn.persist.ziggurat, portal)
46 end
47
48 -- Common setup for ziggurat levels.
49 function ziggurat_level(e)
50   e.tags("allow_dup")
51   e.tags("no_dump")
52   e.orient("encompass")
53
54   if crawl.game_started() then
55     ziggurat_build_level(e)
56   end
57 end
58
59 -----------------------------------------------------------------------------
60 -- Ziggurat level builders.
61
62 beh_wander = mons.behaviour("wander")
63
64 function ziggurat_awaken_all(mons)
65   mons.beh = beh_wander
66 end
67
68 function ziggurat_build_level(e)
69   local builder = zig().builder
70
71   -- Deeper levels can have all monsters awake.
72   -- Does never happen at depths 1-4; does always happen at depths 25-27.
73   local generate_awake = you.depth() > 4 + crawl.random2(21)
74   zig().monster_hook = generate_awake and global_function("ziggurat_awaken_all")
75
76   -- Deeper levels may block controlled teleports.
77   -- Does never happen at depths 1-6; does always happen at depths 25-27.
78   if you.depth() > 6 + crawl.random2(19) then
79     dgn.change_level_flags("no_tele_control")
80   end
81
82   if builder then
83     return ziggurat_builder_map[builder](e)
84   end
85 end
86
87 local zigstair = dgn.gridmark
88
89 -- the estimated total map area for ziggurat maps of given depth
90 -- this is (almost) independent of the layout type
91 local function map_area()
92   return 30 + 18*you.depth() + you.depth()*you.depth()
93 end
94
95 local function clamp_in(val, low, high)
96   if val < low then
97     return low
98   elseif val > high then
99     return high
100   else
101     return val
102   end
103 end
104
105 local function clamp_in_bounds(x, y)
106   return clamp_in(x, 2, dgn.GXM - 3), clamp_in(y, 2, dgn.GYM - 3)
107 end
108
109 local function set_wall_tiles()
110   local tileset = {
111     blue      = "wall_zot_blue",
112     red       = "wall_zot_red",
113     lightblue = "wall_zot_cyan",
114     magenta   = "wall_zot_magenta",
115     green     = "wall_zot_green",
116     white     = "wall_vault"
117   }
118
119   local wall = tileset[zig().colour]
120   if (wall == nil) then
121     wall = "wall_vault"
122   end
123   dgn.change_rock_tile(wall)
124 end
125
126 local function with_props(spec, props)
127   local spec_table = type(spec) == "table" and spec or { spec = spec }
128   return util.cathash(spec_table, props)
129 end
130
131 local function spec_fn(specfn)
132   return { specfn = specfn }
133 end
134
135 local function spec_if(fn, spec)
136   local spec_table = type(spec) == "table" and spec or { spec = spec }
137   return util.cathash(spec_table, { cond = fn })
138 end
139
140 local function depth_ge(lev)
141   return function ()
142            return you.depth() >= lev
143          end
144 end
145
146 local function depth_range(low, high)
147   return function ()
148            return you.depth() >= low and you.depth() <= high
149          end
150 end
151
152 local function depth_lt(lev)
153   return function ()
154            return you.depth() < lev
155          end
156 end
157
158 local function zig_monster_fn(spec)
159   local mfn = dgn.monster_fn(spec)
160   return function (x, y)
161            local mons = mfn(x, y)
162            if mons then
163              local monster_hook = zig().monster_hook
164              if monster_hook then
165                monster_hook(mons)
166              end
167            end
168            return mons
169          end
170 end
171
172 local function monster_creator_fn(arg)
173   local atyp = type(arg)
174   if atyp == "string" then
175     local mcreator = zig_monster_fn(arg)
176
177     local function mspec(x, y, nth)
178       return mcreator(x, y)
179     end
180     return { fn = mspec, spec = arg }
181   elseif atyp == "table" then
182     if not arg.cond or arg.cond() then
183       local spec = arg.spec or arg.specfn()
184       return util.cathash(monster_creator_fn(spec), arg)
185     end
186   elseif atyp == "function" then
187     return { fn = arg }
188   end
189 end
190
191 local mons_populations = { }
192
193 local function mset(...)
194   util.foreach({ ... }, function (spec)
195                           table.insert(mons_populations, spec)
196                         end)
197 end
198
199 local function mset_if(condition, ...)
200   mset(unpack(util.map(util.curry(spec_if, condition), { ... })))
201 end
202
203 mset(with_props("place:Lair:$ w:165 / dire elephant w:12 / " ..
204                 "catoblepas w:12 / hellephant w:6 / spriggan druid w:1 / " ..
205                 "guardian serpent w:1 / deep troll shaman w:1 / " ..
206                 "raiju w:1 / hell beast w:1", { weight = 5 }),
207      with_props("place:Shoals:$ w:125 band / merfolk aquamancer / " ..
208                 "water nymph w:5 / merfolk impaler w:5 / " ..
209                 "merfolk javelineer / octopode crusher w:12", { weight = 5 }),
210      "place:Spider:$ w:115 / ghost moth w:15 / red wasp / " ..
211                 "orb spider",
212      "place:Crypt:$ 9 w:260 / curse skull w:5 / profane servitor w:5 / " ..
213                 "bone dragon / ancient lich / revenant",
214      "place:Abyss:$ w:1990 / corrupter",
215      with_props("place:Slime:$", { jelly_protect = true }),
216      with_props("place:Coc:$ w:460 / Ice Fiend / " ..
217                  "blizzard demon w:30", { weight = 5 }),
218      with_props("place:Geh:$ w:460 / Brimstone Fiend / " ..
219                  "balrug w:30", { weight = 5 }),
220      with_props("place:Dis:$ w:460 / Hell Sentinel / " ..
221                  "dancing weapon / war gargoyle w:20", { weight = 5 }),
222      with_props("place:Tar:$ w:460 / Shadow Fiend / " ..
223                  "curse toe / shadow demon w:20", { weight = 5 }),
224      with_props("daeva / angel / cherub / pearl dragon / " ..
225                 "ophan / apis", { weight = 2 }),
226      with_props("hill giant / cyclops / stone giant / fire giant / " ..
227                 "frost giant / ettin / titan", { weight = 2 }),
228      with_props("fire elemental / fire drake / hell hound / efreet / " ..
229                 "fire dragon / fire giant / orb of fire", { weight = 2 }),
230      with_props("ice beast / freezing wraith / ice dragon / " ..
231                 "frost giant / ice devil / ice fiend / simulacrum / " ..
232                 "white draconian knight / blizzard demon", { weight = 2 }),
233      with_props("insubstantial wisp / air elemental / titan / raiju / " ..
234                 "storm dragon / electric golem / spriggan air mage / " ..
235                 "shock serpent", { weight = 2 }),
236      with_props("gargoyle / earth elemental / torpor snail / boulder beetle / " ..
237                 "stone giant / iron dragon / crystal guardian / " ..
238                 "war gargoyle / iron golem / hell sentinel", { weight = 2 }),
239      with_props("spectral thing / shadow wraith / eidolon w:4 / " ..
240                 "shadow dragon / deep elf death mage w:6 / " ..
241                 "death knight w:4 / revenant w:4 / profane servitor w:6 / " ..
242                 "soul eater / shadow fiend / black sun", { weight = 2 }),
243      with_props("swamp drake / fire drake / wind drake w:2 / death drake / " ..
244                 "wyvern w:5 / hydra w:5 / steam dragon / mottled dragon / " ..
245                 "swamp dragon / fire dragon / ice dragon / storm dragon / " ..
246                 "iron dragon / shadow dragon / quicksilver dragon / " ..
247                 "golden dragon", { weight = 2 }),
248      with_props("centaur / yaktaur / centaur warrior / yaktaur captain / " ..
249                 "cyclops / stone giant / faun w:1 / satyr w:2 / " ..
250                 "thorn hunter w:2 / naga sharpshooter / " ..
251                 "merfolk javelineer / deep elf master archer", { weight = 2 }),
252      with_props("wizard / necromancer / ogre mage w:5 / orc sorcerer w:5 / " ..
253                 "naga mage / naga ritualist w:5 / salamander mystic w:5 / " ..
254                 "greater naga / tengu conjurer / tengu reaver / " ..
255                 "spriggan air mage w:5 / merfolk aquamancer w:5 / " ..
256                 "deep elf conjurer / deep elf annihilator / " ..
257                 "deep elf sorcerer / draconian scorcher w:5 / " ..
258                 "draconian knight w:5 / draconian annihilator w:5 / " ..
259                 "lich w:3 / ancient lich w:2 / blood saint", { weight = 2 }))
260
261 -- spec_fn can be used to wrap a function that returns a monster spec.
262 -- This is useful if you want to adjust monster weights in the spec
263 -- wrt to depth in the ziggurat.
264 mset(spec_fn(function ()
265                local d = 290 - 10 * you.depth()
266                local e = math.max(0, you.depth() - 20)
267                return "place:Orc:$ w:" .. d .. " / orc warlord / " ..
268                  "orc high priest band / orc sorcerer w:5 / stone giant / " ..
269                  "iron troll w:5 / moth of wrath w:" .. e
270              end))
271
272 mset(spec_fn(function ()
273                local d = 300 - 10 * you.depth()
274                return "place:Elf:$ w:" .. d .. " / deep elf high priest / " ..
275                  "deep elf blademaster / deep elf master archer / " ..
276                  "deep elf annihilator / deep elf demonologist"
277              end))
278
279 mset(spec_fn(function ()
280                local d = math.max(6, you.depth() * 2 - 2)
281                local e = math.max(1, you.depth() - 18)
282                return "place:Snake:$ w:130 / guardian serpent w:5 / " ..
283                  "greater naga w:" .. d .. " / quicksilver dragon w:" .. e
284              end))
285
286 mset(spec_fn(function ()
287                local d = math.max(120, 280 - 10 * you.depth())
288                local e = math.max(1, you.depth() - 9)
289                local f = math.max(1, math.floor((you.depth() - 9) / 3))
290                return "place:Swamp:$ w:" .. d .. " / eight-headed hydra w:8 / " ..
291                  "swamp dragon w:8 / tentacled monstrosity w:" .. e ..
292                  " / shambling mangrove w:" .. f .. " / green death w:6 / " ..
293                  "death drake w:1 / golden dragon w:1"
294              end))
295
296 mset(spec_fn(function ()
297                local d = math.max(1, you.depth() - 11)
298                return "place:Vaults:$ w:60 / place:Vaults:$ 9 w:30 / " ..
299                  "titan w:" .. d .. " / golden dragon w:" .. d ..
300                  " / ancient lich w:" .. d
301              end))
302
303 mset(spec_fn(function ()
304                local d = you.depth() + 5
305                return "place:Tomb:$ w:200 / greater mummy w:" .. d
306              end))
307
308 mset(spec_fn(function ()
309                local d = math.max(2, math.floor((32 - you.depth()) / 5))
310                local e = math.min(8, math.floor((you.depth()) / 5) + 4)
311                local f = math.max(1, you.depth() - 5)
312                return "chaos spawn w:" .. d .. " / ugly thing w:" .. d ..
313                  " / very ugly thing w:4 / apocalypse crab w:4 / " ..
314                  "shapeshifter hd:16 w:" ..e .. " / glowing shapeshifter w:" .. e ..
315                  " / killer klown w:8 / chaos champion w:2 / " ..
316                  "greater demon w:2 / pandemonium lord w:" .. f
317              end))
318
319 mset(spec_fn(function ()
320                local d = 41 - you.depth()
321                return "base draconian w:" .. d .. " / nonbase draconian w:40"
322              end))
323
324 local pan_lord_fn = zig_monster_fn("pandemonium lord")
325 local pan_critter_fn = zig_monster_fn("place:Pan / greater demon / nonbase demonspawn w:4")
326
327 local function mons_panlord_gen(x, y, nth)
328   if nth == 1 then
329     local d = math.max(1, you.depth() - 11)
330     dgn.set_random_mon_list("place:Pan / greater demon / nonbase demonspawn w:4")
331     return pan_lord_fn(x, y)
332   else
333     return pan_critter_fn(x, y)
334   end
335 end
336
337 mset_if(depth_ge(8), mons_panlord_gen)
338 mset_if(depth_ge(14), with_props("place:Snake:$ w:14 / place:Swamp:$ w:14 / " ..
339                       "place:Shoals:$ w:14 / place:Spider:$ w:14 / " ..
340                       "greater naga w:12 / guardian serpent w:8 / hydra w:5 / " ..
341                       "swamp dragon w:5 / tentacled monstrosity / " ..
342                       "merfolk aquamancer w:6 / merfolk javelineer w:8 / " ..
343                       "alligator snapping turtle w:6 / " ..
344                       "octopode crusher / ghost moth w:8 / " ..
345                       "emperor scorpion w:8 / moth of wrath w:4",
346                       { weight = 5 }))
347
348 function ziggurat_monster_creators()
349   return util.map(monster_creator_fn, mons_populations)
350 end
351
352 local function ziggurat_vet_monster(fmap)
353   local fn = fmap.fn
354   fmap.fn = function (x, y, nth, hdmax)
355               if nth >= dgn.MAX_MONSTERS then
356                 return nil
357               end
358               for i = 1, 10 do
359                 local mons = fn(x, y, nth)
360                 if mons then
361                   -- Discard zero-exp monsters, and monsters that explode
362                   -- the HD limit.
363                   if mons.experience == 0 or mons.hd > hdmax * 1.3 then
364                     mons.dismiss()
365                   else
366                     -- Monster is ok!
367                     return mons
368                   end
369                 end
370               end
371               -- Give up.
372               return nil
373             end
374   return fmap
375 end
376
377 local function choose_monster_set()
378   return ziggurat_vet_monster(
379            util.random_weighted_from(
380              'weight',
381              ziggurat_monster_creators()))
382 end
383
384 -- Function to find travel-safe squares, excluding closed doors.
385 local dgn_passable = dgn.passable_excluding("closed_door")
386
387 local function ziggurat_create_monsters(p, mfn)
388   local depth = you.depth()
389   local completed = you.zigs_completed() + 1
390   local hd_pool = math.floor(10 + completed * completed + (depth * (depth + 8 * completed )) * math.max(1, completed * 0.75))
391
392   local nth = 1
393
394   local function mons_do_place(p)
395     if hd_pool > 0 then
396       local mons = mfn(p.x, p.y, nth, hd_pool)
397
398       if mons then
399         nth = nth + 1
400         hd_pool = hd_pool - mons.hd
401
402         if nth >= dgn.MAX_MONSTERS then
403           hd_pool = 0
404         end
405       else
406         -- Can't find any suitable monster for the HD we have left.
407         hd_pool = 0
408       end
409     end
410   end
411
412   local function mons_place(point)
413     if hd_pool <= 0 then
414       return true
415     elseif not dgn.mons_at(point.x, point.y) then
416       mons_do_place(point)
417     end
418   end
419
420   dgn.find_adjacent_point(p, mons_place, dgn_passable)
421 end
422
423 local function ziggurat_create_loot_at(c)
424   -- Basically, loot grows linearly with depth finished zigs.
425   local depth = you.depth()
426   local completed = you.zigs_completed()
427   local nloot = depth + crawl.random2(math.floor(depth * 0.5))
428
429   if completed > 0 then
430     nloot = math.min(nloot + crawl.random2(math.floor(completed * depth / 4)), map_area() / 9)
431   end
432
433   local function find_free_space(nspaces)
434     local spaces = { }
435     local function add_spaces(p)
436       if nspaces <= 0 then
437         return true
438       else
439         table.insert(spaces, p)
440         nspaces = nspaces - 1
441       end
442     end
443     dgn.find_adjacent_point(c, add_spaces, dgn_passable)
444     return spaces
445   end
446
447   -- dgn.good_scrolls is a list of items with total weight 1000
448   local good_loot = dgn.item_spec("* no_pickup no_mimic w:7000 / " .. dgn.good_scrolls)
449   local super_loot = dgn.item_spec("| no_pickup no_mimic w:7000 /" ..
450                                    "potion of experience no_pickup no_mimic w:200 /" ..
451                                    "potion of cure mutation no_pickup no_mimic w:200 /" ..
452                                    "potion of porridge no_pickup no_mimic w:100 /" ..
453                                    "wand of heal wounds no_pickup no_mimic w:10 / " ..
454                                    "wand of hasting no_pickup no_mimic w:10 / " ..
455                                    dgn.good_scrolls)
456
457   local loot_spots = find_free_space(nloot * 4)
458
459   if #loot_spots == 0 then
460     return
461   end
462
463   local curspot = 0
464   local function next_loot_spot()
465     curspot = curspot + 1
466     if curspot > #loot_spots then
467       curspot = 1
468     end
469     return loot_spots[curspot]
470   end
471
472   local function place_loot(what)
473     local p = next_loot_spot()
474     dgn.create_item(p.x, p.y, what)
475   end
476
477   for i = 1, nloot do
478     if crawl.one_chance_in(depth) then
479       for j = 1, 4 do
480         place_loot(good_loot)
481       end
482     else
483       place_loot(super_loot)
484     end
485   end
486 end
487
488 -- Suitable for use in loot vaults.
489 function ziggurat_loot_spot(e, key)
490   e.lua_marker(key, portal_desc { ziggurat_loot = "X" })
491   e.kfeat(key .. " = .")
492   e.marker("@ = lua:props_marker({ door_restrict=\"veto\" })")
493   e.kfeat("@ = +")
494 end
495
496 local has_loot_chamber = false
497
498 local function ziggurat_create_loot_vault(entry, exit)
499   local inc = (exit - entry):sgn()
500
501   local connect_point = exit - inc * 3
502   local map = dgn.map_by_tag("ziggurat_loot_chamber")
503
504   if not map then
505     return exit
506   end
507
508   local function place_loot_chamber()
509     local res = dgn.place_map(map, true, true)
510     if res then
511       has_loot_chamber = true
512     end
513     return res
514   end
515
516   local function good_loot_bounds(map, px, py, xs, ys)
517     local vc = dgn.point(px + math.floor(xs / 2),
518                          py + math.floor(ys / 2))
519
520
521     local function safe_area()
522       local p = dgn.point(px, py)
523       local sz = dgn.point(xs, ys)
524       local floor = dgn.fnum("floor")
525       return dgn.rectangle_forall(p, p + sz - 1,
526                                   function (c)
527                                     return dgn.grid(c.x, c.y) == floor
528                                   end)
529     end
530
531     local linc = (exit - vc):sgn()
532     -- The map's positions should be at the same increment to the exit
533     -- as the exit is to the entrance, else reject the place.
534     return (inc == linc) and safe_area()
535   end
536
537   local function connect_loot_chamber()
538     return dgn.with_map_bounds_fn(good_loot_bounds, place_loot_chamber)
539   end
540
541   local res = dgn.with_map_anchors(connect_point.x, connect_point.y,
542                                    connect_loot_chamber)
543   if not res then
544     return exit
545   else
546     -- Find the square to drop the loot.
547     local lootx, looty = dgn.find_marker_position_by_prop("ziggurat_loot")
548
549     if lootx and looty then
550       return dgn.point(lootx, looty)
551     else
552       return exit
553     end
554   end
555 end
556
557 local function ziggurat_locate_loot(entrance, exit, jelly_protect)
558   if jelly_protect then
559     return ziggurat_create_loot_vault(entrance, exit)
560   else
561     return exit
562   end
563 end
564
565 local function ziggurat_place_pillars(c)
566   local range = crawl.random_range
567   local floor = dgn.fnum("floor")
568
569   local map, vplace = dgn.resolve_map(dgn.map_by_tag("ziggurat_pillar"))
570
571   if not map then
572     return
573   end
574
575   local name = dgn.name(map)
576
577   local size = dgn.point(dgn.mapsize(map))
578
579   -- Does the pillar want to be centered?
580   local centered = string.find(dgn.tags(map), " centered ")
581
582   local function good_place(p)
583     local function good_square(where)
584       return dgn.grid(where.x, where.y) == floor
585     end
586     return dgn.rectangle_forall(p, p + size - 1, good_square)
587   end
588
589   local function place_pillar()
590     if centered then
591       if good_place(c) then
592         return dgn.place_map(map, true, true, c.x, c.y)
593       end
594     else
595       for i = 1, 100 do
596         local offset = range(-15, -size.x)
597         local offsets = {
598           dgn.point(offset, offset) - size + 1,
599           dgn.point(offset - size.x + 1, -offset),
600           dgn.point(-offset, -offset),
601           dgn.point(-offset, offset - size.y + 1)
602         }
603
604         offsets = util.map(function (o)
605                              return o + c
606                            end, offsets)
607
608         if util.forall(offsets, good_place) then
609           local function replace(at, hflip, vflip)
610             dgn.reuse_map(vplace, at.x, at.y, hflip, vflip)
611           end
612
613           replace(offsets[1], false, false)
614           replace(offsets[2], false, true)
615           replace(offsets[3], true, false)
616           replace(offsets[4], false, true)
617           return true
618         end
619       end
620     end
621   end
622
623   for i = 1, 5 do
624     if place_pillar() then
625       break
626     end
627   end
628 end
629
630 local function ziggurat_stairs(entry, exit)
631   zigstair(entry.x, entry.y, "stone_arch", "stone_stairs_up_i")
632
633   if you.depth() < dgn.br_depth(you.branch()) then
634     zigstair(exit.x, exit.y, "stone_stairs_down_i")
635   end
636
637   zigstair(exit.x, exit.y + 1, "exit_ziggurat")
638   zigstair(exit.x, exit.y - 1, "exit_ziggurat")
639 end
640
641 local function ziggurat_furnish(centre, entry, exit)
642   has_loot_chamber = false
643   local monster_generation = choose_monster_set()
644
645   if type(monster_generation.spec) == "string" then
646     dgn.set_random_mon_list(monster_generation.spec)
647   end
648
649   -- Identify where we're going to place loot, but don't actually put
650   -- anything down until we've placed pillars.
651   local lootspot = ziggurat_locate_loot(entry, exit,
652     monster_generation.jelly_protect)
653
654   if not has_loot_chamber then
655     -- Place pillars if we did not create a loot chamber.
656     ziggurat_place_pillars(centre)
657   end
658
659   ziggurat_create_loot_at(lootspot)
660
661   ziggurat_create_monsters(exit, monster_generation.fn)
662
663   local function needs_colour(p)
664     return not dgn.in_vault(p.x, p.y)
665       and dgn.grid(p.x, p.y) == dgn.fnum("permarock_wall")
666   end
667
668   dgn.colour_map(needs_colour, zig().colour)
669   set_wall_tiles()
670 end
671
672 -- builds ziggurat maps consisting of two overimposed rectangles
673 local function ziggurat_rectangle_builder(e)
674   local grid = dgn.grid
675   dgn.fill_grd_area(0, 0, dgn.GXM - 1, dgn.GYM - 1, "permarock_wall")
676
677   local area = map_area()
678   area = math.floor(area*3/4)
679
680   local cx, cy = dgn.GXM / 2, dgn.GYM / 2
681
682   -- exc is the local eccentricity for the two rectangles
683   -- exc grows with depth as 0-1, 1, 1-2, 2, 2-3 ...
684   local exc = math.floor(you.depth() / 2)
685   if ((you.depth()-1) % 2) == 0 and crawl.coinflip() then
686     exc = exc + 1
687   end
688
689   local a = math.floor(math.sqrt(area+4*exc*exc))
690   local b = a - 2*exc
691   local a2 = math.floor(a / 2) + (a % 2)
692   local b2 = math.floor(b / 2) + (b % 2)
693   local x1, y1 = clamp_in_bounds(cx - a2, cy - b2)
694   local x2, y2 = clamp_in_bounds(cx + a2, cy + b2)
695   dgn.fill_grd_area(x1, y1, x2, y2, "floor")
696
697   local zig_exc = zig().zig_exc
698   local nx1 = cx + y1 - cy
699   local ny1 = cy + x1 - cx + math.floor(you.depth()/2*(200-zig_exc)/300)
700   local nx2 = cx + y2 - cy
701   local ny2 = cy + x2 - cx - math.floor(you.depth()/2*(200-zig_exc)/300)
702   nx1, ny1 = clamp_in_bounds(nx1, ny1)
703   nx2, ny2 = clamp_in_bounds(nx2, ny2)
704   dgn.fill_grd_area(nx1, ny1, nx2, ny2, "floor")
705
706   local entry = dgn.point(x1, cy)
707   local exit = dgn.point(x2, cy)
708
709   if you.depth() % 2 == 0 then
710     entry, exit = exit, entry
711   end
712
713   ziggurat_stairs(entry, exit)
714   ziggurat_furnish(dgn.point(cx, cy), entry, exit)
715 end
716
717 -- builds elliptic ziggurat maps
718 -- given the area, half axes a and b are determined by:
719 -- pi*a*b=area,
720 -- a=b for zig_exc=0,
721 -- a=b*3/2 for zig_exc=100
722 local function ziggurat_ellipse_builder(e)
723   local grid = dgn.grid
724   dgn.fill_grd_area(0, 0, dgn.GXM - 1, dgn.GYM - 1, "permarock_wall")
725
726   local zig_exc = zig().zig_exc
727
728   local area = map_area()
729   local b = math.floor(math.sqrt(200*area/(200+zig_exc) * 100/314))
730   local a = math.floor(b * (200+zig_exc) / 200)
731   local cx, cy = dgn.GXM / 2, dgn.GYM / 2
732
733   local floor = dgn.fnum("floor")
734
735   for x=0, dgn.GXM-1 do
736     for y=0, dgn.GYM-1 do
737       if b*b*(cx-x)*(cx-x) + a*a*(cy-y)*(cy-y) <= a*a*b*b then
738         grid(x, y, floor)
739       end
740     end
741   end
742
743   local entry = dgn.point(cx-a+2, cy)
744   local exit  = dgn.point(cx+a-2, cy)
745
746   if you.depth() % 2 == 0 then
747     entry, exit = exit, entry
748   end
749
750   ziggurat_stairs(entry, exit)
751   ziggurat_furnish(dgn.point(cx, cy), entry, exit)
752 end
753
754
755 -- builds hexagonal ziggurat maps
756 local function ziggurat_hexagon_builder(e)
757   local grid = dgn.grid
758   dgn.fill_grd_area(0, 0, dgn.GXM - 1, dgn.GYM - 1, "permarock_wall")
759
760   local zig_exc = zig().zig_exc
761
762   local c = dgn.point(dgn.GXM, dgn.GYM) / 2
763   local area = map_area()
764
765   local a = math.floor(math.sqrt(2 * area / math.sqrt(27))) + 2
766   local b = math.floor(a*math.sqrt(3)/4)
767
768   local left = dgn.point(math.floor(c.x - (a + math.sqrt(2 * a)) / 2),
769                          c.y)
770   local right = dgn.point(2 * c.x - left.x, c.y)
771
772   local floor = dgn.fnum("floor")
773
774   for x = 1, dgn.GXM - 2 do
775     for y = 1, dgn.GYM - 2 do
776       local dlx = x - left.x
777       local drx = x - right.x
778       local dly = y - left.y
779       local dry = y - right.y
780
781       if dlx >= dly and drx <= dry
782         and dlx >= -dly and drx <= -dry
783         and y >= c.y - b and y <= c.y + b then
784         grid(x, y, floor)
785       end
786     end
787   end
788
789   local entry = left + dgn.point(1,0)
790   local exit  = right - dgn.point(1, 0)
791
792   if you.depth() % 2 == 0 then
793     entry, exit = exit, entry
794   end
795
796   ziggurat_stairs(entry, exit)
797   ziggurat_furnish(c, entry, exit)
798 end
799
800 ----------------------------------------------------------------------
801
802 ziggurat_builder_map = {
803   rectangle = ziggurat_rectangle_builder,
804   ellipse = ziggurat_ellipse_builder,
805   hex = ziggurat_hexagon_builder
806 }
807
808 local ziggurat_builders = util.keys(ziggurat_builder_map)
809
810 function ziggurat_choose_builder()
811   return util.random_from(ziggurat_builders)
812 end