Compare commits

..

No commits in common. "main" and "v0.1.0" have entirely different histories.
main ... v0.1.0

7 changed files with 188 additions and 126 deletions

View File

@ -1,6 +1,6 @@
# Lottery TUI # Lottery TUI
[![pdm-managed](https://img.shields.io/endpoint?url=https%3A%2F%2Fcdn.jsdelivr.net%2Fgh%2Fpdm-project%2F.github%2Fbadge.json)](https://pdm-project.org) [![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) [![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 - 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/lottery-tui) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/lottery-tui.svg)](https://pypi.org/project/lottery-tui)
@ -25,7 +25,7 @@ uv tool install lottery-tui
*with pipx* *with pipx*
```console ```console
pipx install lottery-tui pipx install lotter-tui
``` ```
The TUI should now be discoverable as lottery-tui. The TUI should now be discoverable as lottery-tui.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 18 KiB

43
pdm.lock generated
View File

@ -5,23 +5,11 @@
groups = ["default"] groups = ["default"]
strategy = ["inherit_metadata"] strategy = ["inherit_metadata"]
lock_version = "4.5.0" lock_version = "4.5.0"
content_hash = "sha256:6cd4ed6668a18d93170023df0e7cf183ac36d04df220f4dcb4c091eb6623b65f" content_hash = "sha256:842afa14523f463c1a73e53c7aeb6d697673d95a2db9adbf935807b1fe5d021a"
[[metadata.targets]] [[metadata.targets]]
requires_python = "==3.13.*" requires_python = "==3.13.*"
[[package]]
name = "colorama"
version = "0.4.6"
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
summary = "Cross-platform colored terminal text."
groups = ["default"]
marker = "sys_platform == \"win32\" and python_version == \"3.13\""
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]] [[package]]
name = "linkify-it-py" name = "linkify-it-py"
version = "2.0.3" version = "2.0.3"
@ -37,23 +25,6 @@ files = [
{file = "linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79"}, {file = "linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79"},
] ]
[[package]]
name = "loguru"
version = "0.7.3"
requires_python = "<4.0,>=3.5"
summary = "Python logging made (stupidly) simple"
groups = ["default"]
marker = "python_version == \"3.13\""
dependencies = [
"aiocontextvars>=0.2.0; python_version < \"3.7\"",
"colorama>=0.3.4; sys_platform == \"win32\"",
"win32-setctime>=1.0.0; sys_platform == \"win32\"",
]
files = [
{file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"},
{file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"},
]
[[package]] [[package]]
name = "markdown-it-py" name = "markdown-it-py"
version = "4.0.0" version = "4.0.0"
@ -196,15 +167,3 @@ files = [
{file = "uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a"}, {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"}, {file = "uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5"},
] ]
[[package]]
name = "win32-setctime"
version = "1.2.0"
requires_python = ">=3.5"
summary = "A small Python utility to set file creation time on Windows"
groups = ["default"]
marker = "sys_platform == \"win32\" and python_version == \"3.13\""
files = [
{file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"},
{file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"},
]

View File

@ -1,33 +1,15 @@
[project] [project]
name = "lottery-tui" name = "lottery-tui"
version = "1.0.1" version = "0.1.0"
description = "A terminal user interface for lottery games." description = "A terminal user interface for lottery games."
authors = [{ name = "onyx-and-iris", email = "code@onyxandiris.online" }] authors = [{ name = "onyx-and-iris", email = "code@onyxandiris.online" }]
dependencies = ["textual>=8.0.0", "loguru>=0.7.3"] dependencies = ["textual>=8.0.0"]
requires-python = ">=3.10" requires-python = ">=3.10"
readme = "README.md" readme = "README.md"
license = { text = "MIT" } license = { text = "MIT" }
classifiers = [
"Development Status :: 5 - Production/Stable",
"Programming Language :: Python",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
[project.scripts] [project.scripts]
lottery-tui = "lottery_tui.tui:main" lottery-tui = "lottery_tui.tui:main"
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"
[tool.pdm] [tool.pdm]
distribution = true distribution = true
[tool.pdm.build]
package-dir = "src"
includes = ["src/**/*", "README.md", "LICENSE"]

View File

@ -1,27 +1,15 @@
import random import random
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass from typing import NamedTuple
from loguru import logger
@dataclass(frozen=True) class Result(NamedTuple):
class Result: """A named tuple to hold the results of a lottery draw."""
"""A dataclass to hold the results of a lottery draw with auto-sorted numbers."""
kind: str kind: str
numbers: list[int] numbers: list[int]
bonus: list[int] | None bonus: list[int] | None
def __post_init__(self):
"""Sort the numbers after initialization.
We use super().__setattr__ to bypass the frozen nature of the dataclass for sorting.
"""
super().__setattr__('numbers', sorted(self.numbers))
if self.bonus:
super().__setattr__('bonus', sorted(self.bonus))
def __str__(self) -> str: def __str__(self) -> str:
"""Return a string representation of the lottery result.""" """Return a string representation of the lottery result."""
out = f'Numbers: {", ".join(str(n) for n in self.numbers)}' out = f'Numbers: {", ".join(str(n) for n in self.numbers)}'
@ -52,24 +40,24 @@ class Lottery(ABC):
"""An abstract base class for different types of lotteries.""" """An abstract base class for different types of lotteries."""
@abstractmethod @abstractmethod
def draw(self) -> Result: def draw(self):
"""Perform a lottery draw.""" """Perform a lottery draw."""
@register_lottery @register_lottery
class Lotto(Lottery): class UKlotto(Lottery):
"""A class representing the Lotto lottery. """A class representing the UK Lotto lottery.
Lotto draws 6 numbers from a pool of 1 to 59, without replacement. Uk Lotto draws 6 numbers from a pool of 1 to 59, without replacement.
There is no bonus number in Lotto. There is no bonus number in UK Lotto.
""" """
POSSIBLE_NUMBERS = range(1, 60) POSSIBLE_NUMBERS = range(1, 60)
def draw(self) -> Result: def draw(self):
"""Perform a Lotto draw.""" """Perform a UK Lotto draw."""
result = random.sample(Lotto.POSSIBLE_NUMBERS, 6) result = random.sample(UKlotto.POSSIBLE_NUMBERS, 6)
return Result(kind=type(self).__name__, numbers=result, bonus=None) return Result(kind='UK Lotto', numbers=result, bonus=None)
@register_lottery @register_lottery
@ -83,11 +71,11 @@ class EuroMillions(Lottery):
POSSIBLE_NUMBERS = range(1, 51) POSSIBLE_NUMBERS = range(1, 51)
POSSIBLE_BONUS_NUMBERS = range(1, 13) POSSIBLE_BONUS_NUMBERS = range(1, 13)
def draw(self) -> Result: def draw(self):
"""Perform a EuroMillions draw.""" """Perform a EuroMillions draw."""
numbers = random.sample(EuroMillions.POSSIBLE_NUMBERS, 5) numbers = random.sample(EuroMillions.POSSIBLE_NUMBERS, 5)
bonus = random.sample(EuroMillions.POSSIBLE_BONUS_NUMBERS, 2) bonus = random.sample(EuroMillions.POSSIBLE_BONUS_NUMBERS, 2)
return Result(kind=type(self).__name__, numbers=numbers, bonus=bonus) return Result(kind='EuroMillions', numbers=numbers, bonus=bonus)
@register_lottery @register_lottery
@ -95,16 +83,16 @@ class SetForLife(Lottery):
"""A class representing the Set For Life lottery. """A class representing the Set For Life lottery.
Set For Life draws 5 numbers from a pool of 1 to 39, without replacement, 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. and 1 "Life Ball" number from a separate pool of 1 to 10, also without replacement.
""" """
POSSIBLE_NUMBERS = range(1, 40) POSSIBLE_NUMBERS = range(1, 40)
def draw(self) -> Result: def draw(self):
"""Perform a Set For Life draw.""" """Perform a Set For Life draw."""
numbers = random.sample(SetForLife.POSSIBLE_NUMBERS, 5) numbers = random.sample(SetForLife.POSSIBLE_NUMBERS, 5)
life_ball = [random.randint(1, 10)] life_ball = [random.randint(1, 10)]
return Result(kind=type(self).__name__, numbers=numbers, bonus=life_ball) return Result(kind='Set For Life', numbers=numbers, bonus=life_ball)
@register_lottery @register_lottery
@ -112,23 +100,21 @@ class Thunderball(Lottery):
"""A class representing the Thunderball lottery. """A class representing the Thunderball lottery.
Thunderball draws 5 numbers from a pool of 1 to 39, without replacement, 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. and 1 "Thunderball" number from a separate pool of 1 to 14, also without replacement.
""" """
POSSIBLE_NUMBERS = range(1, 40) POSSIBLE_NUMBERS = range(1, 40) # Thunderball numbers range from 1 to 39
def draw(self) -> Result: def draw(self):
"""Perform a Thunderball draw.""" """Perform a Thunderball draw."""
numbers = random.sample(Thunderball.POSSIBLE_NUMBERS, 5) numbers = random.sample(Thunderball.POSSIBLE_NUMBERS, 5)
thunderball = [random.randint(1, 14)] thunderball = [random.randint(1, 14)]
return Result(kind=type(self).__name__, numbers=numbers, bonus=thunderball) return Result(kind='Thunderball', numbers=numbers, bonus=thunderball)
def request_lottery_obj(lottery_name: str) -> Lottery: def request_lottery_obj(lottery_name: str) -> Lottery:
"""Return a lottery object based on the provided lottery name.""" """Return a lottery object based on the provided lottery name."""
lottery_cls = registry.get(lottery_name.lower()) lottery_cls = registry.get(lottery_name.lower())
if lottery_cls is None: if lottery_cls is None:
ERR_MSG = f"Lottery '{lottery_name}' not found. Available lotteries: {', '.join(registry.keys())}" raise ValueError(f"Lottery '{lottery_name}' not found.")
logger.error(ERR_MSG)
raise ValueError(ERR_MSG)
return lottery_cls() return lottery_cls()

View File

@ -1,9 +1,5 @@
from typing import NoReturn
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.containers import Container from textual.containers import Container
from textual.events import Key
from textual.types import SelectType
from textual.widgets import Button, Label, Select, Static from textual.widgets import Button, Label, Select, Static
from .lottery import request_lottery_obj from .lottery import request_lottery_obj
@ -21,13 +17,11 @@ class LotteryTUI(App):
Static('Pick a lottery to play:', id='instructions'), Static('Pick a lottery to play:', id='instructions'),
Select( Select(
options=[ options=[
('Lotto', 'lotto'), ('UK Lotto', 'uklotto'),
('EuroMillions', 'euromillions'), ('EuroMillions', 'euromillions'),
('Set For Life', 'setforlife'), ('Set For Life', 'setforlife'),
('Thunderball', 'thunderball'), ('Thunderball', 'thunderball'),
], ],
value='lotto',
allow_blank=False,
id='lottery-select', id='lottery-select',
), ),
Button('Draw', id='draw-button'), Button('Draw', id='draw-button'),
@ -35,29 +29,23 @@ class LotteryTUI(App):
id='main-container', id='main-container',
) )
def on_key(self, event: Key) -> NoReturn: def on_key(self, event):
"""Handle key events.""" """Handle key events."""
if event.key == 'q': if event.key == 'q':
self.exit() self.exit()
def on_button_pressed(self, event: Button.Pressed) -> None: def on_button_pressed(self, event):
"""Handle button press events.""" """Handle button press events."""
if event.button.id == 'draw-button': if event.button.id == 'draw-button':
self._draw_button_handler() selected_lottery = self.query_one('#lottery-select').value
try:
def _draw_button_handler(self) -> None: lottery_obj = request_lottery_obj(selected_lottery)
"""Handle the draw button press."""
lottery_obj = request_lottery_obj(self._read_lottery_selection())
result = lottery_obj.draw() result = lottery_obj.draw()
self._update_result_label(str(result)) self.query_one('#result-label').update(f'Result: {result}')
except ValueError as e:
self.query_one('#result-label').update(str(e))
def _read_lottery_selection(self) -> SelectType: self.query_one('#result-label').update(str(result))
"""Read the selected lottery from the dropdown."""
return self.query_one('#lottery-select').value
def _update_result_label(self, message: str) -> None:
"""Update the result label with a new message."""
self.query_one('#result-label').update(message)
def main(): def main():

View File

@ -61,7 +61,9 @@ LotteryTUI {
text-style: bold italic; text-style: bold italic;
} }
/* Button styling */ /* Additional styling for potential future widgets */
/* Button styling for lottery buttons */
Button { Button {
background: #ffd700; background: #ffd700;
border: round #e6c200; border: round #e6c200;
@ -84,6 +86,22 @@ Button:focus {
color: #1a1a2e; 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 styling */
Label { Label {
color: #e2e8f0; color: #e2e8f0;
@ -114,7 +132,7 @@ Label {
color: #1a1a2e; color: #1a1a2e;
} }
/* Results Label Styling */ /* Results Label Styling - Enhanced Appearance */
#result-label { #result-label {
background: #1a365d; background: #1a365d;
border: thick #ffd700; border: thick #ffd700;
@ -127,3 +145,132 @@ Label {
content-align: left middle; content-align: left middle;
width: 100%; 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;
}