diff --git a/internal/monsters/monsters.go b/internal/monsters/monsters.go index c0cd7e8..eba2b5c 100644 --- a/internal/monsters/monsters.go +++ b/internal/monsters/monsters.go @@ -3,8 +3,6 @@ package monsters import ( "dk/internal/store" "fmt" - "sort" - "sync" ) // Monster represents a monster in the game @@ -21,14 +19,11 @@ type Monster struct { } func (m *Monster) Save() error { - monsterStore := GetStore() - monsterStore.UpdateMonster(m) - return nil + return GetStore().UpdateWithRebuild(m.ID, m) } func (m *Monster) Delete() error { - monsterStore := GetStore() - monsterStore.RemoveMonster(m.ID) + GetStore().RemoveWithRebuild(m.ID) return nil } @@ -36,13 +31,13 @@ func (m *Monster) Delete() error { func New() *Monster { return &Monster{ Name: "", - MaxHP: 10, // Default HP - MaxDmg: 5, // Default damage - Armor: 0, // Default armor - Level: 1, // Default level - MaxExp: 10, // Default exp reward - MaxGold: 5, // Default gold reward - Immune: ImmuneNone, // No immunity by default + MaxHP: 10, + MaxDmg: 5, + Armor: 0, + Level: 1, + MaxExp: 10, + MaxGold: 5, + Immune: ImmuneNone, } } @@ -63,279 +58,122 @@ func (m *Monster) Validate() error { return nil } -// Immunity constants for monster immunity types +// Immunity constants const ( ImmuneNone = 0 - ImmuneHurt = 1 // Immune to Hurt spells - ImmuneSleep = 2 // Immune to Sleep spells + ImmuneHurt = 1 + ImmuneSleep = 2 ) -// MonsterStore provides in-memory storage with O(1) lookups and monster-specific indices +// MonsterStore with enhanced BaseStore type MonsterStore struct { - *store.BaseStore[Monster] // Embedded generic store - 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 + *store.BaseStore[Monster] } -// Global in-memory store -var monsterStore *MonsterStore -var storeOnce sync.Once +// Global store with singleton pattern +var GetStore = store.NewSingleton(func() *MonsterStore { + ms := &MonsterStore{BaseStore: store.NewBaseStore[Monster]()} -// Initialize the in-memory store -func initStore() { - monsterStore = &MonsterStore{ - BaseStore: store.NewBaseStore[Monster](), - byLevel: make(map[int][]int), - byImmunity: make(map[int][]int), - allByLevel: make([]int, 0), - } + // Register indices + ms.RegisterIndex("byLevel", store.BuildIntGroupIndex(func(m *Monster) int { + return m.Level + })) + + ms.RegisterIndex("byImmunity", store.BuildIntGroupIndex(func(m *Monster) int { + 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) { - ms.mu.Lock() - defer ms.mu.Unlock() - - // Remove from base store - ms.Remove(id) - - // Rebuild indices - ms.rebuildIndicesUnsafe() + ms.RemoveWithRebuild(id) } -// UpdateMonster updates a monster efficiently -func (ms *MonsterStore) UpdateMonster(monster *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() +func (ms *MonsterStore) UpdateMonster(monster *Monster) error { + return ms.UpdateWithRebuild(monster.ID, monster) } -// LoadData loads monster data from JSON file, or starts with empty store +// Data persistence func LoadData(dataPath string) error { ms := GetStore() - - // 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 + return ms.BaseStore.LoadData(dataPath) } -// SaveData saves monster data to JSON file func SaveData(dataPath string) error { ms := GetStore() return ms.BaseStore.SaveData(dataPath) } -// rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock) -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 +// Query functions using enhanced store func Find(id int) (*Monster, error) { ms := GetStore() - monster, exists := ms.GetByID(id) + monster, exists := ms.Find(id) if !exists { return nil, fmt.Errorf("monster with ID %d not found", id) } return monster, nil } -// Retrieves all monsters - O(1) lookup (returns pre-sorted slice) func All() ([]*Monster, error) { ms := GetStore() - ms.mu.RLock() - 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 + return ms.AllSorted("allByLevel"), nil } -// Retrieves monsters by level - O(1) lookup func ByLevel(level int) ([]*Monster, error) { ms := GetStore() - ms.mu.RLock() - 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 + return ms.GroupByIndex("byLevel", level), nil } -// Retrieves monsters within a level range (inclusive) - O(k) where k is result size func ByLevelRange(minLevel, maxLevel int) ([]*Monster, error) { ms := GetStore() - ms.mu.RLock() - defer ms.mu.RUnlock() - var result []*Monster for level := minLevel; level <= maxLevel; level++ { - if ids, exists := ms.byLevel[level]; exists { - for _, id := range ids { - if monster, exists := ms.GetByID(id); exists { - result = append(result, monster) - } - } - } + monsters := ms.GroupByIndex("byLevel", level) + result = append(result, monsters...) } return result, nil } -// Retrieves monsters by immunity type - O(1) lookup func ByImmunity(immunityType int) ([]*Monster, error) { ms := GetStore() - ms.mu.RLock() - 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 + return ms.GroupByIndex("byImmunity", immunityType), nil } -// Saves a new monster to the in-memory store and sets the ID +// Insert with ID assignment func (m *Monster) Insert() error { 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 { m.ID = ms.GetNextID() } - - // Add to store - ms.AddMonster(m) - return nil + return ms.AddMonster(m) } -// Returns true if the monster is immune to Hurt spells +// Helper methods func (m *Monster) IsHurtImmune() bool { return m.Immune == ImmuneHurt } -// Returns true if the monster is immune to Sleep spells func (m *Monster) IsSleepImmune() bool { return m.Immune == ImmuneSleep } -// Returns true if the monster has any immunity func (m *Monster) HasImmunity() bool { return m.Immune != ImmuneNone } -// Returns the string representation of the monster's immunity func (m *Monster) ImmunityName() string { switch m.Immune { case ImmuneNone: @@ -349,17 +187,13 @@ func (m *Monster) ImmunityName() string { } } -// Calculates a simple difficulty rating based on stats func (m *Monster) DifficultyRating() float64 { - // Simple formula: (HP + Damage + Armor) / Level - // Higher values indicate tougher monsters relative to their level if m.Level == 0 { return 0 } 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 { if m.MaxHP == 0 { return 0 @@ -367,7 +201,6 @@ func (m *Monster) ExpPerHP() float64 { return float64(m.MaxExp) / float64(m.MaxHP) } -// Returns the gold reward per hit point (efficiency metric) func (m *Monster) GoldPerHP() float64 { if m.MaxHP == 0 { return 0 diff --git a/internal/store/store.go b/internal/store/store.go index 1120a0a..9f8247b 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -6,35 +6,329 @@ import ( "os" "path/filepath" "reflect" + "sort" + "strings" "sync" ) -// Store provides generic storage operations -type Store[T any] interface { - LoadFromJSON(filename string) error - SaveToJSON(filename string) error - LoadData(dataPath string) error - SaveData(dataPath string) error +// Validatable interface for entities that can validate themselves +type Validatable interface { + Validate() 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 { - items map[int]*T - maxID int - mu sync.RWMutex - itemType reflect.Type + items map[int]*T + maxID int + mu sync.RWMutex + itemType reflect.Type + indices map[string]any + indexBuilders map[string]IndexBuilder[T] } // NewBaseStore creates a new base store for type T func NewBaseStore[T any]() *BaseStore[T] { var zero T return &BaseStore[T]{ - items: make(map[int]*T), - maxID: 0, - itemType: reflect.TypeOf(zero), + items: make(map[int]*T), + maxID: 0, + 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 func (bs *BaseStore[T]) GetNextID() int { bs.mu.Lock() @@ -45,10 +339,7 @@ func (bs *BaseStore[T]) GetNextID() int { // GetByID retrieves an item by ID func (bs *BaseStore[T]) GetByID(id int) (*T, bool) { - bs.mu.RLock() - defer bs.mu.RUnlock() - item, exists := bs.items[id] - return item, exists + return bs.Find(id) } // Add adds an item to the store @@ -85,8 +376,11 @@ func (bs *BaseStore[T]) Clear() { defer bs.mu.Unlock() bs.items = make(map[int]*T) bs.maxID = 0 + bs.rebuildIndicesUnsafe() } +// JSON Persistence + // LoadFromJSON loads items from JSON using reflection func (bs *BaseStore[T]) LoadFromJSON(filename string) error { bs.mu.Lock() @@ -148,7 +442,7 @@ func (bs *BaseStore[T]) SaveToJSON(filename string) error { items = append(items, item) } - data, err := json.MarshalIndent(items, "", " ") + data, err := json.MarshalIndent(items, "", "\t") if err != nil { 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)) + bs.RebuildIndices() // Rebuild indices after loading return nil }