Add optional asso pigliatutto rule with a lobby toggle
CI / Build and push docker image (push) Successful in 3m16s
CI / Build and push docker image (push) Successful in 3m16s
An ace played when no equal rank is on the table sweeps the whole table; the sweep never scores a scopa, and an ace already on the table still forces the mandatory equal-rank capture. The rule is off by default, selectable per match via the schema-driven creation form, and announced in the game view.
This commit is contained in:
12 files changed
+291
-16
No files matched your search
@@ -325,6 +325,138 @@ class NapolaTest(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
class AssoPigliatuttoTest(unittest.TestCase):
|
||||
def test_ace_sweeps_the_whole_table_without_scopa(self) -> None:
|
||||
# The other players still hold cards, so emptying the table would
|
||||
# normally score a scopa: the sweep must suppress it.
|
||||
state = make_state(
|
||||
[["01D", "09C"], ["04D"], ["05D"], ["06D"]],
|
||||
table=["05C", "02D", "03S"],
|
||||
)
|
||||
state.asso_pigliatutto = True
|
||||
engine.play(state, "p0", "01D", ["05C", "02D", "03S"])
|
||||
self.assertEqual([], state.table)
|
||||
self.assertEqual(
|
||||
{"05C", "02D", "03S", "01D"},
|
||||
{c.code for c in state.players[0].captured},
|
||||
)
|
||||
self.assertEqual(0, state.players[0].scope)
|
||||
assert state.last_move is not None
|
||||
self.assertTrue(state.last_move.pigliatutto)
|
||||
self.assertFalse(state.last_move.scopa)
|
||||
|
||||
def test_legal_captures_offer_only_the_whole_table(self) -> None:
|
||||
table = [card("05C"), card("02D"), card("03S")]
|
||||
options = engine.legal_captures(
|
||||
table, card("01D"), asso_pigliatutto=True
|
||||
)
|
||||
self.assertEqual(
|
||||
[["05C", "02D", "03S"]], [[c.code for c in o] for o in options]
|
||||
)
|
||||
# Non-ace cards keep the normal rules.
|
||||
self.assertEqual(
|
||||
[],
|
||||
engine.legal_captures(table, card("04B"), asso_pigliatutto=True),
|
||||
)
|
||||
|
||||
def test_equal_rank_ace_blocks_the_sweep(self) -> None:
|
||||
table = ["01C", "02D"]
|
||||
state = make_state([["01D"], ["04D"], ["05D"], ["06D"]], table=table)
|
||||
state.asso_pigliatutto = True
|
||||
options = engine.legal_captures(
|
||||
state.table, card("01D"), asso_pigliatutto=True
|
||||
)
|
||||
self.assertEqual([["01C"]], [[c.code for c in o] for o in options])
|
||||
with self.assertRaises(IllegalMove):
|
||||
engine.play(state, "p0", "01D", ["01C", "02D"])
|
||||
# A rejected play consumes the card, so retry on a fresh state.
|
||||
state = make_state([["01D"], ["04D"], ["05D"], ["06D"]], table=table)
|
||||
state.asso_pigliatutto = True
|
||||
engine.play(state, "p0", "01D", ["01C"])
|
||||
self.assertEqual(["02D"], [c.code for c in state.table])
|
||||
assert state.last_move is not None
|
||||
self.assertFalse(state.last_move.pigliatutto)
|
||||
|
||||
def test_disabled_by_default(self) -> None:
|
||||
state = make_state(
|
||||
[["01D", "09C"], ["04D"], ["05D"], ["06D"]],
|
||||
table=["05C", "02D"],
|
||||
)
|
||||
engine.play(state, "p0", "01D")
|
||||
# Without the rule an ace captures nothing here and stays on the
|
||||
# table.
|
||||
self.assertEqual({"05C", "02D", "01D"}, {c.code for c in state.table})
|
||||
assert state.last_move is not None
|
||||
self.assertFalse(state.last_move.pigliatutto)
|
||||
self.assertEqual(
|
||||
[],
|
||||
engine.legal_captures(
|
||||
[card("05C")], card("01D"), asso_pigliatutto=False
|
||||
),
|
||||
)
|
||||
|
||||
def test_ace_on_empty_table_is_a_normal_play(self) -> None:
|
||||
state = make_state(
|
||||
[["01D", "09C"], ["04D"], ["05D"], ["06D"]], table=[]
|
||||
)
|
||||
state.asso_pigliatutto = True
|
||||
engine.play(state, "p0", "01D")
|
||||
self.assertEqual(["01D"], [c.code for c in state.table])
|
||||
assert state.last_move is not None
|
||||
self.assertFalse(state.last_move.pigliatutto)
|
||||
|
||||
def test_move_pigliatutto_serialization(self) -> None:
|
||||
move = engine.Move(
|
||||
seat=0, name="p0", card="01D", captured=["05C"], pigliatutto=True
|
||||
)
|
||||
restored = engine.Move.from_json(move.to_json())
|
||||
self.assertTrue(restored.pigliatutto)
|
||||
legacy = move.to_json()
|
||||
del legacy["pigliatutto"]
|
||||
self.assertFalse(engine.Move.from_json(legacy).pigliatutto)
|
||||
|
||||
def test_state_serialization_roundtrip(self) -> None:
|
||||
state = make_state([["02D"], [], [], []], table=[])
|
||||
self.assertFalse(state.asso_pigliatutto)
|
||||
state.asso_pigliatutto = True
|
||||
self.assertTrue(
|
||||
ScoponeState.from_json(state.to_json()).asso_pigliatutto
|
||||
)
|
||||
# States serialized before the option existed default to disabled.
|
||||
data = state.to_json()
|
||||
del data["asso_pigliatutto"]
|
||||
self.assertFalse(ScoponeState.from_json(data).asso_pigliatutto)
|
||||
|
||||
def test_create_game_default_and_override(self) -> None:
|
||||
self.assertFalse(engine.create_game("p0", "p0").asso_pigliatutto)
|
||||
self.assertTrue(
|
||||
engine.create_game("p0", "p0", asso_pigliatutto=True).asso_pigliatutto
|
||||
)
|
||||
|
||||
def test_state_for_player_exposes_rule_and_legal_moves(self) -> None:
|
||||
state = make_state(
|
||||
[["01D"], ["04D"], ["05D"], ["06D"]],
|
||||
table=["05C", "02D", "03S"],
|
||||
)
|
||||
state.asso_pigliatutto = True
|
||||
view = engine.state_for_player(state, "p0")
|
||||
self.assertTrue(view["asso_pigliatutto"])
|
||||
self.assertEqual(
|
||||
{"01D": [["05C", "02D", "03S"]]}, view["legal_moves"]
|
||||
)
|
||||
|
||||
def test_auto_play_sweeps_with_the_rule(self) -> None:
|
||||
state = make_state(
|
||||
[["01D"], ["04D"], ["05D"], ["06D"]],
|
||||
table=["05C", "02D"],
|
||||
)
|
||||
state.asso_pigliatutto = True
|
||||
engine.auto_play(state)
|
||||
assert state.last_move is not None
|
||||
self.assertTrue(state.last_move.pigliatutto)
|
||||
self.assertEqual(["05C", "02D"], state.last_move.captured)
|
||||
|
||||
|
||||
class MatchFlowTest(unittest.TestCase):
|
||||
def test_join_starts_when_full(self) -> None:
|
||||
state = engine.create_game("p0", "p0", target_score=11)
|
||||
|
||||
Reference in new issue
Block a user