77# Set up logging
88logging .basicConfig (level = logging .DEBUG , format = '%(asctime)s - %(levelname)s - %(message)s' )
99
10+ # --- New: Color constants and font ---
11+ TILE_CLICKED_BG_COLOR = "#3b82f6" # Blue background for clicked tiles
12+ TILE_CLICKED_TEXT_COLOR = "white"
13+ TILE_UNCLICKED_BG_COLOR = "#facc15" # Yellow background for unclicked tiles
14+ TILE_UNCLICKED_TEXT_COLOR = "black"
15+ FREE_MEAT_TEXT_COLOR = "#FF7f33" # Color for the FREE MEAT tile
16+
17+ HOME_BG_COLOR = "#100079" # Background for home page
18+ STREAM_BG_COLOR = "#00FF00" # Background for stream page
19+ HEADER_TEXT_COLOR = "#0CB2B3" # Color for header text
20+
21+ FONT_FAMILY = "'Super Carnival', sans-serif"
22+
23+ # New constants for line-height adjustments
24+ LINE_HEIGHT_SHORT = "1.5em"
25+ LINE_HEIGHT_DEFAULT = "1em"
26+
27+ def get_line_style_for_lines (line_count : int , default_text_color : str ) -> str :
28+ """
29+ Return a complete style string with an adjusted line-height based on the number of lines
30+ that resulted from splitting the phrase.
31+ Fewer lines (i.e. unsplit phrases) get a higher line-height, while more lines get a lower one.
32+ """
33+ if line_count == 1 :
34+ lh = "1.5em" # More spacing for a single line.
35+ elif line_count == 2 :
36+ lh = "1.2em" # Slightly reduced spacing for two lines.
37+ elif line_count == 3 :
38+ lh = "0.9em" # Even tighter spacing for three lines.
39+ else :
40+ lh = "0.9em" # For four or more lines.
41+ return f"font-family: { FONT_FAMILY } ; padding: 0; margin: 0; color: { default_text_color } ; line-height: { lh } ;"
42+
1043# Read phrases from a text file and convert them to uppercase.
1144with open ("phrases.txt" , "r" ) as f :
1245 phrases = [line .strip ().upper () for line in f if line .strip ()]
@@ -37,6 +70,8 @@ def split_phrase_into_lines(phrase: str) -> list:
3770 return [words [0 ]]
3871 elif len (words ) == 2 :
3972 return [words [0 ], words [1 ]]
73+ elif len (words ) == 3 :
74+ return [words [0 ], words [1 ], words [2 ]]
4075 else :
4176 mid = round (len (words ) / 2 )
4277 return [" " .join (words [:mid ]), " " .join (words [mid :])]
@@ -52,21 +87,22 @@ def create_bingo_board():
5287 for row_idx , row in enumerate (board ):
5388 for col_idx , phrase in enumerate (row ):
5489 # Create a clickable card for this cell with reduced padding and centered content. Added 'relative' class for icon overlay.
55- card = ui .card ().classes ("relative p-2 bg-yellow-500 hover:bg-yellow-400 rounded-lg w-full h-full flex items-center justify-center" ).style ("cursor: pointer;" )
90+ card = ui .card ().classes ("relative p-2 bg-yellow-500 rounded-lg w-full h-full flex items-center justify-center" ).style ("cursor: pointer;" )
5691 with card :
5792 with ui .column ().classes ("flex flex-col items-center justify-center gap-0 w-full" ):
5893 # Set text color: free meat gets #FF7f33, others black
59- default_text_color = "#FF7f33" if phrase .upper () == "FREE MEAT" else "black"
60- for line in split_phrase_into_lines (phrase ):
61- ui .label (line ).classes ("fit-text text-center select-none" ).style (f"font-family: 'Super Carnival', sans-serif; padding: 0; margin: 0; color: { default_text_color } ;" )
62- # After the column, add a hidden check icon overlay
63- icon = ui .icon ("check" ).classes ("absolute inset-0 m-auto text-3xl text-white" ).style ("display: none;" )
94+ default_text_color = FREE_MEAT_TEXT_COLOR if phrase .upper () == "FREE MEAT" else TILE_UNCLICKED_TEXT_COLOR
95+ lines = split_phrase_into_lines (phrase )
96+ line_count = len (lines )
97+ for line in lines :
98+ with ui .row ().classes ("w-full" ):
99+ ui .label (line ).classes ("fit-text text-center select-none" ).style (get_line_style_for_lines (line_count , default_text_color ))
100+
64101 tile_buttons [(row_idx , col_idx )] = card
65- tile_icons [( row_idx , col_idx )] = icon
102+
66103 if phrase .upper () == "FREE MEAT" :
67104 clicked_tiles .add ((row_idx , col_idx ))
68- card .style ("color: #FF7f33; background: #facc15; border: none;" )
69- icon .style ("display: block;" )
105+ card .style ("color: #FF7f33; border: none;" )
70106 else :
71107 card .on ("click" , lambda e , r = row_idx , c = col_idx : toggle_tile (r , c ))
72108
@@ -95,37 +131,19 @@ def check_winner():
95131 ui .notify ("BINGO!" , color = "green" , duration = 5 )
96132
97133def sync_board_state ():
98- # Sync the styles of each tile according to the global clicked_tiles
99- for r in range (5 ):
100- for c in range (5 ):
101- key = (r , c )
102- # Skip updating the FREE MEAT cell
103- if board [r ][c ].upper () == "FREE MEAT" :
104- continue
105- if key in clicked_tiles :
106- tile_buttons [key ].style ("background: #22c55e; color: white; border: none;" )
107- tile_icons [key ].style ("display: block;" )
108- else :
109- tile_buttons [key ].style ("background: #facc15; border: none; color: black;" )
110- tile_icons [key ].style ("display: none;" )
111- tile_buttons [key ].update ()
112- tile_icons [key ].update ()
113- # --- New: update admin panel checkboxes when board state syncs ---
134+ update_tile_styles (tile_buttons )
114135 sync_admin_checkboxes ()
115136 update_admin_visibility ()
116137
117138def sync_admin_checkboxes ():
118139 """
119- Sync the values in both copies of each admin checkbox with the global clicked_tiles.
140+ Sync the value in each admin panel checkbox with the global clicked_tiles.
120141 """
121142 for key , chks in admin_checkboxes .items ():
122143 new_value = key in clicked_tiles
123- if chks ["left" ].value != new_value :
124- chks ["left" ].value = new_value
125- chks ["left" ].update ()
126- if chks ["right" ].value != new_value :
127- chks ["right" ].value = new_value
128- chks ["right" ].update ()
144+ if "single" in chks and chks ["single" ].value != new_value :
145+ chks ["single" ].value = new_value
146+ chks ["single" ].update ()
129147
130148def update_admin_visibility ():
131149 """
@@ -134,11 +152,9 @@ def update_admin_visibility():
134152 - right copy is visible only when checked (value True)
135153 """
136154 for key , chks in admin_checkboxes .items ():
137- val = chks ["left" ].value # both copies hold the same value
138- chks ["left" ].set_visibility (not val ) # show left box only when unchecked
139- chks ["right" ].set_visibility (val ) # show right box only when checked
140- chks ["left" ].update ()
141- chks ["right" ].update ()
155+ val = chks ["single" ].value # both copies hold the same value
156+ chks ["single" ].set_visibility (not val ) # show left box only when unchecked
157+ chks ["single" ].update ()
142158
143159def admin_checkbox_change (e , key ):
144160 # When a checkbox in the admin page is toggled, update the global clicked_tiles
@@ -148,33 +164,19 @@ def admin_checkbox_change(e, key):
148164 clicked_tiles .discard (key )
149165 sync_board_state ()
150166
151- # Set up NiceGUI page and head elements
152- ui .page ("/" )
153- ui .add_head_html ('<link href="https://fonts.cdnfonts.com/css/super-carnival" rel="stylesheet">' )
154- ui .add_head_html ('<script src="https://cdn.jsdelivr.net/npm/fitty@2.3.6/dist/fitty.min.js"></script>' )
155- ui .add_head_html ('<style>body { background-color: #100079; }</style>' )
156-
157- with ui .element ("div" ).classes ("w-full max-w-3xl mx-auto" ):
158- ui .label ("COMMIT BINGO!" ).classes ("fit-header text-center" ).style ("font-family: 'Super Carnival', sans-serif; color: #0CB2B3;" )
159-
160- create_bingo_board ()
167+ @ui .page ("/" )
168+ def home_page ():
169+ # Set up NiceGUI page and head elements
170+ setup_head (HOME_BG_COLOR )
161171
162- # Add a timer that calls sync_board_state every 1 second to push state updates to all clients
163- ui .timer ( 1 , sync_board_state )
172+ global tile_buttons
173+ tile_buttons = build_board ( ui .element ( "div" ). classes ( "flex justify-center items-center w-full" ), tile_buttons , toggle_tile )
164174
165- with ui . element ( "div" ). classes ( "w-full mt-4" ):
166- ui .label ( f"Seed: { today_seed } " ). classes ( "text-md text-gray-300 text-center" )
175+ # Add a timer that calls sync_board_state every 1 second to push state updates to all clients
176+ ui .timer ( 0.1 , sync_board_state )
167177
168- ui .add_head_html ("""<script>
169- document.addEventListener('DOMContentLoaded', () => {
170- fitty('.fit-text', { multiLine: true, maxSize: 100 });
171- fitty('.fit-header', { multiLine: true, maxSize: 200 });
172- });
173- window.addEventListener('resize', () => {
174- fitty('.fit-text', { multiLine: true, maxSize: 100 });
175- fitty('.fit-header', { multiLine: true, maxSize: 200 });
176- });
177- </script>""" )
178+ with ui .element ("div" ).classes ("w-full mt-4" ):
179+ ui .label (f"Seed: { today_seed } " ).classes ("text-md text-gray-300 text-center" )
178180
179181@ui .page ("/admin" )
180182def admin_page ():
@@ -183,55 +185,118 @@ def reset_board():
183185 # Re-add FREE MEAT at the center (position (2,2))
184186 clicked_tiles .add ((2 , 2 ))
185187 sync_board_state ()
186- build_admin_panel ()
188+ build_admin_panel () # rebuild panel to reflect state changes
189+
187190 with ui .row ().classes ("zd max-w-xl mx-auto p-4" ) as container :
188191 ui .label ("Admin Panel" ).classes ("text-h4 text-center" )
189192 ui .button ("Reset Board" , on_click = reset_board )
193+
190194 def build_admin_panel ():
191195 panel .clear () # clear previous panel content
192196 with panel :
193- with ui .row ():
194- with ui .column ():
195- ui .label ("Uncalled" ).classes ("text-h5 text-center" )
196- # Create left (uncalled) checkboxes inside this column.
197- for r in range (5 ):
198- for c in range (5 ):
199- key = (r , c )
200- phrase = board [r ][c ]
201- def on_admin_checkbox_change (e , key = key ):
202- if e .value :
203- clicked_tiles .add (key )
204- else :
205- clicked_tiles .discard (key )
206- sync_board_state ()
207- update_admin_visibility ()
208- build_admin_panel () # re-render admin panel after change
209- left_chk = ui .checkbox (phrase , value = (key in clicked_tiles ), on_change = on_admin_checkbox_change )
210- admin_checkboxes .setdefault (key , {})["left" ] = left_chk
211- with ui .column ():
212- ui .label ("Called" ).classes ("text-h5 text-center" )
213- # Create right (called) checkboxes inside this column.
214- for r in range (5 ):
215- for c in range (5 ):
216- key = (r , c )
217- phrase = board [r ][c ]
218- def on_admin_checkbox_change (e , key = key ):
197+ with ui .column ():
198+ # Single column design: list each tile with a toggle checkbox.
199+ for r in range (5 ):
200+ for c in range (5 ):
201+ key = (r , c )
202+ phrase = board [r ][c ]
203+ with ui .row ().classes ("items-center" ):
204+ ui .label (f"{ phrase } ({ r } ,{ c } )" ).classes ("w-3/4" )
205+ def on_checkbox_change (e , key = key ):
219206 if e .value :
220207 clicked_tiles .add (key )
221208 else :
222209 clicked_tiles .discard (key )
223210 sync_board_state ()
224- update_admin_visibility ()
225- build_admin_panel () # re-render admin panel after change
226- right_chk = ui .checkbox (phrase , value = (key in clicked_tiles ), on_change = on_admin_checkbox_change )
227- admin_checkboxes .setdefault (key , {})["right" ] = right_chk
211+ cb = ui .checkbox ("" , value = (key in clicked_tiles ), on_change = on_checkbox_change )
212+ # Save a single reference to this admin checkbox
213+ admin_checkboxes [key ] = {"single" : cb }
228214
229- panel = ui .row () # container for the checkboxes row
215+ panel = ui .column () # container for the admin panel
216+ build_admin_panel ()
217+ ui .timer (0.1 , sync_admin_checkboxes )
230218
231-
219+ @ui .page ("/stream" )
220+ def stream_page ():
221+ # Set up NiceGUI page and head elements
222+ setup_head (STREAM_BG_COLOR )
232223
233-
234- build_admin_panel ()
235- ui .timer (1 , update_admin_visibility )
224+
225+
226+ # Build the board using the common function (use a local dictionary here)
227+ local_tile_buttons = build_board (ui .element ("div" ).classes ("flex justify-center items-center w-full" ), {}, toggle_tile )
228+
229+ # Timer to update ONLY the stream view's board (using its local_tile_buttons)
230+ ui .timer (0.1 , lambda : update_tile_styles (local_tile_buttons ))
231+
232+ with ui .element ("div" ).classes ("w-full mt-4" ):
233+ ui .label (f"Seed: { today_seed } " ).classes ("text-md text-gray-300 text-center" )
234+
235+ def setup_head (background_color : str ):
236+ """
237+ Set up common head elements: fonts, fitty JS, and background color.
238+ """
239+ ui .add_head_html (f'<link href="https://fonts.cdnfonts.com/css/super-carnival" rel="stylesheet">' )
240+ ui .add_head_html ('<script src="https://cdn.jsdelivr.net/npm/fitty@2.3.6/dist/fitty.min.js"></script>' )
241+ ui .add_head_html (f'<style>body {{ background-color: { background_color } ; }}</style>' )
242+ ui .add_head_html ("""<script>
243+ document.addEventListener('DOMContentLoaded', () => {
244+ fitty('.fit-text', { multiLine: true, minSize: 10, maxSize: 1000 });
245+ fitty('.fit-header', { multiLine: true, minSize: 10, maxSize: 2000 });
246+ });
247+ window.addEventListener('resize', () => {
248+ fitty('.fit-text', { multiLine: true, minSize: 10, maxSize: 1000 });
249+ fitty('.fit-header', { multiLine: true, minSize: 10, maxSize: 2000 });
250+ });
251+ </script>""" )
252+
253+ # Use full width with padding so the header spans edge-to-edge
254+ with ui .element ("div" ).classes ("w-full" ):
255+ ui .label ("COMMIT !BINGO" ).classes ("fit-header text-center" ).style (f"font-family: { FONT_FAMILY } ; color: { HEADER_TEXT_COLOR } ;" )
256+
257+ def build_board (parent , tile_buttons_dict : dict , on_tile_click ):
258+ """
259+ Build the common Bingo board in the given parent element.
260+ The resulting tile UI elements are added to tile_buttons_dict.
261+ """
262+ with parent :
263+ # Use full width and add padding so the board touches the edges with a gap
264+ with ui .element ("div" ).classes ("w-full aspect-square p-4" ):
265+ with ui .grid (columns = 5 ).classes ("gap-2 h-full grid-rows-5" ):
266+ for row_idx , row in enumerate (board ):
267+ for col_idx , phrase in enumerate (row ):
268+ card = ui .card ().classes (
269+ "relative p-2 bg-yellow-500 rounded-lg w-full h-full flex items-center justify-center"
270+ ).style ("cursor: pointer;" )
271+ with card :
272+ with ui .column ().classes ("flex flex-col items-center justify-center gap-0 w-full" ):
273+ default_text_color = FREE_MEAT_TEXT_COLOR if phrase .upper () == "FREE MEAT" else TILE_UNCLICKED_TEXT_COLOR
274+ lines = split_phrase_into_lines (phrase )
275+ line_count = len (lines )
276+ for line in lines :
277+ with ui .row ().classes ("w-full" ):
278+ ui .label (line ).classes ("fit-text text-center select-none" ).style (get_line_style_for_lines (line_count , default_text_color ))
279+ tile_buttons_dict [(row_idx , col_idx )] = card
280+ if phrase .upper () == "FREE MEAT" :
281+ clicked_tiles .add ((row_idx , col_idx ))
282+ card .style (f"color: { FREE_MEAT_TEXT_COLOR } ; border: none;" )
283+ else :
284+ card .on ("click" , lambda e , r = row_idx , c = col_idx : on_tile_click (r , c ))
285+ return tile_buttons_dict
286+
287+ def update_tile_styles (tile_buttons_dict : dict ):
288+ """
289+ Update styles for each tile in the given dictionary based on the global clicked_tiles.
290+ """
291+ for (r , c ), card in tile_buttons_dict .items ():
292+ if board [r ][c ].upper () == "FREE MEAT" :
293+ continue
294+ if (r , c ) in clicked_tiles :
295+ new_style = f"background-color: { TILE_CLICKED_BG_COLOR } ; color: { TILE_CLICKED_TEXT_COLOR } ; border: none;"
296+ else :
297+ new_style = f"background-color: { TILE_UNCLICKED_BG_COLOR } ; color: { TILE_UNCLICKED_TEXT_COLOR } ; border: none;"
298+ card .style (new_style )
299+ card .update ()
236300
301+ # Run the NiceGUI app
237302ui .run (port = 8080 , title = "Commit Bingo" , dark = False )
0 commit comments