Added more functionality to the Pokedex; WIP - add more testing and refactor

This commit is contained in:
V 2025-10-05 15:00:38 +01:00
parent f0b303c0c0
commit f6e8038cdc
14 changed files with 324 additions and 86 deletions

63
command_catch.go Normal file
View File

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

View File

@ -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

43
command_explore.go Normal file
View File

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

View File

@ -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

45
command_inspect.go Normal file
View File

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

View File

@ -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

View File

@ -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

16
command_pokedex.go Normal file
View File

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

2
go.mod
View File

@ -1,3 +1,5 @@
module k3gtpi.jumpingcrab.com/go-learning/pokedexcli
go 1.24.5
require gopkg.in/yaml.v2 v2.4.0

4
go.sum Normal file
View File

@ -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=

View File

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

View File

@ -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")

132
repl.go
View File

@ -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,
},
}
}

View File

@ -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++
}
}