• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

FlysonBot / Mastermind / 23087608066

14 Mar 2026 12:02PM UTC coverage: 86.288%. First build
23087608066

push

github

web-flow
feat: release v2.2.0

feat: release v2.2.0

    variable game parameters (c and d)
    assisted mode undo support and solution elimination %
    hide code input when player sets code
    Python test suite for all gamemodes
    CI coverage workflow for Python and Java
    fix non-standard Maven test directory name

465 of 494 new or added lines in 9 files covered. (94.13%)

623 of 722 relevant lines covered (86.29%)

0.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.46
/src/test/python/mastermind/test_gamemode_assisted.py
1
"""Tests for assisted.play() — Python-side logic only.
2

3
Mocks:
4
  - ask_game_settings               (setup prompt)
5
  - MastermindSession               (Java)
6
  - Prompt.ask / IntPrompt.ask      (user input)
7
  - parse_code                      (Java-backed)
8
  - display                         (Java-backed)
9
  - console.print / pause           (output side effects)
10

11
jpype.JException is replaced with a plain exception subclass so we can
12
raise it from mocks without a running JVM.
13
"""
14

15
from unittest.mock import MagicMock, patch
1✔
16

17
# ---------------------------------------------------------------------------
18
# Helpers
19
# ---------------------------------------------------------------------------
20

21

22
def _fb(black, white):
1✔
NEW
23
    return black * 10 + white
×
24

25

26
class _FakeJException(Exception):
1✔
27
    """Stand-in for jpype.JException in tests."""
28

29

30
def _make_session(suggestions, solution_sizes=None):
1✔
31
    """
32
    suggestions: list of ints returned by suggestGuess (in order).
33
    solution_sizes: list of ints returned by getSolutionSpaceSize (in order).
34
                    Defaults to [100, 50, 10, ...] (shrinking).
35
    """
36
    session = MagicMock()
1✔
37
    session.suggestGuess.side_effect = [int(s) for s in suggestions]
1✔
38
    if solution_sizes is None:
1✔
39
        solution_sizes = list(range(100, 0, -10))
1✔
40
    session.getSolutionSpaceSize.side_effect = solution_sizes
1✔
41
    return session
1✔
42

43

44
BASE_PATCHES = dict(
1✔
45
    ask_settings="mastermind.gamemode.assisted.ask_game_settings",
46
    SessionClass="mastermind.gamemode.assisted.MastermindSession",
47
    prompt="mastermind.gamemode.assisted.Prompt.ask",
48
    int_prompt="mastermind.gamemode.assisted.IntPrompt.ask",
49
    parse_code="mastermind.gamemode.assisted.parse_code",
50
    display="mastermind.gamemode.assisted.display",
51
    console="mastermind.gamemode.assisted.console",
52
    pause="mastermind.gamemode.assisted.pause",
53
    jpype_exc="mastermind.gamemode.assisted.jpype.JException",
54
)
55

56

57
# ---------------------------------------------------------------------------
58
# Tests
59
# ---------------------------------------------------------------------------
60

61

62
class TestAssistedPlay:
1✔
63
    def test_win_using_suggestion(self):
1✔
64
        """Accept the suggested guess and receive a perfect score → win."""
65
        with (
1✔
66
            patch(BASE_PATCHES["ask_settings"]) as ask_settings,
67
            patch(BASE_PATCHES["SessionClass"]) as SessionClass,
68
            patch(BASE_PATCHES["prompt"]) as prompt,
69
            patch(BASE_PATCHES["int_prompt"]),
70
            patch(BASE_PATCHES["parse_code"]),
71
            patch(BASE_PATCHES["display"], return_value="1234"),
72
            patch(BASE_PATCHES["console"]) as console,
73
            patch(BASE_PATCHES["pause"]) as pause,
74
            patch(BASE_PATCHES["jpype_exc"], _FakeJException),
75
        ):
76
            ask_settings.return_value = (6, 4, 10)
1✔
77
            SessionClass.return_value = _make_session([7])
1✔
78
            # Prompt returns suggestion string → accepted; then feedback = "4b0w"
79
            prompt.side_effect = ["1234", "4b0w"]
1✔
80

81
            from mastermind.gamemode.assisted import play
1✔
82

83
            play()
1✔
84

85
            printed = " ".join(str(c) for c in console.print.call_args_list)
1✔
86
            assert "Perfect" in printed or "✓" in printed
