namegen.lua 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. local modpath = minetest.get_modpath(minetest.get_current_modname())
  2. local namegen = {
  3. _VERSION = 'LuaNameGen - Lua Name Generator v1.2.0',
  4. _DESCRIPTION = 'A name generator written in Lua for use with Minetest mods',
  5. _URL = 'https://github.com/FaceDeer/namegen',
  6. _LICENSE = [[
  7. MIT LICENSE
  8. Copyright (c) 2017 Lucas Siqueira, FaceDeer
  9. 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:
  10. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
  11. 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.
  12. ]]
  13. }
  14. local RangedTable = dofile(modpath .. "/rangedtable.lua")
  15. -- ======================
  16. -- CONSTANTS
  17. -- ======================
  18. -- set this to true for a debug/verbose mode
  19. local DEBUG = false
  20. -- ======================
  21. -- VARIABLES
  22. -- ======================
  23. -- the table containing the generators
  24. local namegen_generators_list = {}
  25. -- ======================
  26. -- UTILITIES
  27. -- ======================
  28. local function file_exists(name)
  29. local f=io.open(name,"r")
  30. if f~=nil then io.close(f) return true else return false end
  31. end
  32. local function get_path(filename)
  33. if filename:find("%.cfg$") then
  34. return PATH .. filename
  35. else
  36. return PATH .. filename .. ".cfg"
  37. end
  38. end
  39. local function split(str)
  40. local t = {}
  41. for v in string.gmatch(str:gsub(",", ""), "%S+") do
  42. t[#t + 1] = v
  43. end
  44. return t
  45. end
  46. local function ng_debug(str, ...)
  47. if DEBUG == true then
  48. if ... then
  49. minetest.debug(string.format(str, ...))
  50. else
  51. minetest.debug(str)
  52. end
  53. end
  54. end
  55. -- ======================
  56. -- PARSERS
  57. -- ======================
  58. local function parse_rules(rules)
  59. local rules_weight = {}
  60. local rule_pattern = [[^%%*(%d*)(%S+)]]
  61. for _, v in pairs(rules) do
  62. local chance, rule = string.match(v, rule_pattern)
  63. ng_debug([[chance: "%s", rule: "%s"]], chance, rule)
  64. chance = chance == "" and 100 or tonumber(chance)
  65. rules_weight[#rules_weight + 1] = {chance, rule}
  66. end
  67. ng_debug("rules by weight " .. dump(rules_weight))
  68. return RangedTable(rules_weight)
  69. end
  70. local function parse_property(name, value, parser_data)
  71. local v = name == "name" and value or split(value)
  72. if name == "name" then
  73. ng_debug([[parse_property: "%s"]], value)
  74. parser_data["name"] = v
  75. elseif name == "syllablesStart" then
  76. parser_data["start"] = v
  77. elseif name == "syllablesMiddle" then
  78. parser_data["middle"] = v
  79. elseif name == "syllablesEnd" then
  80. parser_data["end"] = v
  81. elseif name =="syllablesPre" then
  82. parser_data["pre"] = v
  83. elseif name =="syllablesPost" then
  84. parser_data["post"] = v
  85. elseif name == "phonemesVocals" then
  86. parser_data["vocals"] = v
  87. elseif name =="phonemesConsonants" then
  88. parser_data.consonants = v
  89. elseif name == "rules" then
  90. parser_data["rules"] = parse_rules(v)
  91. elseif name == "illegal" then
  92. -- /* illegal strings are converted to lowercase */
  93. parser_data.illegal = split(string.lower(value))
  94. else
  95. local cg = string.match(name, "^customGroup(%a)")
  96. if cg then
  97. parser_data["cg" .. cg:lower()] = v
  98. else
  99. ng_debug("parse_property else")
  100. return false
  101. end
  102. end
  103. return true
  104. end
  105. local function parse_lines(lines)
  106. ng_debug("starting `parse_lines`")
  107. local data
  108. local name_pattern = [[name ?"(.+)" ?{]]
  109. local property_pattern = [[ +(.+) = "(.+)"]]
  110. local end_body_pattern = [[}]]
  111. local body = false
  112. for line in lines do
  113. local name = string.match(line, name_pattern)
  114. if name ~= nil then
  115. namegen_generators_list[name] = {}
  116. data = namegen_generators_list[name]
  117. parse_property("name", name, data)
  118. body = true
  119. elseif body == true then
  120. if string.match(line, end_body_pattern) then
  121. body = false
  122. else
  123. local name, value = string.match(line, property_pattern)
  124. if name and value then
  125. parse_property(name, value, data)
  126. end
  127. end
  128. end
  129. end
  130. ng_debug("ending `parse_lines`")
  131. end
  132. -- ======================
  133. -- WORD VALIDATION
  134. -- ======================
  135. -- check for occurrences of triple characters (case-insensitive)
  136. local function word_has_triples(str)
  137. local str = str:lower()
  138. for i = 1, #str - 2 do
  139. local a = str:sub(i, i)
  140. local b = str:sub(i+1, i+1)
  141. local c = str:sub(i+2, i+2)
  142. if a == b and a == c then
  143. return true
  144. end
  145. end
  146. return false
  147. end
  148. -- check for occurrences of illegal strings (case-insensitive)
  149. local function word_has_illegal(data, str)
  150. local str = str:lower()
  151. if not data.illegal then return false end
  152. for i = 1, #data.illegal do
  153. if str:find(data.illegal[i]) then
  154. return true
  155. end
  156. end
  157. return false
  158. end
  159. -- check for repeated syllables (case-insensitive)
  160. local function word_repeated_syllables(str)
  161. local word = str:lower():gsub("['%-_]", "")
  162. for step = 2, math.min(5, math.floor(#str / 2)) do
  163. for i = 1, #word - step + 1 do
  164. local search = word:sub(i, i + step - 1)
  165. local sub = word:sub(i + step, (i + step) + step - 1)
  166. if search == sub then
  167. return true
  168. end
  169. ng_debug("not repeated", str, step, search, sub)
  170. end
  171. end
  172. return false
  173. end
  174. -- verify if the word passes the above checks
  175. local function word_is_ok(data, str)
  176. return ((#str > 0) and
  177. not word_has_triples(str) and
  178. not word_has_illegal(data, str) and
  179. not word_repeated_syllables(str))
  180. end
  181. -- removes double, leading and ending spaces
  182. local function word_prune_spaces(str)
  183. str = str:gsub(" +$", "")
  184. str = str:gsub("^ +", "")
  185. str = str:gsub(" +", " ")
  186. return str
  187. end
  188. local function get_lst_from_token(token, data)
  189. if token == 'P' then
  190. ng_debug("token case 1")
  191. return data["pre"]
  192. elseif token == 's' then
  193. ng_debug("token case 2")
  194. return data["start"]
  195. elseif token == 'm' then
  196. ng_debug("token case 3")
  197. return data["middle"]
  198. elseif token == 'e' then
  199. ng_debug("token case 4")
  200. return data["end"]
  201. elseif token == 'p' then
  202. ng_debug("token case 5")
  203. return data["post"]
  204. elseif token == 'v' then
  205. ng_debug("token case 6")
  206. return data["vocals"]
  207. elseif token == 'c' then
  208. ng_debug("token case 7")
  209. return data["consonants"]
  210. elseif token == '?' then
  211. ng_debug("token case 8")
  212. return ((random(1, 2) == 1) and data.vocals or
  213. data.consonants)
  214. elseif token >= "A" and token < "P" then
  215. ng_debug("token case 9")
  216. return data["cg" .. token:lower()]
  217. elseif token == "'" then
  218. ng_debug("token case 10")
  219. return {"'"}
  220. end
  221. end
  222. local function generate_custom(name, rule)
  223. local random = math.random
  224. local data = namegen_generators_list[name]
  225. if data == nil then
  226. error(string.format("The name \"%s\" has not been found.\n",name))
  227. end
  228. -- start name generation
  229. local buf, i, it
  230. repeat
  231. buf = ""
  232. i = 1
  233. while i <= #rule do
  234. it = rule:sub(i, i)
  235. -- append normal character
  236. if ((it >= 'a' and it <= 'z') or
  237. (it >= 'A' and it <= 'Z') or
  238. it == '\'' or it == '-')
  239. then
  240. ng_debug("buf case 1")
  241. buf = buf .. it
  242. elseif it == '/' then
  243. -- special character
  244. i = i + 1
  245. ng_debug("buf case 2")
  246. buf = buf .. it
  247. elseif it == '_' then
  248. -- convert underscore to space
  249. ng_debug("buf case 3")
  250. buf = buf .. " "
  251. -- interpret a wildcard
  252. elseif it == '$' then
  253. local chance = 100;
  254. i = i + 1
  255. local it = rule:sub(i, i)
  256. ng_debug("buf case 4, it: %s", it)
  257. -- food for the randomiser
  258. if it >= '0' and it <= '9' then
  259. ng_debug("buf case 4.1")
  260. chance = 0
  261. while it >= '0' and it <= '9' do
  262. chance = chance * 10
  263. chance = chance + tonumber(it)
  264. i = i + 1
  265. it = rule:sub(i, i);
  266. end
  267. end
  268. -- evaluate the wildcard according to its chance
  269. if chance >= random(100) then
  270. ng_debug("buf case 4.2")
  271. local lst = get_lst_from_token(it, data)
  272. if lst == nil then
  273. error(string.format(
  274. [[Wrong rules syntax(it:"%s", rule:"%s")]],
  275. it, rule))
  276. end
  277. -- got the list, now choose something on it
  278. if #lst == 0 then
  279. error(string.format(
  280. "No data found in the requested string (wildcard %s). Check your name generation rule %s.",
  281. it,rule
  282. ))
  283. else
  284. buf = buf .. (lst[random(1, #lst)]:gsub('_', ' '))
  285. end
  286. end
  287. end
  288. ng_debug([[i: %d, buf: %s, it: %s, rule: %s]],
  289. i, buf, it, rule)
  290. i = i + 1
  291. end
  292. until word_is_ok(data, buf)
  293. ng_debug([[ending "generate_custom 'repeat ... until'"]])
  294. -- prune undesired spaces and return the name
  295. return word_prune_spaces(buf)
  296. end
  297. -- generate a new name with one of the rules from set
  298. local function generate(name)
  299. ng_debug([[starting "generate(%s)"]], name)
  300. local data = namegen_generators_list[name]
  301. if data == nil then
  302. error(string.format("The name \"%s\" has not been found.\n",name))
  303. end
  304. -- check if the rules list is present */
  305. if data.rules:size() == 0 then
  306. error("The rules list is empty!")
  307. end
  308. -- choose the rule */
  309. local res = generate_custom(name, data.rules:choice())
  310. ng_debug([[starting "generate(%s)"]], name)
  311. return res
  312. end
  313. local function possible_rules(str)
  314. local res = {[str] = true}
  315. while true do
  316. local changed = false
  317. local count = #res
  318. for rule, _ in pairs(res) do
  319. local chance_rule = rule:match(
  320. "%$?[%a_'%- ]*(%$%d+[%a_'%- ]+)%$*.*$")
  321. -- print(rule, chance_rule)
  322. if chance_rule then
  323. local a = rule:gsub(chance_rule, "")
  324. local b = rule:gsub("%$%d+", "$", 1)
  325. -- print(rule, a, b)
  326. res[rule] = nil
  327. res[a] = true
  328. res[b] = true
  329. changed = true
  330. break
  331. end
  332. end
  333. if not changed then break end
  334. end
  335. --[[
  336. print(str)
  337. for k, _ in pairs(res) do
  338. print(k)
  339. end
  340. ]]--
  341. return res
  342. end
  343. local function exhaust_rules(name)
  344. local data = namegen_generators_list[name]
  345. local rules = {}
  346. for v in data.rules:values() do
  347. rules[#rules + 1] = v
  348. end
  349. local plain_rules = {}
  350. for _, rule in ipairs(rules) do
  351. local possible = possible_rules(rule)
  352. for plain, _ in pairs(possible) do
  353. plain_rules[#plain_rules + 1] = plain
  354. end
  355. end
  356. return plain_rules
  357. end
  358. local function map_all(fcn, tab, idx, ...)
  359. -- http://stackoverflow.com/a/13059680/5496529
  360. if idx < 1 then
  361. fcn(...)
  362. else
  363. local t = tab[idx]
  364. for i = 1, #t do map_all(fcn, tab, idx-1, t[i], ...) end
  365. end
  366. end
  367. local function exhaust_set(name)
  368. local names = {}
  369. local data = namegen_generators_list[name]
  370. local rules = exhaust_rules(name)
  371. local function combine(...)
  372. local t = {...}
  373. local str
  374. for i, v in ipairs(t) do
  375. if v ~= nil and v ~= "" then
  376. str = (str and (str .. v) or v)
  377. end
  378. end
  379. --print(str)
  380. str = word_prune_spaces(str)
  381. names[str] = str
  382. end
  383. for _, rule in ipairs(rules) do
  384. local groups = {}
  385. local tokens = split(rule:gsub("%$", " "))
  386. for _, token in ipairs(tokens) do
  387. for c in string.gmatch(token, ".") do
  388. local lst = (get_lst_from_token(c, data) or
  389. {[1] = c:gsub("_", " ")})
  390. if lst == nil then
  391. error("invalid list", c)
  392. end
  393. -- print("\ntoken", token, "c", c, "lst", inspect(lst))
  394. groups[#groups + 1] = lst
  395. end
  396. end
  397. map_all(combine, groups, #groups)
  398. end
  399. return names
  400. end
  401. local function get_sets()
  402. local t = {}
  403. for k, _ in pairs(namegen_generators_list) do
  404. if namegen_generators_list[k].rules then
  405. t[#t + 1] = k
  406. end
  407. end
  408. return t
  409. end
  410. -- ------------------------------
  411. -- publicly available functions
  412. -- ------------------------------
  413. namegen.get_sets = get_sets
  414. namegen.parse_lines = parse_lines
  415. namegen.generate = generate
  416. namegen.generate_custom = generate_custom
  417. namegen.exhaust_set = exhaust_set
  418. namegen.exhaust_rules = exhaust_rules
  419. return namegen