Add card table item
This commit is contained in:
1
server/app/items/types/card_table/__init__.py
Normal file
1
server/app/items/types/card_table/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Card table item type plugin package."""
|
||||
64
server/app/items/types/card_table/actions.py
Normal file
64
server/app/items/types/card_table/actions.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""Card table item use actions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import Callable
|
||||
|
||||
from ....item_types import ItemUseResult
|
||||
from ....models import WorldItem
|
||||
|
||||
|
||||
def _build_deck(include_jokers: bool) -> list[str]:
|
||||
"""Return a sorted list of 52 (or 54) card codes."""
|
||||
ranks = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
|
||||
suits = ["S", "H", "D", "C"]
|
||||
deck = [f"{r}{s}" for s in suits for r in ranks]
|
||||
if include_jokers:
|
||||
deck += ["JO1", "JO2"]
|
||||
return deck
|
||||
|
||||
|
||||
def use_item(item: WorldItem, nickname: str, _clock_formatter: Callable[[dict], str]) -> ItemUseResult:
|
||||
"""Return status message; client opens menu from existing state."""
|
||||
draw_pile = item.params.get("draw_pile", [])
|
||||
discard_pile = item.params.get("discard_pile", [])
|
||||
hands = item.params.get("hands", {})
|
||||
if not isinstance(draw_pile, list):
|
||||
draw_pile = []
|
||||
if not isinstance(discard_pile, list):
|
||||
discard_pile = []
|
||||
if not isinstance(hands, dict):
|
||||
hands = {}
|
||||
hand = hands.get(nickname, [])
|
||||
if not isinstance(hand, list):
|
||||
hand = []
|
||||
draw_count = len(draw_pile)
|
||||
discard_count = len(discard_pile)
|
||||
hand_count = len(hand)
|
||||
|
||||
return ItemUseResult(
|
||||
self_message=(
|
||||
f"{item.title}: {draw_count} in draw pile, "
|
||||
f"{discard_count} in discard, {hand_count} in your hand."
|
||||
),
|
||||
others_message="",
|
||||
)
|
||||
|
||||
|
||||
def secondary_use_item(item: WorldItem, nickname: str, _clock_formatter: Callable[[dict], str]) -> ItemUseResult:
|
||||
"""Shuffle and reset the card table."""
|
||||
include_jokers = bool(item.params.get("include_jokers", False))
|
||||
deck = _build_deck(include_jokers)
|
||||
random.shuffle(deck)
|
||||
total = len(deck)
|
||||
|
||||
return ItemUseResult(
|
||||
self_message=f"You reset {item.title}. {total} cards shuffled into draw pile.",
|
||||
others_message=f"{nickname} resets {item.title}.",
|
||||
updated_params={
|
||||
"draw_pile": deck,
|
||||
"discard_pile": [],
|
||||
"hands": {},
|
||||
},
|
||||
)
|
||||
39
server/app/items/types/card_table/definition.py
Normal file
39
server/app/items/types/card_table/definition.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""Card table item static metadata and defaults."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
TYPE = "card_table"
|
||||
LABEL = "Card Table"
|
||||
TOOLTIP = "A shared card table with draw pile, discard pile, and per-player hands."
|
||||
EDITABLE_PROPERTIES: tuple[str, ...] = ("title", "include_jokers")
|
||||
CAPABILITIES: tuple[str, ...] = ("editable", "carryable", "deletable", "usable")
|
||||
USE_SOUND = None
|
||||
EMIT_SOUND: str | None = None
|
||||
EMIT_RANGE = 15
|
||||
DIRECTIONAL = False
|
||||
USE_COOLDOWN_MS = 500
|
||||
DEFAULT_TITLE = "Card Table"
|
||||
PARAM_KEYS: tuple[str, ...] = ("draw_pile", "discard_pile", "hands", "include_jokers")
|
||||
|
||||
_RANKS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
|
||||
_SUITS = ["S", "H", "D", "C"]
|
||||
_FULL_DECK: list[str] = [f"{r}{s}" for s in _SUITS for r in _RANKS]
|
||||
|
||||
DEFAULT_PARAMS: dict = {
|
||||
"draw_pile": list(_FULL_DECK),
|
||||
"discard_pile": [],
|
||||
"hands": {},
|
||||
"include_jokers": False,
|
||||
}
|
||||
|
||||
PROPERTY_METADATA: dict[str, dict[str, object]] = {
|
||||
"title": {
|
||||
"valueType": "text",
|
||||
"tooltip": "Display name spoken and shown for this item.",
|
||||
"maxLength": 80,
|
||||
},
|
||||
"include_jokers": {
|
||||
"valueType": "boolean",
|
||||
"tooltip": "Include two Jokers when shuffled and reset.",
|
||||
},
|
||||
}
|
||||
17
server/app/items/types/card_table/plugin.py
Normal file
17
server/app/items/types/card_table/plugin.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Plugin registration for card table item type."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ..plugin_helpers import build_item_module
|
||||
from . import actions, definition, validator
|
||||
|
||||
ITEM_TYPE_PLUGIN = {
|
||||
"type": "card_table",
|
||||
"order": 26,
|
||||
"module": build_item_module(
|
||||
definition,
|
||||
validate_update=validator.validate_update,
|
||||
use_item=actions.use_item,
|
||||
secondary_use_item=actions.secondary_use_item,
|
||||
),
|
||||
}
|
||||
64
server/app/items/types/card_table/validator.py
Normal file
64
server/app/items/types/card_table/validator.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""Card table item validation/normalization."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ....models import WorldItem
|
||||
from ...helpers import keep_only_known_params, parse_bool_like
|
||||
from .definition import PARAM_KEYS
|
||||
|
||||
_VALID_RANKS = frozenset(["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"])
|
||||
_VALID_SUITS = frozenset(["S", "H", "D", "C"])
|
||||
_VALID_JOKERS = frozenset(["JO1", "JO2"])
|
||||
|
||||
|
||||
def _is_valid_card(code: object) -> bool:
|
||||
if not isinstance(code, str):
|
||||
return False
|
||||
if code in _VALID_JOKERS:
|
||||
return True
|
||||
if len(code) < 2:
|
||||
return False
|
||||
suit = code[-1]
|
||||
rank = code[:-1]
|
||||
return rank in _VALID_RANKS and suit in _VALID_SUITS
|
||||
|
||||
|
||||
def validate_update(_item: WorldItem, next_params: dict) -> dict:
|
||||
"""Validate and normalize card table params."""
|
||||
|
||||
draw_pile = next_params.get("draw_pile", [])
|
||||
if not isinstance(draw_pile, list):
|
||||
raise ValueError("draw_pile must be a list.")
|
||||
for card in draw_pile:
|
||||
if not _is_valid_card(card):
|
||||
raise ValueError(f"Invalid card code in draw_pile: {card!r}")
|
||||
next_params["draw_pile"] = draw_pile
|
||||
|
||||
discard_pile = next_params.get("discard_pile", [])
|
||||
if not isinstance(discard_pile, list):
|
||||
raise ValueError("discard_pile must be a list.")
|
||||
for card in discard_pile:
|
||||
if not _is_valid_card(card):
|
||||
raise ValueError(f"Invalid card code in discard_pile: {card!r}")
|
||||
next_params["discard_pile"] = discard_pile
|
||||
|
||||
hands = next_params.get("hands", {})
|
||||
if not isinstance(hands, dict):
|
||||
raise ValueError("hands must be a dict.")
|
||||
if len(hands) > 20:
|
||||
raise ValueError("Too many hands (max 20).")
|
||||
for player, hand in hands.items():
|
||||
if not isinstance(player, str):
|
||||
raise ValueError("Hand keys must be strings.")
|
||||
if not isinstance(hand, list):
|
||||
raise ValueError(f"Hand for {player!r} must be a list.")
|
||||
if len(hand) > 60:
|
||||
raise ValueError(f"Too many cards in hand for {player!r} (max 60).")
|
||||
for card in hand:
|
||||
if not _is_valid_card(card):
|
||||
raise ValueError(f"Invalid card code in hand for {player!r}: {card!r}")
|
||||
next_params["hands"] = hands
|
||||
|
||||
next_params["include_jokers"] = parse_bool_like(next_params.get("include_jokers", False), default=False)
|
||||
|
||||
return keep_only_known_params(next_params, PARAM_KEYS)
|
||||
Reference in New Issue
Block a user