1- from typing import Any , List , Tuple
1+ from typing import Dict
2+
3+ from typing_extensions import override
24
3- from zulip_bots .game_handler import GameInstance
45from zulip_bots .test_lib import BotTestCase , DefaultTests
56
67from .libraries .constants import EMPTY_BOARD
910class TestMerelsBot (BotTestCase , DefaultTests ):
1011 bot_name = "merels"
1112
12- def test_no_command (self ):
13+ def test_no_command (self ) -> None :
14+ # Out-of-game message for arbitrary input.
1315 message = dict (
1416 content = "magic" , type = "stream" , sender_email = "boo@email.com" , sender_full_name = "boo"
1517 )
@@ -18,76 +20,178 @@ def test_no_command(self):
1820 res ["content" ], "You are not in a game at the moment. Type `help` for help."
1921 )
2022
21- # FIXME: Add tests for computer moves
22- # FIXME: Add test lib for game_handler
23+ def test_parse_board_identity_empty_board (self ) -> None :
24+ # Merels parse_board is identity; verify with the canonical empty board.
25+ bot , _ = self ._get_handlers ()
26+ self .assertEqual (bot .game_message_handler .parse_board (EMPTY_BOARD ), EMPTY_BOARD )
2327
24- # Test for unchanging aspects within the game
25- # Player Color, Start Message, Moving Message
26- def test_static_responses (self ) -> None :
27- model , message_handler = self ._get_game_handlers ()
28- self .assertNotEqual (message_handler .get_player_color (0 ), None )
29- self .assertNotEqual (message_handler .game_start_message (), None )
30- self .assertEqual (
31- message_handler .alert_move_message ("foo" , "moved right" ), "foo :moved right"
28+
29+ class GameAdapterTestLib :
30+ """Small helpers for driving GameAdapter-based bots in tests."""
31+
32+ def send (
33+ self ,
34+ bot ,
35+ bot_handler ,
36+ content : str ,
37+ * ,
38+ user : str = "foo@example.com" ,
39+ user_name : str = "foo" ,
40+ ) -> None :
41+ bot .handle_message (
42+ self .make_request_message (content , user = user , user_name = user_name ),
43+ bot_handler ,
3244 )
3345
34- # Test to see if the attributes exist
35- def test_has_attributes (self ) -> None :
36- model , message_handler = self ._get_game_handlers ()
37- # Attributes from the Merels Handler
38- self .assertTrue (hasattr (message_handler , "parse_board" ) is not None )
39- self .assertTrue (hasattr (message_handler , "get_player_color" ) is not None )
40- self .assertTrue (hasattr (message_handler , "alert_move_message" ) is not None )
41- self .assertTrue (hasattr (message_handler , "game_start_message" ) is not None )
42- self .assertTrue (hasattr (message_handler , "alert_move_message" ) is not None )
43- # Attributes from the Merels Model
44- self .assertTrue (hasattr (model , "determine_game_over" ) is not None )
45- self .assertTrue (hasattr (model , "contains_winning_move" ) is not None )
46- self .assertTrue (hasattr (model , "make_move" ) is not None )
47-
48- def test_parse_board (self ) -> None :
49- board = EMPTY_BOARD
50- expect_response = EMPTY_BOARD
51- self ._test_parse_board (board , expect_response )
52-
53- def test_add_user_to_cache (self ):
54- self .add_user_to_cache ("Name" )
55-
56- def test_setup_game (self ):
57- self .setup_game ()
58-
59- def add_user_to_cache (self , name : str , bot : Any = None ) -> Any :
60- if bot is None :
61- bot , bot_handler = self ._get_handlers ()
62- message = {
63- "sender_email" : f"{ name } @example.com" ,
64- "sender_full_name" : f"{ name } " ,
46+ def replies (self , bot_handler ):
47+ # Return the bot message 'content' fields from the transcript.
48+ return [m ["content" ] for (_method , m ) in bot_handler .transcript ]
49+
50+ def send_and_collect (
51+ self ,
52+ bot ,
53+ bot_handler ,
54+ content : str ,
55+ * ,
56+ user : str = "foo@example.com" ,
57+ user_name : str = "foo" ,
58+ ):
59+ bot_handler .reset_transcript ()
60+ self .send (bot , bot_handler , content , user = user , user_name = user_name )
61+ return self .replies (bot_handler )
62+
63+
64+ # Note: Merels has no vs-computer mode (in merels.py, supports_computer=False).
65+ # If computer mode is added in the future, add adapter-level tests here.
66+
67+
68+ class TestMerelsAdapter (BotTestCase , DefaultTests , GameAdapterTestLib ):
69+ """Adapter-focused tests (mirrors connect_four); use stable fragment assertions."""
70+
71+ bot_name = "merels"
72+
73+ @override
74+ def make_request_message (
75+ self , content : str , user : str = "foo@example.com" , user_name : str = "foo"
76+ ) -> Dict [str , str ]:
77+ # Provide stream metadata consumed by GameAdapter.
78+ return {
79+ "sender_email" : user ,
80+ "sender_full_name" : user_name ,
81+ "content" : content ,
82+ "type" : "stream" ,
83+ "display_recipient" : "general" ,
84+ "subject" : "merels-test-topic" ,
6585 }
66- bot .add_user_to_cache (message )
67- return bot
68-
69- def setup_game (self ) -> None :
70- bot = self .add_user_to_cache ("foo" )
71- self .add_user_to_cache ("baz" , bot )
72- instance = GameInstance (
73- bot , False , "test game" , "abc123" , ["foo@example.com" , "baz@example.com" ], "test"
86+
87+ def test_help_is_merels_help (self ) -> None :
88+ bot , bot_handler = self ._get_handlers ()
89+
90+ bot_handler .reset_transcript ()
91+ bot .handle_message (self .make_request_message ("help" ), bot_handler )
92+
93+ responses = [m for (_method , m ) in bot_handler .transcript ]
94+ self .assertTrue (responses , "No bot response to 'help'" )
95+ help_text = responses [0 ]["content" ]
96+
97+ # Assert on stable fragments to avoid brittle exact matches.
98+ self .assertIn ("Merels Bot Help" , help_text )
99+ self .assertIn ("start game" , help_text )
100+ self .assertIn ("play game" , help_text )
101+ self .assertIn ("quit" , help_text )
102+ self .assertIn ("rules" , help_text )
103+ # Present today; OK if dropped in future wording changes.
104+ self .assertIn ("leaderboard" , help_text )
105+ self .assertIn ("cancel game" , help_text )
106+
107+ def test_start_game_emits_invite (self ) -> None :
108+ bot , bot_handler = self ._get_handlers ()
109+ bot_handler .reset_transcript ()
110+
111+ bot .handle_message (
112+ self .make_request_message ("start game" , user = "foo@example.com" , user_name = "foo" ),
113+ bot_handler ,
74114 )
75- bot .instances .update ({"abc123" : instance })
76- instance .start ()
77- return bot
78115
79- def _get_game_handlers (self ) -> Tuple [Any , Any ]:
116+ contents = [m ["content" ] for (_method , m ) in bot_handler .transcript ]
117+ self .assertTrue (contents , "No bot reply recorded for 'start game'" )
118+ first = contents [0 ]
119+ self .assertIn ("wants to play" , first )
120+ self .assertIn ("Merels" , first )
121+ self .assertIn ("join" , first )
122+
123+ def test_join_starts_game_emits_start_message (self ) -> None :
80124 bot , bot_handler = self ._get_handlers ()
81- return bot . model , bot .game_message_handler
125+ expected_fragment = bot .game_message_handler . game_start_message ()
82126
83- def _test_parse_board (self , board : str , expected_response : str ) -> None :
84- model , message_handler = self ._get_game_handlers ()
85- response = message_handler .parse_board (board )
86- self .assertEqual (response , expected_response )
127+ bot_handler .reset_transcript ()
128+ bot .handle_message (
129+ self .make_request_message ("start game" , "foo@example.com" , "foo" ), bot_handler
130+ )
131+ bot .handle_message (self .make_request_message ("join" , "bar@example.com" , "bar" ), bot_handler )
87132
88- def _test_determine_game_over (
89- self , board : List [List [int ]], players : List [str ], expected_response : str
90- ) -> None :
91- model , message_handler = self ._get_game_handlers ()
92- response = model .determine_game_over (players )
93- self .assertEqual (response , expected_response )
133+ contents = [m ["content" ] for (_method , m ) in bot_handler .transcript ]
134+ self .assertTrue (
135+ any (expected_fragment in c for c in contents ),
136+ "Merels start message not found after 'join'" ,
137+ )
138+
139+ def test_message_handler_helpers (self ) -> None :
140+ bot , _ = self ._get_handlers ()
141+
142+ # Identity parse_board.
143+ self .assertEqual (
144+ bot .game_message_handler .parse_board ("sample_board_repr" ), "sample_board_repr"
145+ )
146+
147+ # Token color in allowed set.
148+ self .assertIn (
149+ bot .game_message_handler .get_player_color (0 ),
150+ (":o_button:" , ":cross_mark_button:" ),
151+ )
152+ self .assertIn (
153+ bot .game_message_handler .get_player_color (1 ),
154+ (":o_button:" , ":cross_mark_button:" ),
155+ )
156+
157+ # Basic move alert format.
158+ self .assertEqual (
159+ bot .game_message_handler .alert_move_message ("foo" , "move 1,1" ),
160+ "foo :move 1,1" ,
161+ )
162+
163+ def test_move_after_join_invokes_make_move_and_replies (self ) -> None :
164+ """
165+ After start/join, Merels begins in placement (Phase 1). Use 'put v,h'
166+ and assert the adapter emits an acknowledgement. Try both players to
167+ avoid assuming turn order.
168+ """
169+ bot , bot_handler = self ._get_handlers ()
170+
171+ # Start 2P game.
172+ _ = self .send_and_collect (
173+ bot , bot_handler , "start game" , user = "foo@example.com" , user_name = "foo"
174+ )
175+ _ = self .send_and_collect (bot , bot_handler , "join" , user = "bar@example.com" , user_name = "bar" )
176+
177+ # Stable oracles from the handler's formatter.
178+ ack_foo = bot .game_message_handler .alert_move_message ("foo" , "put 1,1" )
179+ ack_bar = bot .game_message_handler .alert_move_message ("bar" , "put 1,1" )
180+
181+ # Try current player first (unknown), then the other.
182+ contents_foo = self .send_and_collect (
183+ bot , bot_handler , "put 1,1" , user = "foo@example.com" , user_name = "foo"
184+ )
185+ joined = " " .join (contents_foo )
186+
187+ if (ack_foo not in joined ) and (ack_bar not in joined ) and (":put 1,1" not in joined ):
188+ contents_bar = self .send_and_collect (
189+ bot , bot_handler , "put 1,1" , user = "bar@example.com" , user_name = "bar"
190+ )
191+ joined += " " + " " .join (contents_bar )
192+
193+ # Assert the adapter produced a placement acknowledgement.
194+ self .assertTrue (
195+ any (h in joined for h in (":put 1,1" , ack_foo , ack_bar )),
196+ f"No placement acknowledgement found in: { joined } " ,
197+ )
0 commit comments