From 9841ce45ea91c1ad394d706fd748ffc1fd9d9de0 Mon Sep 17 00:00:00 2001 From: Darshan Shaligram Date: Sat, 9 Jan 2010 01:21:44 +0530 Subject: Add -script option to Crawl to run a Lua script. Scripts are similar to tests, but can be parameterised. Add a script to generate 150 level at a named place and report on all the monsters generated there. --- crawl-ref/.gitignore | 1 + crawl-ref/source/ctest.cc | 57 ++++++++--- crawl-ref/source/dat/clua/test.lua | 45 +++++++++ crawl-ref/source/dat/clua/util.lua | 14 ++- crawl-ref/source/initfile.cc | 20 +++- crawl-ref/source/l_debug.cc | 16 +++ crawl-ref/source/l_file.cc | 19 +++- crawl-ref/source/scripts/place-population.lua | 136 ++++++++++++++++++++++++++ crawl-ref/source/state.h | 2 + crawl-ref/source/test/snake-rune.lua | 9 +- 10 files changed, 297 insertions(+), 22 deletions(-) create mode 100644 crawl-ref/source/scripts/place-population.lua diff --git a/crawl-ref/.gitignore b/crawl-ref/.gitignore index b207c2b3bb..8f227bbba9 100644 --- a/crawl-ref/.gitignore +++ b/crawl-ref/.gitignore @@ -33,6 +33,7 @@ morgue *.map map.dump mapgen.log +*.out *.stat # Tile copied files diff --git a/crawl-ref/source/ctest.cc b/crawl-ref/source/ctest.cc index cfff5edecb..dcfd78fe98 100644 --- a/crawl-ref/source/ctest.cc +++ b/crawl-ref/source/ctest.cc @@ -17,10 +17,13 @@ #if DEBUG_DIAGNOSTICS || DEBUG_TESTS #include "clua.h" +#include "cluautil.h" #include "dlua.h" #include "files.h" +#include "libutil.h" #include "maps.h" #include "message.h" +#include "ng-init.h" #include "state.h" #include "stuff.h" @@ -30,9 +33,11 @@ namespace crawl_tests { const std::string test_dir = "test"; + const std::string script_dir = "scripts"; const std::string test_player_name = "Superbug99"; const species_type test_player_species = SP_HUMAN; const job_type test_player_job = JOB_FIGHTER; + const char *activity = "test"; int ntests = 0; int nsuccess = 0; @@ -52,22 +57,31 @@ namespace crawl_tests int crawl_begin_test(lua_State *ls) { - mprf(MSGCH_PROMPT, "Starting tests: %s", luaL_checkstring(ls, 1)); + mprf(MSGCH_PROMPT, "Starting %s: %s", + activity, + luaL_checkstring(ls, 1)); lua_pushnumber(ls, ++ntests); return (1); } int crawl_test_success(lua_State *ls) { - mprf(MSGCH_PROMPT, "Test success: %s", luaL_checkstring(ls, 1)); + if (!crawl_state.script) + mprf(MSGCH_PROMPT, "Test success: %s", luaL_checkstring(ls, 1)); lua_pushnumber(ls, ++nsuccess); return (1); } + int crawl_script_args(lua_State *ls) + { + return clua_stringtable(ls, crawl_state.script_args); + } + static const struct luaL_reg crawl_test_lib[] = { { "begin_test", crawl_begin_test }, { "test_success", crawl_test_success }, + { "script_args", crawl_script_args }, { NULL, NULL } }; @@ -76,6 +90,7 @@ namespace crawl_tests lua_stack_cleaner clean(dlua); luaL_openlib(dlua, "crawl", crawl_test_lib, 0); dlua.execfile("clua/test.lua", true, true); + initialise_branch_depths(); } bool is_test_selected(const std::string &testname) @@ -98,11 +113,12 @@ namespace crawl_tests return; ++ntests; - mprf(MSGCH_DIAGNOSTICS, "Running test %d: %s", - ntests, file.c_str()); + mprf(MSGCH_DIAGNOSTICS, "Running %s %d: %s", + activity, ntests, file.c_str()); flush_prev_message(); - const std::string path(catpath(test_dir, file)); + const std::string path( + catpath(crawl_state.script? script_dir : test_dir, file)); dlua.execfile(path.c_str(), true, false); if (dlua.error.empty()) ++nsuccess; @@ -113,6 +129,9 @@ namespace crawl_tests // Assumes curses has already been initialized. bool run_tests(bool exit_on_complete) { + if (crawl_state.script) + activity = "script"; + flush_prev_message(); run_map_preludes(); @@ -120,9 +139,10 @@ namespace crawl_tests init_test_bindings(); - if (crawl_state.tests_selected.empty() - || (crawl_state.tests_selected[0].find("makeitem") != - std::string::npos)) + if ((crawl_state.tests_selected.empty() + || (crawl_state.tests_selected[0].find("makeitem") != + std::string::npos)) + && !crawl_state.script) { makeitem_tests(); } @@ -130,22 +150,33 @@ namespace crawl_tests // Get a list of Lua files in test. Order of execution of // tests should be irrelevant. const std::vector tests( - get_dir_files_ext(test_dir, ".lua")); + get_dir_files_ext(crawl_state.script? script_dir : test_dir, + ".lua")); std::for_each(tests.begin(), tests.end(), run_test); + if (failures.empty() && !ntests && crawl_state.script) + failures.push_back( + file_error( + "Script setup", + "No scripts found matching " + + comma_separated_line(crawl_state.tests_selected.begin(), + crawl_state.tests_selected.end(), + ", ", + ", "))); + if (exit_on_complete) { cio_cleanup(); for (int i = 0, size = failures.size(); i < size; ++i) { const file_error &fe(failures[i]); - fprintf(stderr, "Test error: %s\n", - fe.second.c_str()); + fprintf(stderr, "%s error: %s\n", + activity, fe.second.c_str()); } const int code = failures.empty() ? 0 : 1; - end(code, false, "%d tests, %d succeeded, %d failed", - ntests, nsuccess, failures.size()); + end(code, false, "%d %ss, %d succeeded, %d failed", + ntests, activity, nsuccess, failures.size()); } return (failures.empty()); } diff --git a/crawl-ref/source/dat/clua/test.lua b/crawl-ref/source/dat/clua/test.lua index 22a4005d20..03a23842e5 100644 --- a/crawl-ref/source/dat/clua/test.lua +++ b/crawl-ref/source/dat/clua/test.lua @@ -31,4 +31,49 @@ function test.level_contains_item(item) end end return false +end + +function test.level_monster_iterator(filter) + return iter.mons_rect_iterator(dgn.point(1, 1), + dgn.point(dgn.GXM - 2, dgn.GYM - 2), + filter) +end + +test.is_down_stair = dgn.feature_set_fn("stone_stairs_down_i", + "stone_stairs_down_ii", + "stone_stairs_down_iii") + +function test.level_has_down_stair() + for y = 1, dgn.GYM - 2 do + for x = 1, dgn.GXM - 2 do + local dfeat = dgn.grid(x, y) + if test.is_down_stair(dfeat) then + return true + end + end + end + return false +end + +function test.deeper_place_from(place) + if test.level_has_down_stair() then + local _, _, branch, depth = string.find(place, "(%w+):(%d+)") + return branch .. ":" .. (tonumber(depth) + 1) + end + return nil +end + +util.namespace('script') + +function script.simple_args() + local args = crawl.script_args() + return util.filter(function (arg) + return string.find(arg, '-') ~= 1 + end, + args) +end + +function script.usage(ustr) + ustr = string.gsub(string.gsub(ustr, "^%s+", ""), "%s+$", "") + error("\n" .. ustr) end \ No newline at end of file diff --git a/crawl-ref/source/dat/clua/util.lua b/crawl-ref/source/dat/clua/util.lua index d6e842fa7e..8dd0b35eba 100644 --- a/crawl-ref/source/dat/clua/util.lua +++ b/crawl-ref/source/dat/clua/util.lua @@ -56,7 +56,7 @@ end -- Returns a list of the keys in the given map. function util.keys(map) local keys = { } - for key, _ in pairs(ziggurat_builder_map) do + for key, _ in pairs(map) do table.insert(keys, key) end return keys @@ -65,12 +65,22 @@ end -- Returns a list of the values in the given map. function util.values(map) local values = { } - for _, value in pairs(ziggurat_builder_map) do + for _, value in pairs(map) do table.insert(values, value) end return values end +-- Returns a list of lists built from the given map, each sublist being +-- in the form { key, value } for each key-value pair in the map. +function util.pairs(map) + local mappairs = { } + for key, value in pairs(map) do + table.insert(mappairs, { key, value }) + end + return mappairs +end + -- Creates a string of the elements in list joined by separator. function util.join(sep, list) return table.concat(list, sep) diff --git a/crawl-ref/source/initfile.cc b/crawl-ref/source/initfile.cc index c3208c425b..f5e0296422 100644 --- a/crawl-ref/source/initfile.cc +++ b/crawl-ref/source/initfile.cc @@ -3472,6 +3472,7 @@ enum commandline_option_type { CLO_MAPSTAT, CLO_ARENA, CLO_TEST, + CLO_SCRIPT, CLO_BUILDDB, CLO_HELP, CLO_VERSION, @@ -3485,8 +3486,8 @@ enum commandline_option_type { static const char *cmd_ops[] = { "scores", "name", "species", "job", "plain", "dir", "rc", "rcdir", "tscores", "vscores", "scorefile", "morgue", "macro", - "mapstat", "arena", "test", "builddb", "help", "version", "save-version", - "extra-opt-first", "extra-opt-last", + "mapstat", "arena", "test", "script", "builddb", "help", "version", + "save-version", "extra-opt-first", "extra-opt-last" }; const int num_cmd_ops = CLO_NOPS; @@ -3792,6 +3793,21 @@ bool parse_args( int argc, char **argv, bool rc_only ) } break; + case CLO_SCRIPT: + crawl_state.test = true; + crawl_state.script = true; + if (current < argc - 1) + { + crawl_state.tests_selected = split_string(",", next_arg); + for (int extra = current + 2; extra < argc; ++extra) + crawl_state.script_args.push_back(argv[extra]); + current = argc; + } + else + end(1, false, + "-script must specify comma-separated script names"); + break; + case CLO_BUILDDB: if (next_is_param) return (false); diff --git a/crawl-ref/source/l_debug.cc b/crawl-ref/source/l_debug.cc index f500b631ce..8fef92b954 100644 --- a/crawl-ref/source/l_debug.cc +++ b/crawl-ref/source/l_debug.cc @@ -9,6 +9,7 @@ #include "l_libs.h" #include "beam.h" +#include "branch.h" #include "chardump.h" #include "coordit.h" #include "dungeon.h" @@ -24,16 +25,31 @@ #include "wiz-dgn.h" // WARNING: This is a very low-level call. +// +// Usage: goto_place("placename", ) +// "placename" is the name of the place as used in maps, such as "Lair:2", +// "Vault:$", etc. +// +// If is specified, the entrance point of +// the branch specified in place_name is bound to the given level in the +// parent branch (the entrance level should be 1-based). This can be helpful +// when testing scenarios that depend on the absolute depth of the current +// place. LUAFN(debug_goto_place) { try { const level_id id = level_id::parse_level_id(luaL_checkstring(ls, 1)); + const int bind_entrance = + lua_isnumber(ls, 2)? luaL_checkint(ls, 2) : -1; you.level_type = id.level_type; if (id.level_type == LEVEL_DUNGEON) { you.where_are_you = static_cast(id.branch); you.your_level = absdungeon_depth(id.branch, id.depth); + + if (bind_entrance != -1) + branches[you.where_are_you].startdepth = bind_entrance; } } catch (const std::string &err) diff --git a/crawl-ref/source/l_file.cc b/crawl-ref/source/l_file.cc index bde31cb558..fae724fcfd 100644 --- a/crawl-ref/source/l_file.cc +++ b/crawl-ref/source/l_file.cc @@ -168,6 +168,23 @@ LUAFN(_file_datadir_files) return clua_stringtable(ls, files); } +LUAFN(_file_writefile) +{ + const std::string fname(luaL_checkstring(ls, 1)); + FILE *f = fopen(fname.c_str(), "w"); + if (f) + { + fprintf(f, "%s", luaL_checkstring(ls, 2)); + fclose(f); + lua_pushboolean(ls, true); + } + else + { + lua_pushboolean(ls, false); + } + return (1); +} + static const struct luaL_reg file_dlib[] = { { "marshall", file_marshall }, @@ -176,7 +193,7 @@ static const struct luaL_reg file_dlib[] = { "unmarshall_number", file_unmarshall_number }, { "unmarshall_string", file_unmarshall_string }, { "unmarshall_fn", file_unmarshall_fn }, - + { "writefile", _file_writefile }, { "datadir_files", _file_datadir_files }, { NULL, NULL } }; diff --git a/crawl-ref/source/scripts/place-population.lua b/crawl-ref/source/scripts/place-population.lua new file mode 100644 index 0000000000..9a95616cdc --- /dev/null +++ b/crawl-ref/source/scripts/place-population.lua @@ -0,0 +1,136 @@ +-- Counts monsters in a specify level, or level range. +local niters = 150 +local output_file = "monster-report.out" + +local excluded_things = util.set({ "plant", "fungus", "bush" }) + +local function count_monsters_at(place, set) + debug.goto_place(place) + test.regenerate_level() + + local monster_set = set or { } + for mons in test.level_monster_iterator() do + local mname = mons.name + if not excluded_things[mname] then + local count = monster_set[mname] or 0 + monster_set[mname] = count + 1 + end + end + return monster_set +end + +local function report_monster_counts_at(place, mcount_map) + local text = '' + text = text .. "\n-------------------------------------------\n" + text = text .. place .. " monsters per-level\n" + text = text .. "-------------------------------------------\n" + + local monster_counts = util.pairs(mcount_map) + table.sort(monster_counts, function (a, b) + return a[2] > b[2] + end) + + local total = 0 + for _, monster_pop in ipairs(monster_counts) do + total = total + monster_pop[2] + end + + for _, monster_pop in ipairs(monster_counts) do + text = text .. string.format("%6.2f (%6.2f%%) %s\n", + monster_pop[2] * 1.0 / niters, + monster_pop[2] * 100.0 / total, + monster_pop[1]) + end + return text +end + +local function report_monster_counts(mcounts) + local places = util.keys(mcounts) + table.sort(places) + + local text = '' + 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 count_monsters_from(start_place, end_place) + local place = start_place + local mcounts = { } + while place do + local mset = { } + for i = 1, niters do + crawl.mesclr() + crawl.mpr("Counting monsters at " .. place .. " (" .. + i .. "/" .. niters .. ")") + count_monsters_at(place, mset) + end + mcounts[place] = mset + + if place == end_place then + break + end + + place = test.deeper_place_from(place) + end + + report_monster_counts(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 + 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 Shoal:1 Shoal: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. +]]) + end + return args[1], args[2] or args[1] +end + +set_branch_depths() +local lstart, lend = start_end_levels() +count_monsters_from(lstart, lend) diff --git a/crawl-ref/source/state.h b/crawl-ref/source/state.h index 387b0e8b33..ffb9f56429 100644 --- a/crawl-ref/source/state.h +++ b/crawl-ref/source/state.h @@ -50,8 +50,10 @@ struct game_state // suspended. bool test; // Set if we want to run self-tests and exit. + bool script; // Set if we want to run a Lua script and exit. bool build_db; // Set if we want to rebuild the db and exit. std::vector tests_selected; // Tests to be run. + std::vector script_args; // Arguments to scripts. bool unicode_ok; // Is unicode support available? diff --git a/crawl-ref/source/test/snake-rune.lua b/crawl-ref/source/test/snake-rune.lua index 56ea8b8273..dbf973afa7 100644 --- a/crawl-ref/source/test/snake-rune.lua +++ b/crawl-ref/source/test/snake-rune.lua @@ -1,3 +1,7 @@ +-- Walks down the dungeon to Snake:$ and tests for the existence of the rune. +-- This is a more exhaustive test than rune-gen.lua since it generates all +-- intermediate levels and activates the dungeon connectivity code. + local niters = 500 local current_iter = 0 @@ -7,9 +11,6 @@ local branch_entrance_feats = { } local junk_feat_fn = dgn.feature_set_fn("rock_wall", "floor", "stone_wall") -local down_stair_fn = dgn.feature_set_fn("stone_stairs_down_i", - "stone_stairs_down_ii", - "stone_stairs_down_iii") local function thing_exists_fn(thing) return function() @@ -38,7 +39,7 @@ local function visit_branch_end_from(start, stair_places, final_predicate) end end - if down_stair_fn(dfeat) then + if test.is_down_stair(dfeat) then table.insert(downstairs, dgn.point(x, y)) end end -- cgit v1.2.3-54-g00ecf