188 lines
4.3 KiB
Go
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]
|
|
}
|