enhance basestore with common operations, simplify monsters

This commit is contained in:
Sky Johnson 2025-08-13 22:29:13 -05:00
parent c2eeaa2f42
commit 21acb38157
2 changed files with 368 additions and 240 deletions

View File

@ -3,8 +3,6 @@ package monsters
import ( import (
"dk/internal/store" "dk/internal/store"
"fmt" "fmt"
"sort"
"sync"
) )
// Monster represents a monster in the game // Monster represents a monster in the game
@ -21,14 +19,11 @@ type Monster struct {
} }
func (m *Monster) Save() error { func (m *Monster) Save() error {
monsterStore := GetStore() return GetStore().UpdateWithRebuild(m.ID, m)
monsterStore.UpdateMonster(m)
return nil
} }
func (m *Monster) Delete() error { func (m *Monster) Delete() error {
monsterStore := GetStore() GetStore().RemoveWithRebuild(m.ID)
monsterStore.RemoveMonster(m.ID)
return nil return nil
} }
@ -36,13 +31,13 @@ func (m *Monster) Delete() error {
func New() *Monster { func New() *Monster {
return &Monster{ return &Monster{
Name: "", Name: "",
MaxHP: 10, // Default HP MaxHP: 10,
MaxDmg: 5, // Default damage MaxDmg: 5,
Armor: 0, // Default armor Armor: 0,
Level: 1, // Default level Level: 1,
MaxExp: 10, // Default exp reward MaxExp: 10,
MaxGold: 5, // Default gold reward MaxGold: 5,
Immune: ImmuneNone, // No immunity by default Immune: ImmuneNone,
} }
} }
@ -63,279 +58,122 @@ func (m *Monster) Validate() error {
return nil return nil
} }
// Immunity constants for monster immunity types // Immunity constants
const ( const (
ImmuneNone = 0 ImmuneNone = 0
ImmuneHurt = 1 // Immune to Hurt spells ImmuneHurt = 1
ImmuneSleep = 2 // Immune to Sleep spells ImmuneSleep = 2
) )
// MonsterStore provides in-memory storage with O(1) lookups and monster-specific indices // MonsterStore with enhanced BaseStore
type MonsterStore struct { type MonsterStore struct {
*store.BaseStore[Monster] // Embedded generic store *store.BaseStore[Monster]
byLevel map[int][]int // Level -> []ID
byImmunity map[int][]int // Immunity -> []ID
allByLevel []int // All IDs sorted by level, then ID
mu sync.RWMutex // Protects indices
} }
// Global in-memory store // Global store with singleton pattern
var monsterStore *MonsterStore var GetStore = store.NewSingleton(func() *MonsterStore {
var storeOnce sync.Once ms := &MonsterStore{BaseStore: store.NewBaseStore[Monster]()}
// Initialize the in-memory store // Register indices
func initStore() { ms.RegisterIndex("byLevel", store.BuildIntGroupIndex(func(m *Monster) int {
monsterStore = &MonsterStore{ return m.Level
BaseStore: store.NewBaseStore[Monster](), }))
byLevel: make(map[int][]int),
byImmunity: make(map[int][]int), ms.RegisterIndex("byImmunity", store.BuildIntGroupIndex(func(m *Monster) int {
allByLevel: make([]int, 0), return m.Immune
} }))
ms.RegisterIndex("allByLevel", store.BuildSortedListIndex(func(a, b *Monster) bool {
if a.Level == b.Level {
return a.ID < b.ID
}
return a.Level < b.Level
}))
return ms
})
// Enhanced CRUD operations
func (ms *MonsterStore) AddMonster(monster *Monster) error {
return ms.AddWithRebuild(monster.ID, monster)
} }
// GetStore returns the global monster store
func GetStore() *MonsterStore {
storeOnce.Do(initStore)
return monsterStore
}
// AddMonster adds a monster to the in-memory store and updates all indices
func (ms *MonsterStore) AddMonster(monster *Monster) {
ms.mu.Lock()
defer ms.mu.Unlock()
// Validate monster
if err := monster.Validate(); err != nil {
return
}
// Add to base store
ms.Add(monster.ID, monster)
// Rebuild indices
ms.rebuildIndicesUnsafe()
}
// RemoveMonster removes a monster from the store and updates indices
func (ms *MonsterStore) RemoveMonster(id int) { func (ms *MonsterStore) RemoveMonster(id int) {
ms.mu.Lock() ms.RemoveWithRebuild(id)
defer ms.mu.Unlock()
// Remove from base store
ms.Remove(id)
// Rebuild indices
ms.rebuildIndicesUnsafe()
} }
// UpdateMonster updates a monster efficiently func (ms *MonsterStore) UpdateMonster(monster *Monster) error {
func (ms *MonsterStore) UpdateMonster(monster *Monster) { return ms.UpdateWithRebuild(monster.ID, monster)
ms.mu.Lock()
defer ms.mu.Unlock()
// Validate monster
if err := monster.Validate(); err != nil {
return
}
// Update base store
ms.Add(monster.ID, monster)
// Rebuild indices
ms.rebuildIndicesUnsafe()
} }
// LoadData loads monster data from JSON file, or starts with empty store // Data persistence
func LoadData(dataPath string) error { func LoadData(dataPath string) error {
ms := GetStore() ms := GetStore()
return ms.BaseStore.LoadData(dataPath)
// Load from base store, which handles JSON loading
if err := ms.BaseStore.LoadData(dataPath); err != nil {
return err
}
// Rebuild indices from loaded data
ms.rebuildIndices()
return nil
} }
// SaveData saves monster data to JSON file
func SaveData(dataPath string) error { func SaveData(dataPath string) error {
ms := GetStore() ms := GetStore()
return ms.BaseStore.SaveData(dataPath) return ms.BaseStore.SaveData(dataPath)
} }
// rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock) // Query functions using enhanced store
func (ms *MonsterStore) rebuildIndicesUnsafe() {
// Clear indices
ms.byLevel = make(map[int][]int)
ms.byImmunity = make(map[int][]int)
ms.allByLevel = make([]int, 0)
// Collect all monsters and build indices
allMonsters := ms.GetAll()
// Build level and immunity indices
for id, monster := range allMonsters {
ms.byLevel[monster.Level] = append(ms.byLevel[monster.Level], id)
ms.byImmunity[monster.Immune] = append(ms.byImmunity[monster.Immune], id)
ms.allByLevel = append(ms.allByLevel, id)
}
// Sort allByLevel by level first, then by ID
sort.Slice(ms.allByLevel, func(i, j int) bool {
monsterI, _ := ms.GetByID(ms.allByLevel[i])
monsterJ, _ := ms.GetByID(ms.allByLevel[j])
if monsterI.Level == monsterJ.Level {
return ms.allByLevel[i] < ms.allByLevel[j]
}
return monsterI.Level < monsterJ.Level
})
// Sort level indices by ID
for level := range ms.byLevel {
sort.Ints(ms.byLevel[level])
}
// Sort immunity indices by level, then ID
for immunity := range ms.byImmunity {
sort.Slice(ms.byImmunity[immunity], func(i, j int) bool {
monsterI, _ := ms.GetByID(ms.byImmunity[immunity][i])
monsterJ, _ := ms.GetByID(ms.byImmunity[immunity][j])
if monsterI.Level == monsterJ.Level {
return ms.byImmunity[immunity][i] < ms.byImmunity[immunity][j]
}
return monsterI.Level < monsterJ.Level
})
}
}
// rebuildIndices rebuilds all monster-specific indices from base store data
func (ms *MonsterStore) rebuildIndices() {
ms.mu.Lock()
defer ms.mu.Unlock()
ms.rebuildIndicesUnsafe()
}
// Retrieves a monster by ID - O(1) lookup
func Find(id int) (*Monster, error) { func Find(id int) (*Monster, error) {
ms := GetStore() ms := GetStore()
monster, exists := ms.GetByID(id) monster, exists := ms.Find(id)
if !exists { if !exists {
return nil, fmt.Errorf("monster with ID %d not found", id) return nil, fmt.Errorf("monster with ID %d not found", id)
} }
return monster, nil return monster, nil
} }
// Retrieves all monsters - O(1) lookup (returns pre-sorted slice)
func All() ([]*Monster, error) { func All() ([]*Monster, error) {
ms := GetStore() ms := GetStore()
ms.mu.RLock() return ms.AllSorted("allByLevel"), nil
defer ms.mu.RUnlock()
result := make([]*Monster, 0, len(ms.allByLevel))
for _, id := range ms.allByLevel {
if monster, exists := ms.GetByID(id); exists {
result = append(result, monster)
}
}
return result, nil
} }
// Retrieves monsters by level - O(1) lookup
func ByLevel(level int) ([]*Monster, error) { func ByLevel(level int) ([]*Monster, error) {
ms := GetStore() ms := GetStore()
ms.mu.RLock() return ms.GroupByIndex("byLevel", level), nil
defer ms.mu.RUnlock()
ids, exists := ms.byLevel[level]
if !exists {
return []*Monster{}, nil
}
result := make([]*Monster, 0, len(ids))
for _, id := range ids {
if monster, exists := ms.GetByID(id); exists {
result = append(result, monster)
}
}
return result, nil
} }
// Retrieves monsters within a level range (inclusive) - O(k) where k is result size
func ByLevelRange(minLevel, maxLevel int) ([]*Monster, error) { func ByLevelRange(minLevel, maxLevel int) ([]*Monster, error) {
ms := GetStore() ms := GetStore()
ms.mu.RLock()
defer ms.mu.RUnlock()
var result []*Monster var result []*Monster
for level := minLevel; level <= maxLevel; level++ { for level := minLevel; level <= maxLevel; level++ {
if ids, exists := ms.byLevel[level]; exists { monsters := ms.GroupByIndex("byLevel", level)
for _, id := range ids { result = append(result, monsters...)
if monster, exists := ms.GetByID(id); exists {
result = append(result, monster)
}
}
}
} }
return result, nil return result, nil
} }
// Retrieves monsters by immunity type - O(1) lookup
func ByImmunity(immunityType int) ([]*Monster, error) { func ByImmunity(immunityType int) ([]*Monster, error) {
ms := GetStore() ms := GetStore()
ms.mu.RLock() return ms.GroupByIndex("byImmunity", immunityType), nil
defer ms.mu.RUnlock()
ids, exists := ms.byImmunity[immunityType]
if !exists {
return []*Monster{}, nil
}
result := make([]*Monster, 0, len(ids))
for _, id := range ids {
if monster, exists := ms.GetByID(id); exists {
result = append(result, monster)
}
}
return result, nil
} }
// Saves a new monster to the in-memory store and sets the ID // Insert with ID assignment
func (m *Monster) Insert() error { func (m *Monster) Insert() error {
ms := GetStore() ms := GetStore()
// Validate before insertion
if err := m.Validate(); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
// Assign new ID if not set
if m.ID == 0 { if m.ID == 0 {
m.ID = ms.GetNextID() m.ID = ms.GetNextID()
} }
return ms.AddMonster(m)
// Add to store
ms.AddMonster(m)
return nil
} }
// Returns true if the monster is immune to Hurt spells // Helper methods
func (m *Monster) IsHurtImmune() bool { func (m *Monster) IsHurtImmune() bool {
return m.Immune == ImmuneHurt return m.Immune == ImmuneHurt
} }
// Returns true if the monster is immune to Sleep spells
func (m *Monster) IsSleepImmune() bool { func (m *Monster) IsSleepImmune() bool {
return m.Immune == ImmuneSleep return m.Immune == ImmuneSleep
} }
// Returns true if the monster has any immunity
func (m *Monster) HasImmunity() bool { func (m *Monster) HasImmunity() bool {
return m.Immune != ImmuneNone return m.Immune != ImmuneNone
} }
// Returns the string representation of the monster's immunity
func (m *Monster) ImmunityName() string { func (m *Monster) ImmunityName() string {
switch m.Immune { switch m.Immune {
case ImmuneNone: case ImmuneNone:
@ -349,17 +187,13 @@ func (m *Monster) ImmunityName() string {
} }
} }
// Calculates a simple difficulty rating based on stats
func (m *Monster) DifficultyRating() float64 { func (m *Monster) DifficultyRating() float64 {
// Simple formula: (HP + Damage + Armor) / Level
// Higher values indicate tougher monsters relative to their level
if m.Level == 0 { if m.Level == 0 {
return 0 return 0
} }
return float64(m.MaxHP+m.MaxDmg+m.Armor) / float64(m.Level) return float64(m.MaxHP+m.MaxDmg+m.Armor) / float64(m.Level)
} }
// Returns the experience reward per hit point (efficiency metric)
func (m *Monster) ExpPerHP() float64 { func (m *Monster) ExpPerHP() float64 {
if m.MaxHP == 0 { if m.MaxHP == 0 {
return 0 return 0
@ -367,7 +201,6 @@ func (m *Monster) ExpPerHP() float64 {
return float64(m.MaxExp) / float64(m.MaxHP) return float64(m.MaxExp) / float64(m.MaxHP)
} }
// Returns the gold reward per hit point (efficiency metric)
func (m *Monster) GoldPerHP() float64 { func (m *Monster) GoldPerHP() float64 {
if m.MaxHP == 0 { if m.MaxHP == 0 {
return 0 return 0

View File

@ -6,35 +6,329 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"sort"
"strings"
"sync" "sync"
) )
// Store provides generic storage operations // Validatable interface for entities that can validate themselves
type Store[T any] interface { type Validatable interface {
LoadFromJSON(filename string) error Validate() error
SaveToJSON(filename string) error
LoadData(dataPath string) error
SaveData(dataPath string) error
} }
// BaseStore provides generic JSON persistence // IndexBuilder function type for building custom indices
type IndexBuilder[T any] func(allItems map[int]*T) any
// BaseStore provides generic storage with index management
type BaseStore[T any] struct { type BaseStore[T any] struct {
items map[int]*T items map[int]*T
maxID int maxID int
mu sync.RWMutex mu sync.RWMutex
itemType reflect.Type itemType reflect.Type
indices map[string]any
indexBuilders map[string]IndexBuilder[T]
} }
// NewBaseStore creates a new base store for type T // NewBaseStore creates a new base store for type T
func NewBaseStore[T any]() *BaseStore[T] { func NewBaseStore[T any]() *BaseStore[T] {
var zero T var zero T
return &BaseStore[T]{ return &BaseStore[T]{
items: make(map[int]*T), items: make(map[int]*T),
maxID: 0, maxID: 0,
itemType: reflect.TypeOf(zero), itemType: reflect.TypeOf(zero),
indices: make(map[string]any),
indexBuilders: make(map[string]IndexBuilder[T]),
} }
} }
// Index Management
// RegisterIndex registers an index builder function
func (bs *BaseStore[T]) RegisterIndex(name string, builder IndexBuilder[T]) {
bs.mu.Lock()
defer bs.mu.Unlock()
bs.indexBuilders[name] = builder
}
// GetIndex retrieves a named index
func (bs *BaseStore[T]) GetIndex(name string) (any, bool) {
bs.mu.RLock()
defer bs.mu.RUnlock()
index, exists := bs.indices[name]
return index, exists
}
// RebuildIndices rebuilds all registered indices
func (bs *BaseStore[T]) RebuildIndices() {
bs.mu.Lock()
defer bs.mu.Unlock()
bs.rebuildIndicesUnsafe()
}
func (bs *BaseStore[T]) rebuildIndicesUnsafe() {
allItems := make(map[int]*T, len(bs.items))
for k, v := range bs.items {
allItems[k] = v
}
for name, builder := range bs.indexBuilders {
bs.indices[name] = builder(allItems)
}
}
// Enhanced CRUD Operations
// AddWithRebuild adds item with validation and index rebuild
func (bs *BaseStore[T]) AddWithRebuild(id int, item *T) error {
bs.mu.Lock()
defer bs.mu.Unlock()
if validatable, ok := any(item).(Validatable); ok {
if err := validatable.Validate(); err != nil {
return err
}
}
bs.items[id] = item
if id > bs.maxID {
bs.maxID = id
}
bs.rebuildIndicesUnsafe()
return nil
}
// RemoveWithRebuild removes item and rebuilds indices
func (bs *BaseStore[T]) RemoveWithRebuild(id int) {
bs.mu.Lock()
defer bs.mu.Unlock()
delete(bs.items, id)
bs.rebuildIndicesUnsafe()
}
// UpdateWithRebuild updates item with validation and index rebuild
func (bs *BaseStore[T]) UpdateWithRebuild(id int, item *T) error {
return bs.AddWithRebuild(id, item)
}
// Common Query Methods
// Find retrieves an item by ID
func (bs *BaseStore[T]) Find(id int) (*T, bool) {
bs.mu.RLock()
defer bs.mu.RUnlock()
item, exists := bs.items[id]
return item, exists
}
// AllSorted returns all items using named sorted index
func (bs *BaseStore[T]) AllSorted(indexName string) []*T {
bs.mu.RLock()
defer bs.mu.RUnlock()
if index, exists := bs.indices[indexName]; exists {
if sortedIDs, ok := index.([]int); ok {
result := make([]*T, 0, len(sortedIDs))
for _, id := range sortedIDs {
if item, exists := bs.items[id]; exists {
result = append(result, item)
}
}
return result
}
}
// Fallback: return all items by ID order
ids := make([]int, 0, len(bs.items))
for id := range bs.items {
ids = append(ids, id)
}
sort.Ints(ids)
result := make([]*T, 0, len(ids))
for _, id := range ids {
result = append(result, bs.items[id])
}
return result
}
// LookupByIndex finds single item using string lookup index
func (bs *BaseStore[T]) LookupByIndex(indexName, key string) (*T, bool) {
bs.mu.RLock()
defer bs.mu.RUnlock()
if index, exists := bs.indices[indexName]; exists {
if lookupMap, ok := index.(map[string]int); ok {
if id, found := lookupMap[key]; found {
if item, exists := bs.items[id]; exists {
return item, true
}
}
}
}
return nil, false
}
// GroupByIndex returns items grouped by key
func (bs *BaseStore[T]) GroupByIndex(indexName string, key any) []*T {
bs.mu.RLock()
defer bs.mu.RUnlock()
if index, exists := bs.indices[indexName]; exists {
switch groupMap := index.(type) {
case map[int][]int:
if intKey, ok := key.(int); ok {
if ids, found := groupMap[intKey]; found {
result := make([]*T, 0, len(ids))
for _, id := range ids {
if item, exists := bs.items[id]; exists {
result = append(result, item)
}
}
return result
}
}
case map[string][]int:
if strKey, ok := key.(string); ok {
if ids, found := groupMap[strKey]; found {
result := make([]*T, 0, len(ids))
for _, id := range ids {
if item, exists := bs.items[id]; exists {
result = append(result, item)
}
}
return result
}
}
}
}
return []*T{}
}
// FilterByIndex returns items matching filter criteria
func (bs *BaseStore[T]) FilterByIndex(indexName string, filterFunc func(*T) bool) []*T {
bs.mu.RLock()
defer bs.mu.RUnlock()
var sourceIDs []int
if index, exists := bs.indices[indexName]; exists {
if sortedIDs, ok := index.([]int); ok {
sourceIDs = sortedIDs
}
}
if sourceIDs == nil {
for id := range bs.items {
sourceIDs = append(sourceIDs, id)
}
sort.Ints(sourceIDs)
}
var result []*T
for _, id := range sourceIDs {
if item, exists := bs.items[id]; exists && filterFunc(item) {
result = append(result, item)
}
}
return result
}
// Common Index Builders
// BuildStringLookupIndex creates string-to-ID mapping
func BuildStringLookupIndex[T any](keyFunc func(*T) string) IndexBuilder[T] {
return func(allItems map[int]*T) any {
index := make(map[string]int)
for id, item := range allItems {
key := keyFunc(item)
index[key] = id
}
return index
}
}
// BuildCaseInsensitiveLookupIndex creates lowercase string-to-ID mapping
func BuildCaseInsensitiveLookupIndex[T any](keyFunc func(*T) string) IndexBuilder[T] {
return func(allItems map[int]*T) any {
index := make(map[string]int)
for id, item := range allItems {
key := strings.ToLower(keyFunc(item))
index[key] = id
}
return index
}
}
// BuildIntGroupIndex creates int-to-[]ID mapping
func BuildIntGroupIndex[T any](keyFunc func(*T) int) IndexBuilder[T] {
return func(allItems map[int]*T) any {
index := make(map[int][]int)
for id, item := range allItems {
key := keyFunc(item)
index[key] = append(index[key], id)
}
// Sort each group by ID
for key := range index {
sort.Ints(index[key])
}
return index
}
}
// BuildStringGroupIndex creates string-to-[]ID mapping
func BuildStringGroupIndex[T any](keyFunc func(*T) string) IndexBuilder[T] {
return func(allItems map[int]*T) any {
index := make(map[string][]int)
for id, item := range allItems {
key := keyFunc(item)
index[key] = append(index[key], id)
}
// Sort each group by ID
for key := range index {
sort.Ints(index[key])
}
return index
}
}
// BuildSortedListIndex creates sorted []ID list
func BuildSortedListIndex[T any](sortFunc func(*T, *T) bool) IndexBuilder[T] {
return func(allItems map[int]*T) any {
ids := make([]int, 0, len(allItems))
for id := range allItems {
ids = append(ids, id)
}
sort.Slice(ids, func(i, j int) bool {
return sortFunc(allItems[ids[i]], allItems[ids[j]])
})
return ids
}
}
// Singleton Management Helper
// NewSingleton creates singleton store pattern with sync.Once
func NewSingleton[S any](initFunc func() *S) func() *S {
var store *S
var once sync.Once
return func() *S {
once.Do(func() {
store = initFunc()
})
return store
}
}
// Legacy Methods (backward compatibility)
// GetNextID returns the next available ID atomically // GetNextID returns the next available ID atomically
func (bs *BaseStore[T]) GetNextID() int { func (bs *BaseStore[T]) GetNextID() int {
bs.mu.Lock() bs.mu.Lock()
@ -45,10 +339,7 @@ func (bs *BaseStore[T]) GetNextID() int {
// GetByID retrieves an item by ID // GetByID retrieves an item by ID
func (bs *BaseStore[T]) GetByID(id int) (*T, bool) { func (bs *BaseStore[T]) GetByID(id int) (*T, bool) {
bs.mu.RLock() return bs.Find(id)
defer bs.mu.RUnlock()
item, exists := bs.items[id]
return item, exists
} }
// Add adds an item to the store // Add adds an item to the store
@ -85,8 +376,11 @@ func (bs *BaseStore[T]) Clear() {
defer bs.mu.Unlock() defer bs.mu.Unlock()
bs.items = make(map[int]*T) bs.items = make(map[int]*T)
bs.maxID = 0 bs.maxID = 0
bs.rebuildIndicesUnsafe()
} }
// JSON Persistence
// LoadFromJSON loads items from JSON using reflection // LoadFromJSON loads items from JSON using reflection
func (bs *BaseStore[T]) LoadFromJSON(filename string) error { func (bs *BaseStore[T]) LoadFromJSON(filename string) error {
bs.mu.Lock() bs.mu.Lock()
@ -148,7 +442,7 @@ func (bs *BaseStore[T]) SaveToJSON(filename string) error {
items = append(items, item) items = append(items, item)
} }
data, err := json.MarshalIndent(items, "", " ") data, err := json.MarshalIndent(items, "", "\t")
if err != nil { if err != nil {
return fmt.Errorf("failed to marshal to JSON: %w", err) return fmt.Errorf("failed to marshal to JSON: %w", err)
} }
@ -178,6 +472,7 @@ func (bs *BaseStore[T]) LoadData(dataPath string) error {
} }
fmt.Printf("Loaded %d items from JSON\n", len(bs.items)) fmt.Printf("Loaded %d items from JSON\n", len(bs.items))
bs.RebuildIndices() // Rebuild indices after loading
return nil return nil
} }