-- Counts monsters in a specified level or level range. local niters = 150 local output_file = "monster-report.out" local excluded_things = util.set({ "plant", "fungus", "bush" }) local use_random_maps = true local multiple_levels = false local start_level = nil local end_level = nil local ignore_uniques = true local group_uniques = false local sort_type = "XP" local function canonical_name(mons) local shapeshifter = mons.shapeshifter if shapeshifter then return shapeshifter end local dancing_weapon = mons.dancing_weapon if dancing_weapon then return dancing_weapon end local mname = mons.name if string.find(mname, 'very ugly thing$') then return "very ugly thing" end if string.find(mname, 'ugly thing$') then return "ugly thing" end local _, _, hydra_suffix = string.find(mname, '.*-headed (.*)') if hydra_suffix then mname = hydra_suffix end return mname end local function count_monsters_at(place, set) debug.goto_place(place) test.regenerate_level(nil, use_random_maps) debug.reveal_mimics() local monsters_here = set or { } for mons in test.level_monster_iterator() do local unique = mons.unique if not ignore_uniques or not unique then local mname = canonical_name(mons) if unique and group_uniques then mname = "[UNIQUE]" end if not excluded_things[mname] then local mstat = monsters_here[mname] or { count = 0, exp = 0 } mstat.count = mstat.count + 1 mstat.exp = mstat.exp + mons.experience monsters_here[mname] = mstat end end end return monsters_here end local function report_monster_counts_at(place, mcount_map) local text = '' text = text .. "\n------------------------------------------------------------\n" text = text .. place .. " monsters, " .. niters .. " iterations (" local extras = util.join(", ", { use_random_maps and "using random maps" or "NO random maps", ignore_uniques and "NO uniques" or "uniques included" }) text = text .. extras .. ")\n" text = text .. "------------------------------------------------------------\n" local monster_counts = util.pairs(mcount_map) table.sort(monster_counts, function (a, b) -- Further sort options can be added later if desired. Default is XP, -sort_count sorts by count/percentage instead. if sort_type == 'count' then return a[2].total > b[2].total else return a[2].etotal > b[2].etotal end end) local total = 0 for _, monster_pop in ipairs(monster_counts) do if monster_pop[1] ~= 'TOTAL' then total = total + monster_pop[2].total end end text = text .. "Count | XPAv | XPMin | XPMax | XPSigma | CountMin | CountMax | CountSigma | % | Monster\n" for _, monster_pop in ipairs(monster_counts) do text = text .. string.format("%6.2f | %5d | %5d | %5d | %7d | %8d | %8d | %10.2f | %6.2f%% | %s\n", monster_pop[2].mean, monster_pop[2].emean, monster_pop[2].emin, monster_pop[2].emax, monster_pop[2].esigma, monster_pop[2].min, monster_pop[2].max, monster_pop[2].sigma, monster_pop[2].total * 100.0 / total, monster_pop[1]) end return text end local function report_monster_counts(summary, mcounts) local places = util.keys(mcounts) table.sort(places) local text = '' if multiple_levels then text = text .. report_monster_counts_at(start_level .. " to " .. end_level, summary) end for _, place in ipairs(places) do text = text .. report_monster_counts_at(place, mcounts[place]) end file.writefile(output_file, text) crawl.mpr("Dumped monster stats to " .. output_file) end local function calculate_monster_stats(iter_mpops) local function blank_stats() return { total = 0, max = 0, min = 10 ^ 10, etotal = 0, emax = 0, emin = 10 ^ 10, eiters = { }, pops = { } } end local function update_mons_stats(stat, instance_stat) local function update_total_max_min(value, total, max, min) stat[total] = stat[total] + value if value > stat[max] then stat[max] = value end if value < stat[min] then stat[min] = value end end update_total_max_min(instance_stat.count, 'total', 'max', 'min') update_total_max_min(instance_stat.exp, 'etotal', 'emax', 'emin') table.insert(stat.pops, instance_stat.count) table.insert(stat.eiters, instance_stat.exp) return stat end local function sum(list) local total = 0 for _, num in ipairs(list) do total = total + num end return total end local function combine_stats(target, src) local total = sum(src.pops) local etotal = sum(src.eiters) table.insert(target.pops, total) table.insert(target.eiters, etotal) target.total = target.total + total target.etotal = target.etotal + etotal target.max = math.max(target.max, total) target.min = math.min(target.min, total) target.emax = math.max(target.emax, etotal) target.emin = math.min(target.emin, etotal) end local final_stats = { TOTAL = blank_stats() } local n = #iter_mpops for _, mpop in ipairs(iter_mpops) do local global_stat = blank_stats() for mons, istat in pairs(mpop) do local mstats = final_stats[mons] or blank_stats() final_stats[mons] = update_mons_stats(mstats, istat) update_mons_stats(global_stat, istat) end combine_stats(final_stats.TOTAL, global_stat) end local function calc_mean_sigma(total, instances) local mean = total / n local totaldelta2 = 0 for _, count in ipairs(instances) do local delta = count - mean totaldelta2 = totaldelta2 + delta * delta end -- There will be no instances with count 0, handle those: for extra = 1, (n - #instances) do totaldelta2 = totaldelta2 + mean * mean end local sigma = math.sqrt(totaldelta2 / n) return mean, sigma end for mons, stat in pairs(final_stats) do stat.mean, stat.sigma = calc_mean_sigma(stat.total, stat.pops) stat.emean, stat.esigma = calc_mean_sigma(stat.etotal, stat.eiters) end return final_stats end -- Given a table mapping places to lists of niters sets of monster counts -- and a list of places, returns one summary stats table. local function calculate_summary_stats(place_totals, places) local function combine_iter_results(master, inst) for mons, stat in pairs(inst) do local mstat = master[mons] or { count = 0, exp = 0 } mstat.count = mstat.count + stat.count mstat.exp = mstat.exp + stat.exp master[mons] = mstat end end local iters = { } for i = 1, niters do local iterstats = { } for _, place in ipairs(places) do combine_iter_results(iterstats, place_totals[place][i]) end table.insert(iters, iterstats) end return calculate_monster_stats(iters) end local function count_monsters_from(start_place, end_place) multiple_levels = start_place ~= end_place local place = start_place local mcounts = { } local summary = { } local places = { } local place_totals = { } while place do table.insert(places, place) local mset = { } local iter_mpops = { } for i = 1, niters do crawl.message("Counting monsters at " .. place .. ", depth: " .. you.absdepth() .. " (" .. i .. "/" .. niters .. ")") local res = count_monsters_at(place) table.insert(iter_mpops, res) if multiple_levels then local totals = place_totals[place] or { } table.insert(totals, res) place_totals[place] = totals end end mcounts[place] = calculate_monster_stats(iter_mpops) if place == end_place then break end place = test.deeper_place_from(place) end if multiple_levels then summary = calculate_summary_stats(place_totals, places) end report_monster_counts(summary, mcounts) end local function parse_resets(resets) local pieces = crawl.split(resets, ",") local resets = { } for _, p in ipairs(pieces) do local _, _, place, depth = string.find(p, "^(.+)=(%d+)$") table.insert(resets, { place, tonumber(depth) }) end return resets end local function branch_resets() local args = crawl.script_args() local resets = { } for _, arg in ipairs(args) do local _, _, rawresets = string.find(arg, "^-reset=(.*)") if rawresets then util.append(resets, parse_resets(rawresets)) end if arg == '-nomaps' then use_random_maps = false end if arg == '-uniques' then ignore_uniques = false end if arg == '-groupuniques' then group_uniques = true end if arg == '-sort_count' then sort_type = "count" end local _, _, optiters = string.find(arg, "^-n=(%d+)") if optiters then niters = tonumber(optiters) end end return resets end local function set_branch_depths() for _, reset in ipairs(branch_resets()) do debug.goto_place(reset[1], reset[2]) end end local function start_end_levels() local args = script.simple_args() if #args == 0 then script.usage([[ Usage: place-population [] For instance: place-population Shoals:1 Shoals:5 place-population Lair:3 You may optionally force branches to have entrances at specific places with: place-population -reset=Lair:1=8,Snake:1=3 Snake:5 With the general form: -reset==[,=,...] where is a valid place name as used in .des files, and is the depth of the branch's entrance in its parent branch. Thus -reset=Snake:1=3 implies that the entrance of the Snake Pit is assumed to be on Lair:3. You can also disable the use of random maps during level generation with: -nomaps Uniques are ignored by default; you can enable counts for uniques with: -uniques You can group all uniques together with -groupuniques And you can set the number of iterations (default 150) with: -n=[newnumber] ]]) end return args[1], args[2] or args[1] end set_branch_depths() start_level, end_level = start_end_levels() count_monsters_from(start_level, end_level)