1✔
87
            pause.assert_called_once()
1✔
88

89
    def test_win_using_custom_guess(self):
1✔
90
        """Enter a different code (not suggestion) and win."""
91
        with (
1✔
92
            patch(BASE_PATCHES["ask_settings"]) as ask_settings,
93
            patch(BASE_PATCHES["SessionClass"]) as SessionClass,
94
            patch(BASE_PATCHES["prompt"]) as prompt,
95
            patch(BASE_PATCHES["int_prompt"]),
96
            patch(BASE_PATCHES["parse_code"]) as parse_code,
97
            patch(BASE_PATCHES["display"], return_value="1234"),
98
            patch(BASE_PATCHES["console"]) as console,
99
            patch(BASE_PATCHES["pause"]),
100
            patch(BASE_PATCHES["jpype_exc"], _FakeJException),
101
        ):
102
            ask_settings.return_value = (6, 4, 10)
1✔
103
            SessionClass.return_value = _make_session([7])
1✔
104
            # User enters "5678" (custom), then feedback = perfect
105
            prompt.side_effect = ["5678", "4b0w"]
1✔
106
            parse_code.return_value = 99  # valid custom guess
1✔
107

108
            from mastermind.gamemode.assisted import play
1✔
109

110
            play()
1✔
111

112
            printed = " ".join(str(c) for c in console.print.call_args_list)
1✔
113
            assert "Perfect" in printed or "✓" in printed
1✔
114

115
    def test_invalid_guess_retried(self):
1✔
116
        """Invalid guess input loops until a valid one is entered."""
117
        with (
1✔
118
            patch(BASE_PATCHES["ask_settings"]) as ask_settings,
119
            patch(BASE_PATCHES["SessionClass"]) as SessionClass,
120
            patch(BASE_PATCHES["prompt"]) as prompt,
121
            patch(BASE_PATCHES["int_prompt"]),
122
            patch(BASE_PATCHES["parse_code"]) as parse_code,
123
            patch(BASE_PATCHES["display"], return_value="1234"),
124
            patch(BASE_PATCHES["console"]) as console,
125
            patch(BASE_PATCHES["pause"]),
126
            patch(BASE_PATCHES["jpype_exc"], _FakeJException),
127
        ):
128
            ask_settings.return_value = (6, 4, 10)
1✔
129
            SessionClass.return_value = _make_session([7])
1✔
130
            # "bad" is not the suggestion, parse_code returns None for it
131
            prompt.side_effect = ["bad", "1234", "4b0w"]
1✔
132
            parse_code.side_effect = [None, 42]
1✔
133

134
            from mastermind.gamemode.assisted import play
1✔
135

136
            play()
1✔
137

138
            printed = " ".join(str(c) for c in console.print.call_args_list)
1✔
139
            assert "Invalid" in printed
1✔
140

141
    def test_invalid_feedback_retried(self):
1✔
142
        """Invalid feedback loops until a valid one is entered."""
143
        with (
1✔
144
            patch(BASE_PATCHES["ask_settings"]) as ask_settings,
145
            patch(BASE_PATCHES["SessionClass"]) as SessionClass,
146
            patch(BASE_PATCHES["prompt"]) as prompt,
147
            patch(BASE_PATCHES["int_prompt"]),
148
            patch(BASE_PATCHES["parse_code"]),
149
            patch(BASE_PATCHES["display"], return_value="1234"),
150
            patch(BASE_PATCHES["console"]) as console,
151
            patch(BASE_PATCHES["pause"]),
152
            patch(BASE_PATCHES["jpype_exc"], _FakeJException),
153
        ):
154
            ask_settings.return_value = (6, 4, 10)
1✔
155
            session = _make_session([7], solution_sizes=[100])
1✔
156
            SessionClass.return_value = session
1✔
157
            # Guess accepted (suggestion), then bad feedback, then good feedback = win
158
            prompt.side_effect = ["1234", "xyz", "4b0w"]
1✔
159

160
            from mastermind.gamemode.assisted import play
1✔
161

162
            play()
1✔
163

164
            printed = " ".join(str(c) for c in console.print.call_args_list)
1✔
165
            assert "Invalid" in printed
1✔
166

167
    def test_u_at_guess_prompt_on_first_turn_does_nothing(self):
1✔
168
        """'u' on turn 1 at guess prompt prints a warning and re-prompts."""
