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.
This commit is contained in:
Dan Anglin 2024-09-21 15:19:53 +01:00
parent 53aba1ffe7
commit 80d1b5b6c0
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
4 changed files with 233 additions and 87 deletions

View file

@ -11,6 +11,11 @@ type LocationArea struct {
PokemonEncounters []PokemonEncounter `json:"pokemon_encounters"` PokemonEncounters []PokemonEncounter `json:"pokemon_encounters"`
} }
type LocationAreaEncounter struct {
LocationArea NamedAPIResource `json:"location_area"`
VersionDetails []VersionEncounterDetails `json:"version_details"`
}
type EncounterMethodRate struct { type EncounterMethodRate struct {
EncounterMethod NamedAPIResource `json:"encounter_method"` EncounterMethod NamedAPIResource `json:"encounter_method"`
VersionDetails []EncounterVersionDetails `json:"version_details"` VersionDetails []EncounterVersionDetails `json:"version_details"`

View file

@ -13,10 +13,10 @@ import (
) )
const ( const (
baseURL string = "https://pokeapi.co/api/v2" baseURL string = "https://pokeapi.co"
LocationAreaPath = baseURL + "/location-area" LocationAreaPath = baseURL + "/api/v2/location-area"
PokemonPath = baseURL + "/pokemon" PokemonPath = baseURL + "/api/v2/pokemon"
) )
type Client struct { type Client struct {
@ -42,7 +42,7 @@ func (c *Client) GetNamedAPIResourceList(url string) (pokeapi.NamedAPIResourceLi
data, exists := c.cache.Get(url) data, exists := c.cache.Get(url)
if exists { if exists {
fmt.Println("Using data from cache.") fmt.Println("(using data from cache)")
if err := decodeJSON(data, &list); err != nil { if err := decodeJSON(data, &list); err != nil {
return pokeapi.NamedAPIResourceList{}, fmt.Errorf("unable to decode the data from the cache: %w", err) 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) data, exists := c.cache.Get(url)
if exists { if exists {
fmt.Println("Using data from cache.") fmt.Println("(using data from cache)")
if err := decodeJSON(data, &locationArea); err != nil { if err := decodeJSON(data, &locationArea); err != nil {
return pokeapi.LocationArea{}, fmt.Errorf("unable to decode the data from the cache: %w", err) 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) data, exists := c.cache.Get(url)
if exists { if exists {
fmt.Println("Using data from cache.") fmt.Println("(using data from cache)")
if err := decodeJSON(data, &pokemon); err != nil { if err := decodeJSON(data, &pokemon); err != nil {
return pokeapi.Pokemon{}, fmt.Errorf("unable to decode the data from the cache: %w", err) 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 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) { func (c *Client) sendRequest(url string) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), c.timeout) ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
defer cancel() defer cancel()

View file

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

202
main.go
View file

@ -11,14 +11,11 @@ import (
"strings" "strings"
"time" "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/pokeclient"
"codeflow.dananglin.me.uk/apollo/pokedex/internal/poketrainer"
) )
type State struct { type callbackFunc func(args []string) error
Previous *string
Next *string
}
type command struct { type command struct {
name string name string
@ -26,12 +23,6 @@ type command struct {
callback callbackFunc callback callbackFunc
} }
type callbackFunc func(args []string) error
type pokedex map[string]pokeapi.Pokemon
var dexter = make(pokedex)
func main() { func main() {
run() run()
} }
@ -42,48 +33,53 @@ func run() {
10*time.Second, 10*time.Second,
) )
var state State trainer := poketrainer.NewTrainer()
commandMap := map[string]command{ commandMap := map[string]command{
"catch": {
name: "catch",
description: "Catch a Pokemon and add it to your Pokedex",
callback: catchFunc(client, trainer),
},
"exit": { "exit": {
name: "exit", name: "exit",
description: "Exit the Pokedex", description: "Exit the Pokedex",
callback: exitFunc, 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": { "explore": {
name: "explore", name: "explore",
description: "Lists all the Pokemon in a given area", description: "List all the Pokemon in a given area",
callback: exploreFunc(client), callback: exploreFunc(client, trainer),
}, },
"catch": { "help": {
name: "catch", name: "help",
description: "Catches a Pokemon and adds them to your Pokedex", description: "Display the help message",
callback: catchFunc(client), callback: nil,
}, },
"inspect": { "inspect": {
name: "inspect", name: "inspect",
description: "Inspects a Pokemon from your Pokedex", description: "Inspect a Pokemon from your Pokedex",
callback: inspectFunc(), 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": { "pokedex": {
name: "pokedex", name: "pokedex",
description: "Lists the names of all the Pokemon in your Pokedex", description: "List the names of all the Pokemon in your Pokedex",
callback: pokedexFunc(), 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 return nil
} }
func mapFunc(client *pokeclient.Client, state *State) callbackFunc { func mapFunc(client *pokeclient.Client, trainer *poketrainer.Trainer) callbackFunc {
return func(_ []string) error { return func(_ []string) error {
url := state.Next url := trainer.NextLocationArea()
if url == nil { if url == nil {
url = new(string) url = new(string)
*url = pokeclient.LocationAreaPath *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 { return func(_ []string) error {
url := state.Previous url := trainer.PreviousLocationArea()
if url == nil { if url == nil {
return fmt.Errorf("no previous locations available") return fmt.Errorf("no previous locations available")
} }
return printResourceList(client, *url, state) return printResourceList(client, *url, trainer.UpdateLocationAreas)
} }
} }
func exploreFunc(client *pokeclient.Client) callbackFunc { func exploreFunc(client *pokeclient.Client, trainer *poketrainer.Trainer) callbackFunc {
return func(args []string) error { return func(_ []string) error {
if args == nil { locationAreaName := trainer.CurrentLocationAreaName()
return errors.New("the location has not been specified")
}
if len(args) != 1 { fmt.Printf("Exploring %s...\n", locationAreaName)
return fmt.Errorf(
"unexpected number of locations: want 1; got %d",
len(args),
)
}
location := args[0] locationArea, err := client.GetLocationArea(locationAreaName)
fmt.Println("Exploring", location)
locationArea, err := client.GetLocationArea(location)
if err != nil { if err != nil {
return fmt.Errorf( return fmt.Errorf(
"unable to get the location area: %w", "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 { return func(args []string) error {
if args == nil { if args == nil {
return errors.New("the name of the Pokemon has not been specified") 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] pokemonName := args[0]
fmt.Printf("Throwing a Pokeball at %s...\n", pokemonName) pokemonDetails, err := client.GetPokemon(pokemonName)
pokemon, err := client.GetPokemon(pokemonName)
if err != nil { if err != nil {
return fmt.Errorf( return fmt.Errorf(
"unable to get the information on %s: %w", "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 chance := 50
fmt.Printf("Throwing a Pokeball at %s...\n", pokemonName)
if caught := catchPokemon(chance); caught { 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) fmt.Printf("%s was caught!\nYou may now inspect it with the inspect command.\n", pokemonName)
} else { } else {
fmt.Printf("%s escaped!\n", pokemonName) 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 { return func(args []string) error {
if args == nil { if args == nil {
return errors.New("the name of the Pokemon has not been specified") return errors.New("the name of the Pokemon has not been specified")
@ -270,7 +315,7 @@ func inspectFunc() callbackFunc {
pokemonName := args[0] pokemonName := args[0]
pokemon, ok := dexter[pokemonName] pokemon, ok := trainer.GetPokemonFromPokedex(pokemonName)
if !ok { if !ok {
return fmt.Errorf("you have not caught %s", pokemonName) 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 { return func(_ []string) error {
if len(dexter) == 0 { trainer.ListAllPokemonFromPokedex()
fmt.Println("You have no Pokemon in your Pokedex")
return nil
}
fmt.Println("Your Pokedex:")
for name := range maps.All(dexter) {
fmt.Println(" -", name)
}
return nil 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) list, err := client.GetNamedAPIResourceList(url)
if err != nil { if err != nil {
return fmt.Errorf("unable to get the list of resources: %w", err) return fmt.Errorf("unable to get the list of resources: %w", err)
} }
state.Next = list.Next if updateStateFunc != nil {
state.Previous = list.Previous updateStateFunc(list.Previous, list.Next)
}
for _, location := range slices.All(list.Results) { for _, location := range slices.All(list.Results) {
fmt.Println(location.Name) fmt.Println(location.Name)