Set scopa targets to their traditional defaults
CI / Build and push docker image (push) Successful in 2m6s
CI / Build and push docker image (push) Successful in 2m6s
Partnership games go to 21: scopone scientifico (the tournament target) and scopa a quattro, while two-player scopa keeps its usual 11. The scopa package resolves the default from the player count and exposes a per variant options_schema default, so the lobby creation form shows the right value without frontend changes.
This commit is contained in:
@@ -23,9 +23,11 @@ The optional *asso pigliatutto* creation option turns either variant into
|
||||
(four players in fixed partnerships): the platform-facing adapters.
|
||||
They translate create/join/websocket actions/deadlines into
|
||||
rules-engine calls and back, validate the
|
||||
`target_score`/`asso_pigliatutto` creation options, and extract the
|
||||
`MatchResult` (teams, winner, per-player scores and the match summary
|
||||
persisted as the match's JSON `result`).
|
||||
`target_score`/`asso_pigliatutto` creation options (the match target
|
||||
defaults to the traditional 11 points for two-player scopa and 21 for
|
||||
scopa a quattro), and extract the `MatchResult` (teams, winner,
|
||||
per-player scores and the match summary persisted as the match's JSON
|
||||
`result`).
|
||||
|
||||
Timeouts are constructor arguments (`turn_timeout_seconds`,
|
||||
`hand_ack_timeout_seconds`), wired from the environment by the
|
||||
|
||||
@@ -60,7 +60,7 @@ from .errors import CardNotInHand
|
||||
|
||||
from .state import (
|
||||
DEFAULT_PLAYER_COUNT,
|
||||
DEFAULT_TARGET_SCORE,
|
||||
DEFAULT_TARGET_SCORES,
|
||||
PHASE_FINISHED,
|
||||
PHASE_HAND_END,
|
||||
PHASE_LOBBY,
|
||||
@@ -153,17 +153,23 @@ def create_game(
|
||||
creator_sub: str,
|
||||
creator_name: str,
|
||||
player_count: int = DEFAULT_PLAYER_COUNT,
|
||||
target_score: int = DEFAULT_TARGET_SCORE,
|
||||
target_score: Optional[int] = None,
|
||||
hand_ack_timeout: int = DEFAULT_HAND_ACK_TIMEOUT_SECONDS,
|
||||
turn_timeout: int = DEFAULT_TURN_TIMEOUT_SECONDS,
|
||||
asso_pigliatutto: bool = False,
|
||||
) -> ScopaState:
|
||||
"""Create a lobby game with the creator seated first."""
|
||||
"""Create a lobby game with the creator seated first.
|
||||
|
||||
``target_score`` defaults to the variant's traditional target: 11
|
||||
points for two-player scopa, 21 for the four-player partnership game.
|
||||
"""
|
||||
if player_count not in PLAYER_COUNTS:
|
||||
raise IllegalMove(
|
||||
"player_count must be one of: "
|
||||
+ ", ".join(str(count) for count in PLAYER_COUNTS)
|
||||
)
|
||||
if target_score is None:
|
||||
target_score = DEFAULT_TARGET_SCORES[player_count]
|
||||
if target_score < 1 or target_score > 100:
|
||||
raise IllegalMove("target_score must be between 1 and 100")
|
||||
return ScopaState(
|
||||
|
||||
@@ -27,6 +27,7 @@ from tavolo.platform.errors import GameError, IllegalMove
|
||||
from . import engine
|
||||
from .state import (
|
||||
DEFAULT_TARGET_SCORE,
|
||||
DEFAULT_TARGET_SCORE_QUATTRO,
|
||||
PHASE_FINISHED,
|
||||
PHASE_HAND_END,
|
||||
PHASE_LOBBY,
|
||||
@@ -49,18 +50,16 @@ def _deadline_ms(iso: Optional[str]) -> Optional[int]:
|
||||
return None
|
||||
|
||||
|
||||
class _ScopaBase(GameEngine):
|
||||
"""Shared platform adapter for the two-player and four-player games."""
|
||||
|
||||
player_count: int
|
||||
options_schema = {
|
||||
def _options_schema(default_target: int) -> Dict[str, Any]:
|
||||
"""The creation options schema with this variant's target default."""
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target_score": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
"default": DEFAULT_TARGET_SCORE,
|
||||
"default": default_target,
|
||||
"description": "Match points the winning side must reach.",
|
||||
},
|
||||
"asso_pigliatutto": {
|
||||
@@ -72,6 +71,14 @@ class _ScopaBase(GameEngine):
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class _ScopaBase(GameEngine):
|
||||
"""Shared platform adapter for the two-player and four-player games."""
|
||||
|
||||
player_count: int
|
||||
#: Traditional match target of the variant; also the schema default.
|
||||
default_target_score: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
turn_timeout_seconds: int = engine.DEFAULT_TURN_TIMEOUT_SECONDS,
|
||||
@@ -83,7 +90,7 @@ class _ScopaBase(GameEngine):
|
||||
# -- lobby ------------------------------------------------------------
|
||||
|
||||
def create(self, session: GameSession, options: Mapping[str, Any]) -> None:
|
||||
target_score = options.get("target_score", DEFAULT_TARGET_SCORE)
|
||||
target_score = options.get("target_score", self.default_target_score)
|
||||
if isinstance(target_score, bool) or not isinstance(target_score, int):
|
||||
raise IllegalMove("target_score must be an integer")
|
||||
asso_pigliatutto = options.get("asso_pigliatutto", False)
|
||||
@@ -277,6 +284,8 @@ class ScopaEngine(_ScopaBase):
|
||||
min_players = 2
|
||||
max_players = 2
|
||||
player_count = 2
|
||||
default_target_score = DEFAULT_TARGET_SCORE
|
||||
options_schema = _options_schema(DEFAULT_TARGET_SCORE)
|
||||
|
||||
|
||||
class ScopaQuattroEngine(_ScopaBase):
|
||||
@@ -292,3 +301,5 @@ class ScopaQuattroEngine(_ScopaBase):
|
||||
min_players = 4
|
||||
max_players = 4
|
||||
player_count = 4
|
||||
default_target_score = DEFAULT_TARGET_SCORE_QUATTRO
|
||||
options_schema = _options_schema(DEFAULT_TARGET_SCORE_QUATTRO)
|
||||
|
||||
@@ -45,7 +45,11 @@ PHASE_PLAYING = "playing"
|
||||
PHASE_HAND_END = "hand_end"
|
||||
PHASE_FINISHED = "finished"
|
||||
|
||||
# Traditional match targets: two-player scopa is played to 11 points,
|
||||
# while the four-player partnership game (like scopone) goes to 21.
|
||||
DEFAULT_TARGET_SCORE = 11
|
||||
DEFAULT_TARGET_SCORE_QUATTRO = 21
|
||||
DEFAULT_TARGET_SCORES = {2: DEFAULT_TARGET_SCORE, 4: DEFAULT_TARGET_SCORE_QUATTRO}
|
||||
DEFAULT_PLAYER_COUNT = 2
|
||||
PLAYER_COUNTS = (2, 4)
|
||||
|
||||
|
||||
@@ -172,11 +172,20 @@ class DealTest(unittest.TestCase):
|
||||
def test_create_game_defaults(self) -> None:
|
||||
state = engine.create_game("p0", "p0")
|
||||
self.assertEqual(2, state.player_count)
|
||||
# Two-player scopa is traditionally played to 11 points.
|
||||
self.assertEqual(11, state.target_score)
|
||||
self.assertFalse(state.asso_pigliatutto)
|
||||
self.assertEqual("p0", state.players[0].sub)
|
||||
self.assertEqual("lobby", state.phase)
|
||||
|
||||
def test_create_game_four_player_default_target(self) -> None:
|
||||
# The four-player partnership game (like scopone) goes to 21.
|
||||
state = engine.create_game("p0", "p0", player_count=4)
|
||||
self.assertEqual(21, state.target_score)
|
||||
# An explicit target still wins over the default.
|
||||
explicit = engine.create_game("p0", "p0", player_count=4, target_score=16)
|
||||
self.assertEqual(16, explicit.target_score)
|
||||
|
||||
def test_create_game_player_count_validation(self) -> None:
|
||||
for bad in (1, 3, 5, 0):
|
||||
with self.assertRaises(IllegalMove):
|
||||
|
||||
@@ -21,8 +21,6 @@ from tavolo.scopa.state import (
|
||||
ScopaState,
|
||||
)
|
||||
|
||||
ENGINES = (ScopaEngine, ScopaQuattroEngine)
|
||||
|
||||
|
||||
def _session(engine, **options) -> GameSession:
|
||||
session = GameSession(
|
||||
@@ -66,14 +64,26 @@ def _drive_to_end(engine, session: GameSession) -> None:
|
||||
|
||||
class CreateTest(unittest.TestCase):
|
||||
def test_create_seats_creator_on_side_a(self) -> None:
|
||||
for engine_type in ENGINES:
|
||||
for engine_type, target in ((ScopaEngine, 11), (ScopaQuattroEngine, 21)):
|
||||
engine = engine_type()
|
||||
session = _session(engine)
|
||||
self.assertEqual("A", session.players[0].team)
|
||||
self.assertIsInstance(session.state, ScopaState)
|
||||
self.assertEqual(11, session.state.target_score)
|
||||
# Two-player scopa is played to 11, the partnership game to 21.
|
||||
self.assertEqual(target, session.state.target_score)
|
||||
self.assertFalse(session.state.asso_pigliatutto)
|
||||
|
||||
def test_schema_defaults_follow_the_variant(self) -> None:
|
||||
self.assertEqual(
|
||||
11, ScopaEngine().options_schema["properties"]["target_score"]["default"]
|
||||
)
|
||||
self.assertEqual(
|
||||
21,
|
||||
ScopaQuattroEngine().options_schema["properties"]["target_score"][
|
||||
"default"
|
||||
],
|
||||
)
|
||||
|
||||
def test_create_options(self) -> None:
|
||||
engine = ScopaEngine()
|
||||
session = _session(engine, target_score=21, asso_pigliatutto=True)
|
||||
|
||||
@@ -19,7 +19,8 @@ implementation.
|
||||
- `plugin.py` — `ScoponeEngine(GameEngine)`: the platform-facing adapter.
|
||||
It translates create/join/websocket actions/deadlines into rules-engine
|
||||
calls and back, validates the `target_score`/`napola`/`asso_pigliatutto`
|
||||
creation options, and extracts the `MatchResult` (teams, winner,
|
||||
creation options (the match target defaults to the traditional 21
|
||||
points), and extracts the `MatchResult` (teams, winner,
|
||||
per-player scores and the match summary persisted as the match's JSON
|
||||
`result`).
|
||||
|
||||
|
||||
@@ -37,7 +37,9 @@ PHASE_PLAYING = "playing"
|
||||
PHASE_HAND_END = "hand_end"
|
||||
PHASE_FINISHED = "finished"
|
||||
|
||||
DEFAULT_TARGET_SCORE = 11
|
||||
# The traditional match target for scopone scientifico: the partnership
|
||||
# game is played to 21 points.
|
||||
DEFAULT_TARGET_SCORE = 21
|
||||
|
||||
|
||||
def team_of(seat: int) -> int:
|
||||
|
||||
@@ -35,7 +35,8 @@ class CreateTest(unittest.TestCase):
|
||||
session = _session(engine)
|
||||
self.assertEqual("A", session.players[0].team)
|
||||
self.assertIsInstance(session.state, ScoponeState)
|
||||
self.assertEqual(11, session.state.target_score)
|
||||
# The traditional partnership target.
|
||||
self.assertEqual(21, session.state.target_score)
|
||||
self.assertTrue(session.state.napola)
|
||||
|
||||
def test_create_options(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user