169
        with (
1✔
170
            patch(BASE_PATCHES["ask_settings"]) as ask_settings,
171
            patch(BASE_PATCHES["SessionClass"]) as SessionClass,
172
            patch(BASE_PATCHES["prompt"]) as prompt,
173
            patch(BASE_PATCHES["int_prompt"]),
174
            patch(BASE_PATCHES["parse_code"]),
175
            patch(BASE_PATCHES["display"], return_value="1234"),
176
            patch(BASE_PATCHES["console"]) as console,
177
            patch(BASE_PATCHES["pause"]),
178
            patch(BASE_PATCHES["jpype_exc"], _FakeJException),
179
        ):
180
            ask_settings.return_value = (6, 4, 10)
1✔
181
            SessionClass.return_value = _make_session([7])
1✔
182
            # 'u' on first turn, then accept suggestion, then win
183
            prompt.side_effect = ["u", "1234", "4b0w"]
1✔
184

185
            from mastermind.gamemode.assisted import play
1✔
186

187
            play()
1✔
188

189
            printed = " ".join(str(c) for c in console.print.call_args_list)
1✔
190
            assert "Nothing to undo" in printed
1✔
191

192
    def test_u_at_guess_prompt_undoes_previous_turn(self):
1✔
193
        """'u' on turn 2 calls session.undo(1) and goes back to turn 1."""
194
        with (
1✔
195
            patch(BASE_PATCHES["ask_settings"]) as ask_settings,
196
            patch(BASE_PATCHES["SessionClass"]) as SessionClass,
197
            patch(BASE_PATCHES["prompt"]) as prompt,
198
            patch(BASE_PATCHES["int_prompt"]),
199
            patch(BASE_PATCHES["parse_code"]),
200
            patch(BASE_PATCHES["display"], return_value="1234"),
201
            patch(BASE_PATCHES["console"]),
202
            patch(BASE_PATCHES["pause"]),
203
            patch(BASE_PATCHES["jpype_exc"], _FakeJException),
204
        ):
205
            ask_settings.return_value = (6, 4, 10)
1✔
206
            session = _make_session([7, 7], solution_sizes=[100, 50])
1✔
207
            SessionClass.return_value = session
1✔
208
            # Turn 1: accept suggestion, non-winning feedback
209
            # Turn 2 guess: 'u' → undo
210
            # Turn 1 again: accept suggestion, win
211
            prompt.side_effect = ["1234", "1b0w", "u", "1234", "4b0w"]
1✔
212

213
            from mastermind.gamemode.assisted import play
1✔
214

215
            play()
1✔
216

217
            session.undo.assert_called_once_with(1)
1✔
218

219
    def test_u_at_feedback_prompt_rerequests_guess(self):
1✔
220
        """'u' at feedback prompt re-asks the guess for the same turn (no session undo)."""
221
        with (
1✔
222
            patch(BASE_PATCHES["ask_settings"]) as ask_settings,
223
            patch(BASE_PATCHES["SessionClass"]) as SessionClass,
224
            patch(BASE_PATCHES["prompt"]) as prompt,
225
            patch(BASE_PATCHES["int_prompt"]),
226
            patch(BASE_PATCHES["parse_code"]),
227
            patch(BASE_PATCHES["display"], return_value="1234"),
228
            patch(BASE_PATCHES["console"]) as console,
229
            patch(BASE_PATCHES["pause"]),
230
            patch(BASE_PATCHES["jpype_exc"], _FakeJException),
231
        ):
232
            ask_settings.return_value = (6, 4, 10)
1✔
233
            session = _make_session([7], solution_sizes=[100])
1✔
234
            SessionClass.return_value = session
1✔
235
            # Guess accepted, feedback 'u' → re-ask guess, then accept again and win
236
            prompt.side_effect = ["1234", "u", "1234", "4b0w"]
1✔
237

238
            from mastermind.gamemode.assisted import play
1✔
239

240
            play()
1✔
241

242
            # session.undo should NOT be called (u at feedback, not guess)
243
            session.undo.assert_not_called()
1✔
244
            printed = " ".join(str(c) for c in console.print.call_args_list)
1✔
245
            assert "Re-enter" in printed
1✔
246

247
    def test_out_of_turns(self):
1✔
248
        """Exhaust max_tries without a win → out-of-turns message."""
