1+ import unittest
2+ import sys
3+ import os
4+ import random
5+ from unittest .mock import patch , MagicMock
6+
7+ # Add the parent directory to sys.path to import from main.py
8+ sys .path .insert (0 , os .path .abspath (os .path .join (os .path .dirname (__file__ ), '..' )))
9+
10+ # We need to mock the NiceGUI imports and other dependencies before importing main
11+ sys .modules ['nicegui' ] = MagicMock ()
12+ sys .modules ['nicegui.ui' ] = MagicMock ()
13+ sys .modules ['fastapi.staticfiles' ] = MagicMock ()
14+
15+ # Now import functions from the main module
16+ from main import generate_board , has_too_many_repeats , check_winner , split_phrase_into_lines
17+
18+ class TestGameLogic (unittest .TestCase ):
19+ def setUp (self ):
20+ # Setup common test data
21+ # Mock the global variables used in main.py
22+ self .patches = [
23+ patch ('main.board' , []),
24+ patch ('main.today_seed' , '' ),
25+ patch ('main.clicked_tiles' , set ()),
26+ patch ('main.phrases' , [f"PHRASE{ i } " for i in range (1 , 30 )]),
27+ patch ('main.FREE_SPACE_TEXT' , 'FREE SPACE' ),
28+ patch ('main.bingo_patterns' , set ())
29+ ]
30+
31+ for p in self .patches :
32+ p .start ()
33+
34+ def tearDown (self ):
35+ # Clean up patches
36+ for p in self .patches :
37+ p .stop ()
38+
39+ def test_generate_board (self ):
40+ """Test that generate_board creates a 5x5 board with the FREE_SPACE in the middle"""
41+ import main
42+
43+ # Generate a board with a known seed
44+ generate_board (42 )
45+
46+ # Check if board is created with 5 rows
47+ self .assertEqual (len (main .board ), 5 )
48+
49+ # Check if each row has 5 columns
50+ for row in main .board :
51+ self .assertEqual (len (row ), 5 )
52+
53+ # Check if FREE_SPACE is in the middle (2,2)
54+ self .assertEqual (main .board [2 ][2 ], 'FREE SPACE' )
55+
56+ # Check if the clicked_tiles set has (2,2) for FREE_SPACE
57+ self .assertIn ((2 , 2 ), main .clicked_tiles )
58+
59+ # Check if the seed is set correctly
60+ expected_seed = f"{ main .datetime .date .today ().strftime ('%Y%m%d' )} .42"
61+ self .assertEqual (main .today_seed , expected_seed )
62+
63+ def test_has_too_many_repeats (self ):
64+ """Test the function for detecting phrases with too many repeated words"""
65+ # Test with a phrase having no repeats
66+ self .assertFalse (has_too_many_repeats ("ONE TWO THREE FOUR" ))
67+
68+ # Test with a phrase having some repeats but below threshold
69+ self .assertFalse (has_too_many_repeats ("ONE TWO ONE THREE FOUR" ))
70+
71+ # Test with a phrase having too many repeats (above default 0.5 threshold)
72+ self .assertTrue (has_too_many_repeats ("ONE ONE ONE ONE TWO" ))
73+
74+ # Test with a custom threshold
75+ self .assertFalse (has_too_many_repeats ("ONE ONE TWO THREE" , threshold = 0.3 ))
76+ self .assertTrue (has_too_many_repeats ("ONE ONE TWO THREE" , threshold = 0.8 ))
77+
78+ # Test with an empty phrase
79+ self .assertFalse (has_too_many_repeats ("" ))
80+
81+ def test_check_winner_row (self ):
82+ """Test detecting a win with a complete row"""
83+ import main
84+
85+ # Setup a board with no wins initially
86+ main .bingo_patterns = set ()
87+ main .clicked_tiles = {(0 , 0 ), (0 , 1 ), (0 , 2 ), (0 , 3 ), (0 , 4 )}
88+
89+ # Mock the ui.notify call
90+ with patch ('main.ui.notify' ) as mock_notify :
91+ check_winner ()
92+
93+ # Check if the bingo pattern was added
94+ self .assertIn ("row0" , main .bingo_patterns )
95+
96+ # Check if the notification was shown
97+ mock_notify .assert_called_once ()
98+ self .assertEqual (mock_notify .call_args [0 ][0 ], "BINGO!" )
99+
100+ def test_check_winner_column (self ):
101+ """Test detecting a win with a complete column"""
102+ import main
103+
104+ # Setup a board with no wins initially
105+ main .bingo_patterns = set ()
106+ main .clicked_tiles = {(0 , 0 ), (1 , 0 ), (2 , 0 ), (3 , 0 ), (4 , 0 )}
107+
108+ # Mock the ui.notify call
109+ with patch ('main.ui.notify' ) as mock_notify :
110+ check_winner ()
111+
112+ # Check if the bingo pattern was added
113+ self .assertIn ("col0" , main .bingo_patterns )
114+
115+ # Check if the notification was shown
116+ mock_notify .assert_called_once ()
117+ self .assertEqual (mock_notify .call_args [0 ][0 ], "BINGO!" )
118+
119+ def test_check_winner_diagonal (self ):
120+ """Test detecting a win with a diagonal"""
121+ import main
122+
123+ # Setup a board with no wins initially
124+ main .bingo_patterns = set ()
125+ main .clicked_tiles = {(0 , 0 ), (1 , 1 ), (2 , 2 ), (3 , 3 ), (4 , 4 )}
126+
127+ # Mock the ui.notify call
128+ with patch ('main.ui.notify' ) as mock_notify :
129+ check_winner ()
130+
131+ # Check if the bingo pattern was added
132+ self .assertIn ("diag_main" , main .bingo_patterns )
133+
134+ # Check if the notification was shown
135+ mock_notify .assert_called_once ()
136+ self .assertEqual (mock_notify .call_args [0 ][0 ], "BINGO!" )
137+
138+ def test_check_winner_anti_diagonal (self ):
139+ """Test detecting a win with an anti-diagonal"""
140+ import main
141+
142+ # Setup a board with no wins initially
143+ main .bingo_patterns = set ()
144+ main .clicked_tiles = {(0 , 4 ), (1 , 3 ), (2 , 2 ), (3 , 1 ), (4 , 0 )}
145+
146+ # Mock the ui.notify call
147+ with patch ('main.ui.notify' ) as mock_notify :
148+ check_winner ()
149+
150+ # Check if the bingo pattern was added
151+ self .assertIn ("diag_anti" , main .bingo_patterns )
152+
153+ # Check if the notification was shown
154+ mock_notify .assert_called_once ()
155+ self .assertEqual (mock_notify .call_args [0 ][0 ], "BINGO!" )
156+
157+ def test_check_winner_special_patterns (self ):
158+ """Test detecting special win patterns like blackout, four corners, etc."""
159+ import main
160+
161+ # Test four corners pattern
162+ main .bingo_patterns = set ()
163+ main .clicked_tiles = {(0 , 0 ), (0 , 4 ), (4 , 0 ), (4 , 4 )}
164+
165+ with patch ('main.ui.notify' ) as mock_notify :
166+ check_winner ()
167+ self .assertIn ("four_corners" , main .bingo_patterns )
168+ mock_notify .assert_called_once ()
169+ self .assertEqual (mock_notify .call_args [0 ][0 ], "Four Corners Bingo!" )
170+
171+ # Test plus pattern
172+ main .bingo_patterns = set ()
173+ main .clicked_tiles = set ()
174+ for i in range (5 ):
175+ main .clicked_tiles .add ((2 , i )) # Middle row
176+ main .clicked_tiles .add ((i , 2 )) # Middle column
177+
178+ with patch ('main.ui.notify' ) as mock_notify :
179+ check_winner ()
180+ self .assertIn ("plus" , main .bingo_patterns )
181+ # The notify may be called multiple times as the clicks also trigger row/col wins
182+ self .assertIn (mock_notify .call_args_list [- 1 ][0 ][0 ], "Plus Bingo!" )
183+
184+ def test_check_winner_multiple_wins (self ):
185+ """Test detecting multiple win patterns in a single check"""
186+ import main
187+
188+ # Setup a board with two potential wins (a row and a column)
189+ main .bingo_patterns = set ()
190+ main .clicked_tiles = set ()
191+
192+ # Add a complete row and a complete column that intersect
193+ for i in range (5 ):
194+ main .clicked_tiles .add ((0 , i )) # First row
195+ main .clicked_tiles .add ((i , 0 )) # First column
196+
197+ # Mock the ui.notify call
198+ with patch ('main.ui.notify' ) as mock_notify :
199+ check_winner ()
200+
201+ # Check if both bingo patterns were added
202+ self .assertIn ("row0" , main .bingo_patterns )
203+ self .assertIn ("col0" , main .bingo_patterns )
204+
205+ # The function should call notify with "DOUBLE BINGO!"
206+ mock_notify .assert_called_once ()
207+ self .assertEqual (mock_notify .call_args [0 ][0 ], "DOUBLE BINGO!" )
208+
209+ def test_split_phrase_into_lines (self ):
210+ """Test splitting phrases into balanced lines"""
211+ # Test with a short phrase (3 words or fewer)
212+ self .assertEqual (split_phrase_into_lines ("ONE TWO THREE" ), ["ONE" , "TWO" , "THREE" ])
213+
214+ # Test with a longer phrase - the actual implementation may return different line counts
215+ # based on the word lengths and balancing algorithm
216+ result = split_phrase_into_lines ("ONE TWO THREE FOUR FIVE" )
217+ self .assertLessEqual (len (result ), 4 ) # Should not exceed 4 lines
218+
219+ # Test forcing a specific number of lines
220+ result = split_phrase_into_lines ("ONE TWO THREE FOUR FIVE SIX" , forced_lines = 3 )
221+ self .assertEqual (len (result ), 3 ) # Should be split into 3 lines
222+
223+ # Test very long phrase
224+ long_phrase = "ONE TWO THREE FOUR FIVE SIX SEVEN EIGHT NINE TEN"
225+ result = split_phrase_into_lines (long_phrase )
226+ self .assertLessEqual (len (result ), 4 ) # Should never return more than 4 lines
227+
228+
229+ if __name__ == '__main__' :
230+ unittest .main ()
0 commit comments