Name generator
This commit is contained in:
@@ -38,6 +38,7 @@ set(EDITSCENE_SOURCES
|
||||
systems/PlayerControllerSystem.cpp
|
||||
systems/CharacterSlotSystem.cpp
|
||||
systems/CharacterRegistry.cpp
|
||||
systems/MarkovNameGenerator.cpp
|
||||
systems/AnimationTreeSystem.cpp
|
||||
systems/BehaviorTreeSystem.cpp
|
||||
systems/NavMeshSystem.cpp
|
||||
|
||||
@@ -45,6 +45,7 @@ void CharacterRegistry::initialize()
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"CharacterRegistry: auto-load failed: " + m_lastError);
|
||||
}
|
||||
learnNamesFromRegistry();
|
||||
scanTemplates();
|
||||
}
|
||||
|
||||
@@ -172,6 +173,39 @@ void CharacterRegistry::scanTemplates()
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================================================================== */
|
||||
/* Name Generation */
|
||||
/* ===================================================================== */
|
||||
|
||||
void CharacterRegistry::learnNamesFromRegistry()
|
||||
{
|
||||
m_firstNameGen.clear();
|
||||
m_lastNameGen.clear();
|
||||
for (const auto &pair : m_characters) {
|
||||
const CharacterRecord &c = pair.second;
|
||||
if (!c.firstName.empty())
|
||||
m_firstNameGen.learn(c.firstName);
|
||||
if (!c.lastName.empty())
|
||||
m_lastNameGen.learn(c.lastName);
|
||||
}
|
||||
}
|
||||
|
||||
std::string CharacterRegistry::generateFirstName() const
|
||||
{
|
||||
std::unordered_set<std::string> existing;
|
||||
for (const auto &pair : m_characters)
|
||||
existing.insert(pair.second.firstName);
|
||||
return m_firstNameGen.generate(3, 12, &existing);
|
||||
}
|
||||
|
||||
std::string CharacterRegistry::generateLastName() const
|
||||
{
|
||||
std::unordered_set<std::string> existing;
|
||||
for (const auto &pair : m_characters)
|
||||
existing.insert(pair.second.lastName);
|
||||
return m_lastNameGen.generate(3, 12, &existing);
|
||||
}
|
||||
|
||||
/* ===================================================================== */
|
||||
/* Spawn / Save */
|
||||
/* ===================================================================== */
|
||||
@@ -1217,11 +1251,62 @@ void CharacterRegistry::drawEditor(bool *p_open)
|
||||
autoSave();
|
||||
}
|
||||
if (ImGui::InputText("Last Name", lnBuf,
|
||||
sizeof(lnBuf))) {
|
||||
sizeof(lnBuf))) {
|
||||
c->lastName = lnBuf;
|
||||
autoSave();
|
||||
}
|
||||
|
||||
/* Name generation */
|
||||
ImGui::Separator();
|
||||
if (ImGui::Button("Generate First Name")) {
|
||||
std::string g = generateFirstName();
|
||||
if (!g.empty()) {
|
||||
snprintf(fnBuf, sizeof(fnBuf), "%s",
|
||||
g.c_str());
|
||||
c->firstName = fnBuf;
|
||||
autoSave();
|
||||
learnNamesFromRegistry();
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Generate Last Name")) {
|
||||
std::string g = generateLastName();
|
||||
if (!g.empty()) {
|
||||
snprintf(lnBuf, sizeof(lnBuf), "%s",
|
||||
g.c_str());
|
||||
c->lastName = lnBuf;
|
||||
autoSave();
|
||||
learnNamesFromRegistry();
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Re-learn Names"))
|
||||
learnNamesFromRegistry();
|
||||
|
||||
/* Preview samples */
|
||||
if (!m_firstNameGen.empty()) {
|
||||
ImGui::TextDisabled("First name samples:");
|
||||
auto samples = m_firstNameGen.generateMany(
|
||||
5, 3, 12, nullptr);
|
||||
for (size_t i = 0; i < samples.size(); ++i) {
|
||||
if (i > 0)
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("%s",
|
||||
samples[i].c_str());
|
||||
}
|
||||
}
|
||||
if (!m_lastNameGen.empty()) {
|
||||
ImGui::TextDisabled("Last name samples:");
|
||||
auto samples = m_lastNameGen.generateMany(
|
||||
5, 3, 12, nullptr);
|
||||
for (size_t i = 0; i < samples.size(); ++i) {
|
||||
if (i > 0)
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("%s",
|
||||
samples[i].c_str());
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Text("Prefab: %s", c->prefabPath.c_str());
|
||||
bool exists =
|
||||
std::filesystem::exists(c->prefabPath);
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "MarkovNameGenerator.hpp"
|
||||
|
||||
class EditorUISystem;
|
||||
|
||||
/**
|
||||
@@ -216,6 +218,12 @@ public:
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Name Generation */
|
||||
/* ------------------------------------------------------------------ */
|
||||
void learnNamesFromRegistry();
|
||||
std::string generateFirstName() const;
|
||||
std::string generateLastName() const;
|
||||
|
||||
/* Spawn / Save */
|
||||
/* ------------------------------------------------------------------ */
|
||||
flecs::entity findSpawnedEntity(uint64_t id) const;
|
||||
@@ -266,6 +274,9 @@ private:
|
||||
std::string m_autoSavePath;
|
||||
std::vector<std::string> m_templates;
|
||||
|
||||
MarkovNameGenerator m_firstNameGen;
|
||||
MarkovNameGenerator m_lastNameGen;
|
||||
|
||||
flecs::world *m_world = nullptr;
|
||||
Ogre::SceneManager *m_sceneMgr = nullptr;
|
||||
EditorUISystem *m_uiSystem = nullptr;
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
#include "MarkovNameGenerator.hpp"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
MarkovNameGenerator::MarkovNameGenerator()
|
||||
{
|
||||
std::random_device rd;
|
||||
m_rng.seed(rd());
|
||||
}
|
||||
|
||||
void MarkovNameGenerator::learn(const std::string &name)
|
||||
{
|
||||
if (name.empty())
|
||||
return;
|
||||
|
||||
std::string s;
|
||||
s.reserve(name.size() + 3);
|
||||
for (char c : name)
|
||||
s.push_back(static_cast<char>(std::tolower(
|
||||
static_cast<unsigned char>(c))));
|
||||
|
||||
/* Pad with start/end tokens */
|
||||
std::string proc = "##" + s + "$";
|
||||
|
||||
/* Order-2 transitions */
|
||||
for (size_t i = 0; i + 2 < proc.size(); ++i) {
|
||||
std::string prefix = proc.substr(i, 2);
|
||||
char next = proc[i + 2];
|
||||
m_order2[prefix][next]++;
|
||||
}
|
||||
|
||||
/* Order-1 transitions (fallback) */
|
||||
for (size_t i = 0; i + 1 < proc.size(); ++i) {
|
||||
char prefix = proc[i];
|
||||
char next = proc[i + 1];
|
||||
m_order1[prefix][next]++;
|
||||
}
|
||||
|
||||
/* Starting bigram: first two real characters after ## */
|
||||
if (s.size() >= 2)
|
||||
m_starters[s.substr(0, 2)]++;
|
||||
else if (s.size() == 1)
|
||||
m_starters[s + "$"]++;
|
||||
}
|
||||
|
||||
void MarkovNameGenerator::learn(const std::vector<std::string> &names)
|
||||
{
|
||||
for (const auto &n : names)
|
||||
learn(n);
|
||||
}
|
||||
|
||||
std::string MarkovNameGenerator::generate(int minLen, int maxLen,
|
||||
const std::unordered_set<std::string>
|
||||
*exclude) const
|
||||
{
|
||||
if (m_order2.empty())
|
||||
return "";
|
||||
|
||||
for (int attempt = 0; attempt < 200; ++attempt) {
|
||||
std::string name;
|
||||
std::string prefix = "##";
|
||||
|
||||
while (static_cast<int>(name.size()) < maxLen) {
|
||||
char next = 0;
|
||||
|
||||
/* Try order-2 first */
|
||||
auto it2 = m_order2.find(prefix);
|
||||
if (it2 != m_order2.end() && !it2->second.empty()) {
|
||||
next = pickNext(it2->second);
|
||||
} else if (!prefix.empty()) {
|
||||
/* Fallback to order-1 */
|
||||
char fallback = prefix.back();
|
||||
auto it1 = m_order1.find(fallback);
|
||||
if (it1 != m_order1.end() && !it1->second.empty())
|
||||
next = pickNext(it1->second);
|
||||
}
|
||||
|
||||
if (next == '$' || next == 0)
|
||||
break;
|
||||
if (next == '#')
|
||||
continue;
|
||||
|
||||
name.push_back(next);
|
||||
prefix = prefix.substr(1) + next;
|
||||
if (prefix.size() < 2)
|
||||
prefix = "##";
|
||||
}
|
||||
|
||||
if (static_cast<int>(name.size()) < minLen)
|
||||
continue;
|
||||
|
||||
std::string result = titleCase(name);
|
||||
if (exclude && exclude->find(result) != exclude->end())
|
||||
continue;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
std::vector<std::string> MarkovNameGenerator::generateMany(
|
||||
int count, int minLen, int maxLen,
|
||||
const std::unordered_set<std::string> *exclude) const
|
||||
{
|
||||
std::vector<std::string> results;
|
||||
results.reserve(count);
|
||||
std::unordered_set<std::string> localExclude;
|
||||
if (exclude)
|
||||
localExclude = *exclude;
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
std::string name = generate(minLen, maxLen, &localExclude);
|
||||
if (!name.empty()) {
|
||||
results.push_back(name);
|
||||
localExclude.insert(name);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
void MarkovNameGenerator::clear()
|
||||
{
|
||||
m_order2.clear();
|
||||
m_order1.clear();
|
||||
m_starters.clear();
|
||||
}
|
||||
|
||||
bool MarkovNameGenerator::empty() const
|
||||
{
|
||||
return m_order2.empty();
|
||||
}
|
||||
|
||||
void MarkovNameGenerator::saveToJson(nlohmann::json &j) const
|
||||
{
|
||||
j["order2"] = nlohmann::json::object();
|
||||
for (const auto &pair : m_order2) {
|
||||
nlohmann::json inner = nlohmann::json::object();
|
||||
for (const auto &p2 : pair.second)
|
||||
inner[std::string(1, p2.first)] = p2.second;
|
||||
j["order2"][pair.first] = inner;
|
||||
}
|
||||
|
||||
j["order1"] = nlohmann::json::object();
|
||||
for (const auto &pair : m_order1) {
|
||||
nlohmann::json inner = nlohmann::json::object();
|
||||
for (const auto &p2 : pair.second)
|
||||
inner[std::string(1, p2.first)] = p2.second;
|
||||
j["order1"][std::string(1, pair.first)] = inner;
|
||||
}
|
||||
|
||||
j["starters"] = nlohmann::json::object();
|
||||
for (const auto &pair : m_starters)
|
||||
j["starters"][pair.first] = pair.second;
|
||||
}
|
||||
|
||||
void MarkovNameGenerator::loadFromJson(const nlohmann::json &j)
|
||||
{
|
||||
clear();
|
||||
|
||||
if (j.contains("order2")) {
|
||||
for (auto &[k, v] : j["order2"].items()) {
|
||||
for (auto &[ck, cv] : v.items()) {
|
||||
if (!ck.empty())
|
||||
m_order2[k][ck[0]] = cv.get<int>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (j.contains("order1")) {
|
||||
for (auto &[k, v] : j["order1"].items()) {
|
||||
for (auto &[ck, cv] : v.items()) {
|
||||
if (!ck.empty() && !k.empty())
|
||||
m_order1[k[0]][ck[0]] = cv.get<int>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (j.contains("starters")) {
|
||||
for (auto &[k, v] : j["starters"].items())
|
||||
m_starters[k] = v.get<int>();
|
||||
}
|
||||
}
|
||||
|
||||
char MarkovNameGenerator::pickNext(
|
||||
const std::unordered_map<char, int> &freqs) const
|
||||
{
|
||||
int total = 0;
|
||||
for (const auto &p : freqs)
|
||||
total += p.second;
|
||||
if (total <= 0)
|
||||
return 0;
|
||||
|
||||
std::uniform_int_distribution<int> dist(0, total - 1);
|
||||
int roll = dist(m_rng);
|
||||
|
||||
for (const auto &p : freqs) {
|
||||
roll -= p.second;
|
||||
if (roll < 0)
|
||||
return p.first;
|
||||
}
|
||||
return freqs.begin()->first;
|
||||
}
|
||||
|
||||
std::string MarkovNameGenerator::titleCase(const std::string &s)
|
||||
{
|
||||
if (s.empty())
|
||||
return s;
|
||||
std::string r = s;
|
||||
r[0] = static_cast<char>(std::toupper(
|
||||
static_cast<unsigned char>(r[0])));
|
||||
return r;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
#ifndef EDITSCENE_MARKOV_NAME_GENERATOR_HPP
|
||||
#define EDITSCENE_MARKOV_NAME_GENERATOR_HPP
|
||||
#pragma once
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#include <random>
|
||||
|
||||
/**
|
||||
* Character-level Markov chain name generator.
|
||||
*
|
||||
* Learns from example names and generates new ones using
|
||||
* order-2 (trigram) transitions with an order-1 (bigram) fallback.
|
||||
*
|
||||
* Names are learned in lowercase and generated title-cased.
|
||||
*/
|
||||
class MarkovNameGenerator {
|
||||
public:
|
||||
MarkovNameGenerator();
|
||||
|
||||
/** Feed a single name into the model. */
|
||||
void learn(const std::string &name);
|
||||
|
||||
/** Feed many names at once. */
|
||||
void learn(const std::vector<std::string> &names);
|
||||
|
||||
/**
|
||||
* Generate a new name.
|
||||
*
|
||||
* @param minLen minimum length (default 3)
|
||||
* @param maxLen maximum length (default 12)
|
||||
* @param exclude optional set of names to avoid generating
|
||||
* @return generated name, or empty string if model is empty
|
||||
*/
|
||||
std::string generate(int minLen = 3, int maxLen = 12,
|
||||
const std::unordered_set<std::string> *exclude = nullptr)
|
||||
const;
|
||||
|
||||
/** Generate multiple candidates at once. */
|
||||
std::vector<std::string> generateMany(int count, int minLen = 3,
|
||||
int maxLen = 12,
|
||||
const std::unordered_set<std::string> *exclude = nullptr)
|
||||
const;
|
||||
|
||||
/** Reset the model. */
|
||||
void clear();
|
||||
|
||||
/** True if no names have been learned. */
|
||||
bool empty() const;
|
||||
|
||||
/** Serialize model to JSON. */
|
||||
void saveToJson(nlohmann::json &j) const;
|
||||
|
||||
/** Load model from JSON. */
|
||||
void loadFromJson(const nlohmann::json &j);
|
||||
|
||||
private:
|
||||
/* Order-2: prefix of 2 chars -> { next_char : weight } */
|
||||
std::unordered_map<std::string,
|
||||
std::unordered_map<char, int>>
|
||||
m_order2;
|
||||
|
||||
/* Order-1: single char -> { next_char : weight } */
|
||||
std::unordered_map<char, std::unordered_map<char, int>>
|
||||
m_order1;
|
||||
|
||||
/* Valid starting bigrams (the first real 2 chars). */
|
||||
std::unordered_map<std::string, int> m_starters;
|
||||
|
||||
/* Random state */
|
||||
mutable std::mt19937 m_rng;
|
||||
|
||||
/* Weighted random pick from a frequency map. */
|
||||
char pickNext(const std::unordered_map<char, int> &freqs) const;
|
||||
std::string pickStarter() const;
|
||||
|
||||
/* Convert first letter to uppercase. */
|
||||
static std::string titleCase(const std::string &s);
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_MARKOV_NAME_GENERATOR_HPP
|
||||
Reference in New Issue
Block a user