Add core application modules and structure

This commit is contained in:
2025-12-07 13:24:33 -05:00
parent e02a6c0bf3
commit 3ee2933f03
6 changed files with 281 additions and 2 deletions
+32 -2
View File
@@ -1,5 +1,35 @@
def main():
print("Hello from custombingo!")
import click
import os
from custom_bingo.card_generator import generate_bingo_cards
from custom_bingo.spreadsheet_reader import read_spreadsheet
from custom_bingo.pdf_generator import export_cards_to_pdf
@click.command()
@click.option('--input-file', '-i', required=True, type=click.Path(exists=True),
help='Input spreadsheet file (Excel/CSV) with B, I, N, G, O columns')
@click.option('--output-file', '-o', required=True, type=click.Path(),
help='Output PDF file for the BINGO cards')
@click.option('--number-of-cards', '-n', default=1, type=int,
help='Number of BINGO cards to generate (default: 1)')
def main(input_file, output_file, number_of_cards):
"""Generate custom BINGO cards from a spreadsheet."""
try:
# Read the spreadsheet data
click.echo(f"Reading data from {input_file}...")
data = read_spreadsheet(input_file)
# Generate the bingo cards
click.echo(f"Generating {number_of_cards} BINGO card(s)...")
cards = generate_bingo_cards(data, number_of_cards)
# Export to PDF
click.echo(f"Exporting cards to {output_file}...")
export_cards_to_pdf(cards, output_file)
click.echo(f"Successfully generated {number_of_cards} BINGO card(s) in {output_file}")
except Exception as e:
click.echo(f"Error: {str(e)}", err=True)
if __name__ == "__main__":
+10
View File
@@ -10,3 +10,13 @@ dependencies = [
"openpyxl>=3.1.0",
"click>=8.0.0"
]
[project.scripts]
custombingo = "main:main"
[tool.setuptools.packages.find]
where = ["src"]
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
+1
View File
@@ -0,0 +1 @@
# CustomBingo package
+63
View File
@@ -0,0 +1,63 @@
import random
from typing import Dict, List, Tuple
def generate_single_card(data: Dict[str, List[str]]) -> List[List[str]]:
"""
Generate a single BINGO card from the provided data.
Args:
data: Dictionary with keys 'B', 'I', 'N', 'G', 'O' and values as lists of strings
Returns:
5x5 matrix representing a BINGO card (list of lists)
"""
card = []
# Column B: numbers 1-15
b_values = random.sample(data['B'], min(len(data['B']), 5))
card.append(b_values + [''] * max(0, 5 - len(b_values)))
# Column I: numbers 16-30
i_values = random.sample(data['I'], min(len(data['I']), 5))
card.append(i_values + [''] * max(0, 5 - len(i_values)))
# Column N: numbers 31-45 (with free space in middle)
n_values = random.sample(data['N'], min(len(data['N']), 5))
# Insert empty string (free space) at position 2 (middle of column)
if len(n_values) >= 3:
n_values.insert(2, "FREE")
else:
n_values.append("FREE")
card.append(n_values + [''] * max(0, 5 - len(n_values)))
# Column G: numbers 46-60
g_values = random.sample(data['G'], min(len(data['G']), 5))
card.append(g_values + [''] * max(0, 5 - len(g_values)))
# Column O: numbers 61-75
o_values = random.sample(data['O'], min(len(data['O']), 5))
card.append(o_values + [''] * max(0, 5 - len(o_values)))
# Transpose to get rows instead of columns
transposed_card = [[card[col][row] for col in range(5)] for row in range(5)]
return transposed_card
def generate_bingo_cards(data: Dict[str, List[str]], number_of_cards: int) -> List[List[List[str]]]:
"""
Generate multiple BINGO cards from the provided data.
Args:
data: Dictionary with keys 'B', 'I', 'N', 'G', 'O' and values as lists of strings
number_of_cards: Number of cards to generate
Returns:
List of 5x5 matrices representing BINGO cards
"""
cards = []
for _ in range(number_of_cards):
card = generate_single_card(data)
cards.append(card)
return cards
+131
View File
@@ -0,0 +1,131 @@
from reportlab.lib.pagesizes import letter, A4
from reportlab.pdfgen import canvas
from reportlab.lib.units import inch
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from typing import List
import math
def calculate_font_size(text: str, available_width: float, available_height: float, max_font_size: int = 24) -> int:
"""
Calculate the largest font size that allows the text to fit in the given space.
Args:
text: Text to fit
available_width: Available width in points
available_height: Available height in points
max_font_size: Maximum font size to consider
Returns:
Font size that allows the text to fit in the space
"""
if not text:
return max_font_size
# Start with the maximum font size and decrease until text fits
for font_size in range(max_font_size, 0, -1):
# Estimate text dimensions (this is a rough approximation)
# For more accuracy, we'd need to use the actual font metrics
text_width = len(text) * font_size * 0.6 # rough character width estimate
text_height = font_size
if text_width <= available_width and text_height <= available_height:
return font_size
return 1 # smallest possible font size
def draw_centered_text(c: canvas.Canvas, text: str, x: float, y: float, width: float, height: float, max_font_size: int = 24):
"""
Draw text centered in a given rectangle with the largest possible font size.
Args:
c: ReportLab canvas
text: Text to draw
x: X coordinate of bottom-left of rectangle
y: Y coordinate of bottom-left of rectangle
width: Width of rectangle
height: Height of rectangle
max_font_size: Maximum font size to use
"""
if not text:
return
font_size = calculate_font_size(text, width, height, max_font_size)
c.setFont("Helvetica", font_size)
# Calculate the text dimensions
text_width = c.stringWidth(text, "Helvetica", font_size)
text_height = font_size
# Calculate centered position
centered_x = x + (width - text_width) / 2
# Vertically center the text
# Using font_size * 0.8 as an approximation of text height
centered_y = y + (height - text_height * 0.8) / 2 + text_height * 0.2
c.drawString(centered_x, centered_y, text)
def export_cards_to_pdf(cards: List[List[List[str]]], output_file: str):
"""
Export the generated BINGO cards to a PDF file.
Args:
cards: List of 5x5 matrices representing BINGO cards
output_file: Path for the output PDF file
"""
# Create a new PDF document
c = canvas.Canvas(output_file, pagesize=letter)
width, height = letter
# Define card dimensions
margin = 1 * inch
card_width = (width - 2 * margin) * 0.8 # Make card 80% of page width
card_height = card_width # Keep card square
cell_width = card_width / 5
cell_height = card_height / 5
# Starting position for the first card (centered horizontally)
start_x = (width - card_width) / 2
start_y = height - margin - card_height
for idx, card in enumerate(cards):
# Add a new page for each card after the first
if idx > 0:
c.showPage()
start_y = height - margin - card_height
# Draw title
c.setFont("Helvetica", 24)
title_width = c.stringWidth("BINGO", "Helvetica", 24)
c.drawString((width - title_width) / 2, height - margin + 0.5 * inch, "BINGO")
# Draw the card grid
for row in range(5):
for col in range(5):
# Calculate the position of this cell
cell_x = start_x + col * cell_width
cell_y = start_y + (4 - row) * cell_height # Flip Y-axis so row 0 is at bottom
# Draw rectangle for cell
c.rect(cell_x, cell_y, cell_width, cell_height)
# Get the text for this cell
cell_text = card[row][col]
# Draw the text centered in the cell
draw_centered_text(c, cell_text, cell_x, cell_y, cell_width, cell_height)
# Draw column headers (B, I, N, G, O)
headers = ['B', 'I', 'N', 'G', 'O']
for col, header in enumerate(headers):
cell_x = start_x + col * cell_width
cell_y = start_y + 5 * cell_height # Above the grid
# Draw header cell (no border, just text)
draw_centered_text(c, header, cell_x, cell_y, cell_width, cell_height, max_font_size=18)
# Save the PDF
c.save()
+44
View File
@@ -0,0 +1,44 @@
import pandas as pd
from typing import Dict, List
def read_spreadsheet(file_path: str) -> Dict[str, List[str]]:
"""
Read a spreadsheet file and return the data organized by BINGO columns.
Args:
file_path: Path to the Excel or CSV file
Returns:
Dictionary with keys 'B', 'I', 'N', 'G', 'O' and values as lists of strings
"""
# Determine file type and read accordingly
if file_path.lower().endswith('.csv'):
df = pd.read_csv(file_path)
elif file_path.lower().endswith(('.xlsx', '.xls')):
df = pd.read_excel(file_path)
else:
raise ValueError("Unsupported file format. Please use CSV or Excel files.")
# Validate that the dataframe has exactly 5 columns
if len(df.columns) != 5:
raise ValueError(f"Spreadsheet must have exactly 5 columns, but found {len(df.columns)}")
# Assign column names if they're not already named B, I, N, G, O
if not all(col in ['B', 'I', 'N', 'G', 'O'] for col in df.columns):
df.columns = ['B', 'I', 'N', 'G', 'O']
else:
# Ensure the order is B, I, N, G, O
df = df[['B', 'I', 'N', 'G', 'O']]
# Convert to dictionary of lists
data = {}
for col in ['B', 'I', 'N', 'G', 'O']:
# Drop NaN values and convert to list of strings
data[col] = df[col].dropna().astype(str).tolist()
# Check for empty columns
if len(data[col]) == 0:
raise ValueError(f"Column {col} is empty. Each column must contain at least one value.")
return data