249
        with (
1✔
250
            patch(BASE_PATCHES["ask_settings"]) as ask_settings,
251
            patch(BASE_PATCHES["SessionClass"]) as SessionClass,
252
            patch(BASE_PATCHES["prompt"]) as prompt,
253
            patch(BASE_PATCHES["int_prompt"]),
254
            patch(BASE_PATCHES["parse_code"]),
255
            patch(BASE_PATCHES["display"], return_value="1234"),
256
            patch(BASE_PATCHES["console"]) as console,
257
            patch(BASE_PATCHES["pause"]) as pause,
258
            patch(BASE_PATCHES["jpype_exc"], _FakeJException),
259
        ):
260
            ask_settings.return_value = (6, 4, 2)
1✔
261
            session = _make_session([7, 8], solution_sizes=[100, 50])
1✔
262
            SessionClass.return_value = session
1✔
263
            # 2 turns, each: accept suggestion + non-winning feedback
264
            prompt.side_effect = ["1234", "1b0w", "1234", "0b1w"]
1✔
265

266
            from mastermind.gamemode.assisted import play
1✔
267

268
            play()
1✔
269

270
            printed = " ".join(str(c) for c in console.print.call_args_list)
1✔
271
            assert "Out of turns" in printed or "✗" in printed
1✔
272
            pause.assert_called_once()
1✔
273

274
    def test_suggestion_cached_on_undo(self):
1✔
275
        """After undo, the cached suggestion for that turn is re-used (suggestGuess not called again)."""
276
        with (
1✔
277
            patch(BASE_PATCHES["ask_settings"]) as ask_settings,
278
            patch(BASE_PATCHES["SessionClass"]) as SessionClass,
279
            patch(BASE_PATCHES["prompt"]) as prompt,
280
            patch(BASE_PATCHES["int_prompt"]),
281
            patch(BASE_PATCHES["parse_code"]),
282
            patch(BASE_PATCHES["display"], return_value="1234"),
283
            patch(BASE_PATCHES["console"]),
284
            patch(BASE_PATCHES["pause"]),
285
            patch(BASE_PATCHES["jpype_exc"], _FakeJException),
286
        ):
287
            ask_settings.return_value = (6, 4, 10)
1✔
288
            session = _make_session([7, 8], solution_sizes=[100, 50])
1✔
289
            SessionClass.return_value = session
1✔
290
            # Turn 1: accept + non-winning; Turn 2: undo; Turn 1 again: accept + win
291
            prompt.side_effect = ["1234", "1b0w", "u", "1234", "4b0w"]
1✔
292

293
            from mastermind.gamemode.assisted import play
1✔
294

295
            play()
1✔
296

297
            # suggestGuess called only once for turn 1 (cached on re-visit)
298
            assert session.suggestGuess.call_count == 2  # turn1 + turn2 before undo
1✔
299

300
    def test_remaining_percentage_printed(self):
1✔
301
        """After a non-winning turn, the eliminated % and remaining count are printed."""
302
        with (
1✔
303
            patch(BASE_PATCHES["ask_settings"]) as ask_settings,
304
            patch(BASE_PATCHES["SessionClass"]) as SessionClass,
305
            patch(BASE_PATCHES["prompt"]) as prompt,
306
            patch(BASE_PATCHES["int_prompt"]),
307
            patch(BASE_PATCHES["parse_code"]),
308
            patch(BASE_PATCHES["display"], return_value="1234"),
309
            patch(BASE_PATCHES["console"]) as console,
310
            patch(BASE_PATCHES["pause"]),
311
            patch(BASE_PATCHES["jpype_exc"], _FakeJException),
312
        ):
313
            ask_settings.return_value = (6, 4, 10)
1✔
314
            # c**d = 6**4 = 1296, after guess: 648 remain → 50% eliminated
315
            session = _make_session([7, 8], solution_sizes=[648, 1])
1✔
316
            SessionClass.return_value = session
1✔
317
            prompt.side_effect = ["1234", "1b0w", "1234", "4b0w"]
1✔
318

319
            from mastermind.gamemode.assisted import play
1✔
320

321
            play()
1✔
322

323
            printed = " ".join(str(c) for c in console.print.call_args_list)
1✔
324
            assert "eliminated" in printed
1✔
325
            assert "remaining" in printed
1✔
326

327
    def test_inconsistent_feedback_offers_undo(self):
1✔
328
        """When session raises 'No valid secrets remain', user is offered undo."""
