Skip to content

Commit 45303b4

Browse files
refactor: add type hints
- Add proper typing to game_logic, board_builder, and constants - Create dedicated types module for shared type definitions - Fix tests to reflect new closed message functionality - Add mypy type checking to development dependencies
1 parent 2911416 commit 45303b4

File tree

6 files changed

+167
-76
lines changed

6 files changed

+167
-76
lines changed

src/config/constants.py

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,53 @@
22
Configuration constants for the Bingo application.
33
"""
44

5+
from typing import Final, Literal
6+
7+
# Type definitions for CSS properties
8+
CssColor = str # Hex color code like "#123456" or named color like "red"
9+
CssFontFamily = str # Font family names like "'Font Name', sans-serif"
10+
CssFontWeight = str # Font weight like "400", "700", etc.
11+
CssFontStyle = Literal["normal", "italic", "oblique"]
12+
CssClass = str # CSS class name or space-separated class names
13+
514
# Header text and display settings
6-
HEADER_TEXT = "COMMIT !BINGO"
7-
HEADER_TEXT_COLOR = "#0CB2B3"
8-
CLOSED_HEADER_TEXT = "Bingo Is Closed"
9-
CLOSED_MESSAGE_TEXT = "GAME CLOSED"
10-
CLOSED_MESSAGE_COLOR = "#FF7f33"
15+
HEADER_TEXT: Final[str] = "COMMIT !BINGO"
16+
HEADER_TEXT_COLOR: Final[CssColor] = "#0CB2B3"
17+
CLOSED_HEADER_TEXT: Final[str] = "Bingo Is Closed"
18+
CLOSED_MESSAGE_TEXT: Final[str] = "GAME CLOSED"
19+
CLOSED_MESSAGE_COLOR: Final[CssColor] = "#FF7f33"
1120

1221
# Free space settings
13-
FREE_SPACE_TEXT = "FREE MEAT"
14-
FREE_SPACE_TEXT_COLOR = "#FF7f33"
22+
FREE_SPACE_TEXT: Final[str] = "FREE MEAT"
23+
FREE_SPACE_TEXT_COLOR: Final[CssColor] = "#FF7f33"
1524

1625
# Tile appearance settings
17-
TILE_CLICKED_BG_COLOR = "#100079"
18-
TILE_CLICKED_TEXT_COLOR = "#1BEFF5"
19-
TILE_UNCLICKED_BG_COLOR = "#1BEFF5"
20-
TILE_UNCLICKED_TEXT_COLOR = "#100079"
26+
TILE_CLICKED_BG_COLOR: Final[CssColor] = "#100079"
27+
TILE_CLICKED_TEXT_COLOR: Final[CssColor] = "#1BEFF5"
28+
TILE_UNCLICKED_BG_COLOR: Final[CssColor] = "#1BEFF5"
29+
TILE_UNCLICKED_TEXT_COLOR: Final[CssColor] = "#100079"
2130

2231
# Page backgrounds
23-
HOME_BG_COLOR = "#100079"
24-
STREAM_BG_COLOR = "#00FF00"
32+
HOME_BG_COLOR: Final[CssColor] = "#100079"
33+
STREAM_BG_COLOR: Final[CssColor] = "#00FF00"
2534

2635
# Font settings
27-
HEADER_FONT_FAMILY = "'Super Carnival', sans-serif"
28-
BOARD_TILE_FONT = "Inter"
29-
BOARD_TILE_FONT_WEIGHT = "700"
30-
BOARD_TILE_FONT_STYLE = "normal"
36+
HEADER_FONT_FAMILY: Final[CssFontFamily] = "'Super Carnival', sans-serif"
37+
BOARD_TILE_FONT: Final[str] = "Inter"
38+
BOARD_TILE_FONT_WEIGHT: Final[CssFontWeight] = "700"
39+
BOARD_TILE_FONT_STYLE: Final[CssFontStyle] = "normal"
3140

3241
# UI Class Constants
33-
BOARD_CONTAINER_CLASS = "flex justify-center items-center w-full"
34-
HEADER_CONTAINER_CLASS = "w-full"
35-
CARD_CLASSES = (
42+
BOARD_CONTAINER_CLASS: Final[CssClass] = "flex justify-center items-center w-full"
43+
HEADER_CONTAINER_CLASS: Final[CssClass] = "w-full"
44+
CARD_CLASSES: Final[CssClass] = (
3645
"relative p-2 rounded-xl shadow-8 w-full h-full flex items-center justify-center"
3746
)
38-
COLUMN_CLASSES = "flex flex-col items-center justify-center gap-0 w-full"
39-
GRID_CONTAINER_CLASS = "w-full aspect-square p-4"
40-
GRID_CLASSES = "gap-2 h-full grid-rows-5"
41-
ROW_CLASSES = "w-full"
42-
LABEL_SMALL_CLASSES = "fit-text-small text-center select-none"
43-
LABEL_CLASSES = "fit-text text-center select-none"
47+
COLUMN_CLASSES: Final[CssClass] = (
48+
"flex flex-col items-center justify-center gap-0 w-full"
49+
)
50+
GRID_CONTAINER_CLASS: Final[CssClass] = "w-full aspect-square p-4"
51+
GRID_CLASSES: Final[CssClass] = "gap-2 h-full grid-rows-5"
52+
ROW_CLASSES: Final[CssClass] = "w-full"
53+
LABEL_SMALL_CLASSES: Final[CssClass] = "fit-text-small text-center select-none"
54+
LABEL_CLASSES: Final[CssClass] = "fit-text text-center select-none"

src/core/game_logic.py

Lines changed: 79 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import datetime
66
import logging
77
import random
8+
from typing import List, Optional, Set, cast
89

910
from nicegui import ui
1011

@@ -18,27 +19,46 @@
1819
TILE_UNCLICKED_BG_COLOR,
1920
TILE_UNCLICKED_TEXT_COLOR,
2021
)
22+
from src.types.ui_types import (
23+
BingoPattern,
24+
BingoPatterns,
25+
BoardType,
26+
BoardViews,
27+
ClickedTiles,
28+
Coordinate,
29+
TileLabelInfo,
30+
TileButtonsDict,
31+
)
2132
from src.utils.text_processing import get_line_style_for_lines, split_phrase_into_lines
2233

2334
# Global variables for game state
24-
board = [] # 2D array of phrases
25-
clicked_tiles = set() # Set of (row, col) tuples that are clicked
26-
bingo_patterns = set() # Set of winning patterns found
27-
board_iteration = 1
28-
is_game_closed = False
29-
today_seed = None
35+
board: BoardType = [] # 2D array of phrases
36+
clicked_tiles: ClickedTiles = set() # Set of (row, col) tuples that are clicked
37+
bingo_patterns: BingoPatterns = set() # Set of winning patterns found
38+
board_iteration: int = 1
39+
is_game_closed: bool = False
40+
today_seed: Optional[str] = None
3041

3142
# Global variables for UI references (initialized in the UI module)
32-
header_label = None
33-
controls_row = None
34-
seed_label = None
35-
board_views = {} # Dictionary mapping view name to (container, tile_buttons) tuple
43+
header_label: Optional[ui.label] = None
44+
controls_row: Optional[ui.row] = None
45+
seed_label: Optional[ui.label] = None
46+
board_views: BoardViews = (
47+
{}
48+
) # Dictionary mapping view name to (container, tile_buttons) tuple
3649

3750

38-
def generate_board(seed_val: int, phrases):
51+
def generate_board(seed_val: int, phrases: List[str]) -> BoardType:
3952
"""
4053
Generate a new board using the provided seed value.
4154
Also resets the clicked_tiles (ensuring the FREE SPACE is clicked) and sets the global today_seed.
55+
56+
Args:
57+
seed_val: Integer used to seed the random generator
58+
phrases: List of phrases to use in the board
59+
60+
Returns:
61+
The generated board as a 2D array of phrases
4262
"""
4363
global board, today_seed, clicked_tiles
4464

@@ -61,18 +81,22 @@ def generate_board(seed_val: int, phrases):
6181
return board
6282

6383

64-
def toggle_tile(row, col):
84+
def toggle_tile(row: int, col: int) -> None:
6585
"""
6686
Toggle the state of a tile (clicked/unclicked).
6787
Updates the UI and checks for winner.
88+
89+
Args:
90+
row: Row index of the tile to toggle
91+
col: Column index of the tile to toggle
6892
"""
6993
global clicked_tiles
7094

7195
# Don't allow toggling the free space
7296
if (row, col) == (2, 2):
7397
return
7498

75-
key = (row, col)
99+
key: Coordinate = (row, col)
76100
if key in clicked_tiles:
77101
clicked_tiles.remove(key)
78102
else:
@@ -90,18 +114,22 @@ def toggle_tile(row, col):
90114
new_card_style = f"background-color: {TILE_UNCLICKED_BG_COLOR}; color: {TILE_UNCLICKED_TEXT_COLOR}; border: none;"
91115
new_label_color = TILE_UNCLICKED_TEXT_COLOR
92116

93-
tile["card"].style(new_card_style)
117+
card = cast(ui.card, tile["card"])
118+
card.style(new_card_style)
119+
94120
lines = split_phrase_into_lines(phrase)
95121
line_count = len(lines)
96122
new_label_style = get_line_style_for_lines(line_count, new_label_color)
97123

98-
for label_info in tile["labels"]:
99-
lbl = label_info["ref"]
100-
lbl.classes(label_info["base_classes"])
124+
label_list = cast(List[TileLabelInfo], tile["labels"])
125+
for label_info in label_list:
126+
lbl = cast(ui.label, label_info["ref"])
127+
base_classes = cast(str, label_info["base_classes"])
128+
lbl.classes(base_classes)
101129
lbl.style(new_label_style)
102130
lbl.update()
103131

104-
tile["card"].update()
132+
card.update()
105133

106134
container.update()
107135

@@ -119,12 +147,12 @@ def toggle_tile(row, col):
119147
logging.debug(f"JavaScript execution failed: {e}")
120148

121149

122-
def check_winner():
150+
def check_winner() -> None:
123151
"""
124152
Check for Bingo win condition and update the UI accordingly.
125153
"""
126154
global bingo_patterns
127-
new_patterns = []
155+
new_patterns: List[BingoPattern] = []
128156

129157
# Check rows and columns.
130158
for i in range(5):
@@ -158,7 +186,9 @@ def check_winner():
158186
new_patterns.append("four_corners")
159187

160188
# Plus shape: complete center row and center column.
161-
plus_cells = {(2, c) for c in range(5)} | {(r, 2) for r in range(5)}
189+
plus_cells: Set[Coordinate] = {(2, c) for c in range(5)} | {
190+
(r, 2) for r in range(5)
191+
}
162192
if all(cell in clicked_tiles for cell in plus_cells):
163193
if "plus" not in bingo_patterns:
164194
new_patterns.append("plus")
@@ -171,7 +201,7 @@ def check_winner():
171201
new_patterns.append("x_shape")
172202

173203
# Outside edges (perimeter): all border cells clicked.
174-
perimeter_cells = (
204+
perimeter_cells: Set[Coordinate] = (
175205
{(0, c) for c in range(5)}
176206
| {(4, c) for c in range(5)}
177207
| {(r, 0) for r in range(5)}
@@ -183,15 +213,24 @@ def check_winner():
183213

184214
if new_patterns:
185215
# Separate new win patterns into standard and special ones.
186-
special_set = {"blackout", "four_corners", "plus", "x_shape", "perimeter"}
187-
standard_new = [p for p in new_patterns if p not in special_set]
188-
special_new = [p for p in new_patterns if p in special_set]
216+
special_set: Set[str] = {
217+
"blackout",
218+
"four_corners",
219+
"plus",
220+
"x_shape",
221+
"perimeter",
222+
}
223+
standard_new: List[BingoPattern] = [
224+
p for p in new_patterns if p not in special_set
225+
]
226+
special_new: List[BingoPattern] = [p for p in new_patterns if p in special_set]
189227

190228
# Process standard win conditions (rows, columns, diagonals).
191229
if standard_new:
192230
for pattern in standard_new:
193231
bingo_patterns.add(pattern)
194-
standard_total = sum(1 for p in bingo_patterns if p not in special_set)
232+
standard_total: int = sum(1 for p in bingo_patterns if p not in special_set)
233+
message: str
195234
if standard_total == 1:
196235
message = "BINGO!"
197236
elif standard_total == 2:
@@ -210,11 +249,11 @@ def check_winner():
210249
for sp in special_new:
211250
bingo_patterns.add(sp)
212251
# Format the name to title-case and append "Bingo!"
213-
sp_message = sp.replace("_", " ").title() + " Bingo!"
252+
sp_message: str = sp.replace("_", " ").title() + " Bingo!"
214253
ui.notify(sp_message, color="blue", duration=5)
215254

216255

217-
def reset_board():
256+
def reset_board() -> None:
218257
"""
219258
Reset the board by clearing all clicked states, clearing winning patterns,
220259
and re-adding the FREE SPACE.
@@ -228,9 +267,12 @@ def reset_board():
228267
clicked_tiles.add((r, c))
229268

