diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index 669a839..acc9b58 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -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 diff --git a/src/features/editScene/systems/CharacterRegistry.cpp b/src/features/editScene/systems/CharacterRegistry.cpp index 94802de..aa955cc 100644 --- a/src/features/editScene/systems/CharacterRegistry.cpp +++ b/src/features/editScene/systems/CharacterRegistry.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 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 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); diff --git a/src/features/editScene/systems/CharacterRegistry.hpp b/src/features/editScene/systems/CharacterRegistry.hpp index ee039bb..1e3e620 100644 --- a/src/features/editScene/systems/CharacterRegistry.hpp +++ b/src/features/editScene/systems/CharacterRegistry.hpp @@ -11,6 +11,8 @@ #include #include +#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 m_templates; + MarkovNameGenerator m_firstNameGen; + MarkovNameGenerator m_lastNameGen; + flecs::world *m_world = nullptr; Ogre::SceneManager *m_sceneMgr = nullptr; EditorUISystem *m_uiSystem = nullptr; diff --git a/src/features/editScene/systems/MarkovNameGenerator.cpp b/src/features/editScene/systems/MarkovNameGenerator.cpp new file mode 100644 index 0000000..bb3271e --- /dev/null +++ b/src/features/editScene/systems/MarkovNameGenerator.cpp @@ -0,0 +1,213 @@ +#include "MarkovNameGenerator.hpp" +#include +#include + +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(std::tolower( + static_cast(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 &names) +{ + for (const auto &n : names) + learn(n); +} + +std::string MarkovNameGenerator::generate(int minLen, int maxLen, + const std::unordered_set + *exclude) const +{ + if (m_order2.empty()) + return ""; + + for (int attempt = 0; attempt < 200; ++attempt) { + std::string name; + std::string prefix = "##"; + + while (static_cast(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(name.size()) < minLen) + continue; + + std::string result = titleCase(name); + if (exclude && exclude->find(result) != exclude->end()) + continue; + + return result; + } + + return ""; +} + +std::vector MarkovNameGenerator::generateMany( + int count, int minLen, int maxLen, + const std::unordered_set *exclude) const +{ + std::vector results; + results.reserve(count); + std::unordered_set 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(); + } + } + } + + 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(); + } + } + } + + if (j.contains("starters")) { + for (auto &[k, v] : j["starters"].items()) + m_starters[k] = v.get(); + } +} + +char MarkovNameGenerator::pickNext( + const std::unordered_map &freqs) const +{ + int total = 0; + for (const auto &p : freqs) + total += p.second; + if (total <= 0) + return 0; + + std::uniform_int_distribution 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(std::toupper( + static_cast(r[0]))); + return r; +} diff --git a/src/features/editScene/systems/MarkovNameGenerator.hpp b/src/features/editScene/systems/MarkovNameGenerator.hpp new file mode 100644 index 0000000..da22a2d --- /dev/null +++ b/src/features/editScene/systems/MarkovNameGenerator.hpp @@ -0,0 +1,84 @@ +#ifndef EDITSCENE_MARKOV_NAME_GENERATOR_HPP +#define EDITSCENE_MARKOV_NAME_GENERATOR_HPP +#pragma once + +#include +#include +#include +#include +#include +#include + +/** + * 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 &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 *exclude = nullptr) + const; + + /** Generate multiple candidates at once. */ + std::vector generateMany(int count, int minLen = 3, + int maxLen = 12, + const std::unordered_set *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> + m_order2; + + /* Order-1: single char -> { next_char : weight } */ + std::unordered_map> + m_order1; + + /* Valid starting bigrams (the first real 2 chars). */ + std::unordered_map m_starters; + + /* Random state */ + mutable std::mt19937 m_rng; + + /* Weighted random pick from a frequency map. */ + char pickNext(const std::unordered_map &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