File: explorer.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 (584 lines) | stat: -rw-r--r-- 19,608 bytes parent folder | download | duplicates (2)
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
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
-- tools for seeing what's in a seed. For example use cases, see both
-- crawl-ref/source/scripts/seed_explorer.lua, and
-- crawl-ref/source/tests/seed_explorer.lua.
-- TODO: LDoc this

util.namespace('explorer')

-- matches pregeneration order
explorer.generation_order = {
                "Temple",
                "D:1",
                "D:2", "D:3", "D:4", "D:5", "D:6", "D:7", "D:8", "D:9",
                "D:10", "D:11", "D:12", "D:13", "D:14", "D:15",
                "Lair:1", "Lair:2", "Lair:3", "Lair:4", "Lair:5",
                "Orc:1", "Orc:2",
                "Spider:1", "Spider:2", "Spider:3", "Spider:4",
                "Snake:1", "Snake:2", "Snake:3", "Snake:4",
                "Shoals:1", "Shoals:2", "Shoals:3", "Shoals:4",
                "Swamp:1", "Swamp:2", "Swamp:3", "Swamp:4",
                "Vaults:1", "Vaults:2", "Vaults:3", "Vaults:4", "Vaults:5",
                "Crypt:1", "Crypt:2", "Crypt:3",
                "Depths:1", "Depths:2", "Depths:3", "Depths:4",
                "Hell",
                "Elf:1", "Elf:2", "Elf:3",
                "Zot:1", "Zot:2", "Zot:3", "Zot:4", "Zot:5",
                "Slime:1", "Slime:2", "Slime:3", "Slime:4", "Slime:5",
                "Tomb:1", "Tomb:2", "Tomb:3",
                "Tar:1", "Tar:2", "Tar:3", "Tar:4", "Tar:5", "Tar:6", "Tar:7",
                "Coc:1", "Coc:2", "Coc:3", "Coc:4", "Coc:5", "Coc:6", "Coc:7",
                "Dis:1", "Dis:2", "Dis:3", "Dis:4", "Dis:5", "Dis:6", "Dis:7",
                "Geh:1", "Geh:2", "Geh:3", "Geh:4", "Geh:5", "Geh:6", "Geh:7",
            }
-- generation order continues: pan, zig. However, these are really only in the
-- official generation order so that entering them forces the rest of the
-- dungeon to generate first, so we ignore them here.

explorer.portal_order = {
    "Sewer",
    "Ossuary",
    "IceCv",
    "Volcano",
    "Bailey",
    "Gauntlet",
    "Bazaar",
    -- Trove is not pregenerated, so should be ignored here
    "WizLab",
    "Desolation"
}


function explorer.level_to_gendepth(lvl)
    -- TODO could parse l, handle things like Hell:1
    for i, l in ipairs(explorer.generation_order) do
        if lvl:lower() == l:lower() then
            return i
        end
    end
    for i, l in ipairs(explorer.portal_order) do
        if lvl:lower() == l:lower() then
            return -i -- fairly hacky
        end
    end
    return nil
end

function explorer.branch_to_gendepth(b)
    if dgn.br_exists(b) then
        local depth = dgn.br_depth(b)
        if depth == 1 then
            return explorer.level_to_gendepth(b)
        else
            return explorer.level_to_gendepth(b .. ":" .. depth)
        end
    end
    return nil
end

function explorer.to_gendepth(depth)
    if type(depth) == "number" then return depth end
    if depth == "all" then return #explorer.generation_order end
    local num = string.match(depth, "^%d+$")
    if num ~= nil then return tonumber(depth) end
    local result = explorer.level_to_gendepth(depth)
    if result == nil then
        result = explorer.branch_to_gendepth(depth)
    end
    return result
end

-- a useful depth preset
explorer.zot_depth = explorer.to_gendepth("Zot")
assert(explorer.zot_depth ~= 0)

