From b28b04c60378adb3e075ed7c9be575acf7171f29 Mon Sep 17 00:00:00 2001 From: onyx-and-iris Date: Sat, 21 Feb 2026 21:06:21 +0000 Subject: [PATCH] initial commit --- .github/workflows/publish.yml | 26 ++++ .github/workflows/ruff.yml | 19 +++ .gitignore | 212 ++++++++++++++++++++++++++ LICENSE | 21 +++ README.md | 41 +++++ img/tui.png | Bin 0 -> 18136 bytes pdm.lock | 169 +++++++++++++++++++++ pyproject.toml | 15 ++ ruff.toml | 77 ++++++++++ src/lottery_tui/lottery.py | 120 +++++++++++++++ src/lottery_tui/tui.py | 54 +++++++ src/lottery_tui/tui.tcss | 276 ++++++++++++++++++++++++++++++++++ 12 files changed, 1030 insertions(+) create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/ruff.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100755 img/tui.png create mode 100644 pdm.lock create mode 100644 pyproject.toml create mode 100644 ruff.toml create mode 100644 src/lottery_tui/lottery.py create mode 100644 src/lottery_tui/tui.py create mode 100644 src/lottery_tui/tui.tcss diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..bbec031 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,26 @@ +name: Publish to PyPI + +on: + release: + types: [published] + push: + tags: + - 'v*.*.*' + +jobs: + pypi-publish: + name: upload release to PyPI + runs-on: ubuntu-latest + environment: pypi + permissions: + # This permission is needed for private repositories. + contents: read + # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write + steps: + - uses: actions/checkout@v4 + + - uses: pdm-project/setup-pdm@v4 + + - name: Publish package distributions to PyPI + run: pdm publish \ No newline at end of file diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 0000000..4d71024 --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,19 @@ +name: Ruff + +on: + push: + branches: [main] + + pull_request: + branches: [main] + + workflow_dispatch: + +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/ruff-action@v3 + with: + args: 'format --check --diff' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..06d0662 --- /dev/null +++ b/.gitignore @@ -0,0 +1,212 @@ +# Generated by ignr: github.com/onyx-and-iris/ignr + +## Python ## +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +#poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +#pdm.lock +#pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +#pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Cursor +# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# End of ignr diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..77c9e18 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Onyx and Iris + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..988192a --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# Lottery TUI + +[![pdm-managed](https://img.shields.io/badge/pdm-managed-blueviolet)](https://pdm.fming.dev) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +[![PyPI - Version](https://img.shields.io/pypi/v/lottery-tui.svg)](https://pypi.org/project/lottery-tui) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/lottery-tui.svg)](https://pypi.org/project/q3rcon-tui) + +----- + +![img](./img/tui.png) + +## Table of Contents + +- [Installation](#installation) +- [License](#license) + +## Installation + +*with uv* + +```console +uv tool install lottery-tui +``` + +*with pipx* + +```console +pipx install lotter-tui +``` + +The TUI should now be discoverable as lottery-tui. + +## Use + +Launch the TUI, select a lottery and press the Draw button! + +To exit from the application press *q* or *Ctrl+q* + +## License + +`lottery-tui` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. diff --git a/img/tui.png b/img/tui.png new file mode 100755 index 0000000000000000000000000000000000000000..2b34523525b59ec055f6db74c6ea5057e05dd90e GIT binary patch literal 18136 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 N+Fk8CrHT)p|39Y(51#-4 literal 0 HcmV?d00001 diff --git a/pdm.lock b/pdm.lock new file mode 100644 index 0000000..0f3fcc2 --- /dev/null +++ b/pdm.lock @@ -0,0 +1,169 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default"] +strategy = ["inherit_metadata"] +lock_version = "4.5.0" +content_hash = "sha256:842afa14523f463c1a73e53c7aeb6d697673d95a2db9adbf935807b1fe5d021a" + +[[metadata.targets]] +requires_python = "==3.13.*" + +[[package]] +name = "linkify-it-py" +version = "2.0.3" +requires_python = ">=3.7" +summary = "Links recognition library with FULL unicode support." +groups = ["default"] +marker = "python_version == \"3.13\"" +dependencies = [ + "uc-micro-py", +] +files = [ + {file = "linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048"}, + {file = "linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79"}, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +requires_python = ">=3.10" +summary = "Python port of markdown-it. Markdown parsing, done right!" +groups = ["default"] +marker = "python_version == \"3.13\"" +dependencies = [ + "mdurl~=0.1", +] +files = [ + {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, + {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +extras = ["linkify"] +requires_python = ">=3.10" +summary = "Python port of markdown-it. Markdown parsing, done right!" +groups = ["default"] +marker = "python_version == \"3.13\"" +dependencies = [ + "linkify-it-py<3,>=1", + "markdown-it-py==4.0.0", +] +files = [ + {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, + {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.5.0" +requires_python = ">=3.10" +summary = "Collection of plugins for markdown-it-py" +groups = ["default"] +marker = "python_version == \"3.13\"" +dependencies = [ + "markdown-it-py<5.0.0,>=2.0.0", +] +files = [ + {file = "mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f"}, + {file = "mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +requires_python = ">=3.7" +summary = "Markdown URL utilities" +groups = ["default"] +marker = "python_version == \"3.13\"" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "platformdirs" +version = "4.9.2" +requires_python = ">=3.10" +summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +groups = ["default"] +marker = "python_version == \"3.13\"" +files = [ + {file = "platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd"}, + {file = "platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291"}, +] + +[[package]] +name = "pygments" +version = "2.19.2" +requires_python = ">=3.8" +summary = "Pygments is a syntax highlighting package written in Python." +groups = ["default"] +marker = "python_version == \"3.13\"" +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[[package]] +name = "rich" +version = "14.3.3" +requires_python = ">=3.8.0" +summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +groups = ["default"] +marker = "python_version == \"3.13\"" +dependencies = [ + "markdown-it-py>=2.2.0", + "pygments<3.0.0,>=2.13.0", +] +files = [ + {file = "rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d"}, + {file = "rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b"}, +] + +[[package]] +name = "textual" +version = "8.0.0" +requires_python = "<4.0,>=3.9" +summary = "Modern Text User Interface framework" +groups = ["default"] +marker = "python_version == \"3.13\"" +dependencies = [ + "markdown-it-py[linkify]>=2.1.0", + "mdit-py-plugins", + "platformdirs<5,>=3.6.0", + "pygments<3.0.0,>=2.19.2", + "rich>=14.2.0", + "typing-extensions<5.0.0,>=4.4.0", +] +files = [ + {file = "textual-8.0.0-py3-none-any.whl", hash = "sha256:8908f4ebe93a6b4f77ca7262197784a52162bc88b05f4ecf50ac93a92d49bb8f"}, + {file = "textual-8.0.0.tar.gz", hash = "sha256:ce48f83a3d686c0fac0e80bf9136e1f8851c653aa6a4502e43293a151df18809"}, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +requires_python = ">=3.9" +summary = "Backported and Experimental Type Hints for Python 3.9+" +groups = ["default"] +marker = "python_version == \"3.13\"" +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "uc-micro-py" +version = "1.0.3" +requires_python = ">=3.7" +summary = "Micro subset of unicode data files for linkify-it-py projects." +groups = ["default"] +marker = "python_version == \"3.13\"" +files = [ + {file = "uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a"}, + {file = "uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fafa6d5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "lottery-tui" +version = "0.1.0" +description = "A terminal user interface for lottery games." +authors = [{ name = "onyx-and-iris", email = "code@onyxandiris.online" }] +dependencies = ["textual>=8.0.0"] +requires-python = ">=3.10" +readme = "README.md" +license = { text = "MIT" } + +[project.scripts] +lottery-tui = "lottery_tui.tui:main" + +[tool.pdm] +distribution = true diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..1d12d30 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,77 @@ +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +# Same as Black. +line-length = 88 +indent-width = 4 + +# Assume Python 3.10 +target-version = "py310" + +[lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or +# McCabe complexity (`C901`) by default. +select = ["E4", "E7", "E9", "F"] +ignore = [] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[format] +# Unlike Black, use single quotes for strings. +quote-style = "single" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +# Enable auto-formatting of code examples in docstrings. Markdown, +# reStructuredText code/literal blocks and doctests are all supported. +# +# This is currently disabled by default, but it is planned for this +# to be opt-out in the future. +docstring-code-format = false + +# Set the line length limit used when formatting code snippets in +# docstrings. +# +# This only has an effect when the `docstring-code-format` setting is +# enabled. +docstring-code-line-length = "dynamic" diff --git a/src/lottery_tui/lottery.py b/src/lottery_tui/lottery.py new file mode 100644 index 0000000..1486167 --- /dev/null +++ b/src/lottery_tui/lottery.py @@ -0,0 +1,120 @@ +import random +from abc import ABC, abstractmethod +from typing import NamedTuple + + +class Result(NamedTuple): + """A named tuple to hold the results of a lottery draw.""" + + kind: str + numbers: list[int] + bonus: list[int] | None + + def __str__(self) -> str: + """Return a string representation of the lottery result.""" + out = f'Numbers: {", ".join(str(n) for n in self.numbers)}' + if self.bonus: + match self.kind: + case 'EuroMillions': + bonus_name = 'Lucky Stars' + case 'Set For Life': + bonus_name = 'Life Ball' + case 'Thunderball': + bonus_name = 'Thunderball' + case _: + bonus_name = 'Bonus Numbers' + out += f'\n{bonus_name}: {", ".join(str(n) for n in self.bonus)}' + return out + + +registry = {} + + +def register_lottery(cls): + """A decorator to register lottery classes in the registry.""" + registry[cls.__name__.lower()] = cls + return cls + + +class Lottery(ABC): + """An abstract base class for different types of lotteries.""" + + @abstractmethod + def draw(self): + """Perform a lottery draw.""" + + +@register_lottery +class UKlotto(Lottery): + """A class representing the UK Lotto lottery. + + Uk Lotto draws 6 numbers from a pool of 1 to 59, without replacement. + There is no bonus number in UK Lotto. + """ + + POSSIBLE_NUMBERS = range(1, 60) + + def draw(self): + """Perform a UK Lotto draw.""" + result = random.sample(UKlotto.POSSIBLE_NUMBERS, 6) + return Result(kind='UK Lotto', numbers=result, bonus=None) + + +@register_lottery +class EuroMillions(Lottery): + """A class representing the EuroMillions lottery. + + EuroMillions draws 5 numbers from a pool of 1 to 50, without replacement, + and 2 "Lucky Star" numbers from a separate pool of 1 to 12, also without replacement. + """ + + POSSIBLE_NUMBERS = range(1, 51) + POSSIBLE_BONUS_NUMBERS = range(1, 13) + + def draw(self): + """Perform a EuroMillions draw.""" + numbers = random.sample(EuroMillions.POSSIBLE_NUMBERS, 5) + bonus = random.sample(EuroMillions.POSSIBLE_BONUS_NUMBERS, 2) + return Result(kind='EuroMillions', numbers=numbers, bonus=bonus) + + +@register_lottery +class SetForLife(Lottery): + """A class representing the Set For Life lottery. + + Set For Life draws 5 numbers from a pool of 1 to 39, without replacement, + and 1 "Life Ball" number from a separate pool of 1 to 10, also without replacement. + """ + + POSSIBLE_NUMBERS = range(1, 40) + + def draw(self): + """Perform a Set For Life draw.""" + numbers = random.sample(SetForLife.POSSIBLE_NUMBERS, 5) + life_ball = [random.randint(1, 10)] + return Result(kind='Set For Life', numbers=numbers, bonus=life_ball) + + +@register_lottery +class Thunderball(Lottery): + """A class representing the Thunderball lottery. + + Thunderball draws 5 numbers from a pool of 1 to 39, without replacement, + and 1 "Thunderball" number from a separate pool of 1 to 14, also without replacement. + """ + + POSSIBLE_NUMBERS = range(1, 40) # Thunderball numbers range from 1 to 39 + + def draw(self): + """Perform a Thunderball draw.""" + numbers = random.sample(Thunderball.POSSIBLE_NUMBERS, 5) + thunderball = [random.randint(1, 14)] + return Result(kind='Thunderball', numbers=numbers, bonus=thunderball) + + +def request_lottery_obj(lottery_name: str) -> Lottery: + """Return a lottery object based on the provided lottery name.""" + lottery_cls = registry.get(lottery_name.lower()) + if lottery_cls is None: + raise ValueError(f"Lottery '{lottery_name}' not found.") + return lottery_cls() diff --git a/src/lottery_tui/tui.py b/src/lottery_tui/tui.py new file mode 100644 index 0000000..8b50b8f --- /dev/null +++ b/src/lottery_tui/tui.py @@ -0,0 +1,54 @@ +from textual.app import App, ComposeResult +from textual.containers import Container +from textual.widgets import Button, Label, Select, Static + +from .lottery import request_lottery_obj + + +class LotteryTUI(App): + """A Textual TUI for the Lottery application.""" + + CSS_PATH = 'tui.tcss' + + def compose(self) -> ComposeResult: + """Create child widgets for the app.""" + yield Container( + Static('Welcome to the Lottery TUI!', id='welcome'), + Static('Pick a lottery to play:', id='instructions'), + Select( + options=[ + ('UK Lotto', 'uklotto'), + ('EuroMillions', 'euromillions'), + ('Set For Life', 'setforlife'), + ('Thunderball', 'thunderball'), + ], + id='lottery-select', + ), + Button('Draw', id='draw-button'), + Label('', id='result-label'), + id='main-container', + ) + + def on_key(self, event): + """Handle key events.""" + if event.key == 'q': + self.exit() + + def on_button_pressed(self, event): + """Handle button press events.""" + if event.button.id == 'draw-button': + selected_lottery = self.query_one('#lottery-select').value + try: + lottery_obj = request_lottery_obj(selected_lottery) + result = lottery_obj.draw() + self.query_one('#result-label').update(f'Result: {result}') + except ValueError as e: + self.query_one('#result-label').update(str(e)) + + self.query_one('#result-label').update(str(result)) + + +def main(): + """Entry point for the Lottery TUI.""" + app = LotteryTUI() + app.run() diff --git a/src/lottery_tui/tui.tcss b/src/lottery_tui/tui.tcss new file mode 100644 index 0000000..5d2cc8b --- /dev/null +++ b/src/lottery_tui/tui.tcss @@ -0,0 +1,276 @@ +/* Lottery TUI CSS Styling */ + +/* Global App Styling */ +LotteryTUI { + background: $surface; +} + +/* Main Container */ +#main-container { + align: center middle; + background: #1a1a2e; + border: round #ffd700; + border-title-align: center; + border-title-color: #ffd700; + border-title-style: bold; + height: auto; + layout: vertical; + margin: 2; + min-height: 20; + min-width: 60; + padding: 3 5; + width: auto; +} + +/* Welcome Message */ +#welcome { + color: #ffd700; + content-align: center middle; + height: auto; + margin: 0 0 2 0; + padding: 1; + text-style: bold; + width: 100%; +} + +/* Instructions */ +#instructions { + color: #a0aec0; + content-align: center middle; + height: auto; + margin: 1 0 0 0; + padding: 1; + text-style: italic; + width: 100%; +} + +/* Lottery Select Styling */ +#lottery-select { + margin: 1 0 2 0; + width: 100%; +} + +/* Hover Effects */ +#welcome:hover { + color: #ffed4a; + text-style: bold italic; +} + +#instructions:hover { + color: #cbd5e0; + text-style: bold italic; +} + +/* Additional styling for potential future widgets */ + +/* Button styling for lottery buttons */ +Button { + background: #ffd700; + border: round #e6c200; + color: #1a1a2e; + height: 3; + margin: 1; + min-width: 12; + text-style: bold; +} + +Button:hover { + background: #ffed4a; + border: round #ffd700; + color: #16213e; +} + +Button:focus { + background: #e6c200; + border: round #b8860b; + color: #1a1a2e; +} + +/* Input styling for lottery number inputs */ +Input { + background: #2d3748; + border: round #4a5568; + color: #ffd700; + height: 3; + margin: 1; + padding: 0 1; +} + +Input:focus { + background: #374151; + border: round #ffd700; + color: #ffed4a; +} + +/* Label styling */ +Label { + color: #e2e8f0; + margin: 0 0 1 0; + text-style: bold; +} + +/* Draw Button Specific Styling */ +#draw-button { + background: #ffd700; + border: round #e6c200; + color: #1a1a2e; + height: 3; + margin: 1 0 0 0; + text-style: bold; + width: 100%; +} + +#draw-button:hover { + background: #ffed4a; + border: round #ffd700; + color: #16213e; +} + +#draw-button:focus { + background: #e6c200; + border: round #b8860b; + color: #1a1a2e; +} + +/* Results Label Styling - Enhanced Appearance */ +#result-label { + background: #1a365d; + border: thick #ffd700; + color: #ffffff; + height: auto; + margin: 1 0 0 0; + min-height: 4; + padding: 1 2; + text-style: bold; + content-align: left middle; + width: 100%; +} + +/* Container for lottery number display */ +.lottery-numbers { + align: center middle; + background: #2d3748; + border: round #ffd700; + height: auto; + margin: 2; + padding: 2; +} + +/* Individual lottery number balls */ +.lottery-ball { + background: #ffd700; + border: round #e6c200; + color: #1a1a2e; + height: 3; + margin: 0 1; + text-align: center; + text-style: bold; + width: 6; +} + +.lottery-ball:hover { + background: #ffed4a; + color: #16213e; +} + +/* Results display */ +.results { + background: #1a202c; + border: round #4a5568; + color: #e2e8f0; + height: auto; + margin: 2; + padding: 2; +} + +.winning-number { + color: #48bb78; + text-style: bold; +} + +.losing-number { + color: #f56565; + text-style: italic; +} + +/* Status bar */ +.status-bar { + background: #2d3748; + color: #a0aec0; + dock: bottom; + height: 1; + padding: 0 1; +} + +/* Header */ +.header { + background: #ffd700; + color: #1a1a2e; + dock: top; + height: 3; + text-align: center; + text-style: bold; +} + +/* Footer */ +.footer { + background: #1a1a2e; + color: #a0aec0; + dock: bottom; + height: 1; + text-align: center; + text-style: italic; +} + +/* Sidebar styling */ +.sidebar { + background: #2d3748; + border-right: solid #4a5568; + dock: left; + width: 20; +} + +/* Content area */ +.content { + background: $surface; + margin: 1; + padding: 1; +} + +/* Error messages */ +.error { + background: #fed7d7; + border: round #f56565; + color: #c53030; + margin: 1; + padding: 1; + text-style: bold; +} + +/* Success messages */ +.success { + background: #c6f6d5; + border: round #48bb78; + color: #22543d; + margin: 1; + padding: 1; + text-style: bold; +} + +/* Loading spinner */ +.loading { + color: #ffd700; + text-align: center; + text-style: bold; +} + +/* Prize display */ +.prize { + background: #ffd700; + border: round #e6c200; + color: #1a1a2e; + margin: 1; + padding: 2; + text-align: center; + text-style: bold; +} \ No newline at end of file