clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

chore/regenerate-vhs-gifs
puzzletea / catalog / catalog.go
catalog.go 4.0K
1// Package catalog indexes puzzle definitions, aliases, and daily metadata2// without importing concrete game implementations.3package catalog45import (6 "fmt"78 "github.com/FelineStateMachine/puzzletea/puzzle"9)1011type DailyEntry struct {12 GameID puzzle.GameID13 GameName string14 ModeID puzzle.ModeID15 Mode string16}1718type Index struct {19 definitions []puzzle.Definition20 names []string21 dailies []DailyEntry22 byName map[string]puzzle.Definition23 byAlias map[string]string24}2526func MustBuild(definitions []puzzle.Definition) Index {27 idx, err := Build(definitions)28 if err != nil {29 panic(err)30 }31 return idx32}3334func Build(definitions []puzzle.Definition) (Index, error) {35 if err := Validate(definitions); err != nil {36 return Index{}, err37 }3839 idx := Index{40 definitions: append([]puzzle.Definition(nil), definitions...),41 names: make([]string, 0, len(definitions)),42 dailies: make([]DailyEntry, 0, len(definitions)*2),43 byName: make(map[string]puzzle.Definition, len(definitions)),44 byAlias: make(map[string]string),45 }4647 for _, def := range definitions {48 idx.names = append(idx.names, def.Name)49 idx.byName[puzzle.NormalizeName(def.Name)] = def50 for _, alias := range def.Aliases {51 idx.byAlias[puzzle.NormalizeName(alias)] = def.Name52 }53 byModeID := make(map[puzzle.ModeID]puzzle.ModeDef, len(def.Modes))54 for _, mode := range def.Modes {55 byModeID[mode.ID] = mode56 }57 for _, dailyID := range def.DailyModeIDs {58 mode, ok := byModeID[dailyID]59 if !ok {60 continue61 }62 idx.dailies = append(idx.dailies, DailyEntry{63 GameID: def.ID,64 GameName: def.Name,65 ModeID: mode.ID,66 Mode: mode.Title,67 })68 }69 }7071 return idx, nil72}7374func Validate(definitions []puzzle.Definition) error {75 names := make(map[string]string, len(definitions))76 aliases := make(map[string]string)7778 for _, def := range definitions {79 normName := puzzle.NormalizeName(def.Name)80 if normName == "" {81 return fmt.Errorf("game %q has empty canonical name", def.Name)82 }83 if def.ID == "" {84 return fmt.Errorf("game %q has empty id", def.Name)85 }86 if prior, exists := names[normName]; exists {87 return fmt.Errorf("duplicate game name %q conflicts with %q", def.Name, prior)88 }89 names[normName] = def.Name9091 modeTitles := make(map[puzzle.ModeID]struct{}, len(def.Modes))92 for _, mode := range def.Modes {93 if mode.ID == "" {94 return fmt.Errorf("game %q has mode %q with empty id", def.Name, mode.Title)95 }96 modeTitles[mode.ID] = struct{}{}97 }9899 for _, dailyID := range def.DailyModeIDs {100 if _, ok := modeTitles[dailyID]; !ok {101 return fmt.Errorf("game %q daily mode %q is not present in Modes", def.Name, dailyID)102 }103 }104105 for _, alias := range def.Aliases {106 normAlias := puzzle.NormalizeName(alias)107 if normAlias == "" {108 return fmt.Errorf("game %q has empty alias", def.Name)109 }110 if normAlias == normName {111 return fmt.Errorf("game %q alias %q duplicates the canonical name", def.Name, alias)112 }113 if prior, exists := names[normAlias]; exists {114 return fmt.Errorf("game %q alias %q conflicts with canonical game %q", def.Name, alias, prior)115 }116 if prior, exists := aliases[normAlias]; exists {117 return fmt.Errorf("game %q alias %q conflicts with alias owned by %q", def.Name, alias, prior)118 }119 aliases[normAlias] = def.Name120 }121 }122123 return nil124}125126func (i Index) Names() []string {127 return append([]string(nil), i.names...)128}129130func (i Index) Definitions() []puzzle.Definition {131 return append([]puzzle.Definition(nil), i.definitions...)132}133134func (i Index) DailyEntries() []DailyEntry {135 return append([]DailyEntry(nil), i.dailies...)136}137138func (i Index) Lookup(name string) (puzzle.Definition, bool) {139 def, ok := i.byName[puzzle.NormalizeName(name)]140 return def, ok141}142143func (i Index) Resolve(name string) (puzzle.Definition, bool) {144 norm := puzzle.NormalizeName(name)145 if def, ok := i.byName[norm]; ok {146 return def, true147 }148 canonical, ok := i.byAlias[norm]149 if !ok {150 return puzzle.Definition{}, false151 }152 return i.Lookup(canonical)153}