Make an absent chiamata bidder pass instead of bidding randomly

The auction timeout bid a random legal rank, which could hand the
auction (and the call) to a disconnected player. An absent bidder now
always passes; only the last player standing before any bid cannot
pass, and is made to bid the ace - its suit is picked at random by the
existing call timeout. This keeps a deal from stalling without a caller
while never letting an absent player outbid someone.

The deadline log line now says which of the two happened.
This commit is contained in:
2026-10-08 22:47:32 +00:00
parent c746a88cfb
commit e0030ee94a
7 changed files with 121 additions and 38 deletions
+53 -6
View File
@@ -241,7 +241,7 @@ class ChiamataTimeoutTest(unittest.TestCase):
return session.id
@async_test
async def test_auction_timeout_auto_bids(self) -> None:
async def test_auction_timeout_auto_passes(self) -> None:
game_id = await self._seed(
"chiamata-auction-timeout-1", "CH0001", lambda state: None
)
@@ -263,15 +263,62 @@ class ChiamataTimeoutTest(unittest.TestCase):
break
if (
update.get("type") == "state"
and update["game"]["auction"]["current"] is not None
and 1 in update["game"]["auction"]["passed"]
):
break
self.assertIsNotNone(update)
assert update is not None
# The absent seat 1 was auto-bid for.
current = update["game"]["auction"]["current"]
self.assertEqual(1, current["seat"])
self.assertIn(current["rank"], (1, 3, 10, 9, 8, 7, 6, 5, 4, 2))
# The absent seat 1 passed: nobody bid and the turn
# moved to seat 2.
auction = update["game"]["auction"]
self.assertIn(1, auction["passed"])
self.assertIsNone(auction["current"])
self.assertEqual(2, auction["turn"])
@async_test
async def test_forced_last_bidder_auto_bids_the_ace(self) -> None:
def seed(state) -> None:
state.phase = PHASE_AUCTION
state.auction = Auction(
turn=0, active=[0], passed=[1, 2, 3, 4], current=None
)
state.turn = 0
state.auction_deadline = (
datetime.now(timezone.utc) + timedelta(seconds=1)
).isoformat()
state.call_deadline = None
game_id = await self._seed("chiamata-forced-ace-1", "CH0003", seed)
ws_transport = ASGIWebSocketTransport(app=app)
async with AsyncClient(transport=ws_transport, base_url="http://testserver") as ws_client:
with ws_users([make_user("alice")]):
async with aconnect_ws(f"/ws/games/{game_id}", ws_client) as ws:
first = await ws.receive_json()
self.assertEqual(PHASE_AUCTION, first["game"]["phase"])
# The auction times out: seat 0 is forced to bid the
# ace; its call then times out too and a random ace
# suit is named.
played = None
for _ in range(20):
try:
played = await asyncio.wait_for(
ws.receive_json(), timeout=2
)
except asyncio.TimeoutError:
break
if (
played.get("type") == "state"
and played["game"]["phase"] == PHASE_PLAYING
):
break
self.assertIsNotNone(played)
assert played is not None
self.assertEqual(PHASE_PLAYING, played["game"]["phase"])
self.assertEqual(0, played["game"]["caller"])
card = played["game"]["called_card"]
self.assertIsNotNone(card)
self.assertTrue(card.startswith("01"))
@async_test
async def test_call_and_turn_timeouts(self) -> None: