Make an absent chiamata bidder pass instead of bidding randomly
CI / Build and push docker image (push) Successful in 1m37s

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:
woggioni committed 2026-10-09 06:51:43 +08:00
1 parent 322ca27bf4
commit 0f9fb9b6bb
7 files changed
+121 -38

No files matched your search

@@ -240,18 +240,43 @@ class AuctionTest(unittest.TestCase):
self.assertEqual(63, view["legal_bids"]["points_min"])
self.assertEqual(120, view["legal_bids"]["points_max"])
def test_auto_bid_bids_or_passes(self) -> None:
def test_auto_bid_always_passes_when_legal(self) -> None:
state = started_state()
rules.auto_bid(state, random.Random(1))
assert state.auction is not None
self.assertIsNotNone(state.auction.current)
# The auction keeps moving and eventually concludes.
for _ in range(50):
if state.phase != PHASE_AUCTION:
break
rules.auto_bid(state, random.Random(2))
rules.auto_bid(state)
# Seat 1 passes: nobody has bid and the turn moves on.
self.assertEqual([1], state.auction.passed)
self.assertIsNone(state.auction.current)
self.assertEqual(2, state.auction.turn)
# An absent bidder still passes when there is a standing bid.
rules.bid_rank(state, "p2", 10)
rules.auto_bid(state)
self.assertEqual([1, 3], state.auction.passed)
assert state.auction.current is not None
self.assertEqual(10, state.auction.current.rank)
def test_auto_bid_passes_at_the_point_stage(self) -> None:
state = started_state()
rules.bid_rank(state, "p1", 2)
rules.bid_points(state, "p2", 62)
rules.auto_bid(state) # seat 3
assert state.auction is not None
self.assertIn(3, state.auction.passed)
assert state.auction.current is not None
self.assertEqual(62, state.auction.current.points)
def test_auto_bid_forced_to_call_the_ace(self) -> None:
state = started_state()
for seat in (1, 2, 3, 4):
rules.pass_auction(state, f"p{seat}")
# Seat 0 is the last one standing and nobody has bid yet: the
# forced auto-bid calls the ace.
rules.auto_bid(state)
self.assertEqual(PHASE_CALLING, state.phase)
self.assertIsNotNone(state.caller)
self.assertEqual(0, state.caller)
assert state.auction is not None and state.auction.current is not None
self.assertEqual(1, state.auction.current.rank)
self.assertEqual(61, state.declared_target)
class CallTest(unittest.TestCase):
@@ -543,7 +568,7 @@ class ScoringTest(unittest.TestCase):
moves = 0
while state.phase != PHASE_FINISHED and moves < 5000:
if state.phase == PHASE_AUCTION:
rules.auto_bid(state, rng)
rules.auto_bid(state)
elif state.phase == PHASE_CALLING:
rules.auto_call(state, rng)
else: