feat(test): add comprehensive TicTacToe gameplay scenario flows
- Introduce multiple async test paths for simulation-based validation: • happy_path (P1 top-row win) • p2_wins_diagonal • draw_game (full board, no winner) • illegal_occupied_cell • illegal_out_of_turn • illegal_out_of_bounds • midgame_disconnect • abandoned_lobby (no opponent joins) • spam_moves (anti-flood behavior) • random_game (stochastic stress playthrough) - Add TEST_SCENARIOS registry for automated execution - Improve coverage of server-side match logic, validation, and cleanup - Enables CI-driven load, rule enforcement, and termination testing
This commit is contained in:
157
game_flow.py
157
game_flow.py
@@ -1,3 +1,4 @@
|
|||||||
|
import random
|
||||||
import asyncio
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
@@ -123,6 +124,149 @@ class PlayerWebSocketHandler(object):
|
|||||||
print(f"[{self.label}] Connection closed")
|
print(f"[{self.label}] Connection closed")
|
||||||
|
|
||||||
|
|
||||||
|
async def happy_path(
|
||||||
|
match_id: str,
|
||||||
|
p1: PlayerWebSocketHandler,
|
||||||
|
p2: PlayerWebSocketHandler
|
||||||
|
):
|
||||||
|
# Play moves
|
||||||
|
await p1.send_move(match_id, 0, 0)
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
await p2.send_move(match_id, 1, 1)
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
await p1.send_move(match_id, 0, 1)
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
await p2.send_move(match_id, 2, 2)
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
await p1.send_move(match_id, 0, 2)
|
||||||
|
|
||||||
|
|
||||||
|
async def p2_wins_diagonal(
|
||||||
|
match_id: str,
|
||||||
|
p1: PlayerWebSocketHandler,
|
||||||
|
p2: PlayerWebSocketHandler
|
||||||
|
):
|
||||||
|
await p1.send_move(match_id, 0, 0)
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
await p2.send_move(match_id, 0, 2)
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
await p1.send_move(match_id, 1, 0)
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
await p2.send_move(match_id, 1, 1)
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
await p1.send_move(match_id, 2, 1)
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
await p2.send_move(match_id, 2, 0) # P2 wins
|
||||||
|
|
||||||
|
|
||||||
|
async def draw_game(
|
||||||
|
match_id: str,
|
||||||
|
p1: PlayerWebSocketHandler,
|
||||||
|
p2: PlayerWebSocketHandler
|
||||||
|
):
|
||||||
|
moves = [
|
||||||
|
(p1, 0, 0), (p2, 0, 1),
|
||||||
|
(p1, 0, 2), (p2, 1, 0),
|
||||||
|
(p1, 1, 2), (p2, 1, 1),
|
||||||
|
(p1, 2, 1), (p2, 2, 2),
|
||||||
|
(p1, 2, 0),
|
||||||
|
]
|
||||||
|
for player, r, c in moves:
|
||||||
|
await player.send_move(match_id, r, c)
|
||||||
|
await asyncio.sleep(0.25)
|
||||||
|
|
||||||
|
|
||||||
|
async def illegal_occupied_cell(
|
||||||
|
match_id: str,
|
||||||
|
p1: PlayerWebSocketHandler,
|
||||||
|
p2: PlayerWebSocketHandler
|
||||||
|
):
|
||||||
|
await p1.send_move(match_id, 0, 0)
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
|
||||||
|
# P2 tries same spot → should trigger server rejection
|
||||||
|
await p2.send_move(match_id, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
|
async def illegal_out_of_turn(
|
||||||
|
match_id: str,
|
||||||
|
p1: PlayerWebSocketHandler,
|
||||||
|
p2: PlayerWebSocketHandler
|
||||||
|
):
|
||||||
|
await p1.send_move(match_id, 2, 2)
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
|
||||||
|
# P1 tries again before P2
|
||||||
|
await p1.send_move(match_id, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
|
async def illegal_out_of_bounds(
|
||||||
|
match_id: str,
|
||||||
|
p1: PlayerWebSocketHandler,
|
||||||
|
p2: PlayerWebSocketHandler
|
||||||
|
):
|
||||||
|
await p1.send_move(match_id, 3, 3) # Invalid indices
|
||||||
|
|
||||||
|
|
||||||
|
async def midgame_disconnect(
|
||||||
|
match_id: str,
|
||||||
|
p1: PlayerWebSocketHandler,
|
||||||
|
p2: PlayerWebSocketHandler
|
||||||
|
):
|
||||||
|
await p1.send_move(match_id, 0, 0)
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
await p2.close() # simulate rage quit
|
||||||
|
|
||||||
|
|
||||||
|
async def abandoned_lobby(
|
||||||
|
match_id: str,
|
||||||
|
p1: PlayerWebSocketHandler,
|
||||||
|
p2: PlayerWebSocketHandler
|
||||||
|
):
|
||||||
|
# No p2.join_match
|
||||||
|
await asyncio.sleep(5) # test timeout, cleanup, match state
|
||||||
|
|
||||||
|
|
||||||
|
async def spam_moves(
|
||||||
|
match_id: str,
|
||||||
|
p1: PlayerWebSocketHandler,
|
||||||
|
p2: PlayerWebSocketHandler
|
||||||
|
):
|
||||||
|
for _ in range(10):
|
||||||
|
await p1.send_move(match_id, 0, 0)
|
||||||
|
await asyncio.sleep(0.05)
|
||||||
|
|
||||||
|
|
||||||
|
async def random_game(
|
||||||
|
match_id: str,
|
||||||
|
p1: PlayerWebSocketHandler,
|
||||||
|
p2: PlayerWebSocketHandler
|
||||||
|
):
|
||||||
|
board = {(r, c) for r in range(3) for c in range(3)}
|
||||||
|
players = [p1, p2]
|
||||||
|
|
||||||
|
for i in range(9):
|
||||||
|
player = players[i % 2]
|
||||||
|
print(f"[{player.label}] Playing move...")
|
||||||
|
r, c = random.choice(list(board))
|
||||||
|
board.remove((r, c))
|
||||||
|
await player.send_move(match_id, r, c)
|
||||||
|
await asyncio.sleep(0.25)
|
||||||
|
|
||||||
|
|
||||||
|
TEST_SCENARIOS = [
|
||||||
|
happy_path,
|
||||||
|
p2_wins_diagonal,
|
||||||
|
draw_game,
|
||||||
|
illegal_occupied_cell,
|
||||||
|
illegal_out_of_turn,
|
||||||
|
illegal_out_of_bounds,
|
||||||
|
midgame_disconnect,
|
||||||
|
abandoned_lobby,
|
||||||
|
spam_moves,
|
||||||
|
random_game,
|
||||||
|
]
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
# Create players
|
# Create players
|
||||||
p1 = PlayerWebSocketHandler("player_one_123456", "P1")
|
p1 = PlayerWebSocketHandler("player_one_123456", "P1")
|
||||||
@@ -142,16 +286,9 @@ async def main():
|
|||||||
|
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
# Play moves
|
for test_scenario in TEST_SCENARIOS:
|
||||||
await p1.send_move(match_id, 0, 0)
|
print(f'running {test_scenario.__name__!r}')
|
||||||
await asyncio.sleep(0.3)
|
await test_scenario(match_id, p1, p2)
|
||||||
await p2.send_move(match_id, 1, 1)
|
|
||||||
await asyncio.sleep(0.3)
|
|
||||||
await p1.send_move(match_id, 0, 1)
|
|
||||||
await asyncio.sleep(0.3)
|
|
||||||
await p2.send_move(match_id, 2, 2)
|
|
||||||
await asyncio.sleep(0.3)
|
|
||||||
await p1.send_move(match_id, 0, 2)
|
|
||||||
|
|
||||||
await asyncio.sleep(2)
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user