This commit is contained in:
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-official-extensions"
|
||||
version = "0.6.2a0+1015"
|
||||
version = "0.6.3a0+1015"
|
||||
description = "Reviewed local command adapters and broker implementation for JWeBot"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"jwebot-sdk==0.6.2a0+1015",
|
||||
"jwebot-sdk==0.6.3a0+1015",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
|
||||
@@ -62,4 +62,4 @@ __all__ = [
|
||||
"render_feature_list",
|
||||
]
|
||||
|
||||
__version__ = "0.6.2a0+1015"
|
||||
__version__ = "0.6.3a0+1015"
|
||||
|
||||
+3
-3
@@ -5,9 +5,9 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
|
||||
PROFILE = 'single-player-games'
|
||||
AUTHOR = 'jwebot'
|
||||
DESCRIPTION = '本人虚拟金币账户、抽奖、彩票和单人21点'
|
||||
HELP_ENTRIES = ('/金币 /wallet;/抽奖 [金额] /draw;/买彩票 [号码] [金额] /lottery', '/21点 开始 [金额](金额=本局下注,默认50,空格可省略)', '/21点 要牌|停牌|状态|结束;/blackjack hit|stand|status|end', '/游戏帮助;/games')
|
||||
COMMANDS = ('/金币', '/wallet', '/抽奖', '/draw', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏帮助', '/games')
|
||||
DESCRIPTION = '虚拟金币账户、抽奖、群内多人彩票和单人21点'
|
||||
HELP_ENTRIES = ('/金币 /wallet;/抽奖 [金额] /draw;/游戏记录 /gamehistory', '/彩票 开始|报名|当前状态|开奖|取消;/lottery', '/21点 开始 [1~500](金额=本局下注,默认50,空格可省略)', '/21点 要牌|停牌|状态|结束;/blackjack hit|stand|status|end', '/游戏帮助;/games')
|
||||
COMMANDS = ('/金币', '/wallet', '/抽奖', '/draw', '/彩票', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏记录', '/个人记录', '/gamehistory', '/游戏帮助', '/games')
|
||||
|
||||
|
||||
def _handler(context: SingleGroupContext, command: str):
|
||||
|
||||
@@ -44,11 +44,15 @@ LOCAL_COMMAND_ALIASES = {
|
||||
"wallet": "wallet",
|
||||
"抽奖": "prize_draw",
|
||||
"draw": "prize_draw",
|
||||
"彩票": "lottery",
|
||||
"买彩票": "lottery",
|
||||
"lottery": "lottery",
|
||||
"21点": "blackjack",
|
||||
"二十一点": "blackjack",
|
||||
"blackjack": "blackjack",
|
||||
"游戏记录": "game_history",
|
||||
"个人记录": "game_history",
|
||||
"gamehistory": "game_history",
|
||||
"游戏帮助": "game_rules",
|
||||
"games": "game_rules",
|
||||
"人物列表": "person_directory",
|
||||
@@ -120,14 +124,22 @@ LOCAL_EXTENSIONS = (
|
||||
LocalExtensionDescriptor(
|
||||
extension_id="single-player-games",
|
||||
author=JWEBOT_AUTHOR,
|
||||
description="本人虚拟金币账户、抽奖、彩票和单人21点",
|
||||
description="虚拟金币账户、抽奖、群内多人彩票和单人21点",
|
||||
help_entries=(
|
||||
"/金币 /wallet;/抽奖 [金额] /draw;/买彩票 [号码] [金额] /lottery",
|
||||
"/21点 开始 [金额](金额=本局下注,默认50,空格可省略)",
|
||||
"/金币 /wallet;/抽奖 [金额] /draw;/游戏记录 /gamehistory",
|
||||
"/彩票 开始|报名|当前状态|开奖|取消;/lottery",
|
||||
"/21点 开始 [1~500](金额=本局下注,默认50,空格可省略)",
|
||||
"/21点 要牌|停牌|状态|结束;/blackjack hit|stand|status|end",
|
||||
"/游戏帮助;/games",
|
||||
),
|
||||
command_kinds=("wallet", "prize_draw", "lottery", "blackjack", "game_rules"),
|
||||
command_kinds=(
|
||||
"wallet",
|
||||
"prize_draw",
|
||||
"lottery",
|
||||
"blackjack",
|
||||
"game_history",
|
||||
"game_rules",
|
||||
),
|
||||
),
|
||||
LocalExtensionDescriptor(
|
||||
extension_id="person-directory",
|
||||
|
||||
+8
-1
@@ -119,7 +119,14 @@ class LocalCommandEngine:
|
||||
return self.reply_cleaner(self.context_summary(row, arguments))
|
||||
if kind in {"person_directory", "person_info"}:
|
||||
return self.reply_cleaner(self.person_index(row, kind, arguments))
|
||||
if kind in {"wallet", "prize_draw", "lottery", "blackjack", "game_rules"}:
|
||||
if kind in {
|
||||
"wallet",
|
||||
"prize_draw",
|
||||
"lottery",
|
||||
"blackjack",
|
||||
"game_history",
|
||||
"game_rules",
|
||||
}:
|
||||
return self.reply_cleaner(self.players.handle(row, kind, arguments))
|
||||
if kind == "dice":
|
||||
return self.reply_cleaner(self.dice(row, arguments))
|
||||
|
||||
+394
-35
@@ -9,8 +9,21 @@ from typing import Any
|
||||
|
||||
|
||||
INITIAL_COINS = 1000
|
||||
MAX_BET = 100
|
||||
MIN_DRAW_BET = 2
|
||||
MAX_DRAW_BET = 100
|
||||
MIN_LOTTERY_BET = 1
|
||||
MAX_LOTTERY_BET = 500
|
||||
DEFAULT_LOTTERY_BET = 10
|
||||
MIN_BLACKJACK_BET = 1
|
||||
MAX_BLACKJACK_BET = 500
|
||||
GAME_TIMEOUT_SECONDS = 600
|
||||
GAME_HISTORY_LIMIT = 50
|
||||
GAME_HISTORY_PAGE_SIZE = 5
|
||||
GAME_LABELS = {
|
||||
"prize_draw": "抽奖",
|
||||
"lottery": "彩票",
|
||||
"blackjack": "21点",
|
||||
}
|
||||
BLACKJACK_ACTIONS = {
|
||||
"要牌": "hit",
|
||||
"hit": "hit",
|
||||
@@ -30,6 +43,68 @@ BLACKJACK_COMBINED_ACTION = re.compile(
|
||||
)
|
||||
|
||||
|
||||
def normalize_player_account(account: dict[str, Any]) -> dict[str, Any]:
|
||||
account.setdefault("balance", INITIAL_COINS)
|
||||
account.setdefault("game", None)
|
||||
account.setdefault("wins", 0)
|
||||
account.setdefault("losses", 0)
|
||||
account.setdefault("ties", 0)
|
||||
counts = account.get("game_counts")
|
||||
if not isinstance(counts, dict):
|
||||
counts = {}
|
||||
account["game_counts"] = counts
|
||||
for kind in GAME_LABELS:
|
||||
value = counts.get(kind, 0)
|
||||
counts[kind] = value if type(value) is int and value >= 0 else 0
|
||||
history = account.get("game_history")
|
||||
if not isinstance(history, list):
|
||||
history = []
|
||||
account["game_history"] = [
|
||||
entry for entry in history if isinstance(entry, dict)
|
||||
][-GAME_HISTORY_LIMIT:]
|
||||
return account
|
||||
|
||||
|
||||
def next_game_index(account: dict[str, Any], kind: str) -> int:
|
||||
normalize_player_account(account)
|
||||
counts = account["game_counts"]
|
||||
counts[kind] = int(counts.get(kind, 0)) + 1
|
||||
return int(counts[kind])
|
||||
|
||||
|
||||
def append_game_record(
|
||||
account: dict[str, Any],
|
||||
*,
|
||||
kind: str,
|
||||
index: int,
|
||||
outcome: str,
|
||||
stake: int,
|
||||
payout: int,
|
||||
summary: str,
|
||||
) -> None:
|
||||
normalize_player_account(account)
|
||||
history = [
|
||||
entry
|
||||
for entry in account["game_history"]
|
||||
if not (
|
||||
entry.get("kind") == kind
|
||||
and int(entry.get("index", 0)) == index
|
||||
)
|
||||
]
|
||||
history.append(
|
||||
{
|
||||
"kind": kind,
|
||||
"index": index,
|
||||
"outcome": outcome[:80],
|
||||
"stake": stake,
|
||||
"payout": payout,
|
||||
"summary": summary[:240],
|
||||
"completed_at": time.time(),
|
||||
}
|
||||
)
|
||||
account["game_history"] = history[-GAME_HISTORY_LIMIT:]
|
||||
|
||||
|
||||
def card_label(card: int) -> str:
|
||||
return ("A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K")[card % 13]
|
||||
|
||||
@@ -52,13 +127,15 @@ class PlayerGames:
|
||||
@staticmethod
|
||||
def rules() -> str:
|
||||
return (
|
||||
"单人游戏:每人首次使用获 1000 虚拟金币;每次下注 2~100。\n"
|
||||
"/金币 /wallet;/抽奖 [金额] /draw;/买彩票 [000~999] [金额] /lottery;"
|
||||
"21点先用 /21点 开始 [金额] 进场,金额是本局下注,默认50;空格可省略。\n"
|
||||
"每人首次使用获 1000 虚拟金币;/金币 查看余额;/游戏记录 查看本人记录。\n"
|
||||
"抽奖:/抽奖 [2~100],5%返5倍、20%返2倍,其余无返还。\n"
|
||||
"多人彩票:/彩票 开始;/彩票 报名 3 4 7 [1~500];"
|
||||
"/彩票 当前状态;/彩票 开奖。三位号码顺序无关,省略金额默认10;"
|
||||
"命中1/2/3个返下注的50%/120%/300%。\n"
|
||||
"21点先用 /21点 开始 [1~500] 进场,金额是本局下注,默认50;空格可省略。\n"
|
||||
"看到牌后再单独发送 /21点 要牌|停牌|状态|结束"
|
||||
"(/blackjack hit|stand|status|end);"
|
||||
"每人最多一局,10分钟无操作视为认输,结束不退下注。\n"
|
||||
"抽奖:5%返5倍、20%返2倍,其余无返还;彩票命中返500倍;"
|
||||
"21点获胜返2倍、平局返下注、自然21点返2.5倍向下取整,庄家软17停牌。"
|
||||
"以上均含本金;虚拟金币不能充值、转账或兑换。"
|
||||
)
|
||||
@@ -69,18 +146,16 @@ class PlayerGames:
|
||||
return self.rules()
|
||||
if arguments.casefold() in {"帮助", "规则", "help", "rules"}:
|
||||
return self.rules()
|
||||
if kind == "game_history":
|
||||
return self.game_history(row, arguments)
|
||||
if kind == "lottery":
|
||||
return self.group_lottery(row, arguments)
|
||||
if kind == "wallet" and arguments:
|
||||
return "用法:/金币(/wallet),查看本人的虚拟金币;/游戏帮助 查看规则"
|
||||
if kind == "prize_draw":
|
||||
if arguments and not re.fullmatch(r"\d{1,3}", arguments):
|
||||
return "用法:/抽奖 [2~100金币](/draw),默认10金币"
|
||||
bet = int(arguments or 10)
|
||||
elif kind == "lottery":
|
||||
match = re.fullmatch(r"(?:(\d{3})(?:\s+(\d{1,3}))?)?", arguments)
|
||||
if match is None:
|
||||
return "用法:/买彩票 [三位号码000~999] [2~100金币](/lottery),默认随机号码、10金币"
|
||||
number = int(match[1]) if match[1] is not None else None
|
||||
bet = int(match[2] or 10)
|
||||
elif kind == "blackjack":
|
||||
compacted = re.sub(r"\s+", "", arguments.casefold()) or "状态"
|
||||
start = BLACKJACK_START.fullmatch(compacted)
|
||||
@@ -100,27 +175,48 @@ class PlayerGames:
|
||||
"/21点 要牌 或 /21点 停牌"
|
||||
)
|
||||
return (
|
||||
"用法:先 /21点 开始 [2~100](金额是本局下注,默认50);"
|
||||
"用法:先 /21点 开始 [1~500](金额是本局下注,默认50);"
|
||||
"开局后再单独发送 /21点 要牌|停牌|状态|结束;空格可省略"
|
||||
)
|
||||
else:
|
||||
bet = 0
|
||||
if kind in {"prize_draw", "lottery"} or kind == "blackjack" and action == "start":
|
||||
if not 2 <= bet <= MAX_BET:
|
||||
return "下注必须为 2~100 的整数金币"
|
||||
if kind == "prize_draw" and not MIN_DRAW_BET <= bet <= MAX_DRAW_BET:
|
||||
return "抽奖下注必须为 2~100 的整数金币"
|
||||
if (
|
||||
kind == "blackjack"
|
||||
and action == "start"
|
||||
and not MIN_BLACKJACK_BET <= bet <= MAX_BLACKJACK_BET
|
||||
):
|
||||
return "21点下注必须为 1~500 的整数金币"
|
||||
person = self.ledger.resolve_person(
|
||||
str(row["group_fingerprint"]),
|
||||
str(row["sender"]),
|
||||
)
|
||||
player_label = self.display_label(person["display_name"])
|
||||
|
||||
def blackjack_reply(text: str) -> str:
|
||||
return f"{player_label}:{text}"
|
||||
def player_reply(text: str, index: int | None = None) -> str:
|
||||
suffix = f" #{index}" if index is not None else ""
|
||||
return f"{player_label}{suffix}:{text}"
|
||||
|
||||
def execute(account: dict[str, Any]) -> str:
|
||||
normalize_player_account(account)
|
||||
expired = ""
|
||||
game = account.get("game")
|
||||
if game and (
|
||||
type(game.get("index")) is not int
|
||||
or int(game["index"]) < 1
|
||||
):
|
||||
game["index"] = next_game_index(account, "blackjack")
|
||||
if game and time.time() - float(game["updated_at"]) >= GAME_TIMEOUT_SECONDS:
|
||||
append_game_record(
|
||||
account,
|
||||
kind="blackjack",
|
||||
index=int(game["index"]),
|
||||
outcome="超时认输",
|
||||
stake=int(game["bet"]),
|
||||
payout=0,
|
||||
summary="10分钟无有效操作,按认输结束",
|
||||
)
|
||||
account["game"] = None
|
||||
account["losses"] += 1
|
||||
expired = "上一局已超时,按认输结束;"
|
||||
@@ -130,44 +226,285 @@ class PlayerGames:
|
||||
f"已结算胜 {account['wins']} / 负 {account['losses']} / 平 {account['ties']}。"
|
||||
"首次赠送1000仅发一次;/游戏帮助 查看规则")
|
||||
if kind == "blackjack" and action in {"hit", "stand", "status", "end"}:
|
||||
return blackjack_reply(
|
||||
expired + self.play_blackjack(account, action)
|
||||
active = account.get("game")
|
||||
index = int(active["index"]) if active else None
|
||||
return player_reply(
|
||||
expired + self.play_blackjack(account, action),
|
||||
index,
|
||||
)
|
||||
if account.get("game"):
|
||||
message = expired + "你已有一局21点,请先 /21点 停牌 或 /21点 结束"
|
||||
return blackjack_reply(message) if kind == "blackjack" else message
|
||||
return (
|
||||
player_reply(message, int(account["game"]["index"]))
|
||||
if kind == "blackjack"
|
||||
else message
|
||||
)
|
||||
if account["balance"] < bet:
|
||||
message = expired + f"金币不足:余额 {account['balance']},本次需要 {bet}"
|
||||
return blackjack_reply(message) if kind == "blackjack" else message
|
||||
return player_reply(message) if kind == "blackjack" else message
|
||||
account["balance"] -= bet
|
||||
if kind == "blackjack":
|
||||
index = next_game_index(account, "blackjack")
|
||||
deck = list(range(52))
|
||||
self.rng.shuffle(deck)
|
||||
game = {"bet": bet, "deck": deck[4:], "player": deck[:2],
|
||||
"dealer": deck[2:4], "updated_at": time.time()}
|
||||
"dealer": deck[2:4], "updated_at": time.time(),
|
||||
"index": index}
|
||||
account["game"] = game
|
||||
if hand_value(game["player"]) == 21 or hand_value(game["dealer"]) == 21:
|
||||
return blackjack_reply(
|
||||
expired + self.finish_blackjack(account, natural=True)
|
||||
return player_reply(
|
||||
expired + self.finish_blackjack(account, natural=True),
|
||||
index,
|
||||
)
|
||||
return blackjack_reply(expired + self.show_hand(game))
|
||||
if kind == "prize_draw":
|
||||
roll = self.rng.randrange(100)
|
||||
payout = bet * (5 if roll < 5 else 2 if roll < 25 else 0)
|
||||
result = "大奖" if roll < 5 else "小奖" if roll < 25 else "未中奖"
|
||||
else:
|
||||
selected = self.rng.randrange(1000) if number is None else number
|
||||
winning = self.rng.randrange(1000)
|
||||
payout = bet * 500 if selected == winning else 0
|
||||
result = f"号码 {selected:03d};开奖号 {winning:03d};" + ("中奖" if payout else "未中奖")
|
||||
return player_reply(expired + self.show_hand(game), index)
|
||||
index = next_game_index(account, "prize_draw")
|
||||
roll = self.rng.randrange(100)
|
||||
payout = bet * (5 if roll < 5 else 2 if roll < 25 else 0)
|
||||
result = "大奖" if roll < 5 else "小奖" if roll < 25 else "未中奖"
|
||||
account["balance"] += payout
|
||||
account["wins" if payout > bet else "losses"] += 1
|
||||
return expired + f"{result};下注 {bet},返还 {payout},净变化 {payout - bet:+d};金币 {account['balance']}"
|
||||
append_game_record(
|
||||
account,
|
||||
kind="prize_draw",
|
||||
index=index,
|
||||
outcome=result,
|
||||
stake=bet,
|
||||
payout=payout,
|
||||
summary=f"抽奖结果 {result}",
|
||||
)
|
||||
return player_reply(
|
||||
expired
|
||||
+ f"{result};下注 {bet},返还 {payout},"
|
||||
f"净变化 {payout - bet:+d};金币 {account['balance']}",
|
||||
index,
|
||||
)
|
||||
|
||||
return self.ledger.apply_player_action(
|
||||
str(row["group_fingerprint"]), str(row["sender"]),
|
||||
str(row["event_id"]), kind + ":" + arguments, execute)
|
||||
|
||||
@staticmethod
|
||||
def parse_lottery_entry(
|
||||
arguments: str,
|
||||
) -> tuple[tuple[int, int, int], int] | None:
|
||||
value = arguments.strip().casefold()
|
||||
for prefix in ("报名", "join"):
|
||||
if value.startswith(prefix):
|
||||
value = value[len(prefix):].strip()
|
||||
break
|
||||
if not value:
|
||||
return None
|
||||
marked = re.fullmatch(
|
||||
r"([0-9])\s*([0-9])\s*([0-9])"
|
||||
r"(?:金额|下注|bet)(\d{1,3})",
|
||||
re.sub(r"[,,、]", "", value),
|
||||
)
|
||||
stake: int
|
||||
if marked is not None:
|
||||
digits = tuple(int(marked[index]) for index in range(1, 4))
|
||||
stake = int(marked[4])
|
||||
else:
|
||||
tokens = re.sub(r"[,,、]", " ", value).split()
|
||||
digits: tuple[int, int, int]
|
||||
if len(tokens) == 1 and tokens[0].isdigit():
|
||||
compact = tokens[0]
|
||||
if not 3 <= len(compact) <= 6:
|
||||
return None
|
||||
digits = tuple(int(item) for item in compact[:3])
|
||||
stake = int(compact[3:] or DEFAULT_LOTTERY_BET)
|
||||
elif (
|
||||
len(tokens) == 2
|
||||
and re.fullmatch(r"\d{3}", tokens[0])
|
||||
and re.fullmatch(r"\d{1,3}", tokens[1])
|
||||
):
|
||||
digits = tuple(int(item) for item in tokens[0])
|
||||
stake = int(tokens[1])
|
||||
elif (
|
||||
len(tokens) in {3, 4}
|
||||
and all(re.fullmatch(r"\d", item) for item in tokens[:3])
|
||||
and (
|
||||
len(tokens) == 3
|
||||
or re.fullmatch(r"\d{1,3}", tokens[3])
|
||||
)
|
||||
):
|
||||
digits = tuple(int(item) for item in tokens[:3])
|
||||
stake = (
|
||||
int(tokens[3])
|
||||
if len(tokens) == 4
|
||||
else DEFAULT_LOTTERY_BET
|
||||
)
|
||||
else:
|
||||
return None
|
||||
if len(set(digits)) != 3:
|
||||
raise ValueError("彩票的三个数字必须互不重复")
|
||||
if not MIN_LOTTERY_BET <= stake <= MAX_LOTTERY_BET:
|
||||
raise ValueError("彩票下注必须为 1~500 的整数金币")
|
||||
return tuple(sorted(digits)), stake
|
||||
|
||||
def group_lottery(self, row: dict[str, Any], arguments: str) -> str:
|
||||
group = str(row["group_fingerprint"])
|
||||
sender = str(row["sender"])
|
||||
event_id = str(row["event_id"])
|
||||
command = "lottery:" + arguments
|
||||
compacted = re.sub(r"\s+", "", arguments.casefold())
|
||||
|
||||
def status_text(data: dict[str, Any]) -> str:
|
||||
if data["status"] == "no_round":
|
||||
return "当前没有进行中的彩票;发送 /彩票 开始 创建新一把"
|
||||
entries = data["entries"]
|
||||
lines = [
|
||||
f"彩票第 {data['round_number']} 把正在报名;"
|
||||
f"主持人 {data['host_display_name']};已报名 {len(entries)} 人"
|
||||
]
|
||||
for entry in entries:
|
||||
lines.append(
|
||||
f"{entry['display_name']} #{entry['player_game_index']}:"
|
||||
f"{' '.join(map(str, entry['numbers']))},下注 {entry['stake']}"
|
||||
)
|
||||
if not entries:
|
||||
lines.append("暂无报名;/彩票 报名 3 4 7 [金额]")
|
||||
return "\n".join(lines)
|
||||
|
||||
if compacted in {"开始", "start"}:
|
||||
return self.ledger.start_group_lottery(
|
||||
group,
|
||||
sender,
|
||||
event_id,
|
||||
command,
|
||||
lambda data: (
|
||||
f"彩票第 {data['round_number']} 把"
|
||||
+ ("已经在报名中" if data["status"] == "already_open" else "已开始")
|
||||
+ f";主持人 {data['host_display_name']}。\n"
|
||||
"报名:/彩票 3 4 7 [金额](金额1~500,默认10,空格可省略);"
|
||||
"查询:/彩票 当前状态;主持人开奖:/彩票 开奖"
|
||||
),
|
||||
)
|
||||
if compacted in {"当前状态", "状态", "status"}:
|
||||
return self.ledger.group_lottery_status(
|
||||
group,
|
||||
sender,
|
||||
event_id,
|
||||
command,
|
||||
status_text,
|
||||
)
|
||||
if compacted in {"开奖", "draw"}:
|
||||
drawn = tuple(sorted(self.rng.sample(range(10), 3)))
|
||||
|
||||
def draw_text(data: dict[str, Any]) -> str:
|
||||
if data["status"] == "no_round":
|
||||
return "当前没有可以开奖的彩票"
|
||||
if data["status"] == "not_host":
|
||||
return (
|
||||
f"只有本轮主持人 {data['host_display_name']} 可以开奖"
|
||||
)
|
||||
if data["status"] == "empty":
|
||||
return "本轮还没有人报名,不能开奖"
|
||||
lines = [
|
||||
f"彩票第 {data['round_number']} 把开奖号:"
|
||||
f"{' '.join(map(str, data['drawn_numbers']))}"
|
||||
]
|
||||
for entry in data["entries"]:
|
||||
lines.append(
|
||||
f"{entry['display_name']} #{entry['player_game_index']}:"
|
||||
f"命中 {entry['matches']} 个;下注 {entry['stake']},"
|
||||
f"返还 {entry['payout']},净变化 "
|
||||
f"{entry['payout'] - entry['stake']:+d};"
|
||||
f"金币 {entry['balance']}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
return self.ledger.draw_group_lottery(
|
||||
group,
|
||||
sender,
|
||||
event_id,
|
||||
command,
|
||||
drawn,
|
||||
draw_text,
|
||||
)
|
||||
if compacted in {"取消", "cancel"}:
|
||||
return self.ledger.cancel_group_lottery(
|
||||
group,
|
||||
sender,
|
||||
event_id,
|
||||
command,
|
||||
lambda data: (
|
||||
"当前没有进行中的彩票"
|
||||
if data["status"] == "no_round"
|
||||
else (
|
||||
f"只有本轮主持人 {data['host_display_name']} 可以取消"
|
||||
if data["status"] == "not_host"
|
||||
else (
|
||||
f"彩票第 {data['round_number']} 把已取消;"
|
||||
f"{len(data['entries'])} 人的下注已全额退回"
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
try:
|
||||
parsed = self.parse_lottery_entry(arguments)
|
||||
except ValueError as error:
|
||||
return str(error)
|
||||
if parsed is None:
|
||||
return (
|
||||
"用法:先 /彩票 开始;再 /彩票 报名 3 4 7 [金额]"
|
||||
"(也可 /彩票347,金额1~500、默认10);"
|
||||
"/彩票 当前状态;主持人 /彩票 开奖"
|
||||
)
|
||||
numbers, stake = parsed
|
||||
|
||||
def register_text(data: dict[str, Any]) -> str:
|
||||
if data["status"] == "no_round":
|
||||
return "当前没有进行中的彩票;先发送 /彩票 开始"
|
||||
if data["status"] == "insufficient":
|
||||
return (
|
||||
f"{data['display_name']}:金币不足,余额 {data['balance']},"
|
||||
f"报名需要 {stake}"
|
||||
)
|
||||
action = "报名已更新" if data["status"] == "updated" else "报名成功"
|
||||
return (
|
||||
f"{data['display_name']} #{data['player_game_index']}:"
|
||||
f"彩票第 {data['round_number']} 把{action};号码 "
|
||||
f"{' '.join(map(str, data['numbers']))};下注 {data['stake']};"
|
||||
f"金币 {data['balance']}"
|
||||
)
|
||||
|
||||
return self.ledger.register_group_lottery(
|
||||
group,
|
||||
sender,
|
||||
event_id,
|
||||
command,
|
||||
numbers,
|
||||
stake,
|
||||
register_text,
|
||||
)
|
||||
|
||||
def game_history(self, row: dict[str, Any], arguments: str) -> str:
|
||||
if arguments and not re.fullmatch(r"\d{1,3}", arguments):
|
||||
return "用法:/游戏记录 [页码](/gamehistory),每页5条"
|
||||
page = int(arguments or 1)
|
||||
if page < 1:
|
||||
return "游戏记录页码必须从 1 开始"
|
||||
data = self.ledger.player_game_history(
|
||||
str(row["group_fingerprint"]),
|
||||
str(row["sender"]),
|
||||
)
|
||||
records = data["history"]
|
||||
pages = max(1, (len(records) + GAME_HISTORY_PAGE_SIZE - 1) // GAME_HISTORY_PAGE_SIZE)
|
||||
if page > pages:
|
||||
return f"{data['display_name']} 只有 {pages} 页游戏记录"
|
||||
start = (page - 1) * GAME_HISTORY_PAGE_SIZE
|
||||
lines = [f"{data['display_name']} 的游戏记录 {page}/{pages}:"]
|
||||
for record in records[start:start + GAME_HISTORY_PAGE_SIZE]:
|
||||
label = GAME_LABELS.get(str(record["kind"]), str(record["kind"]))
|
||||
lines.append(
|
||||
f"{data['display_name']} #{record['index']}:{label} {record['outcome']};"
|
||||
f"下注 {record['stake']},返还 {record['payout']},"
|
||||
f"净变化 {record['payout'] - record['stake']:+d}"
|
||||
)
|
||||
if not records:
|
||||
lines.append("暂无已结算记录")
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def show_hand(game: dict[str, Any]) -> str:
|
||||
player = " ".join(card_label(card) for card in game["player"])
|
||||
@@ -182,6 +519,17 @@ class PlayerGames:
|
||||
if action == "status":
|
||||
return self.show_hand(game)
|
||||
if action == "end":
|
||||
index = int(game["index"])
|
||||
bet = int(game["bet"])
|
||||
append_game_record(
|
||||
account,
|
||||
kind="blackjack",
|
||||
index=index,
|
||||
outcome="主动认输",
|
||||
stake=bet,
|
||||
payout=0,
|
||||
summary="主动结束牌局,不退下注",
|
||||
)
|
||||
account["game"] = None
|
||||
account["losses"] += 1
|
||||
return f"本人的21点已认输结束,不退下注;金币 {account['balance']}"
|
||||
@@ -213,6 +561,17 @@ class PlayerGames:
|
||||
outcome, payout, stat = "你输了", 0, "losses"
|
||||
account["balance"] += payout
|
||||
account[stat] += 1
|
||||
append_game_record(
|
||||
account,
|
||||
kind="blackjack",
|
||||
index=int(game["index"]),
|
||||
outcome=outcome,
|
||||
stake=int(bet),
|
||||
payout=int(payout),
|
||||
summary=(
|
||||
f"玩家{player}点,庄家{dealer}点"
|
||||
),
|
||||
)
|
||||
account["game"] = None
|
||||
return (f"21点结算:{outcome};你的牌 {' '.join(map(card_label, game['player']))}({player}点),"
|
||||
f"庄家 {' '.join(map(card_label, game['dealer']))}({dealer}点);"
|
||||
|
||||
Reference in New Issue
Block a user