From 4152a9d14fb5b8109d9d01bad959b8f3b746791d Mon Sep 17 00:00:00 2001 From: Dan Anglin Date: Tue, 17 Sep 2024 18:43:21 +0100 Subject: [PATCH] feat: add initial project code Add the initial code for the PokeDex project. For now the code can list the next and previous 20 location areas within the Pokemon world. --- .forgejo/workflows/workflow.yaml | 4 +- .gitignore | 3 +- __build/.gitkeep | 0 go.mod | 3 + magefiles/go.mod | 5 + magefiles/go.sum | 2 + magefiles/mage.go | 22 ++-- main.go | 168 +++++++++++++++++++++++++++++-- types.go | 59 +++++++++++ 9 files changed, 242 insertions(+), 24 deletions(-) delete mode 100644 __build/.gitkeep create mode 100644 go.mod create mode 100644 magefiles/go.mod create mode 100644 magefiles/go.sum create mode 100644 types.go diff --git a/.forgejo/workflows/workflow.yaml b/.forgejo/workflows/workflow.yaml index 13935dc..0954bce 100644 --- a/.forgejo/workflows/workflow.yaml +++ b/.forgejo/workflows/workflow.yaml @@ -22,5 +22,5 @@ jobs: with: target: test env: - PROJECT_TEST_VERBOSE: "1" - PROJECT_TEST_COVER: "1" + POKEDEX_TEST_VERBOSE: "1" + POKEDEX_TEST_COVER: "1" diff --git a/.gitignore b/.gitignore index e200850..c3e9be0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ -/__build/* -!__build/.gitkeep +pokedex diff --git a/__build/.gitkeep b/__build/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7039358 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module codeflow.dananglin.me.uk/apollo/pokedex + +go 1.23.0 diff --git a/magefiles/go.mod b/magefiles/go.mod new file mode 100644 index 0000000..70e588f --- /dev/null +++ b/magefiles/go.mod @@ -0,0 +1,5 @@ +module codeflow.dananglin.me.uk/apollo/pokedex/magefiles + +go 1.23.0 + +require github.com/magefile/mage v1.15.0 diff --git a/magefiles/go.sum b/magefiles/go.sum new file mode 100644 index 0000000..4ee1b87 --- /dev/null +++ b/magefiles/go.sum @@ -0,0 +1,2 @@ +github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= +github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= diff --git a/magefiles/mage.go b/magefiles/mage.go index 1a12267..c551625 100644 --- a/magefiles/mage.go +++ b/magefiles/mage.go @@ -14,23 +14,23 @@ import ( ) const ( - app = "binary" + app = "pokedex" defaultInstallPrefix = "/usr/local" - envInstallPrefix = "PROJECT_INSTALL_PREFIX" - envTestVerbose = "PROJECT_TEST_VERBOSE" - envTestCover = "PROJECT_TEST_COVER" - envBuildRebuildAll = "PROJECT_BUILD_REBUILD_ALL" - envBuildVerbose = "PROJECT_BUILD_VERBOSE" + envInstallPrefix = "POKEDEX_INSTALL_PREFIX" + envTestVerbose = "POKEDEX_TEST_VERBOSE" + envTestCover = "POKEDEX_TEST_COVER" + envBuildRebuildAll = "POKEDEX_BUILD_REBUILD_ALL" + envBuildVerbose = "POKEDEX_BUILD_VERBOSE" ) var ( Default = Build - binary = "./__build/" + app + binary = app ) // Test run the go tests. -// To enable verbose mode set PROJECT_TEST_VERBOSE=1. -// To enable coverage mode set PROJECT_TEST_COVER=1. +// To enable verbose mode set POKEDEX_TEST_VERBOSE=1. +// To enable coverage mode set POKEDEX_TEST_COVER=1. func Test() error { goTest := sh.RunCmd("go", "test") @@ -53,8 +53,8 @@ func Lint() error { } // Build build the executable. -// To rebuild packages that are already up-to-date set PROJECT_BUILD_REBUILD_ALL=1 -// To enable verbose mode set PROJECT_BUILD_VERBOSE=1 +// To rebuild packages that are already up-to-date set POKEDEX_BUILD_REBUILD_ALL=1 +// To enable verbose mode set POKEDEX_BUILD_VERBOSE=1 func Build() error { main := "main.go" flags := ldflags() diff --git a/main.go b/main.go index a3255e8..4f88ff5 100644 --- a/main.go +++ b/main.go @@ -1,24 +1,174 @@ package main import ( + "bufio" + "context" + "encoding/json" "fmt" + "maps" + "net/http" "os" + "slices" + "time" ) -var ( - binaryVersion string - buildTime string - goVersion string - gitCommit string +const ( + baseURL string = "https://pokeapi.co/api/v2" + locationAreaEndpoint string = "/location-area" ) +type State struct { + Previous *string + Next *string +} + +var state State + +type command struct { + name string + description string + callback func() error +} + func main() { - if err := run(); err != nil { - fmt.Printf("ERROR: %v.\n", err) - os.Exit(1) + run() +} + +func run() { + fmt.Print("pokedex > ") + + scanner := bufio.NewScanner(os.Stdin) + + for scanner.Scan() { + command := scanner.Text() + + cmdMap := getCommandMap() + if _, ok := cmdMap[command]; !ok { + fmt.Println("ERROR: Unrecognised command.") + + fmt.Print("\npokedex > ") + + continue + } + + if err := cmdMap[command].callback(); err != nil { + fmt.Printf("ERROR: %v.\n", err) + } + + fmt.Print("pokedex > ") } } -func run() error { +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 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 { + os.Exit(0) + + return nil +} + +func commandMap() error { + url := state.Next + if url == nil { + url = new(string) + *url = baseURL + locationAreaEndpoint + } + + return printMap(*url) +} + +func commandMapB() error { + url := state.Previous + if url == nil { + return fmt.Errorf("no previous locations available") + } + + return printMap(*url) +} + +func printMap(url string) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return fmt.Errorf("error creating the HTTP request: %w", err) + } + + client := http.Client{} + + resp, err := client.Do(request) + if err != nil { + return fmt.Errorf("error getting the response from the server: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + return fmt.Errorf( + "received a bad status from %s: (%d) %s", + url, + resp.StatusCode, + resp.Status, + ) + } + + var result NamedAPIResourceList + + 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) { + fmt.Println(location.Name) + } + return nil } diff --git a/types.go b/types.go new file mode 100644 index 0000000..3f8301e --- /dev/null +++ b/types.go @@ -0,0 +1,59 @@ +package main + +// LocationArea is a section of areas such as floors in a building or a cave. +type LocationArea struct { + ID int `json:"id"` + Name string `json:"name"` + GameIndex int `json:"game_index"` + EncounterMethodRates []EncounterMethodRate `json:"encounter_method_rates"` + Location NamedAPIResource `json:"location"` + Names []Name `json:"names"` + PokemonEncounters []PokemonEncounter `json:"pokemon_encounters"` +} + +type EncounterMethodRate struct { + EncounterMethod NamedAPIResource `json:"encounter_method"` + VersionDetails EncounterVersionDetails `json:"version_details"` +} + +type EncounterVersionDetails struct { + Rate int `json:"rate"` + Version NamedAPIResource `json:"version"` +} + +type Name struct { + Name string `json:"name"` + Language NamedAPIResource `json:"language"` +} + +// PokemonEncounter is details of a possible Pokemon encounter. +type PokemonEncounter struct { + Pokemon NamedAPIResource `json:"pokemon"` + VersionDetails []VersionEncounterDetails `json:"version_details"` +} + +type VersionEncounterDetails struct { + Version NamedAPIResource `json:"version"` + MaxChance int `json:"max_chance"` + EncounterDetails []Encounter `json:"encounter_details"` +} + +type Encounter struct { + MinLevel int `json:"min_level"` + MaxLevel int `json:"max_level"` + ConditionValues []NamedAPIResource `json:"condition_values"` + 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"` +}