Skip to content

Commit 04a9ff4

Browse files
feat(core): extract game logic into separate modules
Create core module with dedicated files for different aspects of game logic: - Extract board generation and management to board.py - Extract phrases loading and processing to phrases.py - Extract win pattern detection to win_patterns.py 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 4dd4191 commit 04a9ff4

File tree

4 files changed

+235
-0
lines changed

4 files changed

+235
-0
lines changed

src/core/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Core game logic package initialization

src/core/board.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Board generation and management
2+
import random
3+
import datetime
4+
from typing import List, Dict, Set, Tuple
5+
6+
from src.config.constants import FREE_SPACE_TEXT, FREE_SPACE_POSITION
7+
# Avoid import at module level to prevent circular imports
8+
# We'll import get_phrases inside the function
9+
10+
# Global state
11+
board = []
12+
clicked_tiles = set()
13+
board_iteration = 1
14+
today_seed = ""
15+
board_views = {}
16+
17+
def generate_board(seed_val: int) -> None:
18+
"""Generate a new board using the provided seed value."""
19+
global board, today_seed, clicked_tiles
20+
21+
# Import here to avoid circular imports
22+
from src.core.phrases import get_phrases
23+
24+
todays_seed = datetime.date.today().strftime("%Y%m%d")
25+
random.seed(seed_val)
26+
27+
phrases = get_phrases()
28+
shuffled_phrases = random.sample(phrases, 24)
29+
shuffled_phrases.insert(12, FREE_SPACE_TEXT)
30+
board = [shuffled_phrases[i:i+5] for i in range(0, 25, 5)]
31+
32+
clicked_tiles.clear()
33+
for r, row in enumerate(board):
34+
for c, phrase in enumerate(row):
35+
if phrase.upper() == FREE_SPACE_TEXT:
36+
clicked_tiles.add((r, c))
37+
38+
today_seed = f"{todays_seed}.{seed_val}"
39+
return board
40+
41+
def reset_board() -> None:
42+
"""Clear all clicked tiles except FREE SPACE."""
43+
global clicked_tiles
44+
clicked_tiles.clear()
45+
for r, row in enumerate(board):
46+
for c, phrase in enumerate(row):
47+
if phrase.upper() == FREE_SPACE_TEXT:
48+
clicked_tiles.add((r, c))
49+
50+
def generate_new_board() -> None:
51+
"""Generate a completely new board with a new seed."""
52+
global board_iteration
53+
board_iteration += 1
54+
return generate_board(board_iteration)
55+
56+
def toggle_tile(row: int, col: int) -> None:
57+
"""Toggle the clicked state of a tile."""
58+
if (row, col) == FREE_SPACE_POSITION:
59+
return
60+
61+
key = (row, col)
62+
if key in clicked_tiles:
63+
clicked_tiles.remove(key)
64+
else:
65+
clicked_tiles.add(key)

src/core/phrases.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Phrase loading and processing
2+
import os
3+
import logging
4+
from typing import List
5+
6+
# Global variables
7+
phrases = []
8+
last_phrases_mtime = 0
9+
10+
def has_too_many_repeats(phrase: str, threshold=0.5) -> bool:
11+
"""Returns True if too many words in the phrase repeat."""
12+
words = phrase.split()
13+
if not words:
14+
return False
15+
unique_count = len(set(words))
16+
ratio = unique_count / len(words)
17+
if ratio < threshold:
18+
logging.debug(f"Discarding phrase '{phrase}' due to repeats: {unique_count}/{len(words)} = {ratio:.2f} < {threshold}")
19+
return True
20+
return False
21+
22+
def load_phrases() -> List[str]:
23+
"""Load and process phrases from phrases.txt."""
24+
global phrases, last_phrases_mtime
25+
26+
try:
27+
last_phrases_mtime = os.path.getmtime("phrases.txt")
28+
29+
with open("phrases.txt", "r") as f:
30+
raw_phrases = [line.strip().upper() for line in f if line.strip()]
31+
32+
# Remove duplicates while preserving order
33+
unique_phrases = []
34+
seen = set()
35+
for p in raw_phrases:
36+
if p not in seen:
37+
seen.add(p)
38+
unique_phrases.append(p)
39+
40+
# Filter out phrases with too many repeated words
41+
phrases = [p for p in unique_phrases if not has_too_many_repeats(p)]
42+
return phrases
43+
44+
except Exception as e:
45+
logging.error(f"Error loading phrases: {e}")
46+
return []
47+
48+
def initialize_phrases() -> None:
49+
"""Initialize phrases during app startup."""
50+
load_phrases()
51+
52+
def get_phrases() -> List[str]:
53+
"""Get the current phrases list."""
54+
return phrases
55+
56+
def check_phrases_file_change() -> bool:
57+
"""Check if phrases.txt has changed and reload if needed."""
58+
global last_phrases_mtime
59+
60+
try:
61+
mtime = os.path.getmtime("phrases.txt")
62+
if mtime != last_phrases_mtime:
63+
logging.info("phrases.txt changed, reloading phrases.")
64+
load_phrases()
65+
return True
66+
except Exception as e:
67+
logging.error(f"Error checking phrases.txt: {e}")
68+
69+
return False

