mirror of
https://github.com/onyx-and-iris/lottery-tui.git
synced 2026-04-08 21:23:30 +00:00
initial commit
This commit is contained in:
120
src/lottery_tui/lottery.py
Normal file
120
src/lottery_tui/lottery.py
Normal file
@@ -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()
|
||||
54
src/lottery_tui/tui.py
Normal file
54
src/lottery_tui/tui.py
Normal file
@@ -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()
|
||||
276
src/lottery_tui/tui.tcss
Normal file
276
src/lottery_tui/tui.tcss
Normal file
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user