generated from templates/go-generic
feat: explore the map and catch Pokemon
Changes: - Added in-memory cache. - Created a new package for defining the types from the pokeapi API. - Created an internal package for the client. - Added a command to explore the map. - Added a command to catch Pokemon. - Added a command to inspect a Pokemon from the Pokedex. - Added a command to list all the Pokemon in the Pokedex.
This commit is contained in:
parent
4152a9d14f
commit
c35bd1d2d1
9 changed files with 807 additions and 115 deletions
|
@ -13,6 +13,14 @@ output:
|
||||||
sort-results: true
|
sort-results: true
|
||||||
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
|
depguard:
|
||||||
|
rules:
|
||||||
|
main:
|
||||||
|
files:
|
||||||
|
- $all
|
||||||
|
allow:
|
||||||
|
- $gostd
|
||||||
|
- codeflow.dananglin.me.uk/apollo/pokedex
|
||||||
lll:
|
lll:
|
||||||
line-length: 140
|
line-length: 140
|
||||||
|
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -1,3 +1,3 @@
|
||||||
module codeflow.dananglin.me.uk/apollo/pokedex
|
module codeflow.dananglin.me.uk/apollo/pokedex
|
||||||
|
|
||||||
go 1.23.0
|
go 1.23.1
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package pokeapi
|
||||||
|
|
||||||
// LocationArea is a section of areas such as floors in a building or a cave.
|
// LocationArea is a section of areas such as floors in a building or a cave.
|
||||||
type LocationArea struct {
|
type LocationArea struct {
|
||||||
|
@ -12,8 +12,8 @@ type LocationArea struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type EncounterVersionDetails struct {
|
type EncounterVersionDetails struct {
|
||||||
|
@ -45,15 +45,3 @@ type Encounter struct {
|
||||||
Chance int `json:"chance"`
|
Chance int `json:"chance"`
|
||||||
Method NamedAPIResource `json:"method"`
|
Method NamedAPIResource `json:"method"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NamedAPIResourceList struct {
|
|
||||||
Count int `json:"count"`
|
|
||||||
Next *string `json:"next"`
|
|
||||||
Previous *string `json:"previous"`
|
|
||||||
Results []NamedAPIResource `json:"results"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type NamedAPIResource struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
}
|
|
13
internal/api/pokeapi/namedapiresourcelist.go
Normal file
13
internal/api/pokeapi/namedapiresourcelist.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package pokeapi
|
||||||
|
|
||||||
|
type NamedAPIResourceList struct {
|
||||||
|
Count int `json:"count"`
|
||||||
|
Next *string `json:"next"`
|
||||||
|
Previous *string `json:"previous"`
|
||||||
|
Results []NamedAPIResource `json:"results"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NamedAPIResource struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
87
internal/api/pokeapi/pokemon.go
Normal file
87
internal/api/pokeapi/pokemon.go
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
package pokeapi
|
||||||
|
|
||||||
|
type Pokemon struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
BaseExperience int `json:"base_experience"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
IsDefault bool `json:"is_default"`
|
||||||
|
Order int `json:"order"`
|
||||||
|
Weight int `json:"weight"`
|
||||||
|
Abilities []PokemonAbility `json:"abilities"`
|
||||||
|
Forms []NamedAPIResource `json:"forms"`
|
||||||
|
GameIndices []VersionGameIndex `json:"game_indices"`
|
||||||
|
HeldItems []PokemonHeldItems `json:"held_items"`
|
||||||
|
LocationAreaEncounters string `json:"location_area_encounters"`
|
||||||
|
Moves []PokemonMoves `json:"moves"`
|
||||||
|
PastTypes []PokemonTypePast `json:"past_types"`
|
||||||
|
Sprites PokemonSprites `json:"sprites"`
|
||||||
|
Cries PokemonCries `json:"cries"`
|
||||||
|
Species NamedAPIResource `json:"species"`
|
||||||
|
Stats []PokemonStat `json:"stats"`
|
||||||
|
Types []PokemonType `json:"types"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PokemonAbility struct {
|
||||||
|
IsHidden bool `json:"is_hidden"`
|
||||||
|
Slot int `json:"slot"`
|
||||||
|
Ability NamedAPIResource `json:"ability"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VersionGameIndex struct {
|
||||||
|
GameIndex int `json:"game_index"`
|
||||||
|
Version NamedAPIResource `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PokemonHeldItems struct {
|
||||||
|
Item NamedAPIResource `json:"item"`
|
||||||
|
VersionDetails []PokemonHeldItemVersion `json:"version_details"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PokemonHeldItemVersion struct {
|
||||||
|
Version NamedAPIResource `json:"version"`
|
||||||
|
Rarity int `json:"rarity"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PokemonMoves struct {
|
||||||
|
Move NamedAPIResource `json:"move"`
|
||||||
|
VersionGroupDetails []PokemonMoveVersion `json:"version_group_details"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PokemonMoveVersion struct {
|
||||||
|
MoveLearnMethod NamedAPIResource `json:"move_learn_method"`
|
||||||
|
VersionGroup NamedAPIResource `json:"version_group"`
|
||||||
|
LevelLearnedAt int
|
||||||
|
}
|
||||||
|
|
||||||
|
type PokemonTypePast struct {
|
||||||
|
Generation NamedAPIResource `json:"generation"`
|
||||||
|
Types PokemonType `json:"types"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PokemonSprites struct {
|
||||||
|
FrontDefault string `json:"front_default"`
|
||||||
|
FrontShiny string `json:"front_shiny"`
|
||||||
|
FrontFemale string `json:"front_female"`
|
||||||
|
FrontShinyFemale string `json:"front_shiny_female"`
|
||||||
|
BackDefault string `json:"back_default"`
|
||||||
|
BackShiny string `json:"back_shiny"`
|
||||||
|
BackFemale string `json:"back_female"`
|
||||||
|
BackShinyFemale string `json:"back_shiny_female"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PokemonCries struct {
|
||||||
|
Latest string `json:"latest"`
|
||||||
|
Legacy string `json:"legacy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PokemonStat struct {
|
||||||
|
Stat NamedAPIResource `json:"stat"`
|
||||||
|
Effort int `json:"effort"`
|
||||||
|
BaseStat int `json:"base_stat"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PokemonType struct {
|
||||||
|
Slot int `json:"slot"`
|
||||||
|
Type NamedAPIResource `json:"type"`
|
||||||
|
}
|
85
internal/pokecache/pokecache.go
Normal file
85
internal/pokecache/pokecache.go
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
package pokecache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"maps"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cache struct {
|
||||||
|
stopChan chan struct{}
|
||||||
|
mu *sync.Mutex
|
||||||
|
entries map[string]cacheEntry
|
||||||
|
interval time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type cacheEntry struct {
|
||||||
|
createdAt time.Time
|
||||||
|
val []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCache(interval time.Duration) *Cache {
|
||||||
|
cache := Cache{
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
mu: &sync.Mutex{},
|
||||||
|
entries: make(map[string]cacheEntry),
|
||||||
|
interval: interval,
|
||||||
|
}
|
||||||
|
|
||||||
|
go cache.readLoop()
|
||||||
|
|
||||||
|
return &cache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) Add(key string, val []byte) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
entry := cacheEntry{
|
||||||
|
createdAt: time.Now(),
|
||||||
|
val: val,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.entries[key] = entry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) Get(key string) ([]byte, bool) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
value, exist := c.entries[key]
|
||||||
|
|
||||||
|
return value.val, exist
|
||||||
|
}
|
||||||
|
|
||||||
|
// func (c *Cache) Stop() {
|
||||||
|
// c.stopChan <- struct{}{}
|
||||||
|
// }
|
||||||
|
|
||||||
|
func (c *Cache) readLoop() {
|
||||||
|
ticker := time.NewTicker(c.interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
readloop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.stopChan:
|
||||||
|
break readloop
|
||||||
|
case <-ticker.C:
|
||||||
|
c.cleanupEntries()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) cleanupEntries() {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
expiredTime := time.Now().Add(-c.interval)
|
||||||
|
|
||||||
|
for key := range maps.All(c.entries) {
|
||||||
|
if c.entries[key].createdAt.Before(expiredTime) {
|
||||||
|
delete(c.entries, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
81
internal/pokecache/pokecache_test.go
Normal file
81
internal/pokecache/pokecache_test.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
package pokecache_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/pokedex/internal/pokecache"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
keyNotFoundFormat = "The key %q was not found after adding it to the cache"
|
||||||
|
keyFoundAfterCleanupFormat = "The key %q was found after cache cleanup"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCacheAddGet(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
key string
|
||||||
|
value []byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
key: "https://example.org/path",
|
||||||
|
value: []byte("testdata"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "https://example.org/api/v1/path",
|
||||||
|
value: []byte(`{"version": "v1.0.0", "key": "value"}`),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
interval := 1 * time.Minute
|
||||||
|
|
||||||
|
cache := pokecache.NewCache(interval)
|
||||||
|
|
||||||
|
testFunc := func(key string, value []byte) func(*testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
cache.Add(key, value)
|
||||||
|
gotBytes, exists := cache.Get(key)
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
t.Fatalf(keyNotFoundFormat, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := string(value)
|
||||||
|
got := string(gotBytes)
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("Unexpected value retrieved from the cache: want %s, got %s", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ind, testcase := range slices.All(cases) {
|
||||||
|
t.Run(fmt.Sprintf("Test case: %d", ind+1), testFunc(testcase.key, testcase.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadLoop(t *testing.T) {
|
||||||
|
const (
|
||||||
|
baseTime = 5 * time.Millisecond
|
||||||
|
waitTime = 10 * baseTime
|
||||||
|
)
|
||||||
|
|
||||||
|
key := "https://example.org/api/v1/path"
|
||||||
|
value := []byte(`{"version": "v1.0.0", "key": "value"}`)
|
||||||
|
|
||||||
|
cache := pokecache.NewCache(baseTime)
|
||||||
|
|
||||||
|
cache.Add(key, value)
|
||||||
|
|
||||||
|
if _, exists := cache.Get(key); !exists {
|
||||||
|
t.Fatalf(keyNotFoundFormat, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(waitTime)
|
||||||
|
|
||||||
|
if _, exists := cache.Get(key); exists {
|
||||||
|
t.Errorf(keyFoundAfterCleanupFormat, key)
|
||||||
|
}
|
||||||
|
}
|
212
internal/pokeclient/pokeclient.go
Normal file
212
internal/pokeclient/pokeclient.go
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
package pokeclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/pokedex/internal/api/pokeapi"
|
||||||
|
"codeflow.dananglin.me.uk/apollo/pokedex/internal/pokecache"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
baseURL string = "https://pokeapi.co/api/v2"
|
||||||
|
|
||||||
|
LocationAreaPath = baseURL + "/location-area"
|
||||||
|
PokemonPath = baseURL + "/pokemon"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
httpClient http.Client
|
||||||
|
cache *pokecache.Cache
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(cacheCleanupInterval, timeout time.Duration) *Client {
|
||||||
|
cache := pokecache.NewCache(cacheCleanupInterval)
|
||||||
|
|
||||||
|
client := Client{
|
||||||
|
httpClient: http.Client{},
|
||||||
|
cache: cache,
|
||||||
|
timeout: timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetNamedAPIResourceList(url string) (pokeapi.NamedAPIResourceList, error) {
|
||||||
|
var list pokeapi.NamedAPIResourceList
|
||||||
|
|
||||||
|
dataFromCache, exists := c.cache.Get(url)
|
||||||
|
if exists {
|
||||||
|
fmt.Println("Using data from cache.")
|
||||||
|
|
||||||
|
if err := decodeJSON(dataFromCache, &list); err != nil {
|
||||||
|
return pokeapi.NamedAPIResourceList{}, fmt.Errorf("unable to decode the data from the cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return pokeapi.NamedAPIResourceList{}, fmt.Errorf("error creating the HTTP request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return pokeapi.NamedAPIResourceList{}, fmt.Errorf("error getting the response from the server: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return pokeapi.NamedAPIResourceList{}, fmt.Errorf(
|
||||||
|
"received a bad status from %s: (%d) %s",
|
||||||
|
url,
|
||||||
|
resp.StatusCode,
|
||||||
|
resp.Status,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return pokeapi.NamedAPIResourceList{}, fmt.Errorf(
|
||||||
|
"unable to read the response from the server: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := decodeJSON(body, &list); err != nil {
|
||||||
|
return pokeapi.NamedAPIResourceList{}, fmt.Errorf("unable to decode the data from the server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.cache.Add(url, body)
|
||||||
|
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetLocationArea(location string) (pokeapi.LocationArea, error) {
|
||||||
|
var locationArea pokeapi.LocationArea
|
||||||
|
|
||||||
|
url := LocationAreaPath + "/" + location + "/"
|
||||||
|
|
||||||
|
dataFromCache, exists := c.cache.Get(url)
|
||||||
|
if exists {
|
||||||
|
fmt.Println("Using data from cache.")
|
||||||
|
|
||||||
|
if err := decodeJSON(dataFromCache, &locationArea); err != nil {
|
||||||
|
return pokeapi.LocationArea{}, fmt.Errorf("unable to decode the data from the cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return locationArea, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return pokeapi.LocationArea{}, fmt.Errorf("error creating the HTTP request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return pokeapi.LocationArea{}, fmt.Errorf("error getting the response from the server: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return pokeapi.LocationArea{}, fmt.Errorf(
|
||||||
|
"received a bad status from %s: (%d) %s",
|
||||||
|
url,
|
||||||
|
resp.StatusCode,
|
||||||
|
resp.Status,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return pokeapi.LocationArea{}, fmt.Errorf(
|
||||||
|
"unable to read the response from the server: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := decodeJSON(body, &locationArea); err != nil {
|
||||||
|
return pokeapi.LocationArea{}, fmt.Errorf("unable to decode the data from the server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.cache.Add(url, body)
|
||||||
|
|
||||||
|
return locationArea, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetPokemon(pokemonName string) (pokeapi.Pokemon, error) {
|
||||||
|
var pokemon pokeapi.Pokemon
|
||||||
|
|
||||||
|
url := PokemonPath + "/" + pokemonName + "/"
|
||||||
|
|
||||||
|
dataFromCache, exists := c.cache.Get(url)
|
||||||
|
if exists {
|
||||||
|
fmt.Println("Using data from cache.")
|
||||||
|
|
||||||
|
if err := decodeJSON(dataFromCache, &pokemon); err != nil {
|
||||||
|
return pokeapi.Pokemon{}, fmt.Errorf("unable to decode the data from the cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pokemon, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return pokeapi.Pokemon{}, fmt.Errorf("error creating the HTTP request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return pokeapi.Pokemon{}, fmt.Errorf("error getting the response from the server: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return pokeapi.Pokemon{}, fmt.Errorf(
|
||||||
|
"received a bad status from %s: (%d) %s",
|
||||||
|
url,
|
||||||
|
resp.StatusCode,
|
||||||
|
resp.Status,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return pokeapi.Pokemon{}, fmt.Errorf(
|
||||||
|
"unable to read the response from the server: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := decodeJSON(body, &pokemon); err != nil {
|
||||||
|
return pokeapi.Pokemon{}, fmt.Errorf("unable to decode the data from the server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.cache.Add(url, body)
|
||||||
|
|
||||||
|
return pokemon, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeJSON(data []byte, value any) error {
|
||||||
|
if err := json.Unmarshal(data, value); err != nil {
|
||||||
|
return fmt.Errorf("unable to decode the JSON data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
416
main.go
416
main.go
|
@ -2,19 +2,17 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"errors"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"net/http"
|
"math/rand/v2"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
"codeflow.dananglin.me.uk/apollo/pokedex/internal/api/pokeapi"
|
||||||
baseURL string = "https://pokeapi.co/api/v2"
|
"codeflow.dananglin.me.uk/apollo/pokedex/internal/pokeclient"
|
||||||
locationAreaEndpoint string = "/location-area"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type State struct {
|
type State struct {
|
||||||
|
@ -22,28 +20,93 @@ type State struct {
|
||||||
Next *string
|
Next *string
|
||||||
}
|
}
|
||||||
|
|
||||||
var state State
|
|
||||||
|
|
||||||
type command struct {
|
type command struct {
|
||||||
name string
|
name string
|
||||||
description string
|
description string
|
||||||
callback func() error
|
callback callbackFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type callbackFunc func(args []string) error
|
||||||
|
|
||||||
|
type pokedex map[string]pokeapi.Pokemon
|
||||||
|
|
||||||
|
var dexter = make(pokedex)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
run()
|
run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func run() {
|
func run() {
|
||||||
fmt.Print("pokedex > ")
|
client := pokeclient.NewClient(
|
||||||
|
5*time.Minute,
|
||||||
|
10*time.Second,
|
||||||
|
)
|
||||||
|
|
||||||
|
var state State
|
||||||
|
|
||||||
|
commandMap := map[string]command{
|
||||||
|
"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),
|
||||||
|
},
|
||||||
|
"catch": {
|
||||||
|
name: "catch",
|
||||||
|
description: "Catches a Pokemon and adds them to your Pokedex",
|
||||||
|
callback: catchFunc(client),
|
||||||
|
},
|
||||||
|
"inspect": {
|
||||||
|
name: "inspect",
|
||||||
|
description: "Inspects a Pokemon from your Pokedex",
|
||||||
|
callback: inspectFunc(),
|
||||||
|
},
|
||||||
|
"pokedex": {
|
||||||
|
name: "pokedex",
|
||||||
|
description: "Lists the names of all the Pokemon in your Pokedex",
|
||||||
|
callback: pokedexFunc(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
summaries := summaryMap(commandMap)
|
||||||
|
|
||||||
|
commandMap["help"] = command{
|
||||||
|
name: "help",
|
||||||
|
description: "Displays a help message",
|
||||||
|
callback: helpFunc(summaries),
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nWelcome to the Pokedex!\n")
|
||||||
|
fmt.Print("\npokedex > ")
|
||||||
|
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
command := scanner.Text()
|
input := scanner.Text()
|
||||||
|
|
||||||
cmdMap := getCommandMap()
|
command, args := parseArgs(input)
|
||||||
if _, ok := cmdMap[command]; !ok {
|
|
||||||
|
cmd, ok := commandMap[command]
|
||||||
|
if !ok {
|
||||||
fmt.Println("ERROR: Unrecognised command.")
|
fmt.Println("ERROR: Unrecognised command.")
|
||||||
|
|
||||||
fmt.Print("\npokedex > ")
|
fmt.Print("\npokedex > ")
|
||||||
|
@ -51,7 +114,15 @@ func run() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cmdMap[command].callback(); err != nil {
|
if cmd.callback == nil {
|
||||||
|
fmt.Println("ERROR: This command is defined but does not have a callback function.")
|
||||||
|
|
||||||
|
fmt.Print("\npokedex > ")
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := commandMap[command].callback(args); err != nil {
|
||||||
fmt.Printf("ERROR: %v.\n", err)
|
fmt.Printf("ERROR: %v.\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,116 +130,263 @@ func run() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCommandMap() map[string]command {
|
func helpFunc(summaries map[string]string) callbackFunc {
|
||||||
return map[string]command{
|
return func(_ []string) error {
|
||||||
"exit": {
|
keys := []string{}
|
||||||
name: "exit",
|
|
||||||
description: "Exit the Pokedex",
|
for key := range maps.All(summaries) {
|
||||||
callback: commandExit,
|
keys = append(keys, key)
|
||||||
},
|
}
|
||||||
"help": {
|
|
||||||
name: "help",
|
slices.Sort(keys)
|
||||||
description: "Displays a help message",
|
|
||||||
callback: commandHelp,
|
fmt.Printf("\nCommands:\n")
|
||||||
},
|
|
||||||
"map": {
|
for _, key := range slices.All(keys) {
|
||||||
name: "map",
|
fmt.Printf("\n%s: %s", key, summaries[key])
|
||||||
description: "Displays the next 20 locations in the Pokemon world",
|
}
|
||||||
callback: commandMap,
|
|
||||||
},
|
fmt.Printf("\n\n")
|
||||||
"mapb": {
|
|
||||||
name: "map back",
|
return nil
|
||||||
description: "Displays the previous 20 locations in the Pokemon world",
|
|
||||||
callback: commandMapB,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func commandHelp() error {
|
func exitFunc(_ []string) error {
|
||||||
cmdMap := getCommandMap()
|
|
||||||
|
|
||||||
keys := []string{}
|
|
||||||
|
|
||||||
for key := range maps.All(cmdMap) {
|
|
||||||
keys = append(keys, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
slices.Sort(keys)
|
|
||||||
|
|
||||||
fmt.Printf("\nWelcome to the Pokedex!\nUsage:\n")
|
|
||||||
|
|
||||||
for _, key := range slices.All(keys) {
|
|
||||||
fmt.Printf("\n%s: %s", key, cmdMap[key].description)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("\n")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func commandExit() error {
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func commandMap() error {
|
func mapFunc(client *pokeclient.Client, state *State) callbackFunc {
|
||||||
url := state.Next
|
return func(_ []string) error {
|
||||||
if url == nil {
|
url := state.Next
|
||||||
url = new(string)
|
if url == nil {
|
||||||
*url = baseURL + locationAreaEndpoint
|
url = new(string)
|
||||||
}
|
*url = pokeclient.LocationAreaPath
|
||||||
|
}
|
||||||
|
|
||||||
return printMap(*url)
|
return printResourceList(client, *url, state)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func commandMapB() error {
|
func mapBFunc(client *pokeclient.Client, state *State) callbackFunc {
|
||||||
url := state.Previous
|
return func(_ []string) error {
|
||||||
if url == nil {
|
url := state.Previous
|
||||||
return fmt.Errorf("no previous locations available")
|
if url == nil {
|
||||||
}
|
return fmt.Errorf("no previous locations available")
|
||||||
|
}
|
||||||
|
|
||||||
return printMap(*url)
|
return printResourceList(client, *url, state)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func printMap(url string) error {
|
func exploreFunc(client *pokeclient.Client) callbackFunc {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
return func(args []string) error {
|
||||||
defer cancel()
|
if args == nil {
|
||||||
|
return errors.New("the location has not been specified")
|
||||||
|
}
|
||||||
|
|
||||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
if len(args) != 1 {
|
||||||
if err != nil {
|
return fmt.Errorf(
|
||||||
return fmt.Errorf("error creating the HTTP request: %w", err)
|
"unexpected number of locations: want 1; got %d",
|
||||||
|
len(args),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
location := args[0]
|
||||||
|
|
||||||
|
fmt.Println("Exploring", location)
|
||||||
|
|
||||||
|
locationArea, err := client.GetLocationArea(location)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"unable to get the location area: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Found Pokemon:")
|
||||||
|
|
||||||
|
for _, encounter := range slices.All(locationArea.PokemonEncounters) {
|
||||||
|
fmt.Printf("- %s\n", encounter.Pokemon.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
client := http.Client{}
|
func catchFunc(client *pokeclient.Client) callbackFunc {
|
||||||
|
return func(args []string) error {
|
||||||
|
if args == nil {
|
||||||
|
return errors.New("the name of the Pokemon has not been specified")
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := client.Do(request)
|
if len(args) != 1 {
|
||||||
if err != nil {
|
return fmt.Errorf(
|
||||||
return fmt.Errorf("error getting the response from the server: %w", err)
|
"unexpected number of Pokemon names: want 1; got %d",
|
||||||
|
len(args),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pokemonName := args[0]
|
||||||
|
|
||||||
|
fmt.Printf("Throwing a Pokeball at %s...\n", pokemonName)
|
||||||
|
|
||||||
|
pokemon, err := client.GetPokemon(pokemonName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"unable to get the information on %s: %w",
|
||||||
|
pokemonName,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
chance := 50
|
||||||
|
|
||||||
|
if caught := catchPokemon(chance); caught {
|
||||||
|
dexter[pokemonName] = pokemon
|
||||||
|
fmt.Printf("%s was caught!\nYou may now inspect it with the inspect command.\n", pokemonName)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s escaped!\n", pokemonName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
}
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
func inspectFunc() callbackFunc {
|
||||||
return fmt.Errorf(
|
return func(args []string) error {
|
||||||
"received a bad status from %s: (%d) %s",
|
if args == nil {
|
||||||
url,
|
return errors.New("the name of the Pokemon has not been specified")
|
||||||
resp.StatusCode,
|
}
|
||||||
resp.Status,
|
|
||||||
|
if len(args) != 1 {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"unexpected number of Pokemon names: want 1; got %d",
|
||||||
|
len(args),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pokemonName := args[0]
|
||||||
|
|
||||||
|
pokemon, ok := dexter[pokemonName]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("you have not caught %s", pokemonName)
|
||||||
|
}
|
||||||
|
|
||||||
|
info := fmt.Sprintf(
|
||||||
|
"Name: %s\nHeight: %d\nWeight: %d\nStats:",
|
||||||
|
pokemon.Name,
|
||||||
|
pokemon.Height,
|
||||||
|
pokemon.Weight,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for _, stat := range slices.All(pokemon.Stats) {
|
||||||
|
info += fmt.Sprintf(
|
||||||
|
"\n - %s: %d",
|
||||||
|
stat.Stat.Name,
|
||||||
|
stat.BaseStat,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
info += "\nTypes:"
|
||||||
|
|
||||||
|
for _, pType := range slices.All(pokemon.Types) {
|
||||||
|
info += "\n - " + pType.Type.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(info)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pokedexFunc() 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printResourceList(client *pokeclient.Client, url string, state *State) error {
|
||||||
|
list, err := client.GetNamedAPIResourceList(url)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get the list of resources: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var result NamedAPIResourceList
|
state.Next = list.Next
|
||||||
|
state.Previous = list.Previous
|
||||||
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
for _, location := range slices.All(list.Results) {
|
||||||
return fmt.Errorf("unable to decode the JSON response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
state.Next = result.Next
|
|
||||||
state.Previous = result.Previous
|
|
||||||
|
|
||||||
for _, location := range slices.All(result.Results) {
|
|
||||||
fmt.Println(location.Name)
|
fmt.Println(location.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func summaryMap(commandMap map[string]command) map[string]string {
|
||||||
|
summaries := make(map[string]string)
|
||||||
|
|
||||||
|
for key, value := range maps.All(commandMap) {
|
||||||
|
summaries[key] = value.description
|
||||||
|
}
|
||||||
|
|
||||||
|
return summaries
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseArgs(input string) (string, []string) {
|
||||||
|
split := strings.Split(input, " ")
|
||||||
|
|
||||||
|
if len(split) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(split) == 1 {
|
||||||
|
return split[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return split[0], split[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func catchPokemon(chance int) bool {
|
||||||
|
if chance >= 100 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if chance <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
maxInt := 100
|
||||||
|
|
||||||
|
numGenerator := rand.New(rand.NewPCG(rand.Uint64(), rand.Uint64()))
|
||||||
|
|
||||||
|
luckyNumberSet := make(map[int]struct{})
|
||||||
|
|
||||||
|
for len(luckyNumberSet) < chance {
|
||||||
|
num := numGenerator.IntN(maxInt)
|
||||||
|
if _, ok := luckyNumberSet[num]; !ok {
|
||||||
|
luckyNumberSet[num] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
roller := rand.New(rand.NewPCG(rand.Uint64(), rand.Uint64()))
|
||||||
|
|
||||||
|
got := roller.IntN(maxInt)
|
||||||
|
|
||||||
|
_, ok := luckyNumberSet[got]
|
||||||
|
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue