--[[ LEARNING MAPPER Author: Nick Gammon Date: 24th January 2020 PERMISSION TO DISTRIBUTE Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. LIMITATION OF LIABILITY The software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the software. ------------------------------------------------------------------------- EXPOSED FUNCTIONS set_line_type (linetype, contents) --> set this current line to be definitely linetype with option contents set_line_type_contents (linetype, contents) --> sets the content for to be (for example, if you get a room name on a prompt line) set_not_line_type (linetype) --> set this current line to be definitely not linetype (can call for multiple line types) set_area_name (name) --> sets the name of the area you are in set_uid (uid) --> sets a string to be hashed as the UID for this room do_not_deduce_line_type (linetype) --> do not deduce (do Bayesian analysis) on this type of line - has to be set by set_line_type deduce_line_type (linetype) --> deduce this line type (cancels do_not_deduce_line_type) get_last_line_type () --> get the previous line type as deduced or set by set_line_type get_this_line_type () --> get the current overridden line type (from set_line_type) set_config_option (name, value) --> set a mapper configuration value of to get_config_option (name) --> get the current configuration value of get_corpus () --> get the corpus (serialized table) get_stats () --> get the training stats (serialized table) get_database () --> get the mapper database (rooms table) (serialized table) get_config () --> get the configuration options (serialized table) get_current_room () --> gets the current room's UID and room information (serialized table) set_room_extras (uid, extras) --> sets extra information for the room (user-supplied) extras must be a string which serializes into a variable including a table eg. " { a = 42, b = 666, c = 'Kobold' } " eg. config = CallPlugin ("99c74b2685e425d3b6ed6a7d", "get_config") CallPlugin ("99c74b2685e425d3b6ed6a7d", "set_line_type", "exits") CallPlugin ("99c74b2685e425d3b6ed6a7d", "do_not_deduce_line_type", "exits") Note: The plugin ID is fixed as it is set in the Learning_Mapper.xml file near the top: id="99c74b2685e425d3b6ed6a7d" --]] LEARNING_MAPPER_LUA_VERSION = 2.1 -- version must agree with plugin version -- The probability (in the range 0.0 to 1.0) that a line has to meet to be considered a certain line type. -- The higher, the stricter the requirement. -- Default of 0.7 seems to work OK, but you could tweak that. PROBABILITY_CUTOFF = 0.7 -- other modules needed by this plugin require "mapper" require "serialize" require "copytable" require "commas" require "tprint" require "pairsbykeys" -- our two windows win = "window_type_info_" .. GetPluginID () learn_window = "learn_dialog_" .. GetPluginID () -- ----------------------------------------------------------------- -- Handlers for when a line-type changes -- ----------------------------------------------------------------- description_styles = { } exits_styles = { } room_name_styles = { } UNKNOWN_DUPLICATE_ROOM = string.rep ("F", 25) -- dummy UID DEBUGGING = true function set_last_direction_moved (where) last_direction_moved = where DEBUG ("SET: last_direction_moved: " .. tostring (where)) end -- set_last_direction_moved function get_last_direction_moved () DEBUG ("get: last_direction_moved: " .. tostring (last_direction_moved)) return last_direction_moved end -- get_last_direction_moved function set_from_room (where) from_room = where DEBUG ("SET: from_room: " .. fixuid (tostring (where))) end -- set_from_room function get_from_room (f) if f then DEBUG ("get: from_room: " .. fixuid (tostring (from_room)) .. " (" .. f .. ")") else DEBUG ("get: from_room: " .. fixuid (tostring (from_room))) end -- if return from_room end -- get_from_room function set_current_room (where) current_room = where DEBUG ("SET: current_room: " .. fixuid (tostring (where))) end -- set_current_room function get_current_room_ (f) if f then DEBUG ("get: current_room: " .. fixuid (tostring (current_room)) .. " (" .. f .. ")") else DEBUG ("get: current_room: " .. fixuid (tostring (current_room))) end -- if return current_room end -- get_current_room_ -- ----------------------------------------------------------------- -- description -- ----------------------------------------------------------------- function f_handle_description (saved_lines) if description and ignore_received then return end -- if -- if the description follows the exits, then ignore descriptions that don't follow exits if config.ACTIVATE_DESCRIPTION_AFTER_EXITS then if not exits_str then return end -- if end -- if -- if the description follows the room name, then ignore descriptions that don't follow the room name if config.ACTIVATE_DESCRIPTION_AFTER_ROOM_NAME then if not room_name then return end -- if end -- if local lines = { } description_styles = { } for _, line_info in ipairs (saved_lines) do table.insert (lines, line_info.line) -- get text of line table.insert (description_styles, line_info.styles [1]) -- remember first style run end -- for each line description = table.concat (lines, "\n") if config.WHEN_TO_DRAW_MAP == DRAW_MAP_ON_DESCRIPTION then process_new_room () end -- if end -- f_handle_description -- ----------------------------------------------------------------- -- exits -- ----------------------------------------------------------------- function f_handle_exits () local lines = { } exits_styles = { } for _, line_info in ipairs (saved_lines) do table.insert (lines, line_info.line) -- get text of line table.insert (exits_styles, line_info.styles [1]) -- remember first style run end -- for each line exits_str = table.concat (lines, " "):lower () if config.WHEN_TO_DRAW_MAP == DRAW_MAP_ON_EXITS then process_new_room () end -- if end -- f_handle_exits -- ----------------------------------------------------------------- -- room name -- ----------------------------------------------------------------- function f_handle_name () local lines = { } room_name_styles = { } for _, line_info in ipairs (saved_lines) do table.insert (lines, line_info.line) -- get text of line table.insert (room_name_styles, line_info.styles [1]) -- remember first style run end -- for each line room_name = table.concat (lines, " ") -- a bit of a hack, but look for: Room name [N, S, W] if config.EXITS_ON_ROOM_NAME then local name, exits = string.match (room_name, "^([^%[]+)(%[.*%])%s*$") if name then room_name = name exits_str = exits:lower () end -- if that sort of line found end -- if exits on room name wanted if config.WHEN_TO_DRAW_MAP == DRAW_MAP_ON_ROOM_NAME then process_new_room () end -- if end -- f_handle_name -- ----------------------------------------------------------------- -- prompt -- ----------------------------------------------------------------- function f_handle_prompt () local lines = { } for _, line_info in ipairs (saved_lines) do table.insert (lines, line_info.line) -- get text of line end -- for each line prompt = table.concat (lines, " ") if config.WHEN_TO_DRAW_MAP == DRAW_MAP_ON_PROMPT then if override_contents ['description'] then description = override_contents ['description'] end -- if if override_contents ['exits'] then exits_str = override_contents ['exits']:lower () end -- if if override_contents ['room_name'] then room_name = override_contents ['room_name'] end -- if if description and exits_str then process_new_room () end -- if end -- if time to draw the map end -- f_handle_prompt -- ----------------------------------------------------------------- -- ignore this line type -- ----------------------------------------------------------------- function f_handle_ignore () ignore_received = true end -- f_handle_ignore -- ----------------------------------------------------------------- -- cannot move - cancel speedwalk -- ----------------------------------------------------------------- function f_cannot_move () mapper.cancel_speedwalk () set_last_direction_moved (nil) -- therefore we haven't moved anywhere end -- f_cannot_move -- ----------------------------------------------------------------- -- Handlers for getting the wanted value for a marker for the nominated line -- ----------------------------------------------------------------- -- these are the types of lines we are trying to classify as a certain line IS or IS NOT that type line_types = { room_name = { short = "Room name", handler = f_handle_name, seq = 1 }, description = { short = "Description", handler = f_handle_description, seq = 2 }, exits = { short = "Exits", handler = f_handle_exits, seq = 3 }, prompt = { short = "Prompt", handler = f_handle_prompt, seq = 4 }, ignore = { short = "Ignore", handler = f_handle_ignore, seq = 5 }, cannot_move = { short = "Can't move", handler = f_cannot_move, seq = 6 }, } -- end of line_types table function f_first_style_run_foreground (line) return { GetStyleInfo(line, 1, 14) or -1 } end -- f_first_style_run_foreground function f_show_colour (which, value) mapper.mapprint (string.format (" %20s %5d %5d %7.2f", RGBColourToName (which), value.black, value.red, value.score)) end -- f_show_colour function f_show_word (which, value) if #which > 20 then mapper.mapprint (string.format ("%s\n %20s %5d %5d %7.2f", which, '', value.black, value.red, value.score)) else mapper.mapprint (string.format (" %20s %5d %5d %7.2f", which, value.black, value.red, value.score)) end -- if end -- f_show_colour function f_first_word (line) if not GetLineInfo(line, 1) then return {} end -- no line available return { (string.match (GetLineInfo(line, 1), "^%s*(%a+)") or ""):lower () } end -- f_first_word function f_exact_line (line) if not GetLineInfo(line, 1) then return {} end -- no line available return { GetLineInfo(line, 1) } end -- f_exact_line function f_first_two_words (line) if not GetLineInfo(line, 1) then return {} end -- no line available return { (string.match (GetLineInfo(line, 1), "^%s*(%a+%s+%a+)") or ""):lower () } end -- f_first_two_words function f_first_three_words (line) if not GetLineInfo(line, 1) then return {} end -- no line available return { (string.match (GetLineInfo(line, 1), "^%s*(%a+%s+%a+%s+%a+)") or ""):lower () } end -- f_first_three_words function f_all_words (line) if not GetLineInfo(line, 1) then return {} end -- no line available local words = { } for w in string.gmatch (GetLineInfo(line, 1), "%a+") do table.insert (words, w:lower ()) end -- for return words end -- f_all_words function f_first_character (line) if not GetLineInfo(line, 1) then return {} end -- no line available return { string.match (GetLineInfo(line, 1), "^.") or "" } end -- f_first_character -- ----------------------------------------------------------------- -- markers: things we are looking for, like colour of first style run -- You could add others, for example: -- * colour of the last style run -- * number of words on the line -- * number of style runs on the line -- Whether that would help or not remains to be seen. -- The functions above return the value(s) for the corresponding marker, for the nominated line. -- ----------------------------------------------------------------- markers = { { desc = "Foreground colour of first style run", func = f_first_style_run_foreground, marker = "first_style_run_foreground", show = f_show_colour, accessing_function = pairs, }, { desc = "First word in the line", func = f_first_word, marker = "first_word", show = f_show_word, accessing_function = pairsByKeys, }, { desc = "First two words in the line", func = f_first_two_words, marker = "first_two_words", show = f_show_word, accessing_function = pairsByKeys, }, { desc = "First three words in the line", func = f_first_three_words, marker = "first_three_words", show = f_show_word, accessing_function = pairsByKeys, }, { desc = "All words in the line", func = f_all_words, marker = "all_words", show = f_show_word, accessing_function = pairsByKeys, }, { desc = "Exact line", func = f_exact_line, marker = "exact_line", show = f_show_word, accessing_function = pairsByKeys, }, --[[ { desc = "First character in the line", func = f_first_character, marker = "first_character", show = f_show_word, }, --]] } -- end of markers inverse_markers = { } for k, v in ipairs (markers) do inverse_markers [v.marker] = v end -- for local MAX_NAME_LENGTH = 60 -- when to update the map DRAW_MAP_ON_ROOM_NAME = 1 DRAW_MAP_ON_DESCRIPTION = 2 DRAW_MAP_ON_EXITS = 3 DRAW_MAP_ON_PROMPT = 4 default_config = { -- assorted colours BACKGROUND_COLOUR = { name = "Background", colour = ColourNameToRGB "lightseagreen", }, ROOM_COLOUR = { name = "Room", colour = ColourNameToRGB "cyan", }, EXIT_COLOUR = { name = "Exit", colour = ColourNameToRGB "darkgreen", }, EXIT_COLOUR_UP_DOWN = { name = "Exit up/down", colour = ColourNameToRGB "darkmagenta", }, EXIT_COLOUR_IN_OUT = { name = "Exit in/out", colour = ColourNameToRGB "#3775E8", }, OUR_ROOM_COLOUR = { name = "Our room", colour = ColourNameToRGB "black", }, UNKNOWN_ROOM_COLOUR = { name = "Unknown room", colour = ColourNameToRGB "#00CACA", }, DIFFERENT_AREA_COLOUR = { name = "Another area", colour = ColourNameToRGB "#009393", }, SHOP_FILL_COLOUR = { name = "Shop", colour = ColourNameToRGB "darkolivegreen", }, TRAINER_FILL_COLOUR = { name = "Trainer", colour = ColourNameToRGB "yellowgreen", }, BANK_FILL_COLOUR = { name = "Bank", colour = ColourNameToRGB "gold", }, DUPLICATE_FILL_COLOUR = { name = "Duplicate", colour = ColourNameToRGB "lightgoldenrodyellow", }, BOOKMARK_FILL_COLOUR = { name = "Notes", colour = ColourNameToRGB "lightskyblue", }, MAPPER_NOTE_COLOUR = { name = "Messages", colour = ColourNameToRGB "lightgreen" }, ROOM_NAME_TEXT = { name = "Room name text", colour = ColourNameToRGB "#BEF3F1", }, ROOM_NAME_FILL = { name = "Room name fill", colour = ColourNameToRGB "#105653", }, ROOM_NAME_BORDER = { name = "Room name box", colour = ColourNameToRGB "black", }, AREA_NAME_TEXT = { name = "Area name text", colour = ColourNameToRGB "#BEF3F1",}, AREA_NAME_FILL = { name = "Area name fill", colour = ColourNameToRGB "#105653", }, AREA_NAME_BORDER = { name = "Area name box", colour = ColourNameToRGB "black", }, FONT = { name = get_preferred_font {"Dina", "Lucida Console", "Fixedsys", "Courier", "Sylfaen",} , size = 8 } , -- size of map window WINDOW = { width = 400, height = 400 }, -- how far from where we are standing to draw (rooms) SCAN = { depth = 30 }, -- speedwalk delay DELAY = { time = 0.3 }, -- how many seconds to show "recent visit" lines (default 3 minutes) LAST_VISIT_TIME = { time = 60 * 3 }, -- config for learning mapper STATUS_BACKGROUND_COLOUR = "black", -- the background colour of the status window STATUS_FRAME_COLOUR = "#1B1B1B", -- the frame colour of the status window STATUS_TEXT_COLOUR = "lightgreen", -- palegreen is more visible UID_SIZE = 4, -- how many characters of the UID to show -- learning configuration WHEN_TO_DRAW_MAP = DRAW_MAP_ON_EXITS, -- we need to have name/description/exits to draw the map ACTIVATE_DESCRIPTION_AFTER_EXITS = false, -- descriptions are activated *after* an exit line (used for MUDs with exits then descriptions) ACTIVATE_DESCRIPTION_AFTER_ROOM_NAME = false,-- descriptions are activated *after* a room name line BLANK_LINE_TERMINATES_LINE_TYPE = false, -- if true, a blank line terminates the previous line type ADD_NEWLINE_TO_PROMPT = false, -- if true, attempts to add a newline to a prompt at the end of a packet SHOW_LEARNING_WINDOW = true, -- if true, show the learning status and training windows on startup EXITS_ON_ROOM_NAME = false, -- if true, exits are listed on the room name line (eg. Starter Inventory and Shops [E, U]) INCLUDE_EXITS_IN_HASH = true, -- if true, exits are included in the description hash (UID) INCLUDE_ROOM_NAME_IN_HASH = false, -- if true, the room name is included in the description hash (UID) EXITS_IS_SINGLE_LINE = false, -- if true, exits are assumed to be only a single line PROMPT_IS_SINGLE_LINE = true, -- if true, prompts are assumed to be only a single line EXIT_LINES_START_WITH_DIRECTION = false, -- if true, exit lines must start with a direction (north, south, etc.) SORT_EXITS = false, -- if true, exit lines are extracted into words and sorted, excluding any other characters on the line SAVE_LINE_INFORMATION = true, -- if true, we save to the database the colour of the first style run for name/description/exits -- other stuff SHOW_INFO = false, -- if true, information messages are displayed SHOW_WARNINGS = true, -- if true, warning messages are displayed SHOW_ROOM_AND_EXITS = false, -- if true, exact deduced room name and exits are shown (needs SHOW_INFO) } -- ----------------------------------------------------------------- -- Handlers for validating configuration values (eg. colour, boolean) -- ----------------------------------------------------------------- function config_validate_colour (which) local colour = ColourNameToRGB (which) if colour == -1 then mapper.maperror (string.format ('Colour name "%s" not a valid HTML colour name or code.', which)) mapper.mapprint (" You can use HTML colour codes such as '#ab34cd' or names such as 'green'.") mapper.mapprint (" See the Colour Picker (Edit menu -> Colour Picker: Ctrl+Alt+P).") return nil, nil end -- if bad return which, colour end -- config_validate_colour function config_validate_uid_size (which) local size = tonumber (which) if not size then mapper.maperror ("Bad UID size: " .. which) return nil end -- if if size < 3 or size > 25 then mapper.maperror ("UID size must be in the range 3 to 25") return nil end -- if return size end -- config_validate_uid_size -- ----------------------------------------------------------------- -- when we draw the map (after what sort of line) -- ----------------------------------------------------------------- local when_types = { ["room name"] = DRAW_MAP_ON_ROOM_NAME, ["description"] = DRAW_MAP_ON_DESCRIPTION, ["exits"] = DRAW_MAP_ON_EXITS, ["prompt"] = DRAW_MAP_ON_PROMPT, } -- end of table function config_validate_when_to_draw (which) local when = which:lower () local w = when_types [when] if not w then mapper.maperror ("Unknown time to draw the map: " .. which) mapper.mapprint ("Valid times are:") local t = { } for k, v in ipairs (when_types) do table.insert (t, k) end mapper.mapprint (" " .. table.concat (t, ", ")) return nil end -- if type not found return w end -- when_to_draw function convert_when_to_draw_to_name (which) local when = "Unknown" for k, v in pairs (when_types) do if which == v then when = k break end -- if end -- for return when end -- convert_when_to_draw_to_name local bools = { yes = true, y = true, no = false, n = false } -- end of bools function config_validate_boolean (which) local which = which:lower () local yesno = bools [which] if yesno == nil then mapper.maperror ("Invalid option: must be YES or NO") return end -- not in bools table return yesno end -- config_validate_boolean -- ----------------------------------------------------------------- -- Handlers for displaying configuration values (eg. colour, boolean) -- ----------------------------------------------------------------- function config_display_colour (which) return which end -- config_display_colour function config_display_number (which) return tostring (which) end -- config_display_number function config_display_when_to_draw (which) return convert_when_to_draw_to_name (which) end -- config_display_when_to_draw function config_display_boolean (which) if which then return "Yes" else return "No" end -- if end -- config_display_boolean -- ----------------------------------------------------------------- -- Configuration options (ie. mapper config