Nickname Generator 1.2.1
Gamer-style nickname generation for C++23
Loading...
Searching...
No Matches
nicknamegen.hpp
Go to the documentation of this file.
1#ifndef DASMIG_NICKNAMEGEN_HPP
2#define DASMIG_NICKNAMEGEN_HPP
3
4#include "random.hpp"
5#include <algorithm>
6#include <cmath>
7#include <cstddef>
8#include <cstdint>
9#include <cwctype>
10#include <filesystem>
11#include <fstream>
12#include <iostream>
13#include <map>
14#include <random>
15#include <ranges>
16#include <stdexcept>
17#include <string>
18#include <utility>
19#include <vector>
20
21/// @file nicknamegen.hpp
22/// @brief Nickname generator library — gamer-style nickname generation for C++23.
23/// @author Diego Dasso Migotto (diegomigotto at hotmail dot com)
24/// @see See doc/usage.md for the narrative tutorial.
25
26struct nng_test_access;
27
28namespace dasmig
29{
30
31/// @brief Return type for nickname generation, holding both the transformed
32/// and original strings.
33///
34/// Supports implicit conversion to std::wstring and streaming via operator<<.
36{
37 public:
38 /// @brief Return the original word or name used to generate this nickname.
39 /// @return Original source string before any transforms.
40 [[nodiscard]] std::wstring plain() const
41 {
42 return _original_string;
43 }
44
45 /// @brief Retrieve the random seed used to generate this nickname.
46 /// @return The per-call seed for replay.
47 /// @see nng::get_nickname(const std::wstring&, std::uint64_t)
48 [[nodiscard]] std::uint64_t seed() const
49 {
50 return _seed;
51 }
52
53 /// @brief Implicit conversion to std::wstring.
54 /// @return The transformed nickname string.
55 operator std::wstring() const // NOLINT(hicpp-explicit-conversions)
56 {
57 return _internal_string;
58 }
59
60 /// @brief Stream the nickname to a wide output stream.
61 /// @param wos Output stream.
62 /// @param nickname Nickname to stream.
63 /// @return Reference to the output stream.
64 friend std::wostream& operator<<(std::wostream& wos,
65 const nickname& nickname)
66 {
67 wos << nickname._internal_string;
68 return wos;
69 }
70
71 private:
72 /// @brief Private constructor — nicknames are created only by nng.
73 /// @param nickname_str The transformed nickname text.
74 /// @param original_str The original source text.
75 nickname(std::wstring nickname_str, std::wstring original_str,
76 std::uint64_t seed = 0)
77 : _internal_string(std::move(nickname_str)),
78 _original_string(std::move(original_str)),
79 _seed(seed)
80 {
81 }
82
83 std::wstring _internal_string; ///< Current nickname after transforms.
84
85 std::wstring _original_string; ///< Original source string.
86
87 std::uint64_t _seed{0}; ///< Random seed used to generate this nickname.
88
89 friend class nng; ///< Allows nng to construct nicknames.
90};
91
92/// @brief Nickname generator that produces gamer-style nicknames.
93///
94/// Generates realistic gamer nicknames akin to professional players, optionally
95/// based on a player's real name. Applies random "leetifying" transforms and
96/// case formatting for variety.
97///
98/// Can be used as a singleton via instance() or constructed independently.
99/// Independent instances own their own word lists and random engine,
100/// making them safe for concurrent use without shared state.
101///
102/// @par Thread safety
103/// Each instance is independent. Concurrent calls to get_nickname() on
104/// the **same** instance require external synchronization. load() mutates
105/// internal state and must not be called concurrently with get_nickname()
106/// on the same instance.
107class nng
108{
109 public:
110 /// @brief Default constructor — creates an empty generator with no word lists.
111 ///
112 /// Call load() to populate word lists before generating nicknames.
113 nng() = default;
114
115 nng(const nng&) = delete; ///< Not copyable.
116 nng& operator=(const nng&) = delete; ///< Not copyable.
117 nng(nng&&) = default; ///< Move constructor.
118 nng& operator=(nng&&) = default; ///< Move assignment.
119 ~nng() = default; ///< Default destructor.
120
121 /// @brief Access the global singleton instance.
122 ///
123 /// The singleton auto-probes common resource paths on first access.
124 /// For independent generators (e.g., inside an entity-generator component),
125 /// prefer constructing a separate nng instance.
126 /// @return Reference to the global nng instance.
127 static nng& instance()
128 {
129 static nng inst{auto_probe_tag{}};
130 return inst;
131 }
132
133 /// @brief Generate a nickname, optionally based on a name.
134 /// @param name Optional full name to base the nickname on. If empty, a
135 /// random word from the loaded word lists is used instead.
136 /// @return A nickname object with both transformed and original strings.
137 /// @throws std::invalid_argument If name is empty and no word lists loaded.
138 [[nodiscard]] nickname get_nickname(const std::wstring& name = L"")
139 {
140 auto call_seed = static_cast<std::uint64_t>(_engine());
141 effolkronium::random_local call_engine;
142 call_engine.seed(static_cast<std::mt19937::result_type>(
143 (call_seed ^ (call_seed >> 32U))));
144 auto result = solver(name, call_engine);
145 result._seed = call_seed;
146 return result;
147 }
148
149 /// @brief Generate a deterministic nickname using a specific seed.
150 ///
151 /// Given the same name and seed, this method always produces the same
152 /// nickname. Retrieve the seed from a previous nickname via
153 /// nickname::seed().
154 ///
155 /// @param name Full name to base the nickname on (may be empty).
156 /// @param call_seed Seed for reproducible results.
157 /// @return A nickname object with both transformed and original strings.
158 /// @throws std::invalid_argument If name is empty and no word lists loaded.
159 [[nodiscard]] nickname get_nickname(const std::wstring& name,
160 std::uint64_t call_seed) const
161 {
162 effolkronium::random_local call_engine;
163 call_engine.seed(static_cast<std::mt19937::result_type>(
164 (call_seed ^ (call_seed >> 32U))));
165 auto result = solver(name, call_engine);
166 result._seed = call_seed;
167 return result;
168 }
169
170 /// @name Seeding
171 /// @{
172
173 /// @brief Seed the internal random engine for deterministic sequences.
174 ///
175 /// Subsequent get_nickname() calls (without an explicit seed) draw
176 /// per-call seeds from this engine, producing a reproducible sequence.
177 ///
178 /// @param seed_value The seed value.
179 /// @return `*this` for chaining.
180 nng& seed(std::uint64_t seed_value)
181 {
182 _engine.seed(seed_value);
183 return *this;
184 }
185
186 /// @brief Reseed the engine with a non-deterministic source.
187 ///
188 /// Subsequent get_nickname() calls will produce non-reproducible results.
189 /// @return `*this` for chaining.
191 {
192 _engine.seed(std::random_device{}());
193 return *this;
194 }
195
196 /// @}
197
198 /// @brief Check whether any word lists have been loaded.
199 /// @return `true` if at least one word list is available.
200 [[nodiscard]] bool has_wordlists() const
201 {
202 return !_wordlists.empty();
203 }
204
205 /// @brief Load word list files from a directory.
206 ///
207 /// Recursively scans @p resource_path for `.words` files and indexes them.
208 /// Safe to call multiple times to add from different directories.
209 ///
210 /// @param resource_path Directory containing `.words` files.
211 void load(const std::filesystem::path& resource_path)
212 {
213 if (std::filesystem::exists(resource_path) &&
214 std::filesystem::is_directory(resource_path))
215 {
216 for (const auto& entry :
217 std::filesystem::recursive_directory_iterator(resource_path))
218 {
219 if (entry.is_regular_file() &&
220 (entry.path().extension() == ".words"))
221 {
222 parse_file(entry);
223 }
224 }
225 };
226 }
227
228 private:
229 // Container of words.
230 using word_container = std::vector<std::wstring>;
231
232 // Function pointer type for nickname transform methods.
233 using generator_fn = std::wstring (*)(const std::wstring&,
234 effolkronium::random_local&);
235
236 // Container of methods used to modify the nickname.
237 using generators = std::vector<generator_fn>;
238
239 // Contains all vowel characters.
240 // NOLINTNEXTLINE(bugprone-throwing-static-initialization)
241 static const inline std::wstring _vowels{
242 L"aeiouáàâãäåæçèéêëìíîïðñòóôõöøšùúûüýÿ"};
243
244 // Maps letters to their leet-speak digit equivalents.
245 // NOLINTNEXTLINE(bugprone-throwing-static-initialization)
246 static const inline std::map<wchar_t, wchar_t> _leet_map{
247 {L'o', L'0'}, {L'O', L'0'}, {L'i', L'1'}, {L'I', L'1'}, {L's', L'2'},
248 {L'S', L'2'}, {L'e', L'3'}, {L'E', L'3'}, {L'a', L'4'}, {L'A', L'4'},
249 {L'g', L'6'}, {L'G', L'6'}, {L't', L'7'}, {L'T', L'7'}, {L'b', L'8'},
250 {L'B', L'8'}, {L'q', L'9'}, {L'Q', L'9'}};
251
252 // Vector for randomly accessing wordlists.
253 std::vector<word_container> _wordlists;
254
255 // Per-instance random engine for seed drawing.
256 std::mt19937_64 _engine{std::random_device{}()};
257
258 // Tag type for the auto-probing singleton constructor.
259 struct auto_probe_tag {};
260
261 // Singleton constructor: auto-probes common resource locations.
262 explicit nng(auto_probe_tag /*tag*/)
263 {
264 for (const auto& candidate : {"resources", "../resources",
265 "nickname-generator/resources"})
266 {
267 const std::filesystem::path p{candidate};
268 if (std::filesystem::exists(p) && std::filesystem::is_directory(p))
269 {
270 load(p);
271 break;
272 }
273 }
274 }
275
276 // Add an x to either the front or back of the nickname - or both.
277 static std::wstring xfy(const std::wstring& nickname,
278 effolkronium::random_local& engine)
279 {
280 // Xfied nickname.
281 std::wstring x_nickname{nickname};
282
283 // Distribution of possible xy, yx, xyx probability.
284 switch (engine.get(0, 2))
285 {
286 case 0:
287 x_nickname.push_back(L'X');
288 break;
289 case 1:
290 x_nickname.insert(0, 1, L'X');
291 break;
292 default: // case 2
293 x_nickname.push_back(L'X');
294 x_nickname.insert(0, 1, L'X');
295 break;
296 }
297
298 return x_nickname;
299 }
300
301 // Writes the nickname backwards such as emankcin.
302 static std::wstring reverse(
303 const std::wstring& nickname,
304 [[maybe_unused]] effolkronium::random_local& engine)
305 {
306 // Original nickname reversed.
307 std::wstring reverse_nickname{nickname};
308
309 std::ranges::reverse(reverse_nickname);
310
311 return reverse_nickname;
312 }
313
314 // Adds an y to the end of nickname or replace the last character if it's a
315 // vowel.
316 static std::wstring yfy(
317 const std::wstring& nickname,
318 [[maybe_unused]] effolkronium::random_local& engine)
319 {
320 // Nickname with an y a the end.
321 std::wstring nicknamy{nickname};
322
323 if (_vowels.contains(nickname.back()))
324 {
325 nicknamy.back() = L'y';
326 }
327 else
328 {
329 nicknamy.push_back(L'y');
330 }
331
332 return nicknamy;
333 }
334
335 // Adds a number to the end of nickname.
336 static std::wstring numify(const std::wstring& nickname,
337 effolkronium::random_local& engine)
338 {
339 // Nickname with a number at the end.
340 std::wstring nickname_with_number{nickname};
341
342 // Maximum single digit value for numified nicknames.
343 static constexpr int max_digit{9};
344
345 // Append a random digit character (1-9).
346 nickname_with_number.push_back(
347 L'0' + engine.get(1, max_digit));
348
349 // Append n zeroes to the end of the nickname.
350 for (auto i = engine.get(0, 3); i > 0;
351 i--)
352 {
353 nickname_with_number.push_back(L'0');
354 }
355
356 return nickname_with_number;
357 }
358
359 // Adds a trace to the end of the nickname.
360 static std::wstring tracefy(
361 const std::wstring& nickname,
362 [[maybe_unused]] effolkronium::random_local& engine)
363 {
364 return nickname + L"-";
365 }
366
367 // Adds an ing to the end of nickname or replace the last character if it's
368 // a vowel.
369 static std::wstring ingify(
370 const std::wstring& nickname,
371 [[maybe_unused]] effolkronium::random_local& engine)
372 {
373 // Nickname with an ing a the end.
374 std::wstring nicknaming{nickname};
375
376 if (_vowels.contains(nickname.back()))
377 {
378 nicknaming.back() = L'i';
379 }
380 else
381 {
382 nicknaming.push_back(L'i');
383 }
384
385 nicknaming.append(L"ng");
386
387 return nicknaming;
388 }
389
390 // Finds an aeio vowel and duplicates it, returns same nickname if no
391 // available vowel.
392 static std::wstring duovowel(
393 const std::wstring& nickname,
394 [[maybe_unused]] effolkronium::random_local& engine)
395 {
396 static const std::wstring simple_vowels = L"aeio";
397
398 // Nickname with duplicated vowel.
399 std::wstring nicknamee{nickname};
400
401 // Try duplicating each vowel in the nickname, stop after succeeding
402 // once.
403 for (const auto& vowel : simple_vowels)
404 {
405 if (auto it = std::ranges::find(nicknamee, vowel);
406 it != nicknamee.end())
407 {
408 nicknamee.insert(it, *it);
409 break;
410 }
411 }
412
413 return nicknamee;
414 }
415
416 // Replaces a letter by a numerical character.
417 static std::wstring oneleet(const std::wstring& nickname,
418 effolkronium::random_local& engine)
419 {
420 // Nickname with leetified letter.
421 std::wstring leet_nickname{nickname};
422
423 // Collect indices of leet-mappable characters.
424 std::vector<std::size_t> candidate_positions;
425
426 for (std::size_t i = 0; i < leet_nickname.size(); ++i)
427 {
428 if (_leet_map.contains(leet_nickname[i]))
429 {
430 candidate_positions.push_back(i);
431 }
432 }
433
434 // Pick one position at random and replace it.
435 if (!candidate_positions.empty())
436 {
437 auto pos = *engine.get(candidate_positions);
438 leet_nickname[pos] = _leet_map.at(leet_nickname[pos]);
439 }
440
441 return leet_nickname;
442 }
443
444 // Replaces up to two random letters in the nickname by their numerical
445 // leet-speak equivalents.
446 static std::wstring allleet(const std::wstring& nickname,
447 effolkronium::random_local& engine)
448 {
449 // Nickname leetified.
450 std::wstring leet_nickname{nickname};
451
452 // Maximum number of characters to replace.
453 static constexpr std::size_t max_replacements{2};
454
455 // Collect indices of leet-mappable characters.
456 std::vector<std::size_t> candidate_positions;
457
458 for (std::size_t i = 0; i < leet_nickname.size(); ++i)
459 {
460 if (_leet_map.contains(leet_nickname[i]))
461 {
462 candidate_positions.push_back(i);
463 }
464 }
465
466 // Shuffle and replace up to max_replacements positions.
467 engine.shuffle(candidate_positions);
468
469 for (std::size_t n = 0;
470 n < std::min(max_replacements, candidate_positions.size()); ++n)
471 {
472 auto pos = candidate_positions[n];
473 leet_nickname[pos] = _leet_map.at(leet_nickname[pos]);
474 }
475
476 return leet_nickname;
477 }
478
479 // Slightly modify the nickname to add some flavor.
480 // NOLINTNEXTLINE(misc-no-recursion)
481 static nickname leetify(nickname nickname,
482 effolkronium::random_local& engine,
483 bool force = false, int depth = 0)
484 {
485 // Maximum recursion depth to prevent stack overflow on unmappable
486 // inputs.
487 static constexpr int max_depth{7};
488
489 // We have 1/2 chance of leetifying, force parameter overrides this.
490 if (force || engine.get<bool>())
491 {
492 // When leetifying, there's 1/2 chance of using a finalizer or a
493 // random leetifier.
494 if (engine.get<bool>())
495 {
496 // Possible methods utilized to leetify the nickname.
497 static const generators possible_generators{
498 reverse, // emanckin
499 duovowel, // nicknamee
500 oneleet, // n1ckname
501 allleet // n1ckn4m3
502 };
503
504 // Capture pre-transform value to detect no-ops.
505 auto before = nickname._internal_string;
506
507 // New leetified nickname.
508 nickname._internal_string =
509 (*engine.get(
510 possible_generators))(nickname, engine);
511
512 // If the new nickname didn't change and we haven't exceeded
513 // the depth limit, force leetify again.
514 if (depth < max_depth)
515 {
516 return leetify(nickname, engine,
517 nickname._internal_string == before,
518 depth + 1);
519 }
520 }
521 else
522 {
523 // Possible methods utilized to leetify the nickname.
524 static const generators possible_generators{
525 xfy, // nicknameX
526 reverse, // emanckin
527 yfy, // nicknamy
528 numify, // nickname2000
529 tracefy, // nickname-
530 ingify, // nicknaming
531 };
532
533 // New leetified nickname.
534 nickname._internal_string =
535 (*engine.get(possible_generators))(
536 nickname, engine);
537 }
538 }
539
540 return nickname;
541 }
542
543 // Returns the nickname with an underscore separating its original parts.
544 static std::wstring snake_case(const std::wstring& name)
545 {
546 std::wstring result;
547 result.reserve(name.size() + 4);
548
549 for (auto&& [i, ch] : name | std::views::enumerate)
550 {
551 if (i > 0 && iswupper(ch) != 0)
552 {
553 result.push_back(L'_');
554 }
555 result.push_back(ch);
556 }
557
558 return result;
559 }
560
561 // Returns the nickname in all uppercase.
562 static std::wstring upper_case(
563 const std::wstring& name,
564 [[maybe_unused]] effolkronium::random_local& engine)
565 {
566 // Name in all upper case characters.
567 std::wstring upper_name{name};
568
569 // Transform every character to uppercase if possible.
570 std::ranges::for_each(
571 upper_name,
572 [](wchar_t& character) { character = std::towupper(character); });
573
574 return upper_name;
575 }
576
577 // Returns the nickname in all lowercase.
578 static std::wstring lower_case(
579 const std::wstring& name,
580 [[maybe_unused]] effolkronium::random_local& engine)
581 {
582 // Name in all lower case characters.
583 std::wstring lower_name{name};
584
585 // Transform every character to lower if possible.
586 std::ranges::for_each(
587 lower_name,
588 [](wchar_t& character) { character = std::towlower(character); });
589
590 return lower_name;
591 }
592
593 // Returns the nickname in title case.
594 static std::wstring title_case(
595 const std::wstring& name,
596 [[maybe_unused]] effolkronium::random_local& engine)
597 {
598 // Well, titlecase is actually the default.
599 return name;
600 }
601
602 // Returns the nickname in sentence case.
603 static std::wstring sentence_case(const std::wstring& name,
604 effolkronium::random_local& engine)
605 {
606 if (name.empty())
607 {
608 return name;
609 }
610
611 // Sentence case formatted nickname.
612 std::wstring sentence_name{lower_case(name, engine)};
613
614 // Transform the first character to upper case.
615 sentence_name.at(0) = std::towupper(sentence_name.at(0));
616
617 return sentence_name;
618 }
619
620 // Returns the nickname in camel case.
621 static std::wstring camel_case(
622 const std::wstring& name,
623 [[maybe_unused]] effolkronium::random_local& engine)
624 {
625 if (name.empty())
626 {
627 return name;
628 }
629
630 // Camel case formatted nickname.
631 std::wstring camel_name{name};
632
633 // Transform the first character to lower case.
634 camel_name.at(0) = std::towlower(camel_name.at(0));
635
636 return camel_name;
637 }
638
639 // Returns the nickname in reverse sentence case.
640 static std::wstring reverse_sentence_case(const std::wstring& name,
641 effolkronium::random_local& engine)
642 {
643 if (name.empty())
644 {
645 return name;
646 }
647
648 // Reverse sentence case formatted nickname.
649 std::wstring rsentence_name{lower_case(name, engine)};
650
651 // Transform the last character to upper case.
652 rsentence_name.back() = std::towupper(rsentence_name.back());
653
654 return rsentence_name;
655 }
656
657 // Returns the nickname in bathtub case.
658 static std::wstring bathtub_case(const std::wstring& name,
659 effolkronium::random_local& engine)
660 {
661 if (name.empty())
662 {
663 return name;
664 }
665
666 // Bathtub case formatted nickname.
667 std::wstring bathtub_name{lower_case(name, engine)};
668
669 // Transform the first character to upper case.
670 bathtub_name.at(0) = std::towupper(bathtub_name.at(0));
671
672 // Transform the last character to upper case.
673 bathtub_name.back() = std::towupper(bathtub_name.back());
674
675 return bathtub_name;
676 }
677
678 // Returns the nickname in winding case.
679 static std::wstring winding_case(const std::wstring& name,
680 effolkronium::random_local& engine)
681 {
682 // Winding case formatted nickname.
683 std::wstring winding_name{lower_case(name, engine)};
684
685 // Transform to uppercase every other character.
686 for (auto&& [i, ch] : winding_name | std::views::enumerate)
687 {
688 if ((i % 2) == 0)
689 {
690 ch = std::towupper(ch);
691 }
692 }
693
694 return winding_name;
695 }
696
697 // Returns the nickname with case in a random fashion.
698 static std::wstring random_case(const std::wstring& name,
699 effolkronium::random_local& engine)
700 {
701 // Name in all lower case characters.
702 std::wstring random_name{lower_case(name, engine)};
703
704 // Randomly uppercase each character.
705 std::ranges::for_each(random_name, [&engine](wchar_t& character) {
706 if (engine.get<bool>())
707 {
708 character = std::towupper(character);
709 }
710 });
711
712 return random_name;
713 }
714
715 // Returns the nickname all lower case with a single random character
716 // uppercase.
717 static std::wstring random_single_case(const std::wstring& name,
718 effolkronium::random_local& engine)
719 {
720 // Name in all lower case characters.
721 std::wstring random_name{lower_case(name, engine)};
722
723 // Position of single random character to be uppercased.
724 auto random_char{engine.get(random_name)};
725
726 *random_char = std::towupper(*random_char);
727
728 return random_name;
729 }
730
731 // Format nickname utilizing one of the possible cases.
732 static nickname format(nickname nickname,
733 effolkronium::random_local& engine)
734 {
735 // 1% chance of snake case. nick_name
736 // Probability of applying snake_case formatting.
737 static constexpr double snake_case_probability{0.01};
738
739 if (engine.get<bool>(snake_case_probability))
740 {
741 nickname._internal_string = snake_case(nickname);
742 }
743
744 // Possible methods utilized to format the nickname.
745 // Repeat functions to enforce a distribution.
746 static const generators possible_generators = {
747 upper_case,
748 upper_case,
749 upper_case,
750 upper_case, // NICKNAME
751 lower_case,
752 lower_case,
753 lower_case,
754 lower_case,
755 lower_case,
756 lower_case,
757 lower_case,
758 lower_case, // nickname
759 title_case,
760 title_case, // NickName
761 sentence_case,
762 sentence_case,
763 sentence_case,
764 sentence_case,
765 sentence_case, // Nickname
766 camel_case,
767 camel_case, // nickName
768 reverse_sentence_case,
769 reverse_sentence_case, // nicknamE
770 bathtub_case,
771 bathtub_case,
772 bathtub_case, // NicknamE
773 winding_case, // nIcKnAmE
774 random_case, // niCKnaMe
775 random_single_case // nicknaMe
776 };
777
778 nickname._internal_string = (*engine.get(
779 possible_generators))(nickname, engine);
780
781 return nickname;
782 }
783
784 // Split a full name into a vector containing each name/surname.
785 // Empty parts (from consecutive spaces) are filtered out.
786 [[nodiscard]] static std::vector<std::wstring> split_name(
787 const std::wstring& name)
788 {
789 std::vector<std::wstring> parts;
790
791 for (auto part : name | std::views::split(L' '))
792 {
793 std::wstring s(std::ranges::begin(part),
794 std::ranges::end(part));
795 if (!s.empty())
796 {
797 parts.push_back(std::move(s));
798 }
799 }
800
801 return parts;
802 }
803
804 // Returns the first name.
805 static std::wstring first_name(
806 const std::wstring& name,
807 [[maybe_unused]] effolkronium::random_local& engine)
808 {
809 return *(split_name(name).cbegin());
810 }
811
812 // Returns the last surname.
813 static std::wstring last_name(
814 const std::wstring& name,
815 [[maybe_unused]] effolkronium::random_local& engine)
816 {
817 return *(split_name(name).crbegin());
818 }
819
820 // Returns any name (until it hits a space character).
821 static std::wstring any_name(const std::wstring& name,
822 effolkronium::random_local& engine)
823 {
824 // Container with names/surnames that compose the received name.
825 std::vector<std::wstring> names_list{split_name(name)};
826
827 return *engine.get(names_list);
828 }
829
830 // Returns only the name initials.
831 static std::wstring initials(
832 const std::wstring& name,
833 [[maybe_unused]] effolkronium::random_local& engine)
834 {
835 // Generated nickname containing each name first letter.
836 std::wstring nickname;
837
838 // Retrieve first letter from each part of the name.
839 for (const auto& part : split_name(name))
840 {
841 nickname.push_back(part.front());
842 }
843
844 return nickname;
845 }
846
847 // Mix the last two names.
848 static std::wstring mix_two(const std::wstring& name,
849 effolkronium::random_local& engine)
850 {
851 // Generated nickname containing a part the last two names.
852 std::wstring nickname;
853
854 // Container with names/surnames that compose the received name.
855 auto names_list{split_name(name)};
856
857 // Reduce name list to two names.
858 if (names_list.size() > 2)
859 {
860 names_list.erase(names_list.begin(),
861 names_list.end() - 2);
862 }
863
864 // Iterate through each name retrieving random number of letters.
865 for (const auto& name : names_list)
866 {
867 if (name.size() < 2)
868 {
869 nickname.append(name);
870 }
871 else
872 {
873 nickname.append(name.substr(
874 0, engine.get<std::size_t>(
875 2, name.size())));
876 }
877 }
878
879 return nickname;
880 }
881
882 // Mix first name with last name initial.
883 static std::wstring first_plus_initial(
884 const std::wstring& name,
885 [[maybe_unused]] effolkronium::random_local& engine)
886 {
887 // Container with names/surnames that compose the received name.
888 const std::vector<std::wstring> names_list{split_name(name)};
889
890 if (names_list.size() < 2)
891 {
892 return names_list.front();
893 }
894
895 return names_list.front() + names_list.back().front();
896 }
897
898 // Mix last name with first name initial.
899 static std::wstring initial_plus_last(
900 const std::wstring& name,
901 [[maybe_unused]] effolkronium::random_local& engine)
902 {
903 // Container with names/surnames that compose the received name.
904 const auto names_list{split_name(name)};
905
906 if (names_list.size() < 2)
907 {
908 return names_list.front();
909 }
910
911 return names_list.front().front() + names_list.back();
912 }
913
914 // Reduce a random part of the name.
915 static std::wstring reduce_single_name(const std::wstring& name,
916 effolkronium::random_local& engine)
917 {
918 // Random part of name.
919 std::wstring single_name{any_name(name, engine)};
920
921 if (single_name.size() > 3)
922 {
923 // Remove all vowel characters from the name unless it's already
924 // small enough.
925 single_name.erase(
926 std::remove_if(single_name.begin() + 1, single_name.end() - 1,
927 [&single_name](const wchar_t& character) {
928 return (_vowels.contains(character) &&
929 (single_name.size() > 3));
930 }),
931 single_name.end() - 1);
932 }
933
934 return single_name;
935 }
936
937 // Contains logic to generate a random nickname optionally based on the
938 // player full name.
939 [[nodiscard]] nickname solver(const std::wstring& name,
940 effolkronium::random_local& engine) const
941 {
942 // Holds the original word used to generate the nickname.
943 std::wstring original;
944
945 // Holds the modified version.
946 std::wstring nick;
947
948 // 1/4 chance of nickname being name related.
949 static constexpr double name_related_probability{0.25};
950
951 // When name is provided, use name-based generation with
952 // name_related_probability — but always use the name if there are
953 // no wordlists to fall back to.
954 const bool use_name =
955 !name.empty() &&
956 (_wordlists.empty() ||
957 engine.get<bool>(name_related_probability));
958
959 // Proceed to generate nickname based on name.
960 if (use_name)
961 {
962 // Possible methods utilized to generate a nickname.
963 // Purposefully adds redundancy to first and last name with any name
964 // to add double weight to them.
965 static const generators possible_generators{
966 first_name, // John
967 last_name, // Doe
968 any_name, // Smith
969 initials, // JSD
970 mix_two, // DoSmi
971 initial_plus_last, // JSmith
972 first_plus_initial, // JohnS
973 reduce_single_name // Jhn
974 };
975
976 original = name;
977
978 // Return a nickname from one of the name based possibilities.
979 nick = (*engine.get(
980 possible_generators))(name, engine);
981 }
982 // Proceed to generate nickname based on a word list.
983 else if (!_wordlists.empty())
984 {
985 // Randomly select a wordlist.
986 const word_container& drawn_wordlist =
987 *engine.get(_wordlists);
988
989 // Randomly selects a word from the wordlist.
990 nick = original =
991 *engine.get(drawn_wordlist);
992 }
993 else
994 {
995 throw(std::invalid_argument(
996 "Received no name and word lists are empty"));
997 }
998
999 return format(leetify({nick, original}, engine), engine);
1000 }
1001
1002 // Try parsing the wordlist file and index it into our container.
1003 void parse_file(const std::filesystem::path& file)
1004 {
1005 // Expected wordlist file format is content type string, list of words.
1006 std::wifstream tentative_file{file};
1007
1008 // If managed to open the file proceed.
1009 if (tentative_file.is_open())
1010 {
1011 // Expected delimiter character.
1012 const wchar_t delimiter{'\n'};
1013
1014 // Line being read from the file.
1015 std::wstring file_line;
1016
1017 // List of parsed words.
1018 word_container words_read{std::vector<std::wstring>()};
1019
1020 // Retrieves list of words.
1021 while (std::getline(tentative_file, file_line, delimiter))
1022 {
1023 // Strip trailing carriage return for cross-platform
1024 // compatibility.
1025 if (!file_line.empty() && file_line.back() == L'\r')
1026 {
1027 file_line.pop_back();
1028 }
1029
1030 // Skip empty lines (including blank trailing lines).
1031 if (!file_line.empty())
1032 {
1033 words_read.push_back(file_line);
1034 }
1035 }
1036
1037 // Index our container.
1038 if (!words_read.empty())
1039 {
1040 _wordlists.push_back(std::move(words_read));
1041 }
1042 }
1043 }
1044
1045 friend struct ::nng_test_access;
1046};
1047} // namespace dasmig
1048
1049#endif // DASMIG_NICKNAMEGEN_HPP
Return type for nickname generation, holding both the transformed and original strings.
friend std::wostream & operator<<(std::wostream &wos, const nickname &nickname)
Stream the nickname to a wide output stream.
std::wstring plain() const
Return the original word or name used to generate this nickname.
std::uint64_t seed() const
Retrieve the random seed used to generate this nickname.
Nickname generator that produces gamer-style nicknames.
static nng & instance()
Access the global singleton instance.
nng & operator=(nng &&)=default
Move assignment.
nng & operator=(const nng &)=delete
Not copyable.
nng & seed(std::uint64_t seed_value)
Seed the internal random engine for deterministic sequences.
~nng()=default
Default destructor.
bool has_wordlists() const
Check whether any word lists have been loaded.
void load(const std::filesystem::path &resource_path)
Load word list files from a directory.
nng(const nng &)=delete
Not copyable.
nickname get_nickname(const std::wstring &name, std::uint64_t call_seed) const
Generate a deterministic nickname using a specific seed.
nickname get_nickname(const std::wstring &name=L"")
Generate a nickname, optionally based on a name.
nng & unseed()
Reseed the engine with a non-deterministic source.
nng(nng &&)=default
Move constructor.
nng()=default
Default constructor — creates an empty generator with no word lists.