Skip to content

Commit ec44bef

Browse files
committed
feat: add component weights and modernize internals
- Component weight system (Proposal C hybrid): virtual weight() on component interface (default 1.0), registration-time override via add(comp, weight), post-registration override via weight(key, value) - Weight roll in generate_impl; seed always consumed for deterministic sequence stability regardless of inclusion - Replace raw const component* with std::reference_wrapper<const component> - Refactor for loops to std::ranges algorithms (ranges::transform, ranges::find, ranges::contains, generate_n)
1 parent f625816 commit ec44bef

3 files changed

Lines changed: 324 additions & 42 deletions

File tree

README.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ The library currently supports the following:
3636

3737
- **Component Groups**. Define named groups of components for convenient selective generation.
3838

39+
- **Component Weights**. Assign inclusion probabilities to components. A weight of `1.0` (default) means always included; `0.5` means included roughly half the time. Weights can be defined in the component interface or overridden per-component at registration time.
40+
3941
- **Thread Safety**. Create independent `eg` instances for lock-free concurrent generation.
4042

4143
- **Fluent Registration**. Chain `add()` and `remove()` calls to configure the generator.
@@ -341,4 +343,39 @@ for (int i = 0; i < 4; ++i)
341343
}
342344
```
343345

344-
`eg` is move-constructible and move-assignable, so instances can be transferred between scopes. The `instance()` singleton is still available for single-threaded convenience.
346+
`eg` is move-constructible and move-assignable, so instances can be transferred between scopes. The `instance()` singleton is still available for single-threaded convenience.
347+
348+
### Component Weights
349+
350+
Components can declare an inclusion weight via the `weight()` virtual method (default `1.0`). Values range from `0.0` (never included) to `1.0` (always included). The generator rolls against the weight during generation; components that fail the roll are skipped.
351+
352+
```cpp
353+
class rare_trait : public component
354+
{
355+
public:
356+
std::wstring key() const override { return L"rare_trait"; }
357+
double weight() const override { return 0.2; } // 20% chance
358+
359+
std::any generate(const generation_context& ctx) const override
360+
{
361+
return ctx.random().get<std::wstring>({L"scar", L"tattoo", L"birthmark"});
362+
}
363+
364+
std::wstring to_string(const std::any& value) const override
365+
{
366+
return default_to_string(value);
367+
}
368+
};
369+
```
370+
371+
Weights can also be overridden at registration time or updated later, taking precedence over the component's own `weight()` method:
372+
373+
```cpp
374+
// Override weight at registration.
375+
eg::instance().add(std::make_unique<rare_trait>(), 0.5);
376+
377+
// Update weight after registration.
378+
eg::instance().weight(L"rare_trait", 0.8);
379+
```
380+
381+
**Note:** When a weighted component is skipped, dependent components that call `ctx.get<T>()` for it will throw. Use `ctx.has()` to guard dependency access in weight-sensitive components.

dasmig/entitygen.hpp

Lines changed: 87 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
#include <algorithm>
66
#include <any>
77
#include <cstdint>
8+
#include <functional>
9+
#include <iterator>
810
#include <map>
911
#include <memory>
12+
#include <optional>
1013
#include <ostream>
1114
#include <ranges>
1215
#include <span>
@@ -78,6 +81,11 @@ class component
7881
// implement this. Use default_to_string() for standard type handling.
7982
[[nodiscard]] virtual std::wstring to_string(const std::any& value) const = 0;
8083

84+
// Inclusion weight for this component (0.0 to 1.0). A value of 1.0 means
85+
// always included; 0.5 means included roughly half the time. The generator
86+
// may override this value per-component at registration time.
87+
[[nodiscard]] virtual double weight() const { return 1.0; }
88+
8189
protected:
8290
// Default conversion covering common standard types. Derived classes can
8391
// call this from their to_string() implementation.
@@ -161,12 +169,7 @@ class entity
161169
{
162170
std::vector<std::wstring> result;
163171
result.reserve(_entries.size());
164-
165-
for (const auto& e : _entries)
166-
{
167-
result.push_back(e.key);
168-
}
169-
172+
std::ranges::transform(_entries, std::back_inserter(result), &entry::key);
170173
return result;
171174
}
172175

@@ -249,19 +252,40 @@ class eg
249252
eg& add(std::unique_ptr<component> comp)
250253
{
251254
const auto key = comp->key();
255+
auto it = std::ranges::find(_components, key, &component_entry::first);
252256

253-
// Replace if already registered, preserving position.
254-
for (auto& [existing_key, existing_comp] : _components)
257+
if (it != _components.end())
255258
{
256-
if (existing_key == key)
257-
{
258-
existing_comp = std::move(comp);
259-
return *this;
260-
}
259+
it->second = std::move(comp);
260+
}
261+
else
262+
{
263+
_components.emplace_back(key, std::move(comp));
261264
}
262265

263-
_components.emplace_back(key, std::move(comp));
266+
return *this;
267+
}
264268

269+
// Register a component with a weight override. The override takes
270+
// precedence over the component's own weight() method.
271+
eg& add(std::unique_ptr<component> comp, double weight_override)
272+
{
273+
const auto key = comp->key();
274+
add(std::move(comp));
275+
_weight_overrides[key] = weight_override;
276+
return *this;
277+
}
278+
279+
// Set or update the weight override for an already-registered component.
280+
// Throws std::out_of_range if the component is not registered.
281+
eg& weight(const std::wstring& component_key, double weight_value)
282+
{
283+
if (!has(component_key))
284+
{
285+
throw std::out_of_range("component not found");
286+
}
287+
288+
_weight_overrides[component_key] = weight_value;
265289
return *this;
266290
}
267291

@@ -271,6 +295,7 @@ class eg
271295
std::erase_if(_components, [&component_key](const auto& pair) {
272296
return pair.first == component_key;
273297
});
298+
_weight_overrides.erase(component_key);
274299

275300
return *this;
276301
}
@@ -368,10 +393,8 @@ class eg
368393
entities.reserve(count);
369394

370395
auto refs = all_component_refs();
371-
for (std::size_t i = 0; i < count; ++i)
372-
{
373-
entities.push_back(generate_impl(refs, _engine));
374-
}
396+
std::generate_n(std::back_inserter(entities), count,
397+
[&] { return generate_impl(refs, _engine); });
375398

376399
return entities;
377400
}
@@ -387,10 +410,8 @@ class eg
387410

388411
auto refs = all_component_refs();
389412
std::mt19937 engine{static_cast<std::mt19937::result_type>(call_seed)};
390-
for (std::size_t i = 0; i < count; ++i)
391-
{
392-
entities.push_back(generate_impl(refs, engine));
393-
}
413+
std::generate_n(std::back_inserter(entities), count,
414+
[&] { return generate_impl(refs, engine); });
394415

395416
return entities;
396417
}
@@ -452,8 +473,12 @@ class eg
452473
std::pair<std::wstring, std::unique_ptr<component>>;
453474

454475
// Reference to a component entry for filtered generation.
455-
using component_ref =
456-
std::pair<std::wstring, const component*>;
476+
struct component_ref
477+
{
478+
std::wstring key;
479+
std::reference_wrapper<const component> comp;
480+
double effective_weight;
481+
};
457482

458483
// Core generation logic shared by all overloads.
459484
[[nodiscard]] static entity generate_impl(
@@ -468,17 +493,30 @@ class eg
468493
generated_entity._seed = entity_seed;
469494
std::mt19937 local_engine{static_cast<std::mt19937::result_type>(entity_seed)};
470495

471-
for (const auto& [key, comp] : components)
496+
for (const auto& ref : components)
472497
{
473498
auto component_seed = static_cast<std::uint64_t>(local_engine());
499+
500+
// Roll against the effective weight. A seed is always consumed
501+
// to keep the deterministic sequence stable regardless of which
502+
// components are included.
503+
if (ref.effective_weight < 1.0)
504+
{
505+
std::uniform_real_distribution<double> dist(0.0, 1.0);
506+
if (dist(local_engine) >= ref.effective_weight)
507+
{
508+
continue;
509+
}
510+
}
511+
474512
ctx._random.seed(static_cast<std::mt19937::result_type>(component_seed));
475513

476-
auto value = comp->generate(ctx);
477-
auto display = comp->to_string(value);
514+
auto value = ref.comp.get().generate(ctx);
515+
auto display = ref.comp.get().to_string(value);
478516

479-
ctx._values[key] = value;
517+
ctx._values[ref.key] = value;
480518
generated_entity._entries.push_back(
481-
{.key = key, .value = std::move(value),
519+
{.key = ref.key, .value = std::move(value),
482520
.display = std::move(display),
483521
.seed = component_seed});
484522
}
@@ -491,12 +529,11 @@ class eg
491529
{
492530
std::vector<component_ref> refs;
493531
refs.reserve(_components.size());
494-
495-
for (const auto& [key, comp] : _components)
496-
{
497-
refs.emplace_back(key, comp.get());
498-
}
499-
532+
std::ranges::transform(_components, std::back_inserter(refs),
533+
[this](const auto& entry) -> component_ref {
534+
return {entry.first, std::cref(*entry.second),
535+
effective_weight(entry.first, *entry.second)};
536+
});
500537
return refs;
501538
}
502539

@@ -509,25 +546,34 @@ class eg
509546

510547
for (const auto& [key, comp] : _components)
511548
{
512-
for (const auto& requested_key : component_keys)
549+
if (std::ranges::contains(component_keys, key))
513550
{
514-
if (key == requested_key)
515-
{
516-
filtered.emplace_back(key, comp.get());
517-
break;
518-
}
551+
filtered.push_back({key, std::cref(*comp),
552+
effective_weight(key, *comp)});
519553
}
520554
}
521555

522556
return filtered;
523557
}
524558

559+
// Resolve the effective weight for a component: override wins, then
560+
// the component's own weight() method.
561+
[[nodiscard]] double effective_weight(
562+
const std::wstring& key, const component& comp) const
563+
{
564+
auto it = _weight_overrides.find(key);
565+
return it != _weight_overrides.end() ? it->second : comp.weight();
566+
}
567+
525568
// Registered components in insertion order.
526569
std::vector<component_entry> _components;
527570

528571
// Named groups of component keys.
529572
std::map<std::wstring, std::vector<std::wstring>> _groups;
530573

574+
// Per-component weight overrides (key -> weight).
575+
std::map<std::wstring, double> _weight_overrides;
576+
531577
// Internal random engine for the generator.
532578
std::mt19937 _engine{std::random_device{}()};
533579
};

0 commit comments

Comments
 (0)