src/core/win_patterns.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Win condition detection
2+
from typing import List, Set, Tuple
3+
from nicegui import ui
4+
5+
# Global state
6+
bingo_patterns = set()
7+
8+
def check_winner(clicked_tiles: Set[Tuple[int, int]]) -> List[str]:
9+
"""Check for winning patterns and return newly found ones."""
10+
global bingo_patterns
11+
new_patterns = []
12+
13+
# Check rows and columns
14+
for i in range(5):
15+
if all((i, j) in clicked_tiles for j in range(5)):
16+
if f"row{i}" not in bingo_patterns:
17+
new_patterns.append(f"row{i}")
18+
if all((j, i) in clicked_tiles for j in range(5)):
19+
if f"col{i}" not in bingo_patterns:
20+
new_patterns.append(f"col{i}")
21+
22+
# Check main diagonal
23+
if all((i, i) in clicked_tiles for i in range(5)):
24+
if "diag_main" not in bingo_patterns:
25+
new_patterns.append("diag_main")
26+
27+
# Check anti-diagonal
28+
if all((i, 4-i) in clicked_tiles for i in range(5)):
29+
if "diag_anti" not in bingo_patterns:
30+
new_patterns.append("diag_anti")
31+
32+
# Special patterns
33+
34+
# Blackout: every cell is clicked
35+
if all((r, c) in clicked_tiles for r in range(5) for c in range(5)):
36+
if "blackout" not in bingo_patterns:
37+
new_patterns.append("blackout")
38+
39+
# 4 Corners
40+
if all(pos in clicked_tiles for pos in [(0,0), (0,4), (4,0), (4,4)]):
41+
if "four_corners" not in bingo_patterns:
42+
new_patterns.append("four_corners")
43+
44+
# Plus shape
45+
plus_cells = {(2, c) for c in range(5)} | {(r, 2) for r in range(5)}
46+
if all(cell in clicked_tiles for cell in plus_cells):
47+
if "plus" not in bingo_patterns:
48+
new_patterns.append("plus")
49+
50+
# X shape
51+
if all((i, i) in clicked_tiles for i in range(5)) and all((i, 4-i) in clicked_tiles for i in range(5)):
52+
if "x_shape" not in bingo_patterns:
53+
new_patterns.append("x_shape")
54+
55+
# Perimeter
56+
perimeter_cells = {(0, c) for c in range(5)} | {(4, c) for c in range(5)} | {(r, 0) for r in range(5)} | {(r, 4) for r in range(5)}
57+
if all(cell in clicked_tiles for cell in perimeter_cells):
58+
if "perimeter" not in bingo_patterns:
59+
new_patterns.append("perimeter")
60+
61+
return new_patterns
62+
63+
def process_win_notifications(new_patterns: List[str]) -> None:
64+
"""Process new win patterns and show appropriate notifications."""
65+
global bingo_patterns
66+
67+
if not new_patterns:
68+
return
69+
70+
# Separate new win patterns into standard and special ones
71+
special_set = {"blackout", "four_corners", "plus", "x_shape", "perimeter"}
72+
standard_new = [p for p in new_patterns if p not in special_set]
73+
special_new = [p for p in new_patterns if p in special_set]
74+
75+
# Process standard win conditions
76+
if standard_new:
77+
for pattern in standard_new:
78+
bingo_patterns.add(pattern)
79+
standard_total = sum(1 for p in bingo_patterns if p not in special_set)
80+
81+
if standard_total == 1:
82+
message = "BINGO!"
83+
elif standard_total == 2:
84+
message = "DOUBLE BINGO!"
85+
elif standard_total == 3:
86+
message = "TRIPLE BINGO!"
87+
elif standard_total == 4:
88+
message = "QUADRUPLE BINGO!"
89+
elif standard_total == 5:
90+
message = "QUINTUPLE BINGO!"
91+
else:
92+
message = f"{standard_total}-WAY BINGO!"
93+
94+
ui.notify(message, color="green", duration=5)
95+
96+
# Process special win conditions
97+
for sp in special_new:
98+
bingo_patterns.add(sp)
99+
sp_message = sp.replace("_", " ").title() + " Bingo!"
100+
ui.notify(sp_message, color="blue", duration=5)

0 commit comments

Comments
 (0)