diff --git a/projects/001-lotto-game/python/.gitignore b/projects/001-lotto-game/python/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/projects/001-lotto-game/python/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/projects/001-lotto-game/python/bill_generator/__init__.py b/projects/001-lotto-game/python/bill_generator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/projects/001-lotto-game/python/bill_generator/bill.py b/projects/001-lotto-game/python/bill_generator/bill.py new file mode 100644 index 0000000..e662e0d --- /dev/null +++ b/projects/001-lotto-game/python/bill_generator/bill.py @@ -0,0 +1,28 @@ +class Bill: + """Represents a single Lotto ticket. + + Attributes: + giocata (str): Type of bet (ambata, ambo, etc.). + ruota (str): Wheel on which the game is played. + numbers (List[int]): Numbers generated for the ticket. + """ + + def __init__(self, giocata: str, ruota: str, numbers: list[int]): + """Initializes a Lotto bet slip. + + Args: + giocata (str): Type of bet. + ruota (str): Selected wheel. + numbers (List[int]): List of generated numbers. + """ + self.giocata = giocata + self.ruota = ruota + self.numbers = numbers + + def __str__(self): + """Returns a textual representation of the ticket. + + Returns: + str: String containing the type of giocata, numbers, and ruota. + """ + return f"{self.giocata} {self.ruota} {self.numbers} " diff --git a/projects/001-lotto-game/python/bill_generator/billGenerator.py b/projects/001-lotto-game/python/bill_generator/billGenerator.py new file mode 100644 index 0000000..eba2c93 --- /dev/null +++ b/projects/001-lotto-game/python/bill_generator/billGenerator.py @@ -0,0 +1,59 @@ +import random +from bill_generator.rules import Ruote, Giocate + + +class BillGenerator: + """Provides utility methods to generate Lotto ticket components. + + This class includes methods for generating random numbers, + selecting a wheel, and choosing a bet type (giocata). + """ + + @staticmethod + def get_numbers(n_numbers: int) -> list[int]: + """Generates unique random numbers between 1 and 90. + + Args: + n_numbers (int): Number of unique numbers to generate. + + Returns: + list[int]: List of generated unique numbers. + """ + numbers: list[int] = [] + while len(numbers) < n_numbers: + n = random.randint(1, 90) + if n not in numbers: + numbers.append(n) + return numbers + + @staticmethod + def get_ruota() -> str: + """Prompts the user to select a Lotto wheel. + + Displays the available wheels and returns the selected one. + + Returns: + str: The wheel selected by the user. + """ + ruote = Ruote("ruote.txt") + print("\nChoose a 'ruota' from the list below:\n") + ruote.print_ruote() + selection = ruote.select_ruote() + return selection + + @staticmethod + def giocata() -> tuple[str, int] | None: + """Prompts the user to select a bet type (giocata). + + Displays the available bet types and returns the selected one + along with its associated rule (minimum required matches). + + Returns: + tuple[str, int] | None: A tuple containing the name of the + selected bet and its required number of matches. Returns + None if no valid selection is made. + """ + giocate = Giocate("giocate.txt") + print("\nChoose a 'giocata' from the list below:\n") + giocate.print_giocata() + return giocate.select_giocata() \ No newline at end of file diff --git a/projects/001-lotto-game/python/bill_generator/controller.py b/projects/001-lotto-game/python/bill_generator/controller.py new file mode 100644 index 0000000..c5050ea --- /dev/null +++ b/projects/001-lotto-game/python/bill_generator/controller.py @@ -0,0 +1,183 @@ +import sys +from typing import List, Any + +from bill_generator.bill import Bill +from bill_generator.billGenerator import BillGenerator +from bill_generator.user_interface import UserInterface +from bill_generator.extraction import Extraction +from bill_generator.winning_bill import WinningBill + + +class Controller: + """Coordinates the application flow for Lotto ticket generation. + + This class handles user interaction, ticket creation, number extraction, + and evaluation of winning tickets. + + Attributes: + bill_generator (BillGenerator): Service used to generate ticket data. + extraction (Extraction): Service used to generate Lotto extractions. + winning_bill (WinningBill): Class used to evaluate tickets. + bills (dict[int, Bill]): Collection of generated tickets indexed by ID. + """ + + def __init__(self, bill_generator, extraction, winning_bill): + """Initializes the controller. + + Args: + bill_generator (BillGenerator): Instance responsible for generating tickets. + extraction (Extraction): Instance responsible for generating extractions. + winning_bill (WinningBill): Class used to evaluate winning tickets. + """ + self.winning_bill = winning_bill + self.extraction = extraction + self.bill_generator = bill_generator + self.bills: dict[int, Bill] = {} + + def how_many_bills(self) -> int: + """Prompts the user to choose how many tickets to generate. + + The user can select a number between 1 and 5, or 0 to exit. + + Returns: + int: Number of tickets to generate. + + Raises: + SystemExit: If the user chooses to exit (input = 0). + """ + while True: + try: + if ( + num_bills := int( + input( + "How many bills would you like to generate? (1–5, 0 to exit): " + ) + ) + ) == 0: + sys.exit("Thank you for using this program") + + if 1 <= num_bills <= 5: + return num_bills + + print("Invalid choice. Please select a number between 1 and 5.") + + except ValueError: + print("Invalid input, insert only numbers.") + + def how_many_numbers_for_bills( + self, min_required: int, max_required: int = 10 + ) -> int: + """Prompts the user to choose how many numbers to include in a ticket. + + The number must be within the allowed range defined by the selected + bet type (giocata). + + Args: + min_required (int): Minimum number of numbers required. + max_required (int, optional): Maximum allowed numbers. Defaults to 10. + + Returns: + int: Number of numbers selected by the user. + """ + while True: + try: + if ( + numbers := int( + input( + f"How many numbers would you like to generate? (min {min_required}, max {max_required}): " + ) + ) + ) >= min_required and numbers <= max_required: + return numbers + + print( + f"You must insert a number between {min_required} and {max_required}." + ) + + except ValueError: + print("Invalid input, numbers only.") + + def create_bills(self) -> None: + """Generates all tickets requested by the user. + + For each ticket, the user selects: + - bet type (giocata) + - number of values + - wheel (ruota) + + The generated tickets are stored in `self.bills`. + """ + numbers_of_bills: int = self.how_many_bills() + + for number in range(numbers_of_bills): + print(f"Generating bill number {number + 1}...") + giocata_name, min_numbers = self.bill_generator.giocata() + + numbers_in_bills: int = self.how_many_numbers_for_bills(min_numbers) + ruota: str = self.bill_generator.get_ruota() + numbers: List[int] = self.bill_generator.get_numbers(numbers_in_bills) + + bill: Bill = Bill(giocata_name, ruota, numbers) + self.bills[number] = bill + + def print_bills(self) -> None: + """Prints all generated tickets. + + If no tickets were generated, a message is displayed. + """ + if not self.bills: + print("No bills were generated.") + return + + for number, bill in enumerate(self.bills.values(), start=1): + UserInterface.print_bill( + number, bill.giocata, bill.ruota, bill.numbers + ) + + def control_winning_bill(self) -> list[dict]: + """Evaluates all tickets against the extraction results. + + Returns: + list[dict]: A list of dictionaries containing: + - index (int): Ticket index + - bill (Bill): The ticket object + - is_winner (bool): Whether the ticket is winning + - winning_numbers (list[int]): Matching numbers (if any) + """ + results = [] + + for index, bill in self.bills.items(): + winning_bill = WinningBill(bill, self.extraction.extractions) + is_winner = winning_bill.retrieve_winning_bill() + + results.append( + { + "index": index, + "bill": bill, + "is_winner": is_winner, + "winning_numbers": ( + winning_bill.winning_numbers if is_winner else [] + ), + } + ) + + return results + + def run(self) -> None: + """Executes the full application workflow. + + The flow includes: + - showing introduction and description + - generating tickets + - displaying tickets + - generating and displaying extractions + - evaluating and displaying winning tickets + """ + UserInterface.show_intro() + UserInterface.show_description() + self.create_bills() + self.print_bills() + self.extraction.start_extraction() + self.extraction.print_extractions() + results = self.control_winning_bill() + UserInterface.print_winning_bill(results) \ No newline at end of file diff --git a/projects/001-lotto-game/python/bill_generator/extraction.py b/projects/001-lotto-game/python/bill_generator/extraction.py new file mode 100644 index 0000000..e37e19f --- /dev/null +++ b/projects/001-lotto-game/python/bill_generator/extraction.py @@ -0,0 +1,51 @@ +import random +from bill_generator.rules import Ruote + + +class Extraction: + """Generates and manages Lotto extractions for each wheel. + + Attributes: + extractions (dict[str, list[int]]): Mapping between wheel names + and their extracted numbers. + """ + + def __init__(self) -> None: + """Initializes an empty extraction container.""" + self.extractions: dict[str, list[int]] = {} + + def start_extraction(self) -> dict[str, list[int]]: + """Generates random extractions for each Lotto wheel. + + For each wheel (excluding "Tutte"), generates 5 unique random + numbers between 1 and 90. + + Returns: + dict[str, list[int]]: Dictionary where keys are wheel names + and values are lists of 5 unique extracted numbers. + """ + ruote = Ruote("ruote.txt") + for ruota in ruote.get_ruote(): + if ruota == "Tutte": + continue + + numbers_extracted: list[int] = [] + while len(numbers_extracted) < 5: + number: int = random.randint(1, 90) + if number not in numbers_extracted: + numbers_extracted.append(number) + + self.extractions[ruota] = numbers_extracted + + return self.extractions + + def print_extractions(self) -> None: + """Prints all generated extractions grouped by wheel. + + The output displays each wheel followed by its extracted numbers + in a readable format. + """ + print("\nExtractions Number:\n") + for ruota, numbers_extracted in self.extractions.items(): + numbers_str: str = " ".join(str(n) for n in numbers_extracted) + print(f"{ruota}: {numbers_str}") \ No newline at end of file diff --git a/projects/001-lotto-game/python/bill_generator/rules.py b/projects/001-lotto-game/python/bill_generator/rules.py new file mode 100644 index 0000000..0e0af82 --- /dev/null +++ b/projects/001-lotto-game/python/bill_generator/rules.py @@ -0,0 +1,156 @@ +class Utility: + """Utility class providing helper methods for CLI interactions.""" + + @staticmethod + def print_list(items: list[str]) -> None: + """Prints a numbered list of items. + + Args: + items (list[str]): List of strings to be displayed. + """ + for i, item in enumerate(items, start=1): + print(f"{i}. {item}") + + @staticmethod + def select_from_list(items: list[str], prompt: str) -> int | None: + """Prompts the user to select an item from a list. + + The user must input a number corresponding to the item index. + + Args: + items (list[str]): List of selectable items. + prompt (str): Message displayed to the user for input. + + Returns: + int | None: The selected index (0-based) if valid, otherwise None. + """ + while (choice := input(prompt)) is not None: + if choice.isdigit(): + index = int(choice) + if 1 <= index <= len(items): + return index - 1 + print("Invalid choice, try again.\n") + return None + + +class Ruote: + """Represents a collection of Lotto wheels loaded from a file. + + Attributes: + filename (str): Path to the file containing wheel names. + _ruote (list[str]): List of wheel names. + """ + + def __init__(self, filename: str) -> None: + """Initializes the Ruote instance and loads data from file. + + Args: + filename (str): Path to the file containing wheel names. + """ + self.filename: str = filename + self._ruote: list[str] = [] + self._load_data() + + def _load_data(self) -> None: + """Loads wheel names from the file into the internal list. + + Raises: + FileNotFoundError: If the specified file does not exist. + """ + try: + with open(self.filename, 'r', encoding='utf-8') as file: + self._ruote = [line.strip() for line in file if line.strip()] + except FileNotFoundError: + raise FileNotFoundError(f"File {self.filename} not found.") + + def get_ruote(self) -> list[str]: + """Returns the list of loaded wheel names. + + Returns: + list[str]: List of wheel names. + """ + return self._ruote + + def print_ruote(self) -> None: + """Prints all available wheel names as a numbered list.""" + for i, ruota in enumerate(self._ruote, start=1): + print(f"{i}. {ruota}") + + def select_ruote(self) -> str: + """Prompts the user to select a wheel. + + Returns: + str: The selected wheel name. + """ + index = Utility.select_from_list(self._ruote, "Select a ruota: ") + return self._ruote[index] + + +class Giocate: + """Represents Lotto bet types loaded from a file. + + Each bet type is associated with a numeric value. + + Attributes: + filename (str): Path to the file containing bet types. + _giocata (dict[str, int]): Dictionary mapping bet names to values. + """ + + def __init__(self, filename: str): + """Initializes the Giocate instance and loads data from file. + + Args: + filename (str): Path to the file containing bet types. + """ + self.filename = filename + self._giocata: dict[str, int] = {} + self._load_data() + + def _load_data(self) -> None: + """Loads bet types and their values from the file. + + Expected file format: + key:value + + Raises: + FileNotFoundError: If the specified file does not exist. + """ + try: + with open(self.filename, 'r', encoding='utf-8') as file: + for line in file: + line = line.strip() + if ":" in line: + key, value = line.split(":", 1) + self._giocata[key.strip()] = int(value.strip()) + except FileNotFoundError: + raise FileNotFoundError(f"File {self.filename} not found.") + + def get_giocata(self) -> dict[str, int]: + """Returns all available bet types. + + Returns: + dict[str, int]: Dictionary of bet names and their values. + """ + return self._giocata + + def print_giocata(self) -> None: + """Prints all available bet types as a numbered list.""" + for i, giocata in enumerate(self._giocata, start=1): + print(f"{i}. {giocata}") + + def select_giocata(self) -> tuple[str, int]: + """Prompts the user to select a bet type. + + Returns: + tuple[str, int]: A tuple containing the selected bet name + and its associated value. + """ + keys = list(self._giocata.keys()) + index = Utility.select_from_list(keys, "Select a giocata: ") + selected = keys[index] + return selected, self._giocata[selected] + + + + + diff --git a/projects/001-lotto-game/python/bill_generator/user_interface.py b/projects/001-lotto-game/python/bill_generator/user_interface.py new file mode 100644 index 0000000..aa1d6f8 --- /dev/null +++ b/projects/001-lotto-game/python/bill_generator/user_interface.py @@ -0,0 +1,80 @@ +from typing import List + + +class UserInterface: + """Handles displaying tickets on screen.""" + + def show_intro(): + print("Welcome to Lotto Bill Generator") + + def show_description(): + print("With this software you can generate betting slips for the 'Lotto game'") + print( + "\nDISCLAIMER: There is no scientific proof of the correlation between the tickets generated and the probability of winning.\n" + ) + + @staticmethod + def print_bill( + bill_number: int, giocata: str, ruota: str, numbers: List[int] + ) -> None: + """Prints a ticket in a dynamic centered table. + + Args: + bill_number (int): Number of the generated ticket. + giocata (str): Type of play. + ruota (str): Selected wheel. + numbers (List[int]): Generated numbers for the ticket. + """ + numbers_str: str = " ".join(str(n) for n in numbers) + headers: List[str] = ["Bill", "Giocata", "Ruota", "Generate Numbers"] + row: List[str] = [f"Bill {bill_number}", giocata, ruota, numbers_str] + + col_widths: List[int] = [ + max(len(headers[i]), len(str(row[i]))) + 2 for i in range(len(headers)) + ] + + def make_line(left: str, fill: str, sep: str, right: str) -> str: + return left + sep.join(fill * w for w in col_widths) + right + + def center_text(text: str, width: int) -> str: + return text.center(width) + + def make_row(items: List[str]) -> str: + cells: List[str] = [ + center_text(items[i], col_widths[i]) for i in range(len(items)) + ] + return "║" + "║".join(cells) + "║" + + top: str = make_line("╔", "═", "╦", "╗") + sep: str = make_line("╠", "═", "╬", "╣") + bottom: str = make_line("╚", "═", "╩", "╝") + + bill: str = f""" + + + {top} + {make_row(headers)} + {sep} + {make_row(row)} + {bottom} + + """ + print(bill) + + @staticmethod + def print_winning_bill(results: list[dict]) -> None: + """Prints winning check results for all bills""" + print("\nChecking Winning Bills Numbers...\n") + + for r in results: + index, bill =r["index"], r["bill"] + if r["is_winner"]: + print( + f"Bill {index + 1} WINNER!" + f"\nGiocata: {bill.giocata}" + f"\nRuota: {bill.ruota}" + f"\nNumbers played: {bill.numbers}" + f"\nWinning numbers: {r['winning_numbers']}\n" + ) + else: + print(f"Bill {index + 1} not winning.\n") diff --git a/projects/001-lotto-game/python/bill_generator/winning_bill.py b/projects/001-lotto-game/python/bill_generator/winning_bill.py new file mode 100644 index 0000000..f632119 --- /dev/null +++ b/projects/001-lotto-game/python/bill_generator/winning_bill.py @@ -0,0 +1,69 @@ +from bill_generator.extraction import Extraction +from bill_generator.bill import Bill +from bill_generator.rules import Giocate + + +class WinningBill: + """Evaluates whether a Lotto ticket is a winning one. + + This class compares the numbers on a given ticket (`Bill`) with + the extracted numbers and determines if the ticket satisfies + the winning conditions based on the selected bet type. + + Attributes: + bill (Bill): The Lotto ticket to evaluate. + extractions (Extraction): Object containing extraction results. + winning_numbers (list[int]): List of matched numbers between + the ticket and the extractions. + """ + + def __init__(self, bill: Bill, extractions: Extraction): + """Initializes a WinningBill instance. + + Args: + bill (Bill): The Lotto ticket to evaluate. + extractions (Extraction): Object containing extracted numbers + for each wheel. + """ + self.bill = bill + self.extractions = extractions + self.winning_numbers: list[int] = [] + + def retrieve_winning_bill(self) -> bool: + """Determines whether the ticket is a winning one. + + If the ticket is played on all wheels ("Tutte"), the method checks + against every wheel. Otherwise, it checks only the selected wheel. + + The ticket is considered winning if the number of matched numbers + is greater than or equal to the required amount defined by the + selected bet type (giocata). + + Returns: + bool: True if the ticket is a winning one, False otherwise. + """ + giocate_rules = Giocate("giocate.txt").get_giocata() + ruota = self.bill.ruota + + if ruota == "Tutte": + for extracted_numbers in self.extractions.values(): + self._compare_numbers(extracted_numbers) + else: + extracted_numbers = self.extractions.get(ruota, []) + self._compare_numbers(extracted_numbers) + + required = giocate_rules[self.bill.giocata] + return len(self.winning_numbers) >= required + + def _compare_numbers(self, extracted_numbers: list[int]) -> None: + """Compares ticket numbers with extracted numbers. + + Matching numbers are added to `winning_numbers`, avoiding duplicates. + + Args: + extracted_numbers (list[int]): List of numbers extracted for + a specific wheel. + """ + for number in self.bill.numbers: + if number in extracted_numbers and number not in self.winning_numbers: + self.winning_numbers.append(number) \ No newline at end of file diff --git a/projects/001-lotto-game/python/giocate.txt b/projects/001-lotto-game/python/giocate.txt new file mode 100644 index 0000000..13a19dd --- /dev/null +++ b/projects/001-lotto-game/python/giocate.txt @@ -0,0 +1,5 @@ +Ambata: 1 +Ambo: 2 +Terno: 3 +Quaterna: 4 +Cinquina: 5 \ No newline at end of file diff --git a/projects/001-lotto-game/python/main.py b/projects/001-lotto-game/python/main.py index e69de29..24b9e83 100644 --- a/projects/001-lotto-game/python/main.py +++ b/projects/001-lotto-game/python/main.py @@ -0,0 +1,15 @@ +from bill_generator.controller import Controller +from bill_generator.extraction import Extraction +from bill_generator.winning_bill import WinningBill +from bill_generator.billGenerator import BillGenerator +from bill_generator.bill import Bill + +if __name__ == "__main__": + bill_generator = BillGenerator() + extraction = Extraction() + winning_bill = WinningBill(Bill,extraction) + controller = Controller(bill_generator, extraction, winning_bill) + + controller.run() + + diff --git a/projects/001-lotto-game/python/ruote.txt b/projects/001-lotto-game/python/ruote.txt new file mode 100644 index 0000000..995f162 --- /dev/null +++ b/projects/001-lotto-game/python/ruote.txt @@ -0,0 +1,11 @@ +Bari +Cagliari +Firenze +Genova +Milano +Napoli +Palermo +Roma +Torino +Venezia +Tutte \ No newline at end of file diff --git a/projects/002-lotto-fake-extraction/python/main.py b/projects/002-lotto-fake-extraction/python/main.py index e69de29..384ee9a 100644 --- a/projects/002-lotto-fake-extraction/python/main.py +++ b/projects/002-lotto-fake-extraction/python/main.py @@ -0,0 +1,13 @@ + + + + + + + + + + + + +