From 80d1b5b6c054da926698722662daaa55f6849df2 Mon Sep 17 00:00:00 2001 From: Dan Anglin Date: Sat, 21 Sep 2024 15:19:53 +0100 Subject: [PATCH] feat: add internal trainer package Added an internal package for the Pokemon trainer (the user of the application). The new Trainer type keeps track of the Pokemon in the trainer's Pokedex and the location where the trainer is visiting. Added the visit command to allow the Pokemon trainer to visit and explore an area within the Pokemon world. The explore command no longer reads any arguments and uses the trainer's current location to get a list of Pokemon encounters. Updated the catch command to ensure that the Pokemon the trainer attempts to catch is in the same location area that the trainer is visiting. --- internal/api/pokeapi/locationarea.go | 5 + internal/pokeclient/pokeclient.go | 42 +++++- internal/poketrainer/trainer.go | 71 ++++++++++ main.go | 202 ++++++++++++++++----------- 4 files changed, 233 insertions(+), 87 deletions(-) create mode 100644 internal/poketrainer/trainer.go diff --git a/internal/api/pokeapi/locationarea.go b/internal/api/pokeapi/locationarea.go index c5e9c3c..d3d8c4e 100644 --- a/internal/api/pokeapi/locationarea.go +++ b/internal/api/pokeapi/locationarea.go @@ -11,6 +11,11 @@ type LocationArea struct { PokemonEncounters []PokemonEncounter `json:"pokemon_encounters"` } +type LocationAreaEncounter struct { + LocationArea NamedAPIResource `json:"location_area"` + VersionDetails []VersionEncounterDetails `json:"version_details"` +} + type EncounterMethodRate struct { EncounterMethod NamedAPIResource `json:"encounter_method"` VersionDetails []EncounterVersionDetails `json:"version_details"` diff --git a/internal/pokeclient/pokeclient.go b/internal/pokeclient/pokeclient.go index 02cd988..f2baa7e 100644 --- a/internal/pokeclient/pokeclient.go +++ b/internal/pokeclient/pokeclient.go @@ -13,10 +13,10 @@ import ( ) const ( - baseURL string = "https://pokeapi.co/api/v2" + baseURL string = "https://pokeapi.co" - LocationAreaPath = baseURL + "/location-area" - PokemonPath = baseURL + "/pokemon" + LocationAreaPath = baseURL + "/api/v2/location-area" + PokemonPath = baseURL + "/api/v2/pokemon" ) type Client struct { @@ -42,7 +42,7 @@ func (c *Client) GetNamedAPIResourceList(url string) (pokeapi.NamedAPIResourceLi data, exists := c.cache.Get(url) if exists { - fmt.Println("Using data from cache.") + fmt.Println("(using data from cache)") if err := decodeJSON(data, &list); err != nil { return pokeapi.NamedAPIResourceList{}, fmt.Errorf("unable to decode the data from the cache: %w", err) @@ -75,7 +75,7 @@ func (c *Client) GetLocationArea(location string) (pokeapi.LocationArea, error) data, exists := c.cache.Get(url) if exists { - fmt.Println("Using data from cache.") + fmt.Println("(using data from cache)") if err := decodeJSON(data, &locationArea); err != nil { return pokeapi.LocationArea{}, fmt.Errorf("unable to decode the data from the cache: %w", err) @@ -108,7 +108,7 @@ func (c *Client) GetPokemon(pokemonName string) (pokeapi.Pokemon, error) { data, exists := c.cache.Get(url) if exists { - fmt.Println("Using data from cache.") + fmt.Println("(using data from cache)") if err := decodeJSON(data, &pokemon); err != nil { return pokeapi.Pokemon{}, fmt.Errorf("unable to decode the data from the cache: %w", err) @@ -134,6 +134,36 @@ func (c *Client) GetPokemon(pokemonName string) (pokeapi.Pokemon, error) { return pokemon, nil } +func (c *Client) GetPokemonLocationAreas(url string) ([]pokeapi.LocationAreaEncounter, error) { + var locationAreaEncounters []pokeapi.LocationAreaEncounter + + data, exists := c.cache.Get(url) + if exists { + fmt.Println("(using data from cache)") + + if err := decodeJSON(data, &locationAreaEncounters); err != nil { + return []pokeapi.LocationAreaEncounter{}, fmt.Errorf( + "unable to decode the data from the cache: %w", + err, + ) + } + } + + data, err := c.sendRequest(url) + if err != nil { + return []pokeapi.LocationAreaEncounter{}, fmt.Errorf( + "received an error after sending the request to the server: %w", + err, + ) + } + + if err := decodeJSON(data, &locationAreaEncounters); err != nil { + return []pokeapi.LocationAreaEncounter{}, fmt.Errorf("unable to decode the data from the server: %w", err) + } + + return locationAreaEncounters, nil +} + func (c *Client) sendRequest(url string) ([]byte, error) { ctx, cancel := context.WithTimeout(context.Background(), c.timeout) defer cancel() diff --git a/internal/poketrainer/trainer.go b/internal/poketrainer/trainer.go new file mode 100644 index 0000000..2cece52 --- /dev/null +++ b/internal/poketrainer/trainer.go @@ -0,0 +1,71 @@ +package poketrainer + +import ( + "fmt" + "maps" + + "codeflow.dananglin.me.uk/apollo/pokedex/internal/api/pokeapi" +) + +type Trainer struct { + previousLocationArea *string + nextLocationArea *string + currentLocationAreaName string + pokedex map[string]pokeapi.Pokemon +} + +func NewTrainer() *Trainer { + trainer := Trainer{ + previousLocationArea: nil, + nextLocationArea: nil, + currentLocationAreaName: "", + pokedex: make(map[string]pokeapi.Pokemon), + } + + return &trainer +} + +func (t *Trainer) UpdateLocationAreas(previous, next *string) { + t.previousLocationArea = previous + t.nextLocationArea = next +} + +func (t *Trainer) PreviousLocationArea() *string { + return t.previousLocationArea +} + +func (t *Trainer) NextLocationArea() *string { + return t.nextLocationArea +} + +func (t *Trainer) AddPokemonToPokedex(name string, details pokeapi.Pokemon) { + t.pokedex[name] = details +} + +func (t *Trainer) GetPokemonFromPokedex(name string) (pokeapi.Pokemon, bool) { + details, ok := t.pokedex[name] + + return details, ok +} + +func (t *Trainer) ListAllPokemonFromPokedex() { + if len(t.pokedex) == 0 { + fmt.Println("You have no Pokemon in your Pokedex.") + + return + } + + fmt.Println("Your Pokedex:") + + for name := range maps.All(t.pokedex) { + fmt.Println(" -", name) + } +} + +func (t *Trainer) CurrentLocationAreaName() string { + return t.currentLocationAreaName +} + +func (t *Trainer) UpdateCurrentLocationAreaName(locationName string) { + t.currentLocationAreaName = locationName +} diff --git a/main.go b/main.go index b44954e..c59afa5 100644 --- a/main.go +++ b/main.go @@ -11,14 +11,11 @@ import ( "strings" "time" - "codeflow.dananglin.me.uk/apollo/pokedex/internal/api/pokeapi" "codeflow.dananglin.me.uk/apollo/pokedex/internal/pokeclient" + "codeflow.dananglin.me.uk/apollo/pokedex/internal/poketrainer" ) -type State struct { - Previous *string - Next *string -} +type callbackFunc func(args []string) error type command struct { name string @@ -26,12 +23,6 @@ type command struct { callback callbackFunc } -type callbackFunc func(args []string) error - -type pokedex map[string]pokeapi.Pokemon - -var dexter = make(pokedex) - func main() { run() } @@ -42,48 +33,53 @@ func run() { 10*time.Second, ) - var state State + trainer := poketrainer.NewTrainer() commandMap := map[string]command{ + "catch": { + name: "catch", + description: "Catch a Pokemon and add it to your Pokedex", + callback: catchFunc(client, trainer), + }, "exit": { name: "exit", description: "Exit the Pokedex", callback: exitFunc, }, - "help": { - name: "help", - description: "Displays a help message", - callback: nil, - }, - "map": { - name: "map", - description: "Displays the next 20 locations in the Pokemon world", - callback: mapFunc(client, &state), - }, - "mapb": { - name: "map back", - description: "Displays the previous 20 locations in the Pokemon world", - callback: mapBFunc(client, &state), - }, "explore": { name: "explore", - description: "Lists all the Pokemon in a given area", - callback: exploreFunc(client), + description: "List all the Pokemon in a given area", + callback: exploreFunc(client, trainer), }, - "catch": { - name: "catch", - description: "Catches a Pokemon and adds them to your Pokedex", - callback: catchFunc(client), + "help": { + name: "help", + description: "Display the help message", + callback: nil, }, "inspect": { name: "inspect", - description: "Inspects a Pokemon from your Pokedex", - callback: inspectFunc(), + description: "Inspect a Pokemon from your Pokedex", + callback: inspectFunc(trainer), + }, + "map": { + name: "map", + description: "Display the next 20 locations in the Pokemon world", + callback: mapFunc(client, trainer), + }, + "mapb": { + name: "map back", + description: "Display the previous 20 locations in the Pokemon world", + callback: mapBFunc(client, trainer), }, "pokedex": { name: "pokedex", - description: "Lists the names of all the Pokemon in your Pokedex", - callback: pokedexFunc(), + description: "List the names of all the Pokemon in your Pokedex", + callback: pokedexFunc(trainer), + }, + "visit": { + name: "visit", + description: "Visit a location area", + callback: visitFunc(client, trainer), }, } @@ -158,47 +154,36 @@ func exitFunc(_ []string) error { return nil } -func mapFunc(client *pokeclient.Client, state *State) callbackFunc { +func mapFunc(client *pokeclient.Client, trainer *poketrainer.Trainer) callbackFunc { return func(_ []string) error { - url := state.Next + url := trainer.NextLocationArea() if url == nil { url = new(string) *url = pokeclient.LocationAreaPath } - return printResourceList(client, *url, state) + return printResourceList(client, *url, trainer.UpdateLocationAreas) } } -func mapBFunc(client *pokeclient.Client, state *State) callbackFunc { +func mapBFunc(client *pokeclient.Client, trainer *poketrainer.Trainer) callbackFunc { return func(_ []string) error { - url := state.Previous + url := trainer.PreviousLocationArea() if url == nil { return fmt.Errorf("no previous locations available") } - return printResourceList(client, *url, state) + return printResourceList(client, *url, trainer.UpdateLocationAreas) } } -func exploreFunc(client *pokeclient.Client) callbackFunc { - return func(args []string) error { - if args == nil { - return errors.New("the location has not been specified") - } +func exploreFunc(client *pokeclient.Client, trainer *poketrainer.Trainer) callbackFunc { + return func(_ []string) error { + locationAreaName := trainer.CurrentLocationAreaName() - if len(args) != 1 { - return fmt.Errorf( - "unexpected number of locations: want 1; got %d", - len(args), - ) - } + fmt.Printf("Exploring %s...\n", locationAreaName) - location := args[0] - - fmt.Println("Exploring", location) - - locationArea, err := client.GetLocationArea(location) + locationArea, err := client.GetLocationArea(locationAreaName) if err != nil { return fmt.Errorf( "unable to get the location area: %w", @@ -216,7 +201,38 @@ func exploreFunc(client *pokeclient.Client) callbackFunc { } } -func catchFunc(client *pokeclient.Client) callbackFunc { +func visitFunc(client *pokeclient.Client, trainer *poketrainer.Trainer) callbackFunc { + return func(args []string) error { + if args == nil { + return errors.New("the location area has not been specified") + } + + if len(args) != 1 { + return fmt.Errorf( + "unexpected number of location areas: want 1; got %d", + len(args), + ) + } + + locationAreaName := args[0] + + locationArea, err := client.GetLocationArea(locationAreaName) + if err != nil { + return fmt.Errorf( + "unable to get the location area: %w", + err, + ) + } + + trainer.UpdateCurrentLocationAreaName(locationArea.Name) + + fmt.Println("You are now visiting", locationArea.Name) + + return nil + } +} + +func catchFunc(client *pokeclient.Client, trainer *poketrainer.Trainer) callbackFunc { return func(args []string) error { if args == nil { return errors.New("the name of the Pokemon has not been specified") @@ -231,9 +247,7 @@ func catchFunc(client *pokeclient.Client) callbackFunc { pokemonName := args[0] - fmt.Printf("Throwing a Pokeball at %s...\n", pokemonName) - - pokemon, err := client.GetPokemon(pokemonName) + pokemonDetails, err := client.GetPokemon(pokemonName) if err != nil { return fmt.Errorf( "unable to get the information on %s: %w", @@ -242,10 +256,41 @@ func catchFunc(client *pokeclient.Client) callbackFunc { ) } + encountersPath := pokemonDetails.LocationAreaEncounters + + encounterAreas, err := client.GetPokemonLocationAreas(encountersPath) + if err != nil { + return fmt.Errorf( + "unable to get the Pokemon's possible encounter areas: %w", + err, + ) + } + + validLocationArea := false + currentLocation := trainer.CurrentLocationAreaName() + + for _, area := range slices.All(encounterAreas) { + if currentLocation == area.LocationArea.Name { + validLocationArea = true + + break + } + } + + if !validLocationArea { + return fmt.Errorf( + "%s cannot be found in %s", + pokemonName, + currentLocation, + ) + } + chance := 50 + fmt.Printf("Throwing a Pokeball at %s...\n", pokemonName) + if caught := catchPokemon(chance); caught { - dexter[pokemonName] = pokemon + trainer.AddPokemonToPokedex(pokemonName, pokemonDetails) fmt.Printf("%s was caught!\nYou may now inspect it with the inspect command.\n", pokemonName) } else { fmt.Printf("%s escaped!\n", pokemonName) @@ -255,7 +300,7 @@ func catchFunc(client *pokeclient.Client) callbackFunc { } } -func inspectFunc() callbackFunc { +func inspectFunc(trainer *poketrainer.Trainer) callbackFunc { return func(args []string) error { if args == nil { return errors.New("the name of the Pokemon has not been specified") @@ -270,7 +315,7 @@ func inspectFunc() callbackFunc { pokemonName := args[0] - pokemon, ok := dexter[pokemonName] + pokemon, ok := trainer.GetPokemonFromPokedex(pokemonName) if !ok { return fmt.Errorf("you have not caught %s", pokemonName) } @@ -302,32 +347,27 @@ func inspectFunc() callbackFunc { } } -func pokedexFunc() callbackFunc { +func pokedexFunc(trainer *poketrainer.Trainer) callbackFunc { return func(_ []string) error { - if len(dexter) == 0 { - fmt.Println("You have no Pokemon in your Pokedex") - - return nil - } - - fmt.Println("Your Pokedex:") - - for name := range maps.All(dexter) { - fmt.Println(" -", name) - } + trainer.ListAllPokemonFromPokedex() return nil } } -func printResourceList(client *pokeclient.Client, url string, state *State) error { +func printResourceList( + client *pokeclient.Client, + url string, + updateStateFunc func(previous *string, next *string), +) error { list, err := client.GetNamedAPIResourceList(url) if err != nil { return fmt.Errorf("unable to get the list of resources: %w", err) } - state.Next = list.Next - state.Previous = list.Previous + if updateStateFunc != nil { + updateStateFunc(list.Previous, list.Next) + } for _, location := range slices.All(list.Results) { fmt.Println(location.Name)