Name generator

This commit is contained in:
2026-05-11 16:37:32 +03:00
parent 472af01e94
commit 089d13520e
5 changed files with 395 additions and 1 deletions
+1
View File
@@ -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