--[[ symbol group name parser field ------ ---------- ------------ s syllablesStart parser_data["start"] m syllablesMiddle parser_data["middle"] e syllablesEnd parser_data["end"] P syllablesPre parser_data["pre"] p syllablesPost parser_data["post"] v phonemesVocals parser_data["vocals"] c phonemesConsonants parser_data["consonants"] A customGroupA parser_data["cga"] B customGroupB parser_data["cgb"] ... (custom groups exist from `A` to `O`) N customGroupN parser_data["cgn"] O customGroupO parser_data["cgo"] ? phonemesVocals/ parser_data["vocals"]/ phonemesConsonants parser_data["consonants"] ]] local PATH = string.match(debug.getinfo(1,"S").source, "^@(.+/)[%a%-%d_]+%.lua$") or "./" local function loadrequire(module) local loaded local function requiref(module) loaded = require(module) end pcall(requiref, module) return loaded end local RangedTable = require("namegen.rangedtable") -- ====================== -- VARIABLES -- ====================== -- the table containing the generators local namegen_generators_list = {} local namegen = {} -- ====================== -- UTILITIES -- ====================== local function file_exists(name) local f=io.open(name,"r") if f~=nil then io.close(f) return true else return false end end local function get_path(filename) if filename:find("%.cfg$") then return PATH .. filename else return PATH .. filename .. ".cfg" end end local function split(str) local t = {} for v in string.gmatch(str:gsub(",", ""), "%S+") do t[#t + 1] = v end return t end local function ng_debug(str, ...) if DEBUG == true then if ... then print(string.format(str, ...)) else print(str) end end end -- ====================== -- PARSERS -- ====================== local function parse_rules(rules) local rules_weight = {} local rule_pattern = [[^%%*(%d*)(%S+)]] for _, v in pairs(rules) do local chance, rule = string.match(v, rule_pattern) ng_debug([[chance: "%s", rule: "%s"]], chance, rule) chance = chance == "" and 100 or tonumber(chance) rules_weight[#rules_weight + 1] = {chance, rule} end return RangedTable(rules_weight) end local function parse_property(name, value, parser_data) local v = name == "name" and value or split(value) if name == "name" then ng_debug([[parse_property: "%s"]], value) parser_data["name"] = v elseif name == "syllablesStart" then parser_data["start"] = v elseif name == "syllablesMiddle" then parser_data["middle"] = v elseif name == "syllablesEnd" then parser_data["end"] = v elseif name =="syllablesPre" then parser_data["pre"] = v elseif name =="syllablesPost" then parser_data["post"] = v elseif name == "phonemesVocals" then parser_data["vocals"] = v elseif name =="phonemesConsonants" then parser_data.consonants = v elseif name == "rules" then parser_data["rules"] = parse_rules(v) elseif name == "illegal" then -- /* illegal strings are converted to lowercase */ parser_data.illegal = split(string.lower(value)) else local cg = string.match(name, "^customGroup(%a)") if cg then parser_data["cg" .. cg:lower()] = v else ng_debug("parse_property else") return false end end return true end local function parse_lines(path, data) ng_debug("starting `parse_lines`", path) local data local name_pattern = [[name ?"(.+)" ?{]] local property_pattern = [[ +(.+) = "(.+)"]] local end_body_pattern = [[}]] local body = false for line in io.lines(path) do local name = string.match(line, name_pattern) if name ~= nil then namegen_generators_list[name] = {} data = namegen_generators_list[name] parse_property("name", name, data) body = true elseif body == true then if string.match(line, end_body_pattern) then body = false else local name, value = string.match(line, property_pattern) if name and value then parse_property(name, value, data) end end end end ng_debug("ending `parse_lines`") end local function parse_file(filename) ng_debug("starting `parse_file`") local path = get_path(filename) if not file_exists(path) then error(string.format("File \"%s\" not found!\n",path)) end parse_lines(get_path(filename)) ng_debug("ending `parse_file`") end local function parse_index() ng_debug("starting `parse_index`") parse_file("itemdefinitions.cfg") ng_debug("ending `parse_index`") end -- ====================== -- WORD VALIDATION -- ====================== -- check for occurrences of triple characters (case-insensitive) local function word_has_triples(str) local str = str:lower() for i = 1, #str - 2 do local a = str:sub(i, i) local b = str:sub(i+1, i+1) local c = str:sub(i+2, i+2) if a == b and a == c then return true end end return false end -- check for occurrences of illegal strings (case-insensitive) local function word_has_illegal(data, str) local str = str:lower() if not data.illegal then return false end for i = 1, #data.illegal do if str:find(data.illegal[i]) then return true end end return false end -- check for repeated syllables (case-insensitive) local function word_repeated_syllables(str) local word = str:lower():gsub("['%-_]", "") for step = 2, math.min(5, math.floor(#str / 2)) do for i = 1, #word - step + 1 do local search = word:sub(i, i + step - 1) local sub = word:sub(i + step, (i + step) + step - 1) if search == sub then return true end ng_debug("not repeatted", str, step, search, sub) end end return false end -- verify if the word passes the above checks local function word_is_ok(data, str) return ((#str > 0) and not word_has_triples(str) and not word_has_illegal(data, str) and not word_repeated_syllables(str)) end -- removes double, leading and ending spaces local function word_prune_spaces(str) str = str:gsub(" +$", "") str = str:gsub("^ +", "") str = str:gsub(" +", " ") return str end local function get_lst_from_token(token, data) if token == 'P' then ng_debug("token case 1") return data["pre"] elseif token == 's' then ng_debug("token case 2") return data["start"] elseif token == 'm' then ng_debug("token case 3") return data["middle"] elseif token == 'e' then ng_debug("token case 4") return data["end"] elseif token == 'p' then ng_debug("token case 5") return data["post"] elseif token == 'v' then ng_debug("token case 6") return data["vocals"] elseif token == 'c' then ng_debug("token case 7") return data["consonants"] elseif token == '?' then ng_debug("token case 8") return ((random(1, 2) == 1) and data.vocals or data.consonants) elseif token >= "A" and token < "P" then ng_debug("token case 9") return data["cg" .. token:lower()] elseif token == "'" then ng_debug("token case 10") return {"'"} end end local function generate_custom(name, rule) local random = math.random local data = namegen_generators_list[name] if data == nil then error(string.format("The name \"%s\" has not been found.\n",name)) end -- start name generation local buf, i, it repeat buf = "" i = 1 while i <= #rule do it = rule:sub(i, i) -- append normal character if ((it >= 'a' and it <= 'z') or (it >= 'A' and it <= 'Z') or it == '\'' or it == '-') then ng_debug("buf case 1") buf = buf .. it elseif it == '/' then -- special character i = i + 1 ng_debug("buf case 2") buf = buf .. it elseif it == '_' then -- convert underscore to space ng_debug("buf case 3") buf = buf .. " " -- interpret a wildcard elseif it == '$' then local chance = 100; i = i + 1 local it = rule:sub(i, i) ng_debug("buf case 4, it: %s", it) -- food for the randomiser if it >= '0' and it <= '9' then ng_debug("buf case 4.1") chance = 0 while it >= '0' and it <= '9' do chance = chance * 10 chance = chance + tonumber(it) i = i + 1 it = rule:sub(i, i); end end -- evaluate the wildcard according to its chance if chance >= random(100) then ng_debug("buf case 4.2") local lst = get_lst_from_token(it, data) if lst == nil then error(string.format( [[Wrong rules syntax(it:"%s", rule:"%s")]], it, rule)) end -- got the list, now choose something on it if #lst == 0 then error(string.format( "No data found in the requested string (wildcard %s). Check your name generation rule %s.", it,rule )) else buf = buf .. (lst[random(1, #lst)]:gsub('_', ' ')) end end end ng_debug([[i: %d, buf: %s, it: %s, rule: %s]], i, buf, it, rule) i = i + 1 end until word_is_ok(data, buf) ng_debug([[ending "generate_custom 'repeat ... until'"]]) -- prune undesired spaces and return the name return word_prune_spaces(buf) end -- generate a new name with one of the rules from set local function generate(name) ng_debug([[starting "generate(%s)"]], name) local data = namegen_generators_list[name] if data == nil then error(string.format("The name \"%s\" has not been found.\n",name)) end -- check if the rules list is present */ if data.rules:size() == 0 then error("The rules list is empty!") end -- choose the rule */ local res = generate_custom(name, data.rules:choice()) ng_debug([[starting "generate(%s)"]], name) return res end local function possible_rules(str) local res = {[str] = true} while true do local changed = false local count = #res for rule, _ in pairs(res) do local chance_rule = rule:match( "%$?[%a_'%- ]*(%$%d+[%a_'%- ]+)%$*.*$") -- print(rule, chance_rule) if chance_rule then local a = rule:gsub(chance_rule, "") local b = rule:gsub("%$%d+", "$", 1) -- print(rule, a, b) res[rule] = nil res[a] = true res[b] = true changed = true break end end if not changed then break end end --[[ print(str) for k, _ in pairs(res) do print(k) end ]]-- return res end local function exhaust_rules(name) local data = namegen_generators_list[name] local rules = {} for v in data.rules:values() do rules[#rules + 1] = v end local plain_rules = {} for _, rule in ipairs(rules) do local possible = possible_rules(rule) for plain, _ in pairs(possible) do plain_rules[#plain_rules + 1] = plain end end return plain_rules end local function combine_strings(...) local str for _, v in pairs(...) do str = (str and " " or "") .. v end return str end local function map_all(fcn, tab, idx, ...) -- http://stackoverflow.com/a/13059680/5496529 if idx < 1 then fcn(...) else local t = tab[idx] for i = 1, #t do map_all(fcn, tab, idx-1, t[i], ...) end end end local function exhaust_set(name) local names = {} local data = namegen_generators_list[name] local rules = exhaust_rules(name) local function combine(...) local t = {...} local str for i, v in ipairs(t) do if v ~= nil and v ~= "" then str = (str and (str .. v) or v) end end print(str) str = word_prune_spaces(str) names[str] = str end for _, rule in ipairs(rules) do local groups = {} local tokens = split(rule:gsub("%$", " ")) for _, token in ipairs(tokens) do for c in string.gmatch(token, ".") do local lst = (get_lst_from_token(c, data) or {[1] = c:gsub("_", " ")}) if lst == nil then error("invalid list", c) end groups[#groups + 1] = lst end end map_all(combine, groups, #groups) end return names end local function get_sets() local t = {} for k, _ in pairs(namegen_generators_list) do if namegen_generators_list[k].rules then t[#t + 1] = k end end return t end -- ------------------------------ -- load default sets (as specified on `namegen.index`) -- ------------------------------ parse_index() -- ------------------------------ -- publicly available functions -- ------------------------------ namegen.get_sets = get_sets namegen.parse_file = parse_file namegen.generate = generate namegen.generate_custom = generate_custom namegen.exhaust_set = exhaust_set namegen.exhaust_rules = exhaust_rules return namegen