diff --git a/internal/commands/catch.go b/internal/commands/catch.go new file mode 100644 index 0000000..d270513 --- /dev/null +++ b/internal/commands/catch.go @@ -0,0 +1,117 @@ +package commands + +import ( + "errors" + "fmt" + "math/rand/v2" + "slices" + + "codeflow.dananglin.me.uk/apollo/pokedex/internal/pokeclient" + "codeflow.dananglin.me.uk/apollo/pokedex/internal/poketrainer" +) + +func CatchFunc(client *pokeclient.Client, trainer *poketrainer.Trainer) CommandFunc { + 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] + + if _, caught := trainer.GetPokemonFromPokedex(pokemonName); caught { + return fmt.Errorf( + "you've already caught a %s", + pokemonName, + ) + } + + pokemonDetails, err := client.GetPokemon(pokemonName) + if err != nil { + return fmt.Errorf( + "unable to get the information on %s: %w", + pokemonName, + err, + ) + } + + 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 := success(chance); caught { + 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) + } + + return nil + } +} + +func success(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 +} diff --git a/internal/commands/commands.go b/internal/commands/commands.go new file mode 100644 index 0000000..194b1a3 --- /dev/null +++ b/internal/commands/commands.go @@ -0,0 +1,3 @@ +package commands + +type CommandFunc func(args []string) error diff --git a/internal/commands/exit.go b/internal/commands/exit.go new file mode 100644 index 0000000..73b176c --- /dev/null +++ b/internal/commands/exit.go @@ -0,0 +1,9 @@ +package commands + +import "os" + +func ExitProgram(_ []string) error { + os.Exit(0) + + return nil +} diff --git a/internal/commands/explore.go b/internal/commands/explore.go new file mode 100644 index 0000000..585ceaf --- /dev/null +++ b/internal/commands/explore.go @@ -0,0 +1,33 @@ +package commands + +import ( + "fmt" + "slices" + + "codeflow.dananglin.me.uk/apollo/pokedex/internal/pokeclient" + "codeflow.dananglin.me.uk/apollo/pokedex/internal/poketrainer" +) + +func ExploreFunc(client *pokeclient.Client, trainer *poketrainer.Trainer) CommandFunc { + return func(_ []string) error { + locationAreaName := trainer.CurrentLocationAreaName() + + fmt.Printf("Exploring %s...\n", locationAreaName) + + locationArea, err := client.GetLocationArea(locationAreaName) + 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 + } +} diff --git a/internal/commands/help.go b/internal/commands/help.go new file mode 100644 index 0000000..4545354 --- /dev/null +++ b/internal/commands/help.go @@ -0,0 +1,29 @@ +package commands + +import ( + "fmt" + "maps" + "slices" +) + +func HelpFunc(summaries map[string]string) CommandFunc { + 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 + } +} diff --git a/internal/commands/inspect.go b/internal/commands/inspect.go new file mode 100644 index 0000000..c55cdde --- /dev/null +++ b/internal/commands/inspect.go @@ -0,0 +1,56 @@ +package commands + +import ( + "errors" + "fmt" + "slices" + + "codeflow.dananglin.me.uk/apollo/pokedex/internal/poketrainer" +) + +func InspectFunc(trainer *poketrainer.Trainer) CommandFunc { + 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 := trainer.GetPokemonFromPokedex(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 + } +} diff --git a/internal/commands/map.go b/internal/commands/map.go new file mode 100644 index 0000000..0cc8080 --- /dev/null +++ b/internal/commands/map.go @@ -0,0 +1,53 @@ +package commands + +import ( + "fmt" + "slices" + + "codeflow.dananglin.me.uk/apollo/pokedex/internal/pokeclient" + "codeflow.dananglin.me.uk/apollo/pokedex/internal/poketrainer" +) + +func MapFunc(client *pokeclient.Client, trainer *poketrainer.Trainer) CommandFunc { + return func(_ []string) error { + url := trainer.NextLocationArea() + if url == nil { + url = new(string) + *url = pokeclient.LocationAreaPath + } + + return printResourceList(client, *url, trainer.UpdateLocationAreas) + } +} + +func MapBFunc(client *pokeclient.Client, trainer *poketrainer.Trainer) CommandFunc { + return func(_ []string) error { + url := trainer.PreviousLocationArea() + if url == nil { + return fmt.Errorf("no previous locations available") + } + + return printResourceList(client, *url, trainer.UpdateLocationAreas) + } +} + +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) + } + + if updateStateFunc != nil { + updateStateFunc(list.Previous, list.Next) + } + + for _, location := range slices.All(list.Results) { + fmt.Println(location.Name) + } + + return nil +} diff --git a/internal/commands/pokedex.go b/internal/commands/pokedex.go new file mode 100644 index 0000000..d079628 --- /dev/null +++ b/internal/commands/pokedex.go @@ -0,0 +1,11 @@ +package commands + +import "codeflow.dananglin.me.uk/apollo/pokedex/internal/poketrainer" + +func PokedexFunc(trainer *poketrainer.Trainer) CommandFunc { + return func(_ []string) error { + trainer.ListAllPokemonFromPokedex() + + return nil + } +} diff --git a/internal/commands/visit.go b/internal/commands/visit.go new file mode 100644 index 0000000..2193c11 --- /dev/null +++ b/internal/commands/visit.go @@ -0,0 +1,40 @@ +package commands + +import ( + "errors" + "fmt" + + "codeflow.dananglin.me.uk/apollo/pokedex/internal/pokeclient" + "codeflow.dananglin.me.uk/apollo/pokedex/internal/poketrainer" +) + +func VisitFunc(client *pokeclient.Client, trainer *poketrainer.Trainer) CommandFunc { + 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 + } +} diff --git a/main.go b/main.go index 4498f6f..6577bf3 100644 --- a/main.go +++ b/main.go @@ -2,25 +2,20 @@ package main import ( "bufio" - "errors" "fmt" "maps" - "math/rand/v2" "os" - "slices" "strings" "time" + "codeflow.dananglin.me.uk/apollo/pokedex/internal/commands" "codeflow.dananglin.me.uk/apollo/pokedex/internal/pokeclient" "codeflow.dananglin.me.uk/apollo/pokedex/internal/poketrainer" ) -type callbackFunc func(args []string) error - type command struct { - name string description string - callback callbackFunc + callback commands.CommandFunc } func main() { @@ -28,67 +23,57 @@ func main() { } func run() { - client := pokeclient.NewClient( - 5*time.Minute, - 10*time.Second, + var ( + cacheCleanupInterval = 30 * time.Minute + httpTimeout = 10 * time.Second + client = pokeclient.NewClient(cacheCleanupInterval, httpTimeout) + trainer = poketrainer.NewTrainer() ) - trainer := poketrainer.NewTrainer() - commandMap := map[string]command{ "catch": { - name: "catch", description: "Catch a Pokemon and add it to your Pokedex", - callback: catchFunc(client, trainer), + callback: commands.CatchFunc(client, trainer), }, "exit": { - name: "exit", description: "Exit the Pokedex", - callback: exitFunc, + callback: commands.ExitProgram, }, "explore": { - name: "explore", description: "List all the Pokemon in a given area", - callback: exploreFunc(client, trainer), + callback: commands.ExploreFunc(client, trainer), }, "help": { - name: "help", description: "Display the help message", callback: nil, }, "inspect": { - name: "inspect", description: "Inspect a Pokemon from your Pokedex", - callback: inspectFunc(trainer), + callback: commands.InspectFunc(trainer), }, "map": { - name: "map", description: "Display the next 20 locations in the Pokemon world", - callback: mapFunc(client, trainer), + callback: commands.MapFunc(client, trainer), }, "mapb": { - name: "map back", description: "Display the previous 20 locations in the Pokemon world", - callback: mapBFunc(client, trainer), + callback: commands.MapBFunc(client, trainer), }, "pokedex": { - name: "pokedex", description: "List the names of all the Pokemon in your Pokedex", - callback: pokedexFunc(trainer), + callback: commands.PokedexFunc(trainer), }, "visit": { - name: "visit", description: "Visit a location area", - callback: visitFunc(client, trainer), + callback: commands.VisitFunc(client, trainer), }, } summaries := summaryMap(commandMap) commandMap["help"] = command{ - name: "help", description: "Displays a help message", - callback: helpFunc(summaries), + callback: commands.HelpFunc(summaries), } fmt.Printf("\nWelcome to the Pokedex!\n") @@ -126,263 +111,6 @@ func run() { } } -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 exitFunc(_ []string) error { - os.Exit(0) - - return nil -} - -func mapFunc(client *pokeclient.Client, trainer *poketrainer.Trainer) callbackFunc { - return func(_ []string) error { - url := trainer.NextLocationArea() - if url == nil { - url = new(string) - *url = pokeclient.LocationAreaPath - } - - return printResourceList(client, *url, trainer.UpdateLocationAreas) - } -} - -func mapBFunc(client *pokeclient.Client, trainer *poketrainer.Trainer) callbackFunc { - return func(_ []string) error { - url := trainer.PreviousLocationArea() - if url == nil { - return fmt.Errorf("no previous locations available") - } - - return printResourceList(client, *url, trainer.UpdateLocationAreas) - } -} - -func exploreFunc(client *pokeclient.Client, trainer *poketrainer.Trainer) callbackFunc { - return func(_ []string) error { - locationAreaName := trainer.CurrentLocationAreaName() - - fmt.Printf("Exploring %s...\n", locationAreaName) - - locationArea, err := client.GetLocationArea(locationAreaName) - 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 - } -} - -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") - } - - if len(args) != 1 { - return fmt.Errorf( - "unexpected number of Pokemon names: want 1; got %d", - len(args), - ) - } - - pokemonName := args[0] - - if _, caught := trainer.GetPokemonFromPokedex(pokemonName); caught { - return fmt.Errorf( - "you've already caught a %s", - pokemonName, - ) - } - - pokemonDetails, err := client.GetPokemon(pokemonName) - if err != nil { - return fmt.Errorf( - "unable to get the information on %s: %w", - pokemonName, - err, - ) - } - - 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 { - 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) - } - - return nil - } -} - -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") - } - - if len(args) != 1 { - return fmt.Errorf( - "unexpected number of Pokemon names: want 1; got %d", - len(args), - ) - } - - pokemonName := args[0] - - pokemon, ok := trainer.GetPokemonFromPokedex(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(trainer *poketrainer.Trainer) callbackFunc { - return func(_ []string) error { - trainer.ListAllPokemonFromPokedex() - - return nil - } -} - -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) - } - - if updateStateFunc != nil { - updateStateFunc(list.Previous, list.Next) - } - - 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) @@ -406,34 +134,3 @@ func parseArgs(input string) (string, []string) { 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 -}