-- TODO: generalize, allow changing?
local out = function(s) if not explorer.quiet then crawl.stderr(s) end end

------------------------------
-- some generic list/string manipulation code

function explorer.collapse_dups(l)
    local result = {}
    util.sort(l)
    local cur = ""
    local count = 0
    for i, name in ipairs(l) do
        if name == cur then
            count = count + 1
        else
            if (cur ~= "") then
                result[#result + 1] = { cur, count }
            end
            cur = name
            count = 1
        end
    end
    if (cur ~= "") then
        result[#result + 1] = { cur, count }
    end
    for i, name in ipairs(result) do
        if result[i][2] > 1 then
            result[i] = result[i][1] .. " x" .. result[i][2]
        else
            result[i] = result[i][1]
        end
    end
    return result
end

function explorer.fancy_join(l, indent, width, sep, initial_indent)
    -- TODO: reflow around spaces?
    if #l == 0 then
        return ""
    end
    local lines = {}
    local cur_line = string.rep(" ", indent)
    for i, s in ipairs(l) do
        local full_s = s
        if (i ~= #l) then
            full_s = full_s .. sep
        end
        if #cur_line + #full_s > width and i ~= 1 then
            lines[#lines + 1] = cur_line
            cur_line = string.rep(" ", indent)
        end
        cur_line = cur_line .. full_s
    end
    lines[#lines + 1] = cur_line
    if not initial_indent and #lines > 0 and indent > 0 then
        lines[1] = string.sub(lines[1], indent + 1, #lines[1])
    end
    return table.concat(lines, "\n")
end

------------------------------
-- code for looking at items

function explorer.arts_only(item)
    return item.artefact
end

function explorer.item_ignore_boring(item)
    -- TODO:
    --  show gold totals?
    --  missiles - early on?
    --  unenchanted weapons/armour - early on?
    if item.is_useless then
        return false
    elseif item.base_type == "gold"
            or item.base_type == "missile" then
        return false
    elseif (item.base_type == "weapon" or item.base_type == "armour")
            and item.pluses() <= 0 and not item.branded then
        return false
    end
    return true
end

function explorer.catalog_items_setup()
    wiz.identify_all_items()
end

function explorer.catalog_items(pos, notable)
    local stack = dgn.items_at(pos.x, pos.y)
    if #stack > 0 then
        for i, item in ipairs(stack) do
            if explorer.item_notable(item) then
                notable[#notable + 1] = item.name()
            end
        end
    end
    stack = dgn.shop_inventory_at(pos.x, pos.y)
    if stack ~= nil and #stack > 0 then
        for i, item in ipairs(stack) do
            if explorer.item_notable(item[1]) then
                notable[#notable + 1] = item[1].name() .. " (cost: " .. item[2] ..")"
            end
        end
    end
end

------------------------------
-- code for looking at features

explorer.hell_branches = util.set{ "Geh", "Coc", "Dis", "Tar" }

function explorer.in_hell()
    return explorer.hell_branches[you.branch()]
end

function explorer.feat_interesting(feat_name)
    -- most features are pretty boring...
    if string.find(feat_name, "altar_") == 1 then
        return true
    elseif string.find(feat_name, "enter_") == 1 then -- could be more selective
        return true
    elseif feat_name == "transporter" or string.find(feat_name, "runed_") then
        return true
    end
    return false
end

function explorer.catalog_features_setup()
    you.enter_wizard_mode() -- necessary so that magic mapping behaves
                            -- correctly. this is implied by you.save = false,
                            -- so it shouldn't affect other tests in actual
                            -- test mode...
    wiz.map_level() -- abyss will break this call...
end

function explorer.catalog_features(pos, notable)
    local feat = dgn.grid(pos.x, pos.y)
    if explorer.feat_notable(dgn.feature_name(feat)) then
        notable[#notable + 1] = dgn.feature_desc_at(pos.x, pos.y, "A")
    end
end

------------------------------
-- code for looking at monsters

function explorer.mons_ignore_boring(mons)
    return (mons.unique
            or explorer.always_interesting_mons[mons.name]
            or mons.type_name == "player ghost"
            or mons.type_name == "pandemonium lord")
end

function explorer.mons_always_native(m, mi)
    return (mi:is_unique()
            or m.type_name == "player ghost"
            or mi:is_firewood())
end

function explorer.rare_ood(mi)
    local depth = mi:avg_local_depth()
    -- TODO: really, probability can't be interpreted without some sense of what
    -- the overall distribution for the branch is, so this is fairly heuristic
    local prob = mi:avg_local_prob()
    local ood_threshold = math.max(2, dgn.br_depth() / 3)
    return depth > you.depth() + ood_threshold and prob < 2
end

function explorer.feat_in_set(s)
    return function (f) if s[f] then return f else return nil end end
end

function explorer.describe_mons(mons)
    local mi = mons.get_info()
    -- TODO: weird distribution of labour between mons and moninfo, can this be
    -- cleaned up?
    -- TODO: does it make sense to use the same item notability function here?
    local feats = util.map(function (i) return "item:" .. i.name() end,
                    util.filter(explorer.item_notable, mons.get_inventory()))
    local force_notable = false
    if explorer.mons_feat_filter then
        feats = util.map(explorer.mons_feat_filter, feats) -- TODO don't brute force this
    end
    if mons.type_name == "dancing weapon" and #feats > 0 then
        -- don't repeat the dancing weapon's item, but if the item_notable check
        -- has deemed the item notable, make sure to show it.
        feats = { }
        force_notable = true
    end

    -- may or may not ever be useful -- builder OOD is an indicator of builder
    -- choices, not intrinsic monster quality, and it is possible for monsters
    -- to be chosen as OOD that are within range, e.g. you can sometimes get
    -- the same monster generating as OOD and not OOD at the same depth.
    -- if mons.has_prop("mons_is_ood") then
    --     feats[#feats + 1] = "builder OOD"
    -- end

    if explorer.rare_ood(mi) then
        feats[#feats + 1] = "OOD"
    end
    if not mons.in_local_population and not explorer.mons_always_native(mons, mi)
        and not util.set(explorer.portal_order)[you.branch()] then
        -- this is a different sense of "native" than used internally to crawl,
        -- in that it includes anything that would normally generate in a
        -- branch. The internal sense is just supposed to be about which
        -- monsters "live" in a particular branch, which is covered as well.
        --
        -- portals are ignored because their monsters are mostly determined by
        -- vaults, so too many things show up as "non-native".
        feats[#feats + 1] = "non-native"
    end
    if explorer.mons_feat_filter then
        feats = util.map(explorer.mons_feat_filter, feats)
    end
    -- the `item:` prefix is really only there for filtering purposes, remove it
    feats = util.map(function (s)
            return s:find("item:") == 1 and s:sub(6) or s
        end, feats)
    local feat_string = ""
    if #feats > 0 then
        feat_string = " (" .. table.concat(feats, ", ") .. ")"
    end

    local name_string = "bug"

    if mons.type_name == "player ghost" then
        name_string = mons.type_name
    else
        name_string = mons.name
    end
    if (force_notable or explorer.mons_notable(mons) or #feats > 0) then
        return name_string .. feat_string
    else
        return nil
    end
end

function explorer.catalog_monsters_setup()
    wiz.identify_all_items()
end

function explorer.catalog_monsters(pos, notable)
    local mons = dgn.mons_at(pos.x, pos.y)
    if mons then
        notable[#notable + 1] = explorer.describe_mons(mons)
    end
end

------------------------------
-- code for looking at vaults

function explorer.catalog_vaults()
    -- this list has already been joined into a string, split it
    -- TODO: just pass the list directly to lua...
    local vaults = debug.vault_names()
    first_split = crawl.split(vaults, " and ")
    if #first_split < 2 then
        return first_split
    end
    second_split = crawl.split(first_split[1], ", ")
    second_split[#second_split + 1] = first_split[2]
    return second_split
end

function explorer.catalog_vaults_raw()
    return { debug.vault_names() }
end

------------------------------
-- general code for building highlight descriptions

-- could do this by having each category register itself...
explorer.catalog_funs =     {vaults    = explorer.catalog_vaults,
                             vaults_raw= explorer.catalog_vaults_raw,
                             items     = explorer.catalog_items_setup,
                             monsters  = explorer.catalog_monsters_setup,
                             features  = explorer.catalog_features_setup}

explorer.catalog_pos_funs = {items     = explorer.catalog_items,
                             monsters  = explorer.catalog_monsters,
                             features  = explorer.catalog_features}

explorer.catalog_names =    {vaults    = "   Vaults: ",
                             vaults_raw= "   Vaults: ",
                             items     = "    Items: ",
                             monsters  = " Monsters: ",
                             features  = " Features: "}

-- fairly subjective, some of these should probably be relative to depth
explorer.dangerous_monsters = {
        "ancient lich",
        "dread lich",
        "orb of fire",
        "royal mummy",
        "Hell Sentinel",
        "Ice Fiend",
        "Brimstone Fiend",
        "Tzitzimitl",
        "shard shrike",
        "caustic shrike",
        "iron giant",
        "juggernaut",
        "Killer Klown",
        "Orb Guardian",
        "curse toe",
    }

function explorer.reset_to_defaults()
    --explorer.mons_feat_filter = explorer.feat_in_set(util.set({ "OOD" }))
    explorer.mons_feat_filter = nil
    explorer.mons_notable = explorer.mons_ignore_boring

    explorer.always_interesting_mons = util.set(explorer.dangerous_monsters)
    explorer.item_notable = explorer.item_ignore_boring
    explorer.feat_notable = explorer.feat_interesting
end

function explorer.make_highlight(h, key, hide_empty)
    if not h[key] then
        return ""
    end
    local name = explorer.catalog_names[key]
    local l = h[key]
    if key ~= "vaults" then
        l = explorer.collapse_dups(l)
    end
    s = explorer.fancy_join(l, #name, 80, ", ")
    if (#s == 0 and hide_empty) then
        return ""
    end
    return name .. s
end

function explorer.catalog_all_positions(cats, highlights)
    funs = { }
    for _, c in ipairs(cats) do
        if explorer.catalog_pos_funs[c] ~= nil then
            if highlights[c] == nil then highlights[c] = { } end
            funs[#funs + 1] = explorer.catalog_pos_funs[c]
        end
    end
    -- don't bother if there are no positional funs to run
    if #funs == 0 then return end

    local gxm, gym = dgn.max_bounds()
    for p in iter.rect_iterator(dgn.point(1,1), dgn.point(gxm-2, gym-2)) do
        for _,c in ipairs(cats) do
            if explorer.catalog_pos_funs[c] ~= nil then
                explorer.catalog_pos_funs[c](p, highlights[c])
            end
        end
    end
    for _, c in ipairs(cats) do
        if #highlights[c] == 0 then highlights[c] = nil end
    end
end

function explorer.catalog_current_place(lvl, to_show, hide_empty)
    highlights = {}
    -- setup functions and anything that doesn't require looking at positions
    for _, cat in ipairs(to_show) do
        if explorer.catalog_funs[cat] ~= nil then
            highlights[cat] = explorer.catalog_funs[cat]()
        end
    end
    -- explorer categories that collect information from map positions
    explorer.catalog_all_positions(to_show, highlights)

    -- output
    if not explorer.quiet then
        h_l = { }
        for _, cat in ipairs(to_show) do
            if crawl.seen_hups() > 0 then
                break
            end
            h_l[#h_l+1] = explorer.make_highlight(highlights, cat, hide_empty)
        end
        h_l = util.filter(function (a) return #a > 0 end, h_l)
        if #h_l > 0 or not hide_empty then
            out(lvl .. " highlights:")
            for _, h in ipairs(h_l) do
                out(h)
            end
        end
    end
    return highlights
end

function explorer.catalog_place(i, lvl, cats_to_show, show_level_fun)
    local result = nil
    if (dgn.br_exists(string.match(lvl, "[^:]+"))) then
        debug.goto_place(lvl)
        debug.generate_level()
        local old_quiet = explorer.quiet
        if show_level_fun ~= nil and not show_level_fun(i) then
            explorer.quiet = true
        end
        result = explorer.catalog_current_place(lvl, cats_to_show, true)
        explorer.quiet = old_quiet
    end
    return result
end

function explorer.catalog_portals(i, lvl, cats_to_show, show_level_fun)
    local result = { }
    current_where = you.where()
    for j,port in ipairs(explorer.portal_order) do
        if crawl.seen_hups() > 0 then
            break
        end
        if you.where() == dgn.level_name(dgn.br_entrance(port)) then
            result[port] = explorer.catalog_place(-j, port, cats_to_show, show_level_fun)
            -- restore the level, in case there are multiple portals from a
            -- single level
            debug.goto_place(current_where)
        end
    end
    return result
end

-- a bit redundant with mapstat?
function explorer.catalog_dungeon(max_depth, cats_to_show, show_level_fun)
    local result = {}
    dgn.reset_level()
    debug.reset_player_data()
    debug.dungeon_setup()
    for i,lvl in ipairs(explorer.generation_order) do
        if i > max_depth then break end
        result[lvl] = explorer.catalog_place(i, lvl, cats_to_show, show_level_fun)
        if crawl.seen_hups() > 0 then
            break
        end
        if result[lvl] ~= nil then
            -- only check portals if the place was built
            local portals = explorer.catalog_portals(i, lvl, cats_to_show, show_level_fun)
            for port, cat in pairs(portals) do
                result[port] = cat
            end
        end
    end
    return result
end

function explorer.catalog_seed(seed, depth, cats, show_level_fun, describe_cat)
    seed_used = debug.reset_rng(seed)
    if describe_cat then
        out(describe_cat(seed))
    else
        out("Catalog for seed " .. seed ..
            " (" .. table.concat(cats, ", ") .. "):")
    end
    -- this will return early on hup, so the caller should consider checking
    -- for hups and printing an appropriate message
    return explorer.catalog_dungeon(depth, cats, show_level_fun)
end

explorer.internal_categories = { "vaults_raw" }
explorer.available_categories = { "vaults", "items", "features", "monsters" }
function explorer.is_category(c)
    return util.set(explorer.available_categories)[c] or
           util.set(explorer.internal_categories)[c]
end

-- `seeds` is an array of seeds. If you pass numbers in with this, keep in mind
-- that the max int limit is rather complicated and less than 64bits, because
-- these are doubles behind the scenes. This code will accept strings of digits
-- instead, which is always safe.
function explorer.catalog_seeds(seeds, depth, cats, show_level_fun, describe_cat)
    if depth == nil then depth = #explorer.generation_order end
    if cats == nil then
        cats = util.set(explorer.available_cats)
    end
    for _, i in ipairs(seeds) do
        if crawl.seen_hups() > 0 then
            break
        end
        if _ > 1 then out("") end -- generates a newline for stderr output
        explorer.catalog_seed(i, depth, cats, show_level_fun, describe_cat)
    end
end

-- example custom catalog function
function explorer.catalog_arts(seeds, depth)
    explorer.item_notable = explorer.arts_only
    local run1 = explorer.catalog_dungeon(seeds, depth, { "items" },
        function(seed) out("Artefact catalog for seed " .. seed .. ":") end)
    explorer.item_notable = explorer.ignore_boring
end

explorer.reset_to_defaults()