From f6e8038cdc955d031c9298186628f64066716f6f Mon Sep 17 00:00:00 2001 From: V Date: Sun, 5 Oct 2025 15:00:38 +0100 Subject: [PATCH] Added more functionality to the Pokedex; WIP - add more testing and refactor --- command_catch.go | 63 +++++++++++++ command_exit.go | 3 +- command_explore.go | 43 +++++++++ command_help.go | 6 +- command_inspect.go | 45 +++++++++ command_map.go | 22 ++--- command_mapb.go | 23 +++-- command_pokedex.go | 16 ++++ go.mod | 2 + go.sum | 4 + internal/pokecache/pokecache.go | 24 +++-- internal/pokecache/pokecache_test.go | 5 +- repl.go | 132 +++++++++++++++++++++------ repl_test.go | 22 ++--- 14 files changed, 324 insertions(+), 86 deletions(-) create mode 100644 command_catch.go create mode 100644 command_explore.go create mode 100644 command_inspect.go create mode 100644 command_pokedex.go create mode 100644 go.sum diff --git a/command_catch.go b/command_catch.go new file mode 100644 index 0000000..3607960 --- /dev/null +++ b/command_catch.go @@ -0,0 +1,63 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "math/rand" + "net/http" +) + +func commandCatch(p *PokedexConfig, a *string) error { + if *a == "" { + return fmt.Errorf("you need to specify a Pokemon to catch") + } + var pokemonName string + var baseUrl string + for _, encounter := range p.LocalPokemons.PokemonEncounters { + if encounter.PokemonSummary.Name == *a { + pokemonName = *a + baseUrl = encounter.PokemonSummary.DataUrl + } + } + if pokemonName == "" { + baseUrl = "https://pokeapi.co/api/v2/pokemon/" + *a + } + // if pokemonName != *a { + // fmt.Println("this pokemon is not present in the current area") + // return nil + // } + + body, exists := p.Cache.Get(baseUrl) + if !exists { + client := http.Client{} + + resp, err := client.Get(baseUrl) + if err != nil { + return fmt.Errorf("could not make request to Pokedex API! Err: %w", err) + } + body, err = io.ReadAll(resp.Body) + defer resp.Body.Close() + if resp.StatusCode > 299 { + return fmt.Errorf("request returned non-200 code! Code: %v Body: %v", resp.StatusCode, body) + } + if err != nil { + return fmt.Errorf("could not read request body! Err: %w", err) + } + p.Cache.Add(baseUrl, body) + } + var pokemon PokemonDetails + if err := json.Unmarshal(body, &pokemon); err != nil { + return fmt.Errorf("could not unmarshal Pokemon data! err: %w", err) + } + fmt.Printf("Throwing a Pokeball at %s...\n", *a) + catchSuccess := rand.Intn(pokemon.BaseExperience) + if 2*catchSuccess >= pokemon.BaseExperience { + fmt.Printf("%s was caught!\n", pokemon.Name) + p.CaughtPokemon = append(p.CaughtPokemon, pokemon) + return nil + } + fmt.Printf("%s escaped!\n", *a) + + return nil +} diff --git a/command_exit.go b/command_exit.go index fbf1360..ae0e7b5 100644 --- a/command_exit.go +++ b/command_exit.go @@ -3,10 +3,9 @@ package main import ( "fmt" "os" - "k3gtpi.jumpingcrab.com/go-learning/pokedexcli/internal/pokecache" ) -func commandExit(p *PokedexConfig, c *pokecache.Cache) error { +func commandExit(p *PokedexConfig, a *string) error { fmt.Println("Closing the Pokedex... Goodbye!") os.Exit(0) return nil diff --git a/command_explore.go b/command_explore.go new file mode 100644 index 0000000..8abad7c --- /dev/null +++ b/command_explore.go @@ -0,0 +1,43 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) + +func commandExplore(p *PokedexConfig, a *string) error { + if *a == "" { + return fmt.Errorf("you need to specify an area to explore") + } + baseUrl := "https://pokeapi.co/api/v2/location-area/" + *a + "/" + body, exists := p.Cache.Get(baseUrl) + if !exists { + client := http.Client{} + + resp, err := client.Get(baseUrl) + if err != nil { + return fmt.Errorf("could not make request to Pokedex API! Err: %w", err) + } + + body, err = io.ReadAll(resp.Body) + defer resp.Body.Close() + if resp.StatusCode > 299 { + return fmt.Errorf("request returned non-200 code! Code: %v Body: %v", resp.StatusCode, body) + } + if err != nil { + return fmt.Errorf("could not read request body! Err: %w", err) + } + p.Cache.Add(baseUrl, body) + } + if err := json.Unmarshal(body, &p.LocalPokemons); err != nil { + return fmt.Errorf("could not unmarshal response! Err: %w", err) + } + + for _, pokemon := range p.LocalPokemons.PokemonEncounters { + fmt.Println(pokemon.PokemonSummary.Name) + } + + return nil +} diff --git a/command_help.go b/command_help.go index b099c01..6eac89d 100644 --- a/command_help.go +++ b/command_help.go @@ -2,14 +2,12 @@ package main import ( "fmt" - "k3gtpi.jumpingcrab.com/go-learning/pokedexcli/internal/pokecache" ) - -func commandHelp(p *PokedexConfig, c *pokecache.Cache) error { +func commandHelp(p *PokedexConfig, a *string) error { fmt.Print("Pokedex CLI - available commands:\n\n") for _, v := range getCommands() { - fmt.Printf("%s %s\n", v.name, v.description) + fmt.Printf("%s %s\n", v.name, v.description) } fmt.Println() return nil diff --git a/command_inspect.go b/command_inspect.go new file mode 100644 index 0000000..95b943e --- /dev/null +++ b/command_inspect.go @@ -0,0 +1,45 @@ +package main + +import ( + "fmt" + + "gopkg.in/yaml.v2" +) + +type PokemonInspectOutput struct { + Name string + Height int + Weight int + Stats map[string]int + Types []string +} + +func commandInspect(p *PokedexConfig, a *string) error { + found := false + for _, pokemon := range p.CaughtPokemon { + if pokemon.Name == *a { + found = true + stats := map[string]int{} + types := []string{} + for _, pokeStat := range pokemon.PokeStats { + stats[pokeStat.PokeStat.Name] = pokeStat.BaseStat + } + for _, pokeType := range pokemon.PokeTypes { + types = append(types, pokeType.PokeType.Name) + } + pokemonInspectOutput := PokemonInspectOutput{ + Name: pokemon.Name, + Height: pokemon.Height, + Weight: pokemon.Weight, + Stats: stats, + Types: types, + } + data, _ := yaml.Marshal(pokemonInspectOutput) + fmt.Println(string(data)) + } + } + if !found { + fmt.Println("you have not caught that pokemon") + } + return nil +} diff --git a/command_map.go b/command_map.go index 1e3d73e..20ec578 100644 --- a/command_map.go +++ b/command_map.go @@ -1,25 +1,23 @@ package main import ( - "fmt" - "net/http" "encoding/json" + "fmt" "io" - "k3gtpi.jumpingcrab.com/go-learning/pokedexcli/internal/pokecache" + "net/http" ) - -func commandMap(p *PokedexConfig, c *pokecache.Cache) error { +func commandMap(p *PokedexConfig, a *string) error { var baseUrl string - if p.Next == nil { + if p.LocationData.Next == nil { baseUrl = "https://pokeapi.co/api/v2/location-area/" } else { - baseUrl = *p.Next + baseUrl = *p.LocationData.Next } var body []byte // Check if respones is available in cache - if resp, exists := c.Get(baseUrl); exists { + if resp, exists := p.Cache.Get(baseUrl); exists { body = resp } else { client := &http.Client{} @@ -29,7 +27,7 @@ func commandMap(p *PokedexConfig, c *pokecache.Cache) error { return fmt.Errorf("could not make request to Pokedex API! Err: %w", err) } - body, err = io.ReadAll(resp.Body) + body, err = io.ReadAll(resp.Body) defer resp.Body.Close() if resp.StatusCode > 299 { return fmt.Errorf("request returned non-200 code! Code: %v Body: %v", resp.StatusCode, body) @@ -37,13 +35,13 @@ func commandMap(p *PokedexConfig, c *pokecache.Cache) error { if err != nil { return fmt.Errorf("could not read request body! Err: %w", err) } - c.Add(baseUrl, body) + p.Cache.Add(baseUrl, body) } - if err := json.Unmarshal(body, &p); err != nil { + if err := json.Unmarshal(body, &p.LocationData); err != nil { return fmt.Errorf("could not unmarshal response! Err: %w", err) } - for _, location := range p.Results { + for _, location := range p.LocationData.Locations { fmt.Println(location["name"]) } return nil diff --git a/command_mapb.go b/command_mapb.go index 1d29fe2..5f6eca7 100644 --- a/command_mapb.go +++ b/command_mapb.go @@ -1,33 +1,32 @@ package main import ( + "encoding/json" "fmt" "io" - "encoding/json" "net/http" - "k3gtpi.jumpingcrab.com/go-learning/pokedexcli/internal/pokecache" ) -func commandMapb(p *PokedexConfig, c *pokecache.Cache) error { +func commandMapb(p *PokedexConfig, a *string) error { var baseUrl string - if p.Previous == nil { + if p.LocationData.Previous == nil { fmt.Println("you're on the first page") return nil } else { - baseUrl = *p.Previous + baseUrl = *p.LocationData.Previous } var body []byte - if resp, exists := c.Get(baseUrl); exists { + if resp, exists := p.Cache.Get(baseUrl); exists { body = resp } else { client := &http.Client{} - + resp, err := client.Get(baseUrl) if err != nil { return fmt.Errorf("could not make request to Pokedex API! Err: %w", err) } - - body, err = io.ReadAll(resp.Body) + + body, err = io.ReadAll(resp.Body) defer resp.Body.Close() if resp.StatusCode > 299 { return fmt.Errorf("request returned non-200 code! Code: %v Body: %v", resp.StatusCode, body) @@ -36,12 +35,12 @@ func commandMapb(p *PokedexConfig, c *pokecache.Cache) error { return fmt.Errorf("could not read request body! Err: %w", err) } - c.Add(baseUrl, body) + p.Cache.Add(baseUrl, body) } - if err := json.Unmarshal(body, &p); err != nil { + if err := json.Unmarshal(body, &p.LocationData); err != nil { return fmt.Errorf("could not unmarshal response! Err: %w", err) } - for _, location := range p.Results { + for _, location := range p.LocationData.Locations { fmt.Println(location["name"]) } return nil diff --git a/command_pokedex.go b/command_pokedex.go new file mode 100644 index 0000000..e087a1e --- /dev/null +++ b/command_pokedex.go @@ -0,0 +1,16 @@ +package main + +import "fmt" + +func commandPokedex(p *PokedexConfig, a *string) error { + if len(p.CaughtPokemon) == 0 { + fmt.Println("Your Pokedex is empty.") + return nil + } + fmt.Println("Your Pokedex:") + for _, pokemon := range p.CaughtPokemon { + fmt.Println(" - ", pokemon.Name) + } + + return nil +} diff --git a/go.mod b/go.mod index 0e362a9..6485bd2 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module k3gtpi.jumpingcrab.com/go-learning/pokedexcli go 1.24.5 + +require gopkg.in/yaml.v2 v2.4.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..dd0bc19 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/internal/pokecache/pokecache.go b/internal/pokecache/pokecache.go index ce2d320..9cdd381 100644 --- a/internal/pokecache/pokecache.go +++ b/internal/pokecache/pokecache.go @@ -1,25 +1,25 @@ package pokecache import ( - "time" "sync" + "time" ) type cacheEntry struct { - createdAt time.Time - val []byte + createdAt time.Time + val []byte } type Cache struct { - PokeCache map[string]cacheEntry - Interval time.Duration - Mu sync.Mutex + PokeCache map[string]cacheEntry + Interval time.Duration + Mu sync.Mutex } func (c *Cache) Add(key string, val []byte) { newEntry := cacheEntry{ - createdAt: time.Now(), - val: val, + createdAt: time.Now(), + val: val, } c.Mu.Lock() defer c.Mu.Unlock() @@ -45,7 +45,7 @@ func (c *Cache) reapLoop() { c.Mu.Lock() for k, v := range c.PokeCache { if time.Since(v.createdAt) > c.Interval { - delete(c.PokeCache, k) + delete(c.PokeCache, k) } } c.Mu.Unlock() @@ -55,11 +55,9 @@ func (c *Cache) reapLoop() { func NewCache(interval time.Duration) *Cache { newCache := Cache{ PokeCache: map[string]cacheEntry{}, - Interval: interval, - Mu: sync.Mutex{}, + Interval: interval, + Mu: sync.Mutex{}, } go newCache.reapLoop() return &newCache } - - diff --git a/internal/pokecache/pokecache_test.go b/internal/pokecache/pokecache_test.go index 9758829..ee3fe03 100644 --- a/internal/pokecache/pokecache_test.go +++ b/internal/pokecache/pokecache_test.go @@ -1,12 +1,11 @@ package pokecache import ( + "fmt" "testing" "time" - "fmt" ) - func TestAddGet(t *testing.T) { const interval = 5 * time.Second cases := []struct { @@ -54,7 +53,7 @@ func TestReapLoop(t *testing.T) { const waitTime = baseTime + 5*time.Millisecond cache := NewCache(baseTime) cache.Add("https://example.com", []byte("testdata")) - + _, ok := cache.Get("https://example.com") if !ok { t.Errorf("expected to find key") diff --git a/repl.go b/repl.go index 86db711..6f5d210 100644 --- a/repl.go +++ b/repl.go @@ -1,52 +1,106 @@ package main import ( - "fmt" - "strings" "bufio" + "fmt" "os" - "k3gtpi.jumpingcrab.com/go-learning/pokedexcli/internal/pokecache" + "strings" "time" + + "k3gtpi.jumpingcrab.com/go-learning/pokedexcli/internal/pokecache" ) type cliCommand struct { - name string - description string - callback func(*PokedexConfig, *pokecache.Cache) error + name string + description string + callback func(*PokedexConfig, *string) error +} + +type PokedexLocationData struct { + Next *string `json:"next"` + Previous *string `json:"previous"` + Locations []map[string]string `json:"results"` +} + +type PokemonSummary struct { + Name string `json:"name"` + DataUrl string `json:"url"` +} + +type PokemonStat struct { + Name string `json:"name"` +} + +type PokemonStats struct { + BaseStat int `json:"base_stat"` + PokeStat PokemonStat `json:"stat"` +} + +type PokemonTypeEntry struct { + Name string `json:"name"` +} + +type PokemonTypes struct { + PokeType PokemonTypeEntry `json:"type"` +} + +type PokemonDetails struct { + Name string `json:"name"` + BaseExperience int `json:"base_experience"` + Weight int `json:"weight"` + Height int `json:"height"` + PokeStats []PokemonStats `json:"stats"` + PokeTypes []PokemonTypes `json:"types"` +} + +type PokemonEncounterData struct { + PokemonSummary PokemonSummary `json:"pokemon"` +} + +type PokedexLocalPokemons struct { + LocationName string `json:"name"` + PokemonEncounters []PokemonEncounterData `json:"pokemon_encounters"` } type PokedexConfig struct { - Next *string - Previous *string - Results []map[string]string + Cache *pokecache.Cache + LocationData PokedexLocationData + LocalPokemons PokedexLocalPokemons + CaughtPokemon []PokemonDetails } -var supportedCommands map[string]cliCommand - func startRepl() { reader := bufio.NewScanner(os.Stdin) fmt.Println("Welcome to the Pokedex!") pokedexConfig := PokedexConfig{} - cache := pokecache.NewCache(5 * time.Second) + pokedexConfig.Cache = pokecache.NewCache(5 * time.Second) for { - // Print prompt fmt.Printf("Pokedex > ") reader.Scan() words := cleanInput(reader.Text()) if len(words) == 0 { continue + } else if len(words) > 2 { + fmt.Printf("Commands can only take one argument!") + continue } - commandName := words[0] + commandName := words[0] command, valid := getCommands()[commandName] if !valid { fmt.Println("Unknown command.") continue } - if err := command.callback(&pokedexConfig, cache); err != nil { - fmt.Printf("Encountered error running command: %v\nErr: %v", command.name, err) + + arg := "" + if len(words) == 2 { + arg = words[1] + } + + if err := command.callback(&pokedexConfig, &arg); err != nil { + fmt.Printf("Encountered error running command: %v\nErr: %v\n\n", command.name, err) } } } @@ -61,27 +115,47 @@ func cleanInput(text string) []string { return words } -func getCommands() map[string]cliCommand{ +func getCommands() map[string]cliCommand { return map[string]cliCommand{ "help": { - name: "help", - description: "Prints this help menu.", - callback: commandHelp, + name: "help", + description: "Prints this help menu.", + callback: commandHelp, }, "exit": { - name: "exit", - description: "Exits the Pokedex", - callback: commandExit, + name: "exit", + description: "Exits the Pokedex", + callback: commandExit, }, "map": { - name: "map", - description: "Print Pokemon world locations.", - callback: commandMap, + name: "map", + description: "Print Pokemon world locations.", + callback: commandMap, }, "mapb": { - name: "mapb", - description: "Print previoud Pokemon locationsi.", - callback: commandMapb, + name: "mapb", + description: "Print previous Pokemon locations.", + callback: commandMapb, + }, + "explore": { + name: "explore", + description: "Print Pokemon found in specified area. Requires AREA_NAME argument.", + callback: commandExplore, + }, + "catch": { + name: "catch", + description: "Attempt to catch a Pokemon present in current area. Requires POKEMON_NAME argument.", + callback: commandCatch, + }, + "inspect": { + name: "inspect", + description: "Inspect a caught Pokemon. Requires POKEMON_NAME argument.", + callback: commandInspect, + }, + "pokedex": { + name: "pokedex", + description: "Inspect the Pokedex contents.", + callback: commandPokedex, }, } } diff --git a/repl_test.go b/repl_test.go index e6a58da..780d440 100644 --- a/repl_test.go +++ b/repl_test.go @@ -1,26 +1,26 @@ package main import ( - "testing" "fmt" + "testing" ) -func TestCleanInput(t * testing.T) { +func TestCleanInput(t *testing.T) { cases := []struct { - input string - expected []string + input string + expected []string }{ { - input: " hello world ", - expected: []string{"hello", "world"}, + input: " hello world ", + expected: []string{"hello", "world"}, }, { - input: "Charmander Bulbasaur PIKACHU", - expected: []string{"charmander", "bulbasaur", "pikachu"}, + input: "Charmander Bulbasaur PIKACHU", + expected: []string{"charmander", "bulbasaur", "pikachu"}, }, { - input: "", - expected: []string{}, + input: "", + expected: []string{}, }, } successCount := 0 @@ -51,7 +51,7 @@ Output: %v Input: %v Expected: %v Output: %v -`, c.input, c.expected, output) +`, c.input, c.expected, output) successCount++ } }