From 6df41c18880a375b1104d41f410fff5382e79c90 Mon Sep 17 00:00:00 2001 From: onyx-and-iris Date: Thu, 23 Jul 2026 23:25:26 +0100 Subject: [PATCH] initial commit --- .github/workflows/golang-ci.yml | 30 +++++ .github/workflows/release.yml | 28 +++++ .github/workflows/update-go-modules.yml | 30 +++++ .gitignore | 37 ++++++ .golangci.yml | 142 ++++++++++++++++++++++++ .goreleaser.yaml | 55 +++++++++ .pre-commit-config.yaml | 5 + cmd/lottery/main.go | 75 +++++++++++++ cmd/lottery/render.go | 61 ++++++++++ go.mod | 44 ++++++++ go.sum | 98 ++++++++++++++++ kinds.go | 30 +++++ lottery.go | 111 ++++++++++++++++++ util.go | 19 ++++ 14 files changed, 765 insertions(+) create mode 100644 .github/workflows/golang-ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/update-go-modules.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 .goreleaser.yaml create mode 100644 .pre-commit-config.yaml create mode 100644 cmd/lottery/main.go create mode 100644 cmd/lottery/render.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 kinds.go create mode 100644 lottery.go create mode 100644 util.go diff --git a/.github/workflows/golang-ci.yml b/.github/workflows/golang-ci.yml new file mode 100644 index 0000000..358429a --- /dev/null +++ b/.github/workflows/golang-ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + branches: ['main'] + paths: + - '**.go' + pull_request: + branches: ['main'] + paths: + - '**.go' +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + timeout-minutes: 3 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + - name: golangci-lint + uses: golangci/golangci-lint-action@v9 + with: + version: v2.6.0 + args: --config .golangci.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..bafdbf0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,28 @@ +name: goreleaser + +on: + push: + tags: + - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 + +permissions: + contents: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v5 + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: '~> v2' + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/update-go-modules.yml b/.github/workflows/update-go-modules.yml new file mode 100644 index 0000000..eca8185 --- /dev/null +++ b/.github/workflows/update-go-modules.yml @@ -0,0 +1,30 @@ +name: Auto-Update Go Modules + +on: + schedule: + - cron: '0 0 * * 1' # Runs every Monday at midnight + +jobs: + update-go-modules: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: stable + + - name: Update Dependencies + run: | + go get -u ./... + go mod tidy + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add go.mod go.sum + git commit -m "chore: auto-update Go modules" + git push diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ee501c --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Generated by ignr: github.com/onyx-and-iris/ignr + +## Go ## +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Code coverage profiles and other test artifacts +*.out +coverage.* +*.coverprofile +profile.cov + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# Editor/IDE +# .idea/ +# .vscode/ + +# End of ignr diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..c1e8425 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,142 @@ +version: '2' + +run: + timeout: 3m + tests: true + go: '1.24' + +linters: + disable: [] + enable: + # Default enabled linters + - errcheck # Check for unchecked errors + - govet # Go's built-in vetting tool + - ineffassign # Detect ineffectual assignments + - staticcheck # Advanced static analysis + - unused # Check for unused code + # Additional useful linters + - misspell # Detect common misspellings + - unparam # Check for unused function parameters + - gosec # Security checks + - asciicheck # Check for non-ASCII characters + - errname # Check error variable names + - godot # Check for missing periods in comments + - revive # Highly configurable linter for style and correctness + - gocritic # Detect code issues and suggest improvements + - gocyclo # Check for cyclomatic complexity + - dupl # Check for code duplication + - predeclared # Check for shadowing of predeclared identifiers + - copyloopvar # Check for loop variable capture in goroutines + - errorlint # Check for common mistakes in error handling + - goconst # Check for repeated strings that could be constants + - gosmopolitan # Check for non-portable code + + settings: + misspell: + locale: UK + + errcheck: + check-type-assertions: true + check-blank: true + exclude-functions: + - fmt.Fprintf + - fmt.Fprintln + - fmt.Printf + - fmt.Println + - fmt.Errorf + + revive: + severity: warning + rules: + # Code quality and style + - name: exported + arguments: + - 'checkPrivateReceivers' + - 'sayRepetitiveInsteadOfStutters' + - name: var-naming + - name: package-comments + - name: range-val-in-closure + - name: time-naming + - name: context-as-argument + - name: context-keys-type + - name: dot-imports + - name: empty-block + - name: error-return + - name: error-strings + - name: error-naming + - name: if-return + - name: increment-decrement + - name: indent-error-flow + - name: receiver-naming + - name: redefines-builtin-id + - name: superfluous-else + - name: unexported-return + - name: unreachable-code + - name: unused-parameter + - name: var-declaration + - name: blank-imports + - name: range + + # Disabled rules (can be enabled if needed) + # - name: line-length-limit + # arguments: [120] + # - name: function-length + # arguments: [50, 0] + # - name: cyclomatic + # arguments: [10] + + gosec: + excludes: + - G104 # Duplicated errcheck checks + - G115 # integer overflow conversion int -> uint32 + + exclusions: + warn-unused: false + rules: + # Exclude some linters from running on tests files. + - path: _test\.go + linters: + - gocyclo + - errcheck + - dupl + - gosec + + paths: + - vendor + +# Formatters configuration +formatters: + # Enable specific formatters + enable: + - gofumpt # Stricter gofmt alternative + - goimports # Organizes imports + - gci # Controls import order/grouping + - golines # Enforces line length + + # Formatter-specific settings + settings: + goimports: + local-prefixes: [github.com/onyx-and-iris/lottery-cli] + + gci: + # Define import sections order + sections: + - standard # Standard library + - default # Everything else + - prefix(github.com/onyx-and-iris/lottery-cli) # Current module + + gofumpt: + extra-rules: true # Enable additional formatting rules + + exclusions: + warn-unused: true + + paths: + - vendor + +issues: + # Limit the number of same issues reported to avoid spam + max-same-issues: 50 + + # Limit the number of issues per linter to keep output manageable + max-issues-per-linter: 100 diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..096d59b --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,55 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com + +# The lines below are called `modelines`. See `:help modeline` +# Feel free to remove those if you don't want/need to use them. +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj + +version: 2 + +before: + hooks: + # You may remove this if you don't use go modules. + - go mod tidy + # you may remove this if you don't need go generate + - go generate ./... + +builds: + - main: ./cmd/lottery/ + env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + +archives: + - formats: ['tar.gz'] + # this name template makes the OS and Arch compatible with the results of `uname`. + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + # use zip for windows archives + format_overrides: + - goos: windows + formats: ['zip'] + +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' + - '^chore:' + +release: + footer: >- + + --- + + Released by [GoReleaser](https://github.com/goreleaser/goreleaser). diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..bd31bc2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +repos: + - repo: https://github.com/tekwizely/pre-commit-golang + rev: v1.0.0-rc.4 + hooks: + - id: go-mod-tidy diff --git a/cmd/lottery/main.go b/cmd/lottery/main.go new file mode 100644 index 0000000..ca04825 --- /dev/null +++ b/cmd/lottery/main.go @@ -0,0 +1,75 @@ +package main + +import ( + "context" + "fmt" + "os" + "runtime/debug" + "strings" + + "charm.land/huh/v2" + "github.com/charmbracelet/fang" + "github.com/onyx-and-iris/lottery-cli" + "github.com/spf13/cobra" +) + +var version string + +func versionFromBuild() string { + if version != "" { + return version + } + + info, ok := debug.ReadBuildInfo() + if !ok { + return "(unable to read version)" + } + return strings.Split(info.Main.Version, "-")[0] +} + +var cmd = &cobra.Command{ + Use: "lottery", + Short: "A CLI for National Lottery games.", + RunE: func(cmd *cobra.Command, args []string) error { + var selected string + + form := huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Pick a lottery."). + Options( + huh.NewOption("Lotto", "lotto"), + huh.NewOption("EuroMillions", "euromillions"), + huh.NewOption("SetForLife", "setforlife"), + huh.NewOption("Thunderball", "thunderball"), + huh.NewOption("Powerball", "powerball"), + ). + Value(&selected), + ), + ) + err := form.Run() + if err != nil { + return err + } + + kind, err := lottery.ParseKind(selected) + if err != nil { + return err + } + + l, err := lottery.New(kind) + if err != nil { + return err + } + l.Draw() + fmt.Println(renderDraw(l)) + + return nil + }, +} + +func main() { + if err := fang.Execute(context.Background(), cmd, fang.WithVersion(versionFromBuild())); err != nil { + os.Exit(1) + } +} diff --git a/cmd/lottery/render.go b/cmd/lottery/render.go new file mode 100644 index 0000000..7874b9f --- /dev/null +++ b/cmd/lottery/render.go @@ -0,0 +1,61 @@ +package main + +import ( + "strconv" + "strings" + + "charm.land/lipgloss/v2" + "github.com/onyx-and-iris/lottery-cli" +) + +func formatNumberList(numbers []int) string { + parts := make([]string, len(numbers)) + for i, n := range numbers { + parts[i] = strconv.Itoa(n) + } + return strings.Join(parts, " ") +} + +func renderDraw(l lottery.Lottery) string { + titleStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12")) + labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8")) + numbersStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("10")) + specialStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("11")) + + var title string + var lines []string + + switch game := l.(type) { + case *lottery.Lotto: + title = "Lotto" + lines = append(lines, labelStyle.Render("Numbers:")+" "+numbersStyle.Render(formatNumberList(game.Numbers[:]))) + case *lottery.EuroMillions: + title = "EuroMillions" + lines = append(lines, labelStyle.Render("Main:")+" "+numbersStyle.Render(formatNumberList(game.Numbers[:]))) + lines = append(lines, labelStyle.Render("Lucky Stars:")+" "+specialStyle.Render(formatNumberList(game.LuckyStars[:]))) + case *lottery.SetForLife: + title = "Set For Life" + lines = append(lines, labelStyle.Render("Main:")+" "+numbersStyle.Render(formatNumberList(game.Numbers[:]))) + lines = append(lines, labelStyle.Render("Life Ball:")+" "+specialStyle.Render(strconv.Itoa(game.LifeBall))) + case *lottery.Thunderball: + title = "Thunderball" + lines = append(lines, labelStyle.Render("Main:")+" "+numbersStyle.Render(formatNumberList(game.Numbers[:]))) + lines = append(lines, labelStyle.Render("Thunderball:")+" "+specialStyle.Render(strconv.Itoa(game.Thunderball))) + case *lottery.Powerball: + title = "Powerball" + lines = append(lines, labelStyle.Render("Main:")+" "+numbersStyle.Render(formatNumberList(game.Numbers[:]))) + lines = append(lines, labelStyle.Render("Powerball:")+" "+specialStyle.Render(strconv.Itoa(game.Powerball))) + default: + title = "Lottery" + lines = append(lines, "Unknown lottery type") + } + + body := strings.Join(lines, "\n") + card := lipgloss.NewStyle(). + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("8")). + Padding(0, 1). + MarginTop(1) + + return card.Render(titleStyle.Render(title+" draw") + "\n" + body) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3926add --- /dev/null +++ b/go.mod @@ -0,0 +1,44 @@ +module github.com/onyx-and-iris/lottery-cli + +go 1.26.1 + +require ( + charm.land/huh/v2 v2.0.3 + charm.land/lipgloss/v2 v2.0.5 + github.com/charmbracelet/fang v1.0.0 + github.com/spf13/cobra v1.10.2 +) + +require ( + charm.land/bubbles/v2 v2.0.0 // indirect + charm.land/bubbletea/v2 v2.0.2 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/catppuccin/go v0.2.0 // indirect + github.com/charmbracelet/colorprofile v0.4.3 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect + github.com/charmbracelet/x/ansi v0.11.7 // indirect + github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 // indirect + github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/charmbracelet/x/termios v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.4.0 // indirect + github.com/mattn/go-runewidth v0.0.23 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/mango v0.1.0 // indirect + github.com/muesli/mango-cobra v1.2.0 // indirect + github.com/muesli/mango-pflag v0.1.0 // indirect + github.com/muesli/roff v0.1.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/spf13/pflag v1.0.9 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/text v0.24.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..45864a5 --- /dev/null +++ b/go.sum @@ -0,0 +1,98 @@ +charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s= +charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI= +charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0= +charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ= +charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU= +charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc= +charm.land/lipgloss/v2 v2.0.5 h1:kbNxgeeUOYv5J0YdpxFjfvf3dFvqH8Aci4zB6xqFtrY= +charm.land/lipgloss/v2 v2.0.5/go.mod h1:9oqhxt4yxIMe6q5A4kHr44DremZk7J9UNh74GlWa5nc= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= +github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= +github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= +github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= +github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= +github.com/charmbracelet/fang v1.0.0 h1:jESBY40agJOlLYnnv9jE0mLqDGTxEk0hkOnx7YGyRlQ= +github.com/charmbracelet/fang v1.0.0/go.mod h1:P5/DNb9DddQ0Z0dbc0P3ol4/ix5Po7Ofr2KMBfAqoCo= +github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA= +github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98= +github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= +github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= +github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs= +github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 h1:IJDiTgVE56gkAGfq0lBEloWgkXMk4hl/bmuPoicI4R0= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= +github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE= +github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= +github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= +github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw= +github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= +github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= +github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI= +github.com/muesli/mango v0.1.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4= +github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg= +github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA= +github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg= +github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0= +github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= +github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/kinds.go b/kinds.go new file mode 100644 index 0000000..c41dbba --- /dev/null +++ b/kinds.go @@ -0,0 +1,30 @@ +package lottery + +import "fmt" + +type Kind string + +const ( + KindLotto Kind = "lotto" + KindEuroMillions Kind = "euromillions" + KindSetForLife Kind = "setforlife" + KindThunderball Kind = "thunderball" + KindPowerball Kind = "powerball" +) + +func ParseKind(kind string) (Kind, error) { + switch kind { + case "lotto": + return KindLotto, nil + case "euromillions": + return KindEuroMillions, nil + case "setforlife": + return KindSetForLife, nil + case "thunderball": + return KindThunderball, nil + case "powerball": + return KindPowerball, nil + default: + return "", fmt.Errorf("invalid lottery kind: %s", kind) + } +} diff --git a/lottery.go b/lottery.go new file mode 100644 index 0000000..267b375 --- /dev/null +++ b/lottery.go @@ -0,0 +1,111 @@ +package lottery + +import ( + "fmt" + "math/rand" +) + +// Lottery is an interface that defines the methods that all lottery games must implement. +type Lottery interface { + Draw() +} + +// Lotto represents the Lotto lottery game. +// It draws 6 numbers from a pool of 1 to 59 without replacement. +// There is no bonus ball in this game. +type Lotto struct { + Numbers [6]int +} + +func (l *Lotto) Draw() { + numbers := drawUnique(len(l.Numbers), 59) + for i := range l.Numbers { + l.Numbers[i] = numbers[i] + } +} + +// EuroMillions represents the EuroMillions lottery game. +// It draws 5 numbers from a pool of 1 to 50 without replacement. +// Additionally, it draws 2 Lucky Stars from a pool of 1 to 12 without replacement. +type EuroMillions struct { + Numbers [5]int + LuckyStars [2]int +} + +func (e *EuroMillions) Draw() { + numbers := drawUnique(len(e.Numbers), 50) + for i := range e.Numbers { + e.Numbers[i] = numbers[i] + } + + luckyStars := drawUnique(len(e.LuckyStars), 12) + for i := range e.LuckyStars { + e.LuckyStars[i] = luckyStars[i] + } +} + +// SetForLife represents the Set For Life lottery game. +// It draws 5 numbers from a pool of 1 to 47 without replacement. +// Additionally, it draws 1 Life Ball from a pool of 1 to 10 without replacement. +type SetForLife struct { + Numbers [5]int + LifeBall int +} + +func (s *SetForLife) Draw() { + numbers := drawUnique(len(s.Numbers), 47) + for i := range s.Numbers { + s.Numbers[i] = numbers[i] + } + s.LifeBall = rand.Intn(10) + 1 +} + +// Thunderball represents the Thunderball lottery game. +// It draws 5 numbers from a pool of 1 to 39 without replacement. +// Additionally, it draws 1 Thunderball from a pool of 1 to 14 without replacement. +type Thunderball struct { + Numbers [5]int + Thunderball int +} + +func (t *Thunderball) Draw() { + numbers := drawUnique(len(t.Numbers), 39) + for i := range t.Numbers { + t.Numbers[i] = numbers[i] + } + t.Thunderball = rand.Intn(14) + 1 +} + +// Powerball represents the Powerball lottery game. +// It draws 5 numbers from a pool of 1 to 69 without replacement. +// Additionally, it draws 1 Powerball from a pool of 1 to 26 without replacement. +type Powerball struct { + Numbers [5]int + Powerball int +} + +func (p *Powerball) Draw() { + numbers := drawUnique(len(p.Numbers), 69) + for i := range p.Numbers { + p.Numbers[i] = numbers[i] + } + p.Powerball = rand.Intn(26) + 1 +} + +// New returns a new instance of the selected lottery game. +func New(selected Kind) (Lottery, error) { + switch selected { + case KindLotto: + return &Lotto{}, nil + case KindEuroMillions: + return &EuroMillions{}, nil + case KindSetForLife: + return &SetForLife{}, nil + case KindThunderball: + return &Thunderball{}, nil + case KindPowerball: + return &Powerball{}, nil + default: + return nil, fmt.Errorf("unknown lottery type: %s", string(selected)) + } +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..f58c11a --- /dev/null +++ b/util.go @@ -0,0 +1,19 @@ +package lottery + +import ( + "math/rand" + "sort" +) + +// drawUnique returns count unique numbers in the range [1, max]. +// It uses the rand.Perm function to generate a random permutation of numbers and selects the first count numbers from it. +// The result is sorted before being returned. +func drawUnique(count, max int) []int { + perm := rand.Perm(max) + result := make([]int, count) + for i := range result { + result[i] = perm[i] + 1 + } + sort.Ints(result) + return result +}