Add multiplayer blackjack table flow
check / local-gate (push) Failing after 51s

This commit is contained in:
JWeBot Contributors
2026-10-16 11:03:32 +08:00
parent afc71e63a7
commit 3fa04755d0
24 changed files with 1959 additions and 14 deletions
@@ -0,0 +1,45 @@
"""Generated reviewed adapter for coin-taffy."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
PROFILE = 'coin-taffy'
AUTHOR = 'jwebot'
DESCRIPTION = 'Taffy金币'
HELP_ENTRIES = ('/Taffy金币',)
COMMANDS = ('/Taffy金币', '/taffycoins')
def _handler(context: SingleGroupContext, command: str):
def handle(event: Event, arguments: str) -> ExtensionResult:
response = context.services.call(
PROFILE,
operation="execute",
payload={
"command": command,
"arguments": arguments,
"event": event.to_mapping(),
},
)
texts = response.get("texts")
if isinstance(texts, list) and texts and all(
isinstance(item, str) and item.strip() for item in texts
):
return ExtensionResult(
reply=ReplyPlan.multi_text(texts),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
text = response.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("local service returned invalid text")
return ExtensionResult(
reply=ReplyPlan.text(text),
metadata={"local_extension": PROFILE, "author": AUTHOR},
)
return handle
def plugin(context: SingleGroupContext) -> None:
for command in COMMANDS:
context.commands.register(command, _handler(context, command))
@@ -5,7 +5,7 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
PROFILE = 'context-summary'
AUTHOR = 'official'
DESCRIPTION = '提取当前群最近100、200或300条本地文字的上下文'
DESCRIPTION = '上下文总结'
HELP_ENTRIES = ('/上下文 [100|200|300];/context [100|200|300]',)
COMMANDS = ('/上下文', '/context')
@@ -6,7 +6,7 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
PROFILE = 'single-player-games'
AUTHOR = 'jwebot'
DESCRIPTION = '金币游戏'
HELP_ENTRIES = ('/金币 /wallet;/游戏记录 /gamehistory', '/抽奖 [金额];/draw', '/彩票 开始|报名|状态|开奖|取消;/lottery', '/21点 开始 [1~500];/blackjack start', '/21点 要牌|停牌|状态|结束;/blackjack', '/游戏帮助;/games')
HELP_ENTRIES = ('/金币 /wallet;/游戏记录 /gamehistory', '/抽奖 [金额];/draw', '/彩票 开始|报名|状态|开奖|取消;/lottery', '/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200', '/游戏帮助;/games')
COMMANDS = ('/金币', '/wallet', '/抽奖', '/draw', '/彩票', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏记录', '/个人记录', '/gamehistory', '/游戏帮助', '/games')
@@ -46,6 +46,8 @@ LOCAL_COMMAND_ALIASES = {
"金币排行榜": "coin_ranking",
"大富翁榜": "coin_ranking",
"richlist": "coin_ranking",
"Taffy金币": "taffy_coins",
"taffycoins": "taffy_coins",
"转账": "transfer",
"transfer": "transfer",
"贷款": "loan",
@@ -143,7 +145,7 @@ LOCAL_EXTENSIONS = (
LocalExtensionDescriptor(
extension_id="context-summary",
author=OFFICIAL_AUTHOR,
description="提取当前群最近100、200或300条本地文字的上下文",
description="上下文总结",
help_entries=("/上下文 [100|200|300];/context [100|200|300]",),
command_kinds=("context_summary",),
),
@@ -155,8 +157,7 @@ LOCAL_EXTENSIONS = (
"/金币 /wallet;/游戏记录 /gamehistory",
"/抽奖 [金额];/draw",
"/彩票 开始|报名|状态|开奖|取消;/lottery",
"/21点 开始 [1~500];/blackjack start",
"/21点 要牌|停牌|状态|结束;/blackjack",
"/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200",
"/游戏帮助;/games",
),
command_kinds=(
@@ -182,6 +183,13 @@ LOCAL_EXTENSIONS = (
help_entries=("/大富翁榜",),
command_kinds=("coin_ranking",),
),
LocalExtensionDescriptor(
extension_id="coin-taffy",
author=JWEBOT_AUTHOR,
description="Taffy金币",
help_entries=("/Taffy金币",),
command_kinds=("taffy_coins",),
),
LocalExtensionDescriptor(
extension_id="coin-loan",
author=JWEBOT_AUTHOR,
@@ -157,6 +157,7 @@ class LocalCommandEngine:
if kind in {
"wallet",
"coin_ranking",
"taffy_coins",
"transfer",
"loan",
"repay",
@@ -16,6 +16,9 @@ MAX_LOTTERY_BET = 500
DEFAULT_LOTTERY_BET = 10
MIN_BLACKJACK_BET = 1
MAX_BLACKJACK_BET = 500
MIN_BLACKJACK_TABLE_BET = 1
MAX_BLACKJACK_TABLE_BET = 10_000
MAX_BLACKJACK_TABLE_PLAYERS = 8
MIN_TRANSFER_AMOUNT = 1
MAX_TRANSFER_AMOUNT = 100_000
MIN_LOAN_AMOUNT = 1
@@ -45,6 +48,10 @@ BLACKJACK_COMBINED_ACTION = re.compile(
r"^(?:开始|start)(?P<bet>\d{1,3})?"
r"(?:要牌|hit|停牌|stand)$"
)
BLACKJACK_TABLE_BET = re.compile(
r"^(?:放|下注|bet)(?P<bet>\d{1,5})$",
re.IGNORECASE,
)
TRANSFER_MARKED = re.compile(
r"^(?:给|to)?\s*(?P<target>.+?)\s*"
r"(?:金额|金币|coin|coins|:|:)\s*(?P<amount>\d{1,6})$",
@@ -161,6 +168,9 @@ class PlayerGames:
"看到牌后再单独发送 /21点 要牌|停牌|状态|结束"
"(/blackjack hit|stand|status|end);"
"每人最多一局,10分钟无操作视为认输,结束不退下注。\n"
"多人21点:/21点 多人 启动 创建牌桌;/21点 加入;"
"主持人 /21点 多人 开始 后按提示 /21点 放200"
"(1~10000);轮到自己 /21点 要牌|停牌。\n"
"21点获胜返2倍、平局返下注、自然21点返2.5倍向下取整,庄家软17停牌。"
"以上均含本金;虚拟金币不能充值或兑换。"
)
@@ -177,6 +187,8 @@ class PlayerGames:
return self.group_lottery(row, arguments)
if kind == "coin_ranking":
return self.coin_ranking(row, arguments)
if kind == "taffy_coins":
return self.taffy_coins(row, arguments)
if kind == "transfer":
return self.transfer(row, arguments)
if kind in {"loan", "repay"}:
@@ -188,6 +200,15 @@ class PlayerGames:
return "用法:/抽奖 [2~100金币](/draw),默认10金币"
bet = int(arguments or 10)
elif kind == "blackjack":
table_command = self.parse_blackjack_table_command(arguments)
if table_command is not None:
table_action, table_bet = table_command
return self.group_blackjack(
row,
table_action,
table_bet,
arguments,
)
compacted = re.sub(r"\s+", "", arguments.casefold()) or "状态"
start = BLACKJACK_START.fullmatch(compacted)
if start is not None:
@@ -209,6 +230,28 @@ class PlayerGames:
"用法:先 /21点 开始 [1~500](金额是本局下注,默认50);"
"开局后再单独发送 /21点 要牌|停牌|状态|结束;空格可省略"
)
group = str(row["group_fingerprint"])
if (
action in {"hit", "stand", "status"}
and self.ledger.has_active_group_blackjack(group)
):
return self.group_blackjack(row, action, 0, arguments)
if (
action == "start"
and self.ledger.has_active_group_blackjack(group)
):
return (
"当前已有Taffy的21点多人牌桌;"
"发送 /21点 状态 查看,或等待本桌结束"
)
if (
action == "end"
and self.ledger.has_active_group_blackjack(group)
):
return (
"当前是多人21点牌桌;轮到你时请 /21点 停牌,"
"主持人可 /21点 多人 取消"
)
else:
bet = 0
if kind == "prize_draw" and not MIN_DRAW_BET <= bet <= MAX_DRAW_BET:
@@ -341,6 +384,451 @@ class PlayerGames:
)
return "\n".join(lines)
def taffy_coins(self, row: dict[str, Any], arguments: str) -> str:
value = arguments.strip()
if value and not re.fullmatch(r"\d{1,2}", value):
return "用法:/Taffy金币 [人数]"
limit = int(value or 8)
if not 1 <= limit <= 20:
return "Taffy金币人数必须为 1~20"
stats = self.ledger.player_taffy_coin_stats(
str(row["group_fingerprint"]),
limit=limit,
)
if int(stats["records"]) == 0:
return "Taffy金币:当前群暂无已结算游戏记录"
net = int(stats["taffy_net"])
result = f"Taffy累计赢 {net} 金币" if net >= 0 else f"Taffy累计倒贴 {-net} 金币"
lines = [
(
f"Taffy金币:已结算 {stats['records']} 局;"
f"玩家总下注 {stats['stake']},总返还 {stats['payout']};"
f"{result}"
)
]
if stats["players"]:
lines.append("最大上贡者排行(含负数):")
else:
lines.append("最大上贡者排行:暂无已结算玩家明细")
for index, record in enumerate(stats["players"], start=1):
player_net = int(record["taffy_net"])
if player_net > 0:
verdict = f"上贡 {player_net}"
elif player_net < 0:
verdict = f"从Taffy赢走 {-player_net}"
else:
verdict = "打平 0"
lines.append(
f"{index}. {record['display_name']}:{verdict};"
f"下注 {record['stake']},返还 {record['payout']},"
f"{record['records']} 局"
)
return "\n".join(lines)
@staticmethod
def parse_blackjack_table_command(
arguments: str,
) -> tuple[str, int] | None:
compacted = re.sub(r"\s+", "", arguments.casefold())
if not compacted:
return None
if compacted in {"多人", "牌桌", "table", "multiplayer"}:
return "help", 0
if compacted in {
"多人启动",
"多人开桌",
"多人创建",
"牌桌启动",
"牌桌开桌",
"tablestart",
"multiplayerstart",
}:
return "start", 0
if compacted in {"加入", "join"}:
return "join", 0
if compacted in {
"多人开始",
"多人发牌",
"开始多人",
"开桌",
"发牌",
"tabledeal",
"multiplayerdeal",
}:
return "begin", 0
if compacted in {
"多人取消",
"牌桌取消",
"取消牌桌",
"tablecancel",
"multiplayercancel",
}:
return "cancel", 0
bet = BLACKJACK_TABLE_BET.fullmatch(compacted)
if bet is not None:
return "bet", int(bet["bet"])
return None
@staticmethod
def blackjack_players_text(players: list[dict[str, Any]]) -> str:
names = [str(player["display_name"]) for player in players]
return "、".join(names) if names else "暂无"
@staticmethod
def blackjack_hand_text(hand: list[int]) -> str:
if not hand:
return "未发牌"
return (
f"{' '.join(card_label(card) for card in hand)}"
f"({hand_value(hand)}点)"
)
@staticmethod
def blackjack_prompt(player: dict[str, Any]) -> str:
return (
f"请{player['display_name']} 放自己的筹码\n"
"输入/21点 放200(1~10000)"
)
def group_blackjack(
self,
row: dict[str, Any],
action: str,
amount: int,
arguments: str,
) -> str:
group = str(row["group_fingerprint"])
sender = str(row["sender"])
event_id = str(row["event_id"])
command = f"blackjack-table:{arguments}"
if action == "help":
return (
"多人21点用法:/21点 多人 启动;/21点 加入;"
"主持人 /21点 多人 开始;按提示 /21点 放200"
"(1~10000);轮到自己 /21点 要牌 或 /21点 停牌"
)
if action == "start":
return self.ledger.start_group_blackjack(
group,
sender,
event_id,
command,
max_players=MAX_BLACKJACK_TABLE_PLAYERS,
render=self.render_blackjack_table_start,
)
if action == "join":
return self.ledger.join_group_blackjack(
group,
sender,
event_id,
command,
max_players=MAX_BLACKJACK_TABLE_PLAYERS,
render=self.render_blackjack_table_join,
)
if action == "begin":
return self.ledger.begin_group_blackjack_betting(
group,
sender,
event_id,
command,
self.render_blackjack_table_begin,
)
if action == "cancel":
return self.ledger.cancel_group_blackjack(
group,
sender,
event_id,
command,
self.render_blackjack_table_cancel,
)
if action == "status":
return self.ledger.group_blackjack_status(
group,
sender,
event_id,
command,
self.render_blackjack_table_status,
)
if action == "bet":
if not MIN_BLACKJACK_TABLE_BET <= amount <= MAX_BLACKJACK_TABLE_BET:
return "21点多人牌桌筹码必须为 1~10000 的整数金币"
deck = list(range(52))
self.rng.shuffle(deck)
return self.ledger.place_group_blackjack_bet(
group,
sender,
event_id,
command,
amount,
deck,
self.render_blackjack_table_bet,
)
if action in {"hit", "stand"}:
return self.ledger.play_group_blackjack(
group,
sender,
event_id,
command,
action,
self.render_blackjack_table_action,
)
raise ValueError(f"unsupported blackjack table action: {action}")
def render_blackjack_table_start(self, data: dict[str, Any]) -> str:
status = data["status"]
if status == "private_game":
return (
f"{data['display_name']}:你已有单人21点,"
"请先 /21点 停牌 或 /21点 结束"
)
players = data.get("players", [])
prefix = (
"当前已有Taffy 的黑杰克Blackjack 21点牌桌"
if status == "already_open"
else "欢迎来到Taffy 的黑杰克Blackjack 21点牌桌"
)
return (
f"{prefix}\n"
"输入/21点 加入 加入当前游戏!"
f"目前玩家:{self.blackjack_players_text(players)}\n"
"主持人发送 /21点 多人 开始 后,"
"Taffy会按顺序喊每个人放筹码"
)
def render_blackjack_table_join(self, data: dict[str, Any]) -> str:
status = data["status"]
if status == "no_table":
return "当前没有21点多人牌桌;发送 /21点 多人 启动"
if status == "closed":
return "这桌已经停止加入;发送 /21点 状态 查看当前进度"
if status == "full":
return f"这桌已满,最多 {data['max_players']} 人"
if status == "private_game":
return (
f"{data['display_name']}:你已有单人21点,"
"请先 /21点 停牌 或 /21点 结束"
)
if status == "already_joined":
return (
f"{data['display_name']} 已经在这桌;"
f"目前玩家:{self.blackjack_players_text(data['players'])}"
)
return (
f"{data['display_name']} 已加入Taffy的21点牌桌;"
f"目前玩家:{self.blackjack_players_text(data['players'])}"
)
def render_blackjack_table_begin(self, data: dict[str, Any]) -> str:
status = data["status"]
if status == "no_table":
return "当前没有21点多人牌桌;发送 /21点 多人 启动"
if status == "not_host":
return f"只有本桌主持人 {data['host_display_name']} 可以结束加入"
if status == "not_joining":
return "这桌已经结束加入;发送 /21点 状态 查看当前进度"
if status == "not_enough":
return (
"至少需要 2 名玩家才能开始多人21点;"
f"目前玩家:{self.blackjack_players_text(data['players'])}"
)
return (
"加入结束,Taffy 按顺序收筹码。\n"
f"{self.blackjack_prompt(data['prompt_player'])}"
)
def render_blackjack_table_status(self, data: dict[str, Any]) -> str:
if data["status"] == "no_table":
return "当前没有21点多人牌桌;发送 /21点 多人 启动"
state = str(data["state"])
table_number = int(data["table_number"])
players = data["players"]
if state == "joining":
return (
f"21点牌桌第 {table_number} 桌正在加入;"
f"目前玩家:{self.blackjack_players_text(players)}\n"
"输入/21点 加入 加入当前游戏;"
"主持人 /21点 多人 开始"
)
if state == "betting":
prompt = next(
(
player for player in players
if str(player["person_key"]) == str(data["turn_person_key"])
),
None,
)
lines = [f"21点牌桌第 {table_number} 桌正在下注:"]
for player in players:
bet = int(player["bet"])
lines.append(
f"{player['display_name']} #{player['player_game_index']}:"
f"{bet if bet else '未放'}"
)
if prompt is not None:
lines.append(self.blackjack_prompt(prompt))
return "\n".join(lines)
if state == "playing":
prompt = next(
(
player for player in players
if str(player["person_key"]) == str(data["turn_person_key"])
),
None,
)
dealer = data.get("dealer", [])
dealer_text = (
f"{card_label(dealer[0])} + 暗牌"
if dealer
else "未发牌"
)
lines = [
f"21点牌桌第 {table_number} 桌正在出牌;Taffy {dealer_text}"
]
for player in players:
lines.append(
f"{player['display_name']} #{player['player_game_index']}:"
f"{self.blackjack_hand_text(player['hand'])};"
f"下注 {player['bet']};{player['state']}"
)
if prompt is not None:
lines.append(
f"请{prompt['display_name']} 操作:"
"/21点 要牌 或 /21点 停牌"
)
return "\n".join(lines)
return "当前没有进行中的21点多人牌桌"
def render_blackjack_table_bet(self, data: dict[str, Any]) -> str:
status = data["status"]
if status == "no_table":
return "当前没有21点多人牌桌;发送 /21点 多人 启动"
if status == "not_betting":
return "现在不是放筹码阶段;发送 /21点 状态 查看当前进度"
if status == "not_player":
return "你不在当前21点牌桌;下一桌可发送 /21点 加入"
if status == "wrong_turn":
return self.blackjack_prompt(data["prompt_player"])
if status == "insufficient":
return (
f"{data['display_name']}:金币不足,余额 {data['balance']},"
f"本次需要 {data['bet']}"
)
if status == "bet_accepted":
return (
f"{data['display_name']} 放下 {data['bet']};"
f"金币 {data['balance']}\n"
f"{self.blackjack_prompt(data['prompt_player'])}"
)
if status == "dealt":
return self.render_blackjack_table_dealt(data)
if status == "settled":
return self.render_blackjack_table_settlement(data)
raise ValueError(f"unsupported blackjack bet status: {status}")
def render_blackjack_table_dealt(self, data: dict[str, Any]) -> str:
dealer = data.get("dealer", [])
dealer_text = (
f"{card_label(dealer[0])} + 暗牌"
if dealer
else "未发牌"
)
lines = [
(
f"{data['display_name']} 放下 {data['bet']};"
"全部筹码已确认。Taffy 发牌。"
),
f"Taffy 明牌 {dealer_text}",
]
for player in data["players"]:
lines.append(
f"{player['display_name']} #{player['player_game_index']}:"
f"{self.blackjack_hand_text(player['hand'])};"
f"下注 {player['bet']}"
)
prompt = data["prompt_player"]
lines.append(
f"请{prompt['display_name']} 操作:/21点 要牌 或 /21点 停牌"
)
return "\n".join(lines)
def render_blackjack_table_action(self, data: dict[str, Any]) -> str:
status = data["status"]
if status == "no_table":
return "当前没有21点多人牌桌;发送 /21点 多人 启动"
if status == "not_playing":
return "现在不是出牌阶段;发送 /21点 状态 查看当前进度"
if status == "not_player":
return "你不在当前21点牌桌;下一桌可发送 /21点 加入"
if status == "wrong_turn":
prompt = data["prompt_player"]
return (
f"现在轮到 {prompt['display_name']};"
"/21点 要牌 或 /21点 停牌"
)
if status == "not_actionable":
return "你这一手已经结束;等待本桌结算"
if status == "settled":
return self.render_blackjack_table_settlement(data)
player = data["player"]
lines: list[str] = []
if status == "hit":
lines.append(
f"{player['display_name']} 要牌,拿到 "
f"{card_label(data['drawn_card'])};"
f"当前 {self.blackjack_hand_text(player['hand'])}"
)
elif status == "busted":
lines.append(
f"{player['display_name']} 要牌,拿到 "
f"{card_label(data['drawn_card'])};"
f"{self.blackjack_hand_text(player['hand'])} 爆牌"
)
elif status == "stood":
lines.append(
f"{player['display_name']} 停牌;"
f"{self.blackjack_hand_text(player['hand'])}"
)
else:
raise ValueError(f"unsupported blackjack action status: {status}")
prompt = data["prompt_player"]
lines.append(
f"请{prompt['display_name']} 操作:/21点 要牌 或 /21点 停牌"
)
return "\n".join(lines)
def render_blackjack_table_settlement(self, data: dict[str, Any]) -> str:
dealer = data.get("dealer", [])
dealer_text = self.blackjack_hand_text(dealer)
lines = [
(
"Taffy 的黑杰克Blackjack 21点牌桌"
f"第 {data['table_number']} 桌结算:"
),
f"Taffy 庄家 {dealer_text}",
"这一桌每个人的变化:",
]
for player in data["players"]:
bet = int(player["bet"])
payout = int(player["payout"])
lines.append(
f"{player['display_name']} #{player['player_game_index']}:"
f"{player['outcome']};牌 {self.blackjack_hand_text(player['hand'])};"
f"下注 {bet},返还 {payout},净变化 {payout - bet:+d};"
f"金币 {player['balance']}"
)
return "\n".join(lines)
def render_blackjack_table_cancel(self, data: dict[str, Any]) -> str:
status = data["status"]
if status == "no_table":
return "当前没有21点多人牌桌"
if status == "not_host":
return f"只有本桌主持人 {data['host_display_name']} 可以取消"
return (
f"21点牌桌第 {data['table_number']} 桌已取消;"
f"{data['refunded']} 人的下注已退回"
)
@staticmethod
def parse_transfer(
arguments: str,