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:
Dan Anglin 2024-09-20 11:51:35 +01:00
parent 4152a9d14f
commit c35bd1d2d1
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
9 changed files with 807 additions and 115 deletions

View file

@ -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
View file

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

View file

@ -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 {
@ -13,7 +13,7 @@ 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"`
}

View 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"`
}

View 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"`
}

View 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)
}
}
}

View 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)
}
}

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

376
main.go
View file

@ -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": {
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{} keys := []string{}
for key := range maps.All(cmdMap) { for key := range maps.All(summaries) {
keys = append(keys, key) keys = append(keys, key)
} }
slices.Sort(keys) slices.Sort(keys)
fmt.Printf("\nWelcome to the Pokedex!\nUsage:\n") fmt.Printf("\nCommands:\n")
for _, key := range slices.All(keys) { for _, key := range slices.All(keys) {
fmt.Printf("\n%s: %s", key, cmdMap[key].description) fmt.Printf("\n%s: %s", key, summaries[key])
} }
fmt.Println("\n") fmt.Printf("\n\n")
return nil return nil
} }
}
func commandExit() error { func exitFunc(_ []string) error {
os.Exit(0) os.Exit(0)
return nil return nil
} }
func commandMap() error { func mapFunc(client *pokeclient.Client, state *State) callbackFunc {
return func(_ []string) error {
url := state.Next url := state.Next
if url == nil { if url == nil {
url = new(string) url = new(string)
*url = baseURL + locationAreaEndpoint *url = pokeclient.LocationAreaPath
} }
return printMap(*url) return printResourceList(client, *url, state)
}
} }
func commandMapB() error { func mapBFunc(client *pokeclient.Client, state *State) callbackFunc {
return func(_ []string) error {
url := state.Previous url := state.Previous
if url == nil { if url == nil {
return fmt.Errorf("no previous locations available") 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 err != nil {
return fmt.Errorf("error creating the HTTP request: %w", err)
} }
client := http.Client{} if len(args) != 1 {
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( return fmt.Errorf(
"received a bad status from %s: (%d) %s", "unexpected number of locations: want 1; got %d",
url, len(args),
resp.StatusCode,
resp.Status,
) )
} }
var result NamedAPIResourceList location := args[0]
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { fmt.Println("Exploring", location)
return fmt.Errorf("unable to decode the JSON response: %w", err)
locationArea, err := client.GetLocationArea(location)
if err != nil {
return fmt.Errorf(
"unable to get the location area: %w",
err,
)
} }
state.Next = result.Next fmt.Println("Found Pokemon:")
state.Previous = result.Previous
for _, location := range slices.All(result.Results) { for _, encounter := range slices.All(locationArea.PokemonEncounters) {
fmt.Printf("- %s\n", encounter.Pokemon.Name)
}
return nil
}
}
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")
}
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
}
}
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)
}
state.Next = list.Next
state.Previous = list.Previous
for _, location := range slices.All(list.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
}