I’m creating a small poker game with Python. I’m struggling with waiting for the player input (e.g., RAISE) in the main game loop, without blocking the responses to the user. If the user takes too long to take action, the game should automatically choose an action for the player (i.e., FOLD).
Strawberry GraphQL is used for all game-user connections. The user subscribes to a poker table using a mutation, the user starts the game with a mutation, the user sends their actions with a mutation, and gets the cards via a subscription, through which as the game progresses new cards are being sent (e.g., FLOP). A UserService
class coordinates the message queues.
PlayerAction Mutation
"""
Rest of class is omitted for brevity
"""
@strawberry.mutation(description="Represents the player action")
async def player_action(
self, info: strawberry.Info, input: PlayerActionInput
) -> PlayerActionPayload:
user_service = UserService()
message_queue = user_service.get_action_message_queue_by_player_id(input.player_token)
try:
message_queue.put_nowait([input.player_action, input.bet_amount])
return PlayerActionPayload(
code=200,
success=True,
message="Successfully executed player action.",
player_credit=-1
)
except Exception as e:
return PlayerActionPayload(
code=500,
success=False,
message=str(e),
player_credit=-1
)
DealCards Subscription
"""
Rest of class is omitted for brevity
"""
@strawberry.type
class Subscription:
@strawberry.subscription
async def get_cards(self, player_id_token: str) -> AsyncGenerator[List[str], None]:
user_service = UserService()
message_queue = user_service.get_card_message_queue_by_player_id(player_id_token)
while True:
message = await message_queue.get()
payload = []
for card in message:
payload.append(Card.int_to_str(card))
yield payload
UserService Class
I tried the following two methods:
card_message_queues: Dict[str, asyncio.Queue] = {}
action_message_queues: Dict[str, asyncio.Queue] = {}
class UserService:
def __init__(self):
self._card_message_queues_by_player_id: Dict[str, asyncio.Queue] = card_message_queues
self._action_queues_by_player_id: Dict[str, asyncio.Queue] = action_message_queues
def send_cards_to_clients(self, phase: GamePhases, players: List[Player], cards):
match phase:
case GamePhases.PREFLOP:
for p in players:
self._card_message_queues_by_player_id[p.id].put_nowait(p.cards)
case _:
for p in players:
self._card_message_queues_by_player_id[p.id].put_nowait(cards)
def fetch_player_action_from_client(self, player, possible_actions: List[PlayerAction]) -> (PlayerAction, int):
"""See below for the different approaches"""
a) Start a new event loop and wait for the future containing the player input. This did not work at all. I’m not sure why this was the case:
print("start async init")
def run_event_loop(loop):
asyncio.set_event_loop(loop)
loop.run_forever()
new_loop = asyncio.new_event_loop()
t = threading.Thread(target=run_event_loop, args=(new_loop,))
t.start()
print("started thread")
future = asyncio.run_coroutine_threadsafe(self._action_queues_by_player_id[player.id].get(), new_loop)
result = future.result()
print("waited for thread")
print(result)
return PlayerAction.FOLD, 0
b) Sleep the main thread and wait this way for the user action to arrive in the message queue. This kind of worked, as the player input was registered and passed on to the main game loop. The problem here was that the GraphQL response would only send after the loop to check for the player input has run:
def fetch_player_action_from_client(self, player, possible_actions: List[PlayerAction]) -> (PlayerAction, int):
result = None
q = self._action_queues_by_player_id[player.id]
for i in range(20):
print(f"trying to fetch {player.id} action")
time.sleep(1)
try:
result = q.get_nowait()
break
except:
print("queue empty")
print(result)
return PlayerAction.FOLD, 0