Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,32 +1,33 @@
.PHONY: help install test lint format clean run build docker-build docker-run
.PHONY: help install test lint format clean run run-monolithic build docker-build docker-run

# Help command
help:
@echo "BINGO Application Makefile Commands"
@echo ""
@echo "Usage:"
@echo " make install - Install dependencies"
@echo " make run - Run the application"
@echo " make test - Run tests"
@echo " make lint - Run linters"
@echo " make format - Format code"
@echo " make clean - Clean build artifacts and cache"
@echo " make build - Build the package"
@echo " make docker-build - Build Docker image"
@echo " make docker-run - Run Docker container"
@echo " make install - Install dependencies"
@echo " make run - Run the modular application"
@echo " make run-monolithic - Run the original monolithic application"
@echo " make test - Run tests"
@echo " make lint - Run linters"
@echo " make format - Format code"
@echo " make clean - Clean build artifacts and cache"
@echo " make build - Build the package"
@echo " make docker-build - Build Docker image"
@echo " make docker-run - Run Docker container"

# Install dependencies
install:
poetry install

# Run application
# Run application (modular version)
run:
poetry run python main.py

# Run modular app (when available)
run-modular:
poetry run python app.py

# Run original monolithic application
run-monolithic:
poetry run python main.py

# Run tests
test:
poetry run pytest --cov=src
Expand Down
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Bingo Board Generator

A customizable bingo board generator built with NiceGUI and Python.

## Features

- Generate interactive 5x5 bingo boards
- Customizable phrases from phrases.txt file
- Automatic text sizing to fit all content
- Real-time detection of win patterns
- Support for different views (home and stream)
- Mobile-friendly responsive design

## Installation

1. Clone the repository

2. Install dependencies with Poetry:
```bash
poetry install
```

3. Run the application:
```bash
python app.py
```

The application will be available at http://localhost:8080

## Docker

You can also run the application using Docker:

```bash
docker build -t bingo .
docker run -p 8080:8080 bingo
```

## Testing

Run tests with pytest:

```bash
poetry run pytest
```

Or for full coverage report:

```bash
poetry run pytest --cov=src --cov-report=html
```

## Project Structure

- `app.py`: Main entry point for the application
- `src/`: Source code directory
- `config/`: Configuration and constants
- `core/`: Core game logic
- `ui/`: User interface components
- `utils/`: Utility functions
- `tests/`: Unit tests
- `static/`: Static assets (fonts, etc.)
- `phrases.txt`: Customizable bingo phrases

## Customization

Modify the `phrases.txt` file to add your own bingo phrases. The application will reload them automatically.
32 changes: 32 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Main entry point for the application
import logging
from nicegui import ui, app
from fastapi.staticfiles import StaticFiles

# Import our modules
from src.config.constants import HEADER_TEXT
from src.ui.pages import setup_pages, home_page, stream_page
from src.core.phrases import initialize_phrases
from src.core.board import generate_board, board_iteration

# Set up logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')

def main():
# Initialize core components
initialize_phrases()

# Generate initial board
generate_board(board_iteration)

# Set up routes and pages
setup_pages()

# Mount static files
app.mount("/static", StaticFiles(directory="static"), name="static")

# Run the app
ui.run(port=8080, title=f"{HEADER_TEXT}", dark=False)

if __name__ in {"__main__", "__mp_main__"}:
main()
1 change: 1 addition & 0 deletions src/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Main application package
1 change: 1 addition & 0 deletions src/config/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Config package initialization
25 changes: 25 additions & 0 deletions src/config/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# All application constants

# Text constants
HEADER_TEXT = "COMMIT !BINGO"
HEADER_TEXT_COLOR = "#0CB2B3"
FREE_SPACE_TEXT = "FREE MEAT"
FREE_SPACE_TEXT_COLOR = "#FF7f33"

# Colors
TILE_CLICKED_BG_COLOR = "#100079"
TILE_CLICKED_TEXT_COLOR = "#1BEFF5"
TILE_UNCLICKED_BG_COLOR = "#1BEFF5"
TILE_UNCLICKED_TEXT_COLOR = "#100079"
HOME_BG_COLOR = "#100079"
STREAM_BG_COLOR = "#00FF00"

# Fonts
HEADER_FONT_FAMILY = "'Super Carnival', sans-serif"
BOARD_TILE_FONT = "Inter"
BOARD_TILE_FONT_WEIGHT = "700"
BOARD_TILE_FONT_STYLE = "normal"

# Board configuration
BOARD_SIZE = 5 # 5x5 grid
FREE_SPACE_POSITION = (2, 2) # Middle of the board
12 changes: 12 additions & 0 deletions src/config/styles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# UI style-related constants

