diff --git a/.golangci.yaml b/.golangci.yaml index 059f5bc..23cb346 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -13,6 +13,14 @@ output: sort-results: true linters-settings: + depguard: + rules: + main: + files: + - $all + allow: + - $gostd + - codeflow.dananglin.me.uk/apollo/pokedex lll: line-length: 140 diff --git a/go.mod b/go.mod index 7039358..e0329de 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module codeflow.dananglin.me.uk/apollo/pokedex -go 1.23.0 +go 1.23.1 diff --git a/types.go b/internal/api/pokeapi/locationarea.go similarity index 77% rename from types.go rename to internal/api/pokeapi/locationarea.go index 3f8301e..c5e9c3c 100644 --- a/types.go +++ b/internal/api/pokeapi/locationarea.go @@ -1,4 +1,4 @@ -package main +package pokeapi // LocationArea is a section of areas such as floors in a building or a cave. type LocationArea struct { @@ -12,8 +12,8 @@ type LocationArea struct { } type EncounterMethodRate struct { - EncounterMethod NamedAPIResource `json:"encounter_method"` - VersionDetails EncounterVersionDetails `json:"version_details"` + EncounterMethod NamedAPIResource `json:"encounter_method"` + VersionDetails []EncounterVersionDetails `json:"version_details"` } type EncounterVersionDetails struct { @@ -45,15 +45,3 @@ type Encounter struct { Chance int `json:"chance"` 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"` -} diff --git a/internal/api/pokeapi/namedapiresourcelist.go b/internal/api/pokeapi/namedapiresourcelist.go new file mode 100644 index 0000000..5526854 --- /dev/null +++ b/internal/api/pokeapi/namedapiresourcelist.go @@ -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"` +} diff --git a/internal/api/pokeapi/pokemon.go b/internal/api/pokeapi/pokemon.go new file mode 100644 index 0000000..de04240 --- /dev/null +++ b/internal/api/pokeapi/pokemon.go @@ -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"` +} diff --git a/internal/pokecache/pokecache.go b/internal/pokecache/pokecache.go new file mode 100644 index 0000000..2428c5a --- /dev/null +++ b/internal/pokecache/pokecache.go @@ -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) + } + } +} diff --git a/internal/pokecache/pokecache_test.go b/internal/pokecache/pokecache_test.go new file mode 100644 index 0000000..43a15e2 --- /dev/null +++ b/internal/pokecache/pokecache_test.go @@ -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) + } +} diff --git a/internal/pokeclient/pokeclient.go b/internal/pokeclient/pokeclient.go new file mode 100644 index 0000000..9bbacb5 --- /dev/null +++ b/internal/pokeclient/pokeclient.go @@ -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 +} diff --git a/main.go b/main.go index 4f88ff5..b44954e 100644 --- a/main.go +++ b/main.go @@ -2,19 +2,17 @@ package main import ( "bufio" - "context" - "encoding/json" + "errors" "fmt" "maps" - "net/http" + "math/rand/v2" "os" "slices" + "strings" "time" -) -const ( - baseURL string = "https://pokeapi.co/api/v2" - locationAreaEndpoint string = "/location-area" + "codeflow.dananglin.me.uk/apollo/pokedex/internal/api/pokeapi" + "codeflow.dananglin.me.uk/apollo/pokedex/internal/pokeclient" ) type State struct { @@ -22,28 +20,93 @@ type State struct { Next *string } -var state State - type command struct { name 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() { 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) for scanner.Scan() { - command := scanner.Text() + input := scanner.Text() - cmdMap := getCommandMap() - if _, ok := cmdMap[command]; !ok { + command, args := parseArgs(input) + + cmd, ok := commandMap[command] + if !ok { fmt.Println("ERROR: Unrecognised command.") fmt.Print("\npokedex > ") @@ -51,7 +114,15 @@ func run() { 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) } @@ -59,116 +130,263 @@ func run() { } } -func getCommandMap() map[string]command { - return map[string]command{ - "exit": { - name: "exit", - description: "Exit the Pokedex", - callback: commandExit, - }, - "help": { - name: "help", - description: "Displays a help message", - callback: commandHelp, - }, - "map": { - name: "map", - description: "Displays the next 20 locations in the Pokemon world", - callback: commandMap, - }, - "mapb": { - name: "map back", - description: "Displays the previous 20 locations in the Pokemon world", - callback: commandMapB, - }, +func helpFunc(summaries map[string]string) callbackFunc { + return func(_ []string) error { + keys := []string{} + + for key := range maps.All(summaries) { + keys = append(keys, key) + } + + slices.Sort(keys) + + fmt.Printf("\nCommands:\n") + + for _, key := range slices.All(keys) { + fmt.Printf("\n%s: %s", key, summaries[key]) + } + + fmt.Printf("\n\n") + + return nil } } -func commandHelp() 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 { +func exitFunc(_ []string) error { os.Exit(0) return nil } -func commandMap() error { - url := state.Next - if url == nil { - url = new(string) - *url = baseURL + locationAreaEndpoint - } +func mapFunc(client *pokeclient.Client, state *State) callbackFunc { + return func(_ []string) error { + url := state.Next + if url == nil { + url = new(string) + *url = pokeclient.LocationAreaPath + } - return printMap(*url) + return printResourceList(client, *url, state) + } } -func commandMapB() error { - url := state.Previous - if url == nil { - return fmt.Errorf("no previous locations available") - } +func mapBFunc(client *pokeclient.Client, state *State) callbackFunc { + return func(_ []string) error { + url := state.Previous + if url == nil { + return fmt.Errorf("no previous locations available") + } - return printMap(*url) + return printResourceList(client, *url, state) + } } -func printMap(url string) error { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() +func exploreFunc(client *pokeclient.Client) callbackFunc { + return func(args []string) error { + if args == nil { + return errors.New("the location has not been specified") + } - request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - return fmt.Errorf("error creating the HTTP request: %w", err) + if len(args) != 1 { + return fmt.Errorf( + "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 err != nil { - return fmt.Errorf("error getting the response from the server: %w", err) + if len(args) != 1 { + return fmt.Errorf( + "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 { - return fmt.Errorf( - "received a bad status from %s: (%d) %s", - url, - resp.StatusCode, - resp.Status, +func inspectFunc() callbackFunc { + return func(args []string) error { + if args == nil { + return errors.New("the name of the Pokemon has not been specified") + } + + 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 { - 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) { + for _, location := range slices.All(list.Results) { fmt.Println(location.Name) } 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 +}