4545# Keys can be "home" and "stream". Each value is a tuple: (container, tile_buttons).
4646board_views = {}
4747
48+ board_iteration = 1
49+
50+ def generate_board (seed_val : int ):
51+ """
52+ Generate a new board using the provided seed value.
53+ Also resets the clicked_tiles (ensuring the FREE SPACE is clicked) and sets the global today_seed.
54+ """
55+ global board , today_seed , clicked_tiles
56+ todays_seed = datetime .date .today ().strftime ("%Y%m%d" )
57+ random .seed (seed_val )
58+ shuffled_phrases = random .sample (phrases , 24 )
59+ shuffled_phrases .insert (12 , FREE_SPACE_TEXT )
60+ board = [shuffled_phrases [i :i + 5 ] for i in range (0 , 25 , 5 )]
61+ clicked_tiles .clear ()
62+ for r , row in enumerate (board ):
63+ for c , phrase in enumerate (row ):
64+ if phrase .upper () == FREE_SPACE_TEXT :
65+ clicked_tiles .add ((r , c ))
66+ today_seed = f"{ todays_seed } .{ seed_val } "
67+
4868def get_line_style_for_lines (line_count : int , default_text_color : str ) -> str :
4969 """
5070 Return a complete style string with an adjusted line-height based on the number of lines
@@ -92,20 +112,13 @@ def has_too_many_repeats(phrase, threshold=0.5):
92112
93113phrases = [p for p in unique_phrases if not has_too_many_repeats (p )]
94114
95- # Use today's date as the seed for deterministic shuffling
96- today_seed = datetime .date .today ().strftime ("%Y%m%d" )
97- random .seed (int (today_seed )) # Everyone gets the same shuffle per day
98-
99- # Shuffle and create the 5x5 board:
100- shuffled_phrases = random .sample (phrases , 24 ) # Random but fixed order per day
101- shuffled_phrases .insert (12 , FREE_SPACE_TEXT ) # Center slot
102- board = [shuffled_phrases [i :i + 5 ] for i in range (0 , 25 , 5 )]
103-
104115# Track clicked tiles and store chip references
105116clicked_tiles = set ()
106117tile_buttons = {} # {(row, col): chip}
107118tile_icons = {} # {(row, col): icon reference}
108- admin_checkboxes = {} # {(row, col): admin checkbox element}
119+
120+ # Initialize the board using the default iteration value.
121+ generate_board (board_iteration )
109122
110123def split_phrase_into_lines (phrase : str , forced_lines : int = None ) -> list :
111124 """
@@ -225,38 +238,9 @@ def check_winner():
225238 ui .notify ("BINGO!" , color = "green" , duration = 5 )
226239
227240def sync_board_state ():
228- update_tile_styles (tile_buttons )
229- sync_admin_checkboxes ()
230- update_admin_visibility ()
231-
232- def sync_admin_checkboxes ():
233- """
234- Sync the value in each admin panel checkbox with the global clicked_tiles.
235- """
236- for key , chks in admin_checkboxes .items ():
237- new_value = key in clicked_tiles
238- if "single" in chks and chks ["single" ].value != new_value :
239- chks ["single" ].value = new_value
240- chks ["single" ].update ()
241-
242- def update_admin_visibility ():
243- """
244- Bind the visibility of the admin checkboxes:
245- - left copy is visible only when unchecked (value False)
246- - right copy is visible only when checked (value True)
247- """
248- for key , chks in admin_checkboxes .items ():
249- val = chks ["single" ].value # both copies hold the same value
250- chks ["single" ].set_visibility (not val ) # show left box only when unchecked
251- chks ["single" ].update ()
252-
253- def admin_checkbox_change (e , key ):
254- # When a checkbox in the admin page is toggled, update the global clicked_tiles
255- if e .value :
256- clicked_tiles .add (key )
257- else :
258- clicked_tiles .discard (key )
259- sync_board_state ()
241+ # Update tile styles in every board view (e.g., home and stream)
242+ for view_key , (container , tile_buttons_local ) in board_views .items ():
243+ update_tile_styles (tile_buttons_local )
260244
261245def create_board_view (background_color : str , is_global : bool ):
262246 """
@@ -265,25 +249,37 @@ def create_board_view(background_color: str, is_global: bool):
265249 otherwise it uses a local board (stream page).
266250 """
267251 setup_head (background_color )
268- # Create the board container.
269- container = ui .element ("div" ).classes ("flex justify-center items-center w-full" )
252+ # Create the board container. For the home view, assign an ID to capture it.
253+ if is_global :
254+ container = ui .element ("div" ).classes ("home-board-container flex justify-center items-center w-full" )
255+ ui .run_javascript ("document.querySelector('.home-board-container').id = 'board-container'" )
256+ else :
257+ container = ui .element ("div" ).classes ("stream-board-container flex justify-center items-center w-full" )
258+ ui .run_javascript ("document.querySelector('.stream-board-container').id = 'board-container-stream'" )
259+
270260 if is_global :
271- global home_board_container , tile_buttons
261+ global home_board_container , tile_buttons , seed_label
272262 home_board_container = container
273263 tile_buttons = {} # Start with an empty dictionary.
274264 build_board (container , tile_buttons , toggle_tile )
275265 board_views ["home" ] = (container , tile_buttons )
276266 # Add timers for synchronizing the global board.
277267 ui .timer (0.1 , sync_board_state )
278268 ui .timer (1 , check_phrases_file_change )
269+ global seed_label
270+ with ui .row ().classes ("w-full mt-4 items-center justify-center gap-4" ):
271+ with ui .button ("" , icon = "refresh" , on_click = reset_board ).classes ("rounded-full w-12 h-12" ) as reset_btn :
272+ ui .tooltip ("Reset Board" )
273+ with ui .button ("" , icon = "autorenew" , on_click = generate_new_board ).classes ("rounded-full w-12 h-12" ) as new_board_btn :
274+ ui .tooltip ("New Board" )
275+ seed_label = ui .label (f"Seed: { today_seed } " ).classes ("text-sm text-center" ).style (
276+ f"font-family: '{ BOARD_TILE_FONT } ', sans-serif; color: { TILE_UNCLICKED_BG_COLOR } ;"
277+ )
279278 else :
280279 local_tile_buttons = {}
281280 build_board (container , local_tile_buttons , toggle_tile )
282281 board_views ["stream" ] = (container , local_tile_buttons )
283282 ui .timer (0.1 , lambda : update_tile_styles (local_tile_buttons ))
284- # Display the seed beneath the board.
285- with ui .element ("div" ).classes ("w-full mt-4" ):
286- ui .label (f"Seed: { today_seed } " ).classes ("text-md text-center" ).style (f"color: { TILE_UNCLICKED_BG_COLOR } ;" )
287283
288284@ui .page ("/" )
289285def home_page ():
@@ -293,44 +289,6 @@ def home_page():
293289def stream_page ():
294290 create_board_view (STREAM_BG_COLOR , False )
295291
296- @ui .page ("/admin" )
297- def admin_page ():
298- def reset_board ():
299- clicked_tiles .clear ()
300- # Re-add FREE SPACE at the center (position (2,2))
301- clicked_tiles .add ((2 , 2 ))
302- sync_board_state ()
303- build_admin_panel () # rebuild panel to reflect state changes
304-
305- with ui .row ().classes ("zd max-w-xl mx-auto p-4" ) as container :
306- ui .label ("Admin Panel" ).classes ("text-h4 text-center" )
307- ui .button ("Reset Board" , on_click = reset_board )
308-
309- def build_admin_panel ():
310- panel .clear () # clear previous panel content
311- with panel :
312- with ui .column ():
313- # Single column design: list each tile with a toggle checkbox.
314- for r in range (5 ):
315- for c in range (5 ):
316- key = (r , c )
317- phrase = board [r ][c ]
318- with ui .row ().classes ("items-center" ):
319- ui .label (f"{ phrase } ({ r } ,{ c } )" ).classes ("w-3/4" )
320- def on_checkbox_change (e , key = key ):
321- if e .value :
322- clicked_tiles .add (key )
323- else :
324- clicked_tiles .discard (key )
325- sync_board_state ()
326- cb = ui .checkbox ("" , value = (key in clicked_tiles ), on_change = on_checkbox_change )
327- # Save a single reference to this admin checkbox
328- admin_checkboxes [key ] = {"single" : cb }
329-
330- panel = ui .column () # container for the admin panel
331- build_admin_panel ()
332- ui .timer (0.1 , sync_admin_checkboxes )
333-
334292def setup_head (background_color : str ):
335293 """
336294 Set up common head elements: fonts, fitty JS, and background color.
@@ -345,6 +303,38 @@ def setup_head(background_color: str):
345303 ui .add_head_html (get_google_font_css (BOARD_TILE_FONT , BOARD_TILE_FONT_WEIGHT , BOARD_TILE_FONT_STYLE , "board_tile" ))
346304
347305 ui .add_head_html ('<script src="https://cdn.jsdelivr.net/npm/fitty@2.3.6/dist/fitty.min.js"></script>' )
306+ # Add html2canvas library and capture function.
307+ ui .add_head_html ("""
308+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
309+ <script>
310+ function captureBoardAndDownload(seed) {
311+ var boardElem = document.getElementById('board-container');
312+ if (!boardElem) {
313+ alert("Board container not found!");
314+ return;
315+ }
316+ // Run fitty to ensure text is resized and centered
317+ fitty('.fit-text', { multiLine: true, minSize: 10, maxSize: 1000 });
318+ fitty('.fit-text-small', { multiLine: true, minSize: 10, maxSize: 72 });
319+
320+ // Wait a short period to ensure that the board is fully rendered and styles have settled.
321+ setTimeout(function() {
322+ html2canvas(boardElem, {
323+ useCORS: true,
324+ scale: 10, // Increase scale for higher resolution
325+ logging: true,
326+ backgroundColor: null
327+ }).then(function(canvas) {
328+ var link = document.createElement('a');
329+ link.download = `bingo_board_${seed}.png`; // Include seed in filename
330+ link.href = canvas.toDataURL('image/png');
331+ link.click();
332+ });
333+ }, 500); // Adjust delay if necessary
334+ }
335+ </script>
336+ """ )
337+
348338 ui .add_head_html (f'<style>body {{ background-color: { background_color } ; }}</style>' )
349339
350340 ui .add_head_html ("""<script>
@@ -423,7 +413,7 @@ def update_tile_styles(tile_buttons_dict: dict):
423413 phrase = board [r ][c ]
424414
425415 if (r , c ) in clicked_tiles :
426- new_card_style = f"background-color: { TILE_CLICKED_BG_COLOR } ; color: { TILE_CLICKED_TEXT_COLOR } ; border: none ;"
416+ new_card_style = f"background-color: { TILE_CLICKED_BG_COLOR } ; color: { TILE_CLICKED_TEXT_COLOR } ; border: 8px solid { TILE_UNCLICKED_BG_COLOR } ;"
427417 new_label_color = TILE_CLICKED_TEXT_COLOR
428418 else :
429419 new_card_style = f"background-color: { TILE_UNCLICKED_BG_COLOR } ; color: { TILE_UNCLICKED_TEXT_COLOR } ; border: none;"
@@ -497,9 +487,7 @@ def has_too_many_repeats(phrase, threshold=0.5):
497487
498488 phrases = [p for p in unique_phrases if not has_too_many_repeats (p )]
499489 # Rebuild board data: re-shuffle and re-create board structure.
500- shuffled_phrases = random .sample (phrases , 24 )
501- shuffled_phrases .insert (12 , FREE_SPACE_TEXT )
502- board = [shuffled_phrases [i :i + 5 ] for i in range (0 , 25 , 5 )]
490+ generate_board (board_iteration )
503491 # Update all board views (both home and stream)
504492 for view , (container , tile_buttons_local ) in board_views .items ():
505493 container .clear ()
@@ -511,5 +499,35 @@ def has_too_many_repeats(phrase, threshold=0.5):
511499 "fitty('.fit-text-small', { multiLine: true, minSize: 10, maxSize: 72 });"
512500 )
513501
502+ def reset_board ():
503+ """
504+ Reset the board by clearing all clicked states and re-adding the FREE SPACE.
505+ """
506+ clicked_tiles .clear ()
507+ for r , row in enumerate (board ):
508+ for c , phrase in enumerate (row ):
509+ if phrase .upper () == FREE_SPACE_TEXT :
510+ clicked_tiles .add ((r , c ))
511+ sync_board_state ()
512+
513+ def generate_new_board ():
514+ """
515+ Generate a new board with an incremented iteration seed and update all board views.
516+ """
517+ global board_iteration
518+ board_iteration += 1
519+ generate_board (board_iteration )
520+ # Update all board views (both home and stream)
521+ for view_key , (container , tile_buttons_local ) in board_views .items ():
522+ container .clear ()
523+ tile_buttons_local .clear ()
524+ build_board (container , tile_buttons_local , toggle_tile )
525+ container .update ()
526+ # Update the seed label if available
527+ if 'seed_label' in globals ():
528+ seed_label .set_text (f"Seed: { today_seed } " )
529+ seed_label .update ()
530+ reset_board ()
531+
514532# Run the NiceGUI app
515533ui .run (port = 8080 , title = "Commit Bingo" , dark = False )
0 commit comments