230269

231-
def generate_new_board(phrases):
270+
def generate_new_board(phrases: List[str]) -> None:
232271
"""
233272
Generate a new board with an incremented iteration seed and update all board views.
273+
274+
Args:
275+
phrases: List of phrases to use for the board
234276
"""
235277
global board_iteration
236278
board_iteration += 1
@@ -246,14 +288,14 @@ def generate_new_board(phrases):
246288
container.update()
247289

248290
# Update the seed label if available
249-
if "seed_label" in globals() and seed_label:
291+
if seed_label is not None:
250292
seed_label.set_text(f"Seed: {today_seed}")
251293
seed_label.update()
252294

253295
reset_board()
254296

255297

256-
def close_game():
298+
def close_game() -> None:
257299
"""
258300
Close the game - show closed message instead of the board and update the header text.
259301
This function is called when the close button is clicked.
@@ -262,7 +304,7 @@ def close_game():
262304
is_game_closed = True
263305

264306
# Update header text on the current view
265-
if header_label:
307+
if header_label is not None:
266308
header_label.set_text(CLOSED_HEADER_TEXT)
267309
header_label.update()
268310

@@ -277,7 +319,7 @@ def close_game():
277319
container.update()
278320

279321
# Modify the controls row to only show the New Board button
280-
if controls_row:
322+
if controls_row is not None:
281323
controls_row.clear()
282324
with controls_row:
283325
with ui.button("", icon="autorenew", on_click=reopen_game).classes(
@@ -298,7 +340,7 @@ def close_game():
298340
ui.notify("Game has been closed", color="red", duration=3)
299341

300342

301-
def reopen_game():
343+
def reopen_game() -> None:
302344
"""
303345
Reopen the game after it has been closed.
304346
This regenerates a new board and resets the UI.
@@ -309,22 +351,22 @@ def reopen_game():
309351
is_game_closed = False
310352