329
        with (
1✔
330
            patch(BASE_PATCHES["ask_settings"]) as ask_settings,
331
            patch(BASE_PATCHES["SessionClass"]) as SessionClass,
332
            patch(BASE_PATCHES["prompt"]) as prompt,
333
            patch(BASE_PATCHES["int_prompt"]) as int_prompt,
334
            patch(BASE_PATCHES["parse_code"]),
335
            patch(BASE_PATCHES["display"], return_value="1234"),
336
            patch(BASE_PATCHES["console"]) as console,
337
            patch(BASE_PATCHES["pause"]),
338
            patch(BASE_PATCHES["jpype_exc"], _FakeJException),
339
        ):
340
            ask_settings.return_value = (6, 4, 10)
1✔
341
            session = MagicMock()
1✔
342
            session.suggestGuess.side_effect = [7, 8]
1✔
343
            session.getSolutionSpaceSize.return_value = 100
1✔
344
            # First recordGuess raises; second (after undo) is fine and leads to win
345
            exc = _FakeJException("No valid secrets remain")
1✔
346
            session.recordGuess.side_effect = [exc, None]
1✔
347
            SessionClass.return_value = session
1✔
348

349
            # Turn 1: accept suggestion, bad feedback (raises) → undo 1 → turn 1 again → win
350
            prompt.side_effect = ["1234", "1b0w", "y", "1234", "4b0w"]
1✔
351
            int_prompt.return_value = 1
1✔
352

353
            from mastermind.gamemode.assisted import play
1✔
354

355
            play()
1✔
356

357
            printed = " ".join(str(c) for c in console.print.call_args_list)
1✔
358
            assert "inconsistent" in printed.lower() or "No valid" in printed
1✔
359

360
    def test_inconsistent_feedback_user_declines_undo_exits(self):
1✔
361
        """When inconsistency is detected and user says 'n', game exits."""
362
        with (
1✔
363
            patch(BASE_PATCHES["ask_settings"]) as ask_settings,
364
            patch(BASE_PATCHES["SessionClass"]) as SessionClass,
365
            patch(BASE_PATCHES["prompt"]) as prompt,
366
            patch(BASE_PATCHES["int_prompt"]),
367
            patch(BASE_PATCHES["parse_code"]),
368
            patch(BASE_PATCHES["display"], return_value="1234"),
369
            patch(BASE_PATCHES["console"]),
370
            patch(BASE_PATCHES["pause"]) as pause,
371
            patch(BASE_PATCHES["jpype_exc"], _FakeJException),
372
        ):
373
            ask_settings.return_value = (6, 4, 10)
1✔
374
            session = MagicMock()
1✔
375
            session.suggestGuess.return_value = 7
1✔
376
            exc = _FakeJException("No valid secrets remain")
1✔
377
            session.recordGuess.side_effect = exc
1✔
378
            SessionClass.return_value = session
1✔
379

380
            # Turn 1: accept suggestion, bad feedback (raises) → user says "n" → exit
381
            prompt.side_effect = ["1234", "1b0w", "n"]
1✔
382

383
            from mastermind.gamemode.assisted import play
1✔
384

385
            play()
1✔
386

387
            pause.assert_called_once()
1✔
388

389
    def test_inconsistent_undo_1_resumes_from_turn_1(self):
1✔
390
        """After inconsistency on turn 1, undo 1 → session.undo(1) called, resumes from turn 1."""
391
        with (
1✔
392
            patch(BASE_PATCHES["ask_settings"]) as ask_settings,
393
            patch(BASE_PATCHES["SessionClass"]) as SessionClass,
394
            patch(BASE_PATCHES["prompt"]) as prompt,
395
            patch(BASE_PATCHES["int_prompt"]) as int_prompt,
396
            patch(BASE_PATCHES["parse_code"]),
397
            patch(BASE_PATCHES["display"], return_value="1234"),
398
            patch(BASE_PATCHES["console"]),
399
            patch(BASE_PATCHES["pause"]),
400
            patch(BASE_PATCHES["jpype_exc"], _FakeJException),
401
        ):
402
            ask_settings.return_value = (6, 4, 10)
1✔
403
            session = MagicMock()
1✔
404
            session.suggestGuess.side_effect = [7, 8]
1✔
405
            session.getSolutionSpaceSize.return_value = 100
1✔
406
            exc = _FakeJException("No valid secrets remain")