# CSS Classes
BOARD_CONTAINER_CLASS = "flex justify-center items-center w-full"
HEADER_CONTAINER_CLASS = "w-full"
CARD_CLASSES = "relative p-2 rounded-xl shadow-8 w-full h-full flex items-center justify-center"
COLUMN_CLASSES = "flex flex-col items-center justify-center gap-0 w-full"
GRID_CONTAINER_CLASS = "w-full aspect-square p-4"
GRID_CLASSES = "gap-2 h-full grid-rows-5"
ROW_CLASSES = "w-full"
LABEL_SMALL_CLASSES = "fit-text-small text-center select-none"
LABEL_CLASSES = "fit-text text-center select-none"
1 change: 1 addition & 0 deletions src/core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Core game logic package initialization
65 changes: 65 additions & 0 deletions src/core/board.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Board generation and management
import random
import datetime
from typing import List, Dict, Set, Tuple

from src.config.constants import FREE_SPACE_TEXT, FREE_SPACE_POSITION
# Avoid import at module level to prevent circular imports
# We'll import get_phrases inside the function

# Global state
board = []
clicked_tiles = set()
board_iteration = 1
today_seed = ""
board_views = {}

def generate_board(seed_val: int) -> None:
"""Generate a new board using the provided seed value."""
global board, today_seed, clicked_tiles

# Import here to avoid circular imports
from src.core.phrases import get_phrases

todays_seed = datetime.date.today().strftime("%Y%m%d")
random.seed(seed_val)

phrases = get_phrases()
shuffled_phrases = random.sample(phrases, 24)
shuffled_phrases.insert(12, FREE_SPACE_TEXT)
board = [shuffled_phrases[i:i+5] for i in range(0, 25, 5)]

clicked_tiles.clear()
for r, row in enumerate(board):
for c, phrase in enumerate(row):
if phrase.upper() == FREE_SPACE_TEXT:
clicked_tiles.add((r, c))

today_seed = f"{todays_seed}.{seed_val}"
return board

def reset_board() -> None:
"""Clear all clicked tiles except FREE SPACE."""
global clicked_tiles
clicked_tiles.clear()
for r, row in enumerate(board):
for c, phrase in enumerate(row):
if phrase.upper() == FREE_SPACE_TEXT:
clicked_tiles.add((r, c))

def generate_new_board() -> None:
"""Generate a completely new board with a new seed."""
global board_iteration
board_iteration += 1
return generate_board(board_iteration)

def toggle_tile(row: int, col: int) -> None:
"""Toggle the clicked state of a tile."""
if (row, col) == FREE_SPACE_POSITION:
return

key = (row, col)
if key in clicked_tiles:
clicked_tiles.remove(key)
else:
clicked_tiles.add(key)
69 changes: 69 additions & 0 deletions src/core/phrases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Phrase loading and processing
import os
import logging
from typing import List

# Global variables
phrases = []
last_phrases_mtime = 0

def has_too_many_repeats(phrase: str, threshold=0.5) -> bool:
"""Returns True if too many words in the phrase repeat."""
words = phrase.split()
if not words:
return False
unique_count = len(set(words))
ratio = unique_count / len(words)
if ratio < threshold:
logging.debug(f"Discarding phrase '{phrase}' due to repeats: {unique_count}/{len(words)} = {ratio:.2f} < {threshold}")
return True
return False

def load_phrases() -> List[str]:
"""Load and process phrases from phrases.txt."""
global phrases, last_phrases_mtime

try:
last_phrases_mtime = os.path.getmtime("phrases.txt")

with open("phrases.txt", "r") as f:
raw_phrases = [line.strip().upper() for line in f if line.strip()]

# Remove duplicates while preserving order
unique_phrases = []
seen = set()
for p in raw_phrases:
if p not in seen:
seen.add(p)
unique_phrases.append(p)

# Filter out phrases with too many repeated words
phrases = [p for p in unique_phrases if not has_too_many_repeats(p)]
return phrases

except Exception as e:
logging.error(f"Error loading phrases: {e}")
return []

def initialize_phrases() -> None:
"""Initialize phrases during app startup."""
load_phrases()

def get_phrases() -> List[str]:
"""Get the current phrases list."""
return phrases

def check_phrases_file_change() -> bool:
"""Check if phrases.txt has changed and reload if needed."""
global last_phrases_mtime

try:
mtime = os.path.getmtime("phrases.txt")
if mtime != last_phrases_mtime:
logging.info("phrases.txt changed, reloading phrases.")
load_phrases()
return True
except Exception as e:
logging.error(f"Error checking phrases.txt: {e}")

return False
Loading
Loading