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] }