311353
# Update header text back to original for the current view
312-
if header_label:
354+
if header_label is not None:
313355
header_label.set_text(HEADER_TEXT)
314356
header_label.update()
315357

316358
# Generate a new board
317359
from src.utils.file_operations import read_phrases_file
318360

319-
phrases = read_phrases_file()
361+
phrases: List[str] = read_phrases_file()
320362

321363
board_iteration += 1
322364
generate_board(board_iteration, phrases)
323365

324366
# Rebuild the controls row with all buttons
325367
from src.ui.controls import rebuild_controls_row
326368

327-
if controls_row:
369+
if controls_row is not None:
328370
rebuild_controls_row(controls_row)
329371

330372
# Recreate and show all board views

src/types/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""
2+
Type definitions for the Bingo application.
3+
"""

src/types/ui_types.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""
2+
UI Type definitions for the Bingo application.
3+
"""
4+
5+
from typing import Dict, List, Set, Tuple, Union
6+
7+
from nicegui import ui
8+
9+
# Basic types
10+
Coordinate = Tuple[int, int]
11+
BoardType = List[List[str]]
12+
ClickedTiles = Set[Coordinate]
13+
BingoPattern = str
14+
BingoPatterns = Set[BingoPattern]
15+
16+
# UI Element types
17+
TileLabelInfo = Dict[str, Union[ui.label, str]]
18+
TileInfo = Dict[str, Union[ui.card, List[TileLabelInfo]]]
19+
TileButtonsDict = Dict[Coordinate, TileInfo]
20+
BoardViewTuple = Tuple[ui.element, TileButtonsDict]
21+
BoardViews = Dict[str, BoardViewTuple]

0 commit comments

Comments
 (0)