1✔
407
            session.recordGuess.side_effect = [exc, None]
1✔
408
            SessionClass.return_value = session
1✔
409

410
            # Turn 1: accept, bad feedback → exception → undo 1 → turn 1 again → win
411
            prompt.side_effect = ["1234", "1b0w", "y", "1234", "4b0w"]
1✔
412
            int_prompt.return_value = 1
1✔
413

414
            from mastermind.gamemode.assisted import play
1✔
415

416
            play()
1✔
417

418
            session.undo.assert_called_once_with(1)
1✔
419

420
    def test_inconsistent_undo_2_resumes_from_correct_turn(self):
1✔
421
        """After inconsistency on turn 2, undo 2 → session.undo(2), resumes from turn 1."""
422
        with (
1✔
423
            patch(BASE_PATCHES["ask_settings"]) as ask_settings,
424
            patch(BASE_PATCHES["SessionClass"]) as SessionClass,
425
            patch(BASE_PATCHES["prompt"]) as prompt,
426
            patch(BASE_PATCHES["int_prompt"]) as int_prompt,
427
            patch(BASE_PATCHES["parse_code"]),
428
            patch(BASE_PATCHES["display"], return_value="1234"),
429
            patch(BASE_PATCHES["console"]),
430
            patch(BASE_PATCHES["pause"]),
431
            patch(BASE_PATCHES["jpype_exc"], _FakeJException),
432
        ):
433
            ask_settings.return_value = (6, 4, 10)
1✔
434
            session = MagicMock()
1✔
435
            # Turn 1 succeeds, turn 2 raises, then turn 1 again wins
436
            session.suggestGuess.side_effect = [7, 8, 9]
1✔
437
            session.getSolutionSpaceSize.side_effect = [50, 50]
1✔
438
            exc = _FakeJException("No valid secrets remain")
1✔
439
            session.recordGuess.side_effect = [None, exc, None]
1✔
440
            SessionClass.return_value = session
1✔
441

442
            # Turn 1: accept + good feedback
443
            # Turn 2: accept + bad feedback → exception → undo 2 → turn 1 again → win
444
            prompt.side_effect = ["1234", "1b0w", "1234", "0b1w", "y", "1234", "4b0w"]
1✔
445
            int_prompt.return_value = 2
1✔
446

447
            from mastermind.gamemode.assisted import play
1✔
448

449
            play()
1✔
450

451
            session.undo.assert_called_once_with(2)
1✔
452

453
    def test_inconsistent_invalid_undo_count_reprompted(self):
1✔
454
        """Out-of-range undo count is rejected and re-prompted."""
455
        with (
1✔
456
            patch(BASE_PATCHES["ask_settings"]) as ask_settings,
457
            patch(BASE_PATCHES["SessionClass"]) as SessionClass,
458
            patch(BASE_PATCHES["prompt"]) as prompt,
459
            patch(BASE_PATCHES["int_prompt"]) as int_prompt,
460
            patch(BASE_PATCHES["parse_code"]),
461
            patch(BASE_PATCHES["display"], return_value="1234"),
462
            patch(BASE_PATCHES["console"]) as console,
463
            patch(BASE_PATCHES["pause"]),
464
            patch(BASE_PATCHES["jpype_exc"], _FakeJException),
465
        ):
466
            ask_settings.return_value = (6, 4, 10)
1✔
467
            session = MagicMock()
1✔
468
            session.suggestGuess.side_effect = [7, 8]
1✔
469
            session.getSolutionSpaceSize.return_value = 100
1✔
470
            exc = _FakeJException("No valid secrets remain")
1✔
471
            session.recordGuess.side_effect = [exc, None]
1✔
472
            SessionClass.return_value = session
1✔
473

474
            # Turn 1: accept, bad feedback → exception → undo prompt: 0 (invalid), then 1 (valid) → win
475
            prompt.side_effect = ["1234", "1b0w", "y", "1234", "4b0w"]
1✔
476
            int_prompt.side_effect = [0, 1]  # first out of range, second valid
1✔
477

478
            from mastermind.gamemode.assisted import play
1✔
479

480
            play()
1✔
481

482
            # IntPrompt asked twice (once invalid, once valid)
483
            assert int_prompt.call_count == 2
1✔
484
            printed = " ".join(str(c) for c in console.print.call_args_list)
1✔
485
            assert "Must be between" in printed
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc