persona-community-2/pkg/persona/pools.go
jordan cb3d4d5786
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/manual/woodpecker Pipeline was successful
Initialize project from skeleton template
2026-02-23 10:53:55 +00:00

188 lines
4.3 KiB
Go

package persona
import (
"fmt"
"hash/fnv"
"math/rand"
)
// Pool represents a collection of items that can be selected from.
type Pool[T any] struct {
Items []T
}
// NewPool creates a new pool with the given items.
func NewPool[T any](items []T) *Pool[T] {
return &Pool[T]{Items: items}
}
// Random selects a random item from the pool.
// Returns the zero value if the pool is empty.
func (p *Pool[T]) Random() T {
var zero T
if len(p.Items) == 0 {
return zero
}
return p.Items[rand.Intn(len(p.Items))]
}
// RandomWithSeed selects an item deterministically based on a seed.
// The same seed will always return the same item.
func (p *Pool[T]) RandomWithSeed(seed int64) T {
var zero T
if len(p.Items) == 0 {
return zero
}
r := rand.New(rand.NewSource(seed))
return p.Items[r.Intn(len(p.Items))]
}
// RandomN selects n random items from the pool.
// May return duplicates. Returns up to n items or all items if less available.
func (p *Pool[T]) RandomN(n int) []T {
if len(p.Items) == 0 || n <= 0 {
return nil
}
if n > len(p.Items) {
n = len(p.Items)
}
result := make([]T, n)
for i := 0; i < n; i++ {
result[i] = p.Items[rand.Intn(len(p.Items))]
}
return result
}
// SelectDescription selects a description from a pool using a deterministic seed.
// This ensures the same character always gets the same descriptions.
func SelectDescription(pool []string, characterID string, fieldName string) string {
if len(pool) == 0 {
return ""
}
// Create a deterministic seed from character ID and field name
h := fnv.New64a()
h.Write([]byte(characterID))
h.Write([]byte(fieldName))
seed := int64(h.Sum64())
r := rand.New(rand.NewSource(seed))
return pool[r.Intn(len(pool))]
}
// SelectDescriptionN selects n unique descriptions from a pool.
func SelectDescriptionN(pool []string, characterID string, fieldName string, n int) []string {
if len(pool) == 0 || n <= 0 {
return nil
}
// Create a deterministic seed
h := fnv.New64a()
h.Write([]byte(characterID))
h.Write([]byte(fieldName))
seed := int64(h.Sum64())
r := rand.New(rand.NewSource(seed))
// If n >= pool size, shuffle and return all
if n >= len(pool) {
result := make([]string, len(pool))
copy(result, pool)
r.Shuffle(len(result), func(i, j int) {
result[i], result[j] = result[j], result[i]
})
return result
}
// Select n unique items
indices := r.Perm(len(pool))[:n]
result := make([]string, n)
for i, idx := range indices {
result[i] = pool[idx]
}
return result
}
// WeightedPool allows weighted random selection.
type WeightedPool[T any] struct {
Items []T
Weights []float64
total float64
}
// NewWeightedPool creates a weighted pool.
// Weights should be positive numbers; they don't need to sum to 1.
// Returns an error if items and weights have different lengths or if any weight is negative.
func NewWeightedPool[T any](items []T, weights []float64) (*WeightedPool[T], error) {
if len(items) != len(weights) {
return nil, fmt.Errorf("%w: items length (%d) != weights length (%d)",
ErrInvalidPoolConfig, len(items), len(weights))
}
var total float64
for i, w := range weights {
if w < 0 {
return nil, fmt.Errorf("%w: negative weight at index %d: %f",
ErrInvalidPoolConfig, i, w)
}
total += w
}
return &WeightedPool[T]{
Items: items,
Weights: weights,
total: total,
}, nil
}
// MustNewWeightedPool creates a weighted pool or panics on error.
// Use this only at initialization time. For runtime use, prefer NewWeightedPool.
func MustNewWeightedPool[T any](items []T, weights []float64) *WeightedPool[T] {
pool, err := NewWeightedPool(items, weights)
if err != nil {
panic(err)
}
return pool
}
// Random selects a weighted random item.
func (p *WeightedPool[T]) Random() T {
var zero T
if len(p.Items) == 0 {
return zero
}
r := rand.Float64() * p.total
var cumulative float64
for i, w := range p.Weights {
cumulative += w
if r <= cumulative {
return p.Items[i]
}
}
return p.Items[len(p.Items)-1]
}
// RandomWithSeed selects a weighted random item deterministically.
func (p *WeightedPool[T]) RandomWithSeed(seed int64) T {
var zero T
if len(p.Items) == 0 {
return zero
}
rng := rand.New(rand.NewSource(seed))
r := rng.Float64() * p.total
var cumulative float64
for i, w := range p.Weights {
cumulative += w
if r <= cumulative {
return p.Items[i]
}
}
return p.Items[len(p.Items)-1]
}