Release beta 1.0.5 primogem economy
This commit is contained in:
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-official-extensions"
|
||||
version = "1.0.4b1+1016"
|
||||
version = "1.0.5b1+1016"
|
||||
description = "Reviewed local command adapters and broker implementation for JWeBot"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"jwebot-sdk==1.0.4b1+1016",
|
||||
"jwebot-sdk==1.0.5b1+1016",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
|
||||
@@ -90,4 +90,4 @@ __all__ = [
|
||||
"render_feature_list",
|
||||
]
|
||||
|
||||
__version__ = "1.0.4b1+1016"
|
||||
__version__ = "1.0.5b1+1016"
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
"""Generated reviewed adapter for game-history."""
|
||||
|
||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
|
||||
|
||||
PROFILE = 'game-history'
|
||||
AUTHOR = 'jwebot'
|
||||
DESCRIPTION = '游戏记录'
|
||||
HELP_ENTRIES = ('/游戏记录 /个人记录 /gamehistory',)
|
||||
COMMANDS = ('/游戏记录', '/个人记录', '/gamehistory')
|
||||
|
||||
|
||||
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))
|
||||
+2
-2
@@ -6,8 +6,8 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
PROFILE = 'jwebot-version'
|
||||
AUTHOR = 'official'
|
||||
DESCRIPTION = 'JWeBot 版本'
|
||||
HELP_ENTRIES = ('/version',)
|
||||
COMMANDS = ('/version',)
|
||||
HELP_ENTRIES = ('/version /版本',)
|
||||
COMMANDS = ('/版本', '/version')
|
||||
|
||||
|
||||
def _handler(context: SingleGroupContext, command: str):
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
|
||||
PROFILE = 'person-directory'
|
||||
AUTHOR = 'official'
|
||||
DESCRIPTION = '查询当前群已配置人物、基本属性和别名'
|
||||
DESCRIPTION = '人物索引'
|
||||
HELP_ENTRIES = ('/人物列表 [页码] /people;/人物 [名称或别名] /person',)
|
||||
COMMANDS = ('/人物列表', '/别名列表', '/people', '/人物', '/个人属性', '/person')
|
||||
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
"""Generated reviewed adapter for primogem-economy."""
|
||||
|
||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
|
||||
|
||||
PROFILE = 'primogem-economy'
|
||||
AUTHOR = 'jwebot'
|
||||
DESCRIPTION = '原石系统'
|
||||
HELP_ENTRIES = ('/原石祈愿 /原石榜;/扫码加权 人;/Taffy发话 [定时HH:MM] 内容',)
|
||||
COMMANDS = ('/原石祈愿', '/祈愿', '/wish', '/原石榜', '/primogem榜', '/扫码加权', '/扫码概率', '/Taffy发话', '/taffysay')
|
||||
|
||||
|
||||
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))
|
||||
+2
-2
@@ -6,8 +6,8 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
PROFILE = 'single-player-games'
|
||||
AUTHOR = 'jwebot'
|
||||
DESCRIPTION = '摩拉游戏'
|
||||
HELP_ENTRIES = ('/摩拉(兼容 /金币)/wallet;/游戏记录 /gamehistory', '/抽奖 [金额];/draw', '/彩票 开始|报名|状态|开奖|取消;/lottery', '/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200', '/游戏帮助;/games')
|
||||
COMMANDS = ('/摩拉', '/金币', '/wallet', '/抽奖', '/draw', '/彩票', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏记录', '/个人记录', '/gamehistory', '/游戏帮助', '/games')
|
||||
HELP_ENTRIES = ('/摩拉/原石 /抽奖 /彩票 /21点 /游戏帮助',)
|
||||
COMMANDS = ('/摩拉', '/金币', '/wallet', '/原石', '/抽奖', '/draw', '/彩票', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏帮助', '/games')
|
||||
|
||||
|
||||
def _handler(context: SingleGroupContext, command: str):
|
||||
|
||||
@@ -34,6 +34,7 @@ LOCAL_COMMAND_ALIASES = {
|
||||
"历史搜索": "history_search",
|
||||
"状态": "local_status",
|
||||
"status": "server_status",
|
||||
"版本": "jwebot_version",
|
||||
"version": "jwebot_version",
|
||||
"debug": "telemetry_debug",
|
||||
"余额": "deepseek_balance",
|
||||
@@ -52,6 +53,16 @@ LOCAL_COMMAND_ALIASES = {
|
||||
"Taffy摩拉": "taffy_coins",
|
||||
"Taffy金币": "taffy_coins",
|
||||
"taffycoins": "taffy_coins",
|
||||
"原石": "wallet",
|
||||
"原石祈愿": "primogem_wish",
|
||||
"祈愿": "primogem_wish",
|
||||
"wish": "primogem_wish",
|
||||
"原石榜": "primogem_ranking",
|
||||
"primogem榜": "primogem_ranking",
|
||||
"扫码加权": "scan_boost",
|
||||
"扫码概率": "scan_boost",
|
||||
"Taffy发话": "taffy_say",
|
||||
"taffysay": "taffy_say",
|
||||
"转账": "transfer",
|
||||
"transfer": "transfer",
|
||||
"贷款": "loan",
|
||||
@@ -158,21 +169,23 @@ LOCAL_EXTENSIONS = (
|
||||
author=JWEBOT_AUTHOR,
|
||||
description="摩拉游戏",
|
||||
help_entries=(
|
||||
"/摩拉(兼容 /金币)/wallet;/游戏记录 /gamehistory",
|
||||
"/抽奖 [金额];/draw",
|
||||
"/彩票 开始|报名|状态|开奖|取消;/lottery",
|
||||
"/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200",
|
||||
"/游戏帮助;/games",
|
||||
"/摩拉/原石 /抽奖 /彩票 /21点 /游戏帮助",
|
||||
),
|
||||
command_kinds=(
|
||||
"wallet",
|
||||
"prize_draw",
|
||||
"lottery",
|
||||
"blackjack",
|
||||
"game_history",
|
||||
"game_rules",
|
||||
),
|
||||
),
|
||||
LocalExtensionDescriptor(
|
||||
extension_id="game-history",
|
||||
author=JWEBOT_AUTHOR,
|
||||
description="游戏记录",
|
||||
help_entries=("/游戏记录 /个人记录 /gamehistory",),
|
||||
command_kinds=("game_history",),
|
||||
),
|
||||
LocalExtensionDescriptor(
|
||||
extension_id="coin-transfer",
|
||||
author=JWEBOT_AUTHOR,
|
||||
@@ -194,6 +207,20 @@ LOCAL_EXTENSIONS = (
|
||||
help_entries=("/Taffy摩拉(兼容 /Taffy金币)",),
|
||||
command_kinds=("taffy_coins",),
|
||||
),
|
||||
LocalExtensionDescriptor(
|
||||
extension_id="primogem-economy",
|
||||
author=JWEBOT_AUTHOR,
|
||||
description="原石系统",
|
||||
help_entries=(
|
||||
"/原石祈愿 /原石榜;/扫码加权 人;/Taffy发话 [定时HH:MM] 内容",
|
||||
),
|
||||
command_kinds=(
|
||||
"primogem_wish",
|
||||
"primogem_ranking",
|
||||
"scan_boost",
|
||||
"taffy_say",
|
||||
),
|
||||
),
|
||||
LocalExtensionDescriptor(
|
||||
extension_id="coin-loan",
|
||||
author=JWEBOT_AUTHOR,
|
||||
@@ -213,7 +240,7 @@ LOCAL_EXTENSIONS = (
|
||||
LocalExtensionDescriptor(
|
||||
extension_id="person-directory",
|
||||
author=OFFICIAL_AUTHOR,
|
||||
description="查询当前群已配置人物、基本属性和别名",
|
||||
description="人物索引",
|
||||
help_entries=("/人物列表 [页码] /people;/人物 [名称或别名] /person",),
|
||||
command_kinds=("person_directory", "person_info"),
|
||||
),
|
||||
@@ -244,7 +271,7 @@ LOCAL_EXTENSIONS = (
|
||||
extension_id="jwebot-version",
|
||||
author=OFFICIAL_AUTHOR,
|
||||
description="JWeBot 版本",
|
||||
help_entries=("/version",),
|
||||
help_entries=("/version /版本",),
|
||||
command_kinds=("jwebot_version",),
|
||||
),
|
||||
LocalExtensionDescriptor(
|
||||
|
||||
+9
-1
@@ -27,7 +27,9 @@ BEIJING_TIMEZONE = ZoneInfo("Asia/Shanghai")
|
||||
DICE_PATTERN = re.compile(r"^(?:(\d{1,2})[dD](\d{1,3}))?$")
|
||||
RANDOM_NUMBER_PATTERN = re.compile(r"^(-?\d{1,6})\s+(-?\d{1,6})$")
|
||||
MULTI_REPLY_MARKER = "\x1eJWEBOT_MULTI_REPLY_V1\x1e"
|
||||
CHANGELOG_HEADING = re.compile(r"^##\s+(.+?)(?:\s+-\s+(\d{4}-\d{2}-\d{2}))?\s*$")
|
||||
CHANGELOG_HEADING = re.compile(
|
||||
r"^##\s+(.+?)(?:\s+-\s+(\d{4}-\d{2}-\d{2}(?:\s+\d{2}:\d{2}:\d{2})?))?\s*$"
|
||||
)
|
||||
|
||||
|
||||
class LocalCommandError(RuntimeError):
|
||||
@@ -166,6 +168,10 @@ class LocalCommandEngine:
|
||||
"blackjack",
|
||||
"game_history",
|
||||
"game_rules",
|
||||
"primogem_wish",
|
||||
"primogem_ranking",
|
||||
"scan_boost",
|
||||
"taffy_say",
|
||||
}:
|
||||
return self.reply_cleaner(self.players.handle(row, kind, arguments))
|
||||
if kind == "cs_autochess":
|
||||
@@ -433,6 +439,8 @@ class LocalCommandEngine:
|
||||
continue
|
||||
version = match.group(1).strip()
|
||||
updated_at = (match.group(2) or "").strip()
|
||||
if re.fullmatch(r"\d{4}-\d{2}-\d{2}", updated_at):
|
||||
updated_at = f"{updated_at} 00:00:00"
|
||||
bullet_parts: list[str] = []
|
||||
collecting = False
|
||||
for item in lines[index + 1 :]:
|
||||
|
||||
+528
-7
@@ -28,6 +28,26 @@ GAME_HISTORY_LIMIT = 50
|
||||
GAME_HISTORY_PAGE_SIZE = 5
|
||||
COIN_EVENT_LIMIT = 50
|
||||
COIN_EVENT_DISPLAY_LIMIT = 10
|
||||
PRIMOGEM_MORA_VALUE = 3600
|
||||
PRIMOGEM_EVENT_LIMIT = 50
|
||||
PRIMOGEM_EVENT_DISPLAY_LIMIT = 5
|
||||
WISH_HISTORY_LIMIT = 30
|
||||
SCAN_BOOST_USES = 24
|
||||
SCAN_BOOST_WEIGHT = 4
|
||||
TAFFY_SAY_CREDITS_PER_PRIMOGEM = 2
|
||||
TAFFY_SAY_MAX_CHARS = 160
|
||||
WISH_MORA_TABLE = (
|
||||
(20, 5),
|
||||
(100, 12),
|
||||
(500, 11),
|
||||
(1000, 16),
|
||||
(1800, 8),
|
||||
(3600, 13),
|
||||
(6000, 12),
|
||||
(8000, 13),
|
||||
(10000, 10),
|
||||
)
|
||||
WISH_SCAN_TICKET_PERCENT = 10
|
||||
GAME_LABELS = {
|
||||
"prize_draw": "抽奖",
|
||||
"lottery": "彩票",
|
||||
@@ -59,6 +79,12 @@ TRANSFER_MARKED = re.compile(
|
||||
r"(?:金额|摩拉|金币|coin|coins|:|:)\s*(?P<amount>\d{1,6})$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
TAFFY_SAY_SCHEDULE = re.compile(
|
||||
r"^(?:定时|预约|schedule)\s+"
|
||||
r"(?P<time>(?:今天|明天)?\s*\d{1,2}:\d{2})\s+"
|
||||
r"(?P<content>.+)$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def coin_event_reason(kind: str, outcome: str) -> str:
|
||||
@@ -98,9 +124,141 @@ def append_coin_event(
|
||||
account["coin_events"] = events[-COIN_EVENT_LIMIT:]
|
||||
|
||||
|
||||
def append_primogem_event(
|
||||
account: dict[str, Any],
|
||||
delta: int,
|
||||
reason: str,
|
||||
) -> None:
|
||||
if delta == 0:
|
||||
return
|
||||
events = account.get("primogem_events")
|
||||
if not isinstance(events, list):
|
||||
events = []
|
||||
events = [
|
||||
event for event in events
|
||||
if isinstance(event, dict) and type(event.get("delta")) is int
|
||||
]
|
||||
events.append(
|
||||
{
|
||||
"delta": delta,
|
||||
"reason": str(reason)[:120],
|
||||
"balance": int(account.get("primogems", 0)),
|
||||
"created_at": time.time(),
|
||||
}
|
||||
)
|
||||
account["primogem_events"] = events[-PRIMOGEM_EVENT_LIMIT:]
|
||||
|
||||
|
||||
def grant_primogems(
|
||||
account: dict[str, Any],
|
||||
amount: int,
|
||||
reason: str,
|
||||
) -> str:
|
||||
if amount <= 0:
|
||||
return ""
|
||||
normalize_player_account(account)
|
||||
account["primogems"] += amount
|
||||
append_primogem_event(account, amount, reason)
|
||||
return (
|
||||
f"达成成就:{reason}!+{amount} 原石"
|
||||
f"(价值 {amount * PRIMOGEM_MORA_VALUE} 摩拉);"
|
||||
f"当前原石 {account['primogems']} 个。"
|
||||
)
|
||||
|
||||
|
||||
def spend_primogems(
|
||||
account: dict[str, Any],
|
||||
amount: int,
|
||||
reason: str,
|
||||
) -> None:
|
||||
normalize_player_account(account)
|
||||
if amount <= 0:
|
||||
raise ValueError("原石消耗必须为正数")
|
||||
if int(account["primogems"]) < amount:
|
||||
raise ValueError("原石不足")
|
||||
account["primogems"] -= amount
|
||||
append_primogem_event(account, -amount, reason)
|
||||
|
||||
|
||||
def append_wish_record(
|
||||
account: dict[str, Any],
|
||||
*,
|
||||
mora: int,
|
||||
scan_ticket: bool,
|
||||
) -> None:
|
||||
records = account.get("wish_history")
|
||||
if not isinstance(records, list):
|
||||
records = []
|
||||
records = [record for record in records if isinstance(record, dict)]
|
||||
records.append(
|
||||
{
|
||||
"mora": mora,
|
||||
"scan_ticket": scan_ticket,
|
||||
"created_at": time.time(),
|
||||
}
|
||||
)
|
||||
account["wish_history"] = records[-WISH_HISTORY_LIMIT:]
|
||||
|
||||
|
||||
def weighted_wish_mora(rng: random.Random) -> int:
|
||||
total = sum(weight for _, weight in WISH_MORA_TABLE)
|
||||
roll = rng.randrange(total)
|
||||
cursor = 0
|
||||
for value, weight in WISH_MORA_TABLE:
|
||||
cursor += weight
|
||||
if roll < cursor:
|
||||
return value
|
||||
return WISH_MORA_TABLE[-1][0]
|
||||
|
||||
|
||||
def recent_primogem_event_lines(
|
||||
account: dict[str, Any],
|
||||
*,
|
||||
limit: int = PRIMOGEM_EVENT_DISPLAY_LIMIT,
|
||||
) -> list[str]:
|
||||
normalize_player_account(account)
|
||||
events = sorted(
|
||||
account["primogem_events"],
|
||||
key=lambda event: float(event.get("created_at", 0) or 0),
|
||||
reverse=True,
|
||||
)
|
||||
lines: list[str] = []
|
||||
for event in events[:limit]:
|
||||
delta = int(event["delta"])
|
||||
reason = str(event.get("reason", "原石变动")).strip() or "原石变动"
|
||||
lines.append(f"{delta:+d}:{reason}")
|
||||
return lines
|
||||
|
||||
|
||||
def normalize_player_account(account: dict[str, Any]) -> dict[str, Any]:
|
||||
account.setdefault("balance", INITIAL_COINS)
|
||||
account.setdefault("game", None)
|
||||
primogems = account.get("primogems", 0)
|
||||
account["primogems"] = primogems if type(primogems) is int and primogems >= 0 else 0
|
||||
taffy_credits = account.get("taffy_say_credits", 0)
|
||||
account["taffy_say_credits"] = (
|
||||
taffy_credits if type(taffy_credits) is int and taffy_credits >= 0 else 0
|
||||
)
|
||||
scan_tickets = account.get("scan_boost_tickets", 0)
|
||||
account["scan_boost_tickets"] = (
|
||||
scan_tickets if type(scan_tickets) is int and scan_tickets >= 0 else 0
|
||||
)
|
||||
blackjack_streak = account.get("blackjack_taffy_win_streak", 0)
|
||||
account["blackjack_taffy_win_streak"] = (
|
||||
blackjack_streak
|
||||
if type(blackjack_streak) is int and blackjack_streak >= 0
|
||||
else 0
|
||||
)
|
||||
lottery_streak = account.get("lottery_three_hit_streak", 0)
|
||||
account["lottery_three_hit_streak"] = (
|
||||
lottery_streak if type(lottery_streak) is int and lottery_streak >= 0 else 0
|
||||
)
|
||||
previous_lottery_matches = account.get("lottery_previous_matches", -1)
|
||||
account["lottery_previous_matches"] = (
|
||||
previous_lottery_matches
|
||||
if type(previous_lottery_matches) is int and -1 <= previous_lottery_matches <= 3
|
||||
else -1
|
||||
)
|
||||
loan = account.get("loan")
|
||||
try:
|
||||
loan_principal = int(loan.get("principal", 0) or 0) if isinstance(loan, dict) else 0
|
||||
@@ -156,6 +314,48 @@ def normalize_player_account(account: dict[str, Any]) -> dict[str, Any]:
|
||||
event for event in events
|
||||
if isinstance(event, dict) and type(event.get("delta")) is int
|
||||
][-COIN_EVENT_LIMIT:]
|
||||
primogem_events = account.get("primogem_events")
|
||||
if not isinstance(primogem_events, list):
|
||||
primogem_events = []
|
||||
account["primogem_events"] = [
|
||||
event for event in primogem_events
|
||||
if isinstance(event, dict) and type(event.get("delta")) is int
|
||||
][-PRIMOGEM_EVENT_LIMIT:]
|
||||
wish_history = account.get("wish_history")
|
||||
if not isinstance(wish_history, list):
|
||||
wish_history = []
|
||||
account["wish_history"] = [
|
||||
record for record in wish_history if isinstance(record, dict)
|
||||
][-WISH_HISTORY_LIMIT:]
|
||||
boosts = account.get("scan_boosts")
|
||||
if not isinstance(boosts, list):
|
||||
boosts = []
|
||||
normalized_boosts = []
|
||||
for boost in boosts:
|
||||
if not isinstance(boost, dict):
|
||||
continue
|
||||
target_key = str(boost.get("target_person_key", "")).strip()
|
||||
target_name = str(boost.get("target_display_name", "")).strip()
|
||||
remaining = boost.get("remaining", 0)
|
||||
weight = boost.get("weight", 0)
|
||||
if (
|
||||
target_key
|
||||
and target_name
|
||||
and type(remaining) is int
|
||||
and remaining > 0
|
||||
and type(weight) is int
|
||||
and weight > 0
|
||||
):
|
||||
normalized_boosts.append(
|
||||
{
|
||||
"target_person_key": target_key[:128],
|
||||
"target_display_name": target_name[:120],
|
||||
"remaining": min(remaining, 10_000),
|
||||
"weight": min(weight, 100),
|
||||
"created_at": float(boost.get("created_at", 0) or 0),
|
||||
}
|
||||
)
|
||||
account["scan_boosts"] = normalized_boosts[-20:]
|
||||
return account
|
||||
|
||||
|
||||
@@ -233,6 +433,74 @@ def recent_coin_event_lines(
|
||||
return lines
|
||||
|
||||
|
||||
def recent_account_event_lines(
|
||||
account: dict[str, Any],
|
||||
*,
|
||||
limit: int = COIN_EVENT_DISPLAY_LIMIT,
|
||||
) -> list[str]:
|
||||
normalize_player_account(account)
|
||||
events: list[dict[str, Any]] = []
|
||||
for event in account["coin_events"]:
|
||||
events.append({**event, "unit": "mora"})
|
||||
for event in account["primogem_events"]:
|
||||
events.append({**event, "unit": "primogem"})
|
||||
events.sort(
|
||||
key=lambda event: float(event.get("created_at", 0) or 0),
|
||||
reverse=True,
|
||||
)
|
||||
lines: list[str] = []
|
||||
for event in events[:limit]:
|
||||
delta = int(event["delta"])
|
||||
reason = str(event.get("reason", "账户变动")).strip() or "账户变动"
|
||||
if event.get("unit") == "primogem":
|
||||
lines.append(f"{delta:+d}原石:{reason}")
|
||||
else:
|
||||
lines.append(f"{delta:+d}:{reason}")
|
||||
return lines
|
||||
|
||||
|
||||
def blackjack_achievement_lines(
|
||||
account: dict[str, Any],
|
||||
*,
|
||||
outcome: str,
|
||||
natural_player_21: bool = False,
|
||||
extreme_21: bool = False,
|
||||
) -> list[str]:
|
||||
normalize_player_account(account)
|
||||
lines: list[str] = []
|
||||
if natural_player_21:
|
||||
lines.append(grant_primogems(account, 1, "21点自然抽到21"))
|
||||
if extreme_21:
|
||||
lines.append(grant_primogems(account, 2, "21点极限挑战"))
|
||||
if "赢" in outcome or "获胜" in outcome:
|
||||
account["blackjack_taffy_win_streak"] += 1
|
||||
streak = int(account["blackjack_taffy_win_streak"])
|
||||
if streak == 3:
|
||||
lines.append(grant_primogems(account, 1, "对战Taffy 21点3连胜"))
|
||||
elif streak == 5:
|
||||
lines.append(grant_primogems(account, 2, "对战Taffy 21点5连胜"))
|
||||
else:
|
||||
account["blackjack_taffy_win_streak"] = 0
|
||||
return [line for line in lines if line]
|
||||
|
||||
|
||||
def lottery_achievement_lines(
|
||||
account: dict[str, Any],
|
||||
*,
|
||||
matches: int,
|
||||
) -> list[str]:
|
||||
normalize_player_account(account)
|
||||
previous = int(account.get("lottery_previous_matches", -1))
|
||||
lines: list[str] = []
|
||||
if previous >= 0 and previous + matches >= 5:
|
||||
account["lottery_previous_matches"] = -1
|
||||
account["lottery_three_hit_streak"] += 1
|
||||
lines.append(grant_primogems(account, 2, "彩票三中挑战"))
|
||||
else:
|
||||
account["lottery_previous_matches"] = matches
|
||||
return [line for line in lines if line]
|
||||
|
||||
|
||||
def card_label(card: int) -> str:
|
||||
return ("A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K")[card % 13]
|
||||
|
||||
@@ -256,11 +524,15 @@ class PlayerGames:
|
||||
def rules() -> str:
|
||||
return (
|
||||
"每人首次使用获 1000 虚拟摩拉;/摩拉 查看余额(兼容 /金币);"
|
||||
"/游戏记录 查看本人记录。\n"
|
||||
"/原石 查看稀有资源;/游戏记录 查看本人记录。\n"
|
||||
"转账:/转账 对方 金额,或 /transfer name amount;"
|
||||
"对方需先用 /摩拉 或 /金币 初始化账户。\n"
|
||||
"贷款:/贷款 [1~500] 借入摩拉;/还款 [金额|全部] 归还,"
|
||||
"同一时间最多一笔未还贷款。\n"
|
||||
"原石:达成特定成就获得;/原石祈愿 消耗1原石抽20~10000摩拉"
|
||||
"(理论期望3600摩拉)并可能获得扫码概率券;"
|
||||
"/扫码加权 人名 提升TA的整点扫码权重;"
|
||||
"/Taffy发话 内容 或 /Taffy发话 定时 HH:MM 内容。\n"
|
||||
"抽奖:/抽奖 [2~100],5%返5倍、20%返2倍,其余无返还。\n"
|
||||
"多人彩票:/彩票 开始;/彩票 报名 3 4 7 [1~500];"
|
||||
"/彩票 当前状态;/彩票 开奖。三位号码顺序无关,省略金额默认10;"
|
||||
@@ -284,6 +556,14 @@ class PlayerGames:
|
||||
return self.rules()
|
||||
if kind == "game_history":
|
||||
return self.game_history(row, arguments)
|
||||
if kind in {
|
||||
"primogem",
|
||||
"primogem_wish",
|
||||
"primogem_ranking",
|
||||
"scan_boost",
|
||||
"taffy_say",
|
||||
}:
|
||||
return self.primogem_command(row, kind, arguments)
|
||||
if kind == "lottery":
|
||||
return self.group_lottery(row, arguments)
|
||||
if kind == "coin_ranking":
|
||||
@@ -401,6 +681,7 @@ class PlayerGames:
|
||||
)
|
||||
account["game"] = None
|
||||
account["losses"] += 1
|
||||
account["blackjack_taffy_win_streak"] = 0
|
||||
expired = "上一局已超时,按认输结束;"
|
||||
if kind == "wallet":
|
||||
state = "进行中" if account.get("game") else "无进行中的牌局"
|
||||
@@ -409,7 +690,10 @@ class PlayerGames:
|
||||
total = account["wins"] + account["losses"] + account["ties"]
|
||||
win_rate = (account["wins"] / total * 100) if total else 0.0
|
||||
lines = [
|
||||
f"{player_label}:{expired}当前摩拉 {account['balance']};{loan_text}",
|
||||
(
|
||||
f"{player_label}:{expired}当前摩拉 {account['balance']};"
|
||||
f"原石 {account['primogems']} 个;{loan_text}"
|
||||
),
|
||||
(
|
||||
f"21点 {state};已结算胜 {account['wins']} / "
|
||||
f"负 {account['losses']} / 平 {account['ties']},"
|
||||
@@ -417,7 +701,7 @@ class PlayerGames:
|
||||
),
|
||||
"最近10次账户变动:",
|
||||
]
|
||||
lines.extend(recent_coin_event_lines(account) or ["暂无"])
|
||||
lines.extend(recent_account_event_lines(account) or ["暂无"])
|
||||
lines.append("/游戏帮助 查看规则")
|
||||
return "\n".join(lines)
|
||||
if kind == "blackjack" and action in {"hit", "stand", "status", "end"}:
|
||||
@@ -485,6 +769,224 @@ class PlayerGames:
|
||||
str(row["group_fingerprint"]), str(row["sender"]),
|
||||
str(row["event_id"]), kind + ":" + arguments, execute)
|
||||
|
||||
def primogem_command(
|
||||
self,
|
||||
row: dict[str, Any],
|
||||
kind: str,
|
||||
arguments: str,
|
||||
) -> str:
|
||||
group = str(row["group_fingerprint"])
|
||||
sender = str(row["sender"])
|
||||
event_id = str(row["event_id"])
|
||||
arguments = arguments.strip()
|
||||
person = self.ledger.resolve_person(group, sender)
|
||||
player_label = self.display_label(person["display_name"])
|
||||
|
||||
if kind == "primogem_ranking":
|
||||
if arguments and not re.fullmatch(r"\d{1,2}", arguments):
|
||||
return "用法:/原石榜 [人数]"
|
||||
limit = int(arguments or 10)
|
||||
if not 1 <= limit <= 20:
|
||||
return "原石榜人数必须为 1~20"
|
||||
records = self.ledger.player_primogem_ranking(group, limit=limit)
|
||||
if not records:
|
||||
return "当前群还没有原石记录"
|
||||
lines = ["原石榜:"]
|
||||
for index, record in enumerate(records, start=1):
|
||||
lines.append(
|
||||
f"{index}. {record['display_name']} {record['primogems']} 个;"
|
||||
f"来源:{record['sources']}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
if kind == "primogem":
|
||||
if arguments:
|
||||
return (
|
||||
"用法:/原石;/原石祈愿;/原石榜;"
|
||||
"/扫码加权 人名;/Taffy发话 内容"
|
||||
)
|
||||
|
||||
def show(account: dict[str, Any]) -> str:
|
||||
normalize_player_account(account)
|
||||
lines = [
|
||||
(
|
||||
f"{player_label}:原石 {account['primogems']} 个"
|
||||
f"(折合 {account['primogems'] * PRIMOGEM_MORA_VALUE} 摩拉);"
|
||||
f"扫码概率券 {account['scan_boost_tickets']} 张;"
|
||||
f"Taffy发话额度 {account['taffy_say_credits']} 次。"
|
||||
),
|
||||
"最近原石变动:",
|
||||
]
|
||||
lines.extend(recent_primogem_event_lines(account) or ["暂无"])
|
||||
if account["wish_history"]:
|
||||
recent = account["wish_history"][-3:]
|
||||
wish_text = ";".join(
|
||||
(
|
||||
f"+{int(record.get('mora', 0))}摩拉"
|
||||
+ (",扫码券+1" if record.get("scan_ticket") else "")
|
||||
)
|
||||
for record in reversed(recent)
|
||||
)
|
||||
lines.append(f"最近祈愿:{wish_text}")
|
||||
return "\n".join(lines)
|
||||
|
||||
return self.ledger.apply_player_action(
|
||||
group, sender, event_id, "primogem:" + arguments, show
|
||||
)
|
||||
|
||||
if kind == "primogem_wish":
|
||||
if arguments:
|
||||
return "用法:/原石祈愿,消耗1原石抽20~10000摩拉,可能获得扫码概率券"
|
||||
|
||||
def wish(account: dict[str, Any]) -> str:
|
||||
normalize_player_account(account)
|
||||
if account["primogems"] < 1:
|
||||
return f"{player_label}:原石不足;当前原石 {account['primogems']} 个"
|
||||
spend_primogems(account, 1, "原石祈愿")
|
||||
mora = weighted_wish_mora(self.rng)
|
||||
scan_ticket = self.rng.randrange(100) < WISH_SCAN_TICKET_PERCENT
|
||||
account["balance"] += mora
|
||||
if scan_ticket:
|
||||
account["scan_boost_tickets"] += 1
|
||||
append_coin_event(account, mora, "原石祈愿")
|
||||
append_wish_record(account, mora=mora, scan_ticket=scan_ticket)
|
||||
ticket_text = ";额外获得扫码概率券 1 张" if scan_ticket else ""
|
||||
return (
|
||||
f"{player_label}:原石祈愿消耗 1 原石,抽到 {mora} 摩拉"
|
||||
f"{ticket_text};当前摩拉 {account['balance']},"
|
||||
f"原石 {account['primogems']} 个,"
|
||||
f"扫码概率券 {account['scan_boost_tickets']} 张"
|
||||
)
|
||||
|
||||
return self.ledger.apply_player_action(
|
||||
group, sender, event_id, "primogem_wish:" + arguments, wish
|
||||
)
|
||||
|
||||
if kind == "scan_boost":
|
||||
if not arguments:
|
||||
return "用法:/扫码加权 人名;优先消耗扫码概率券,没有券时消耗1原石"
|
||||
target = self.ledger.resolve_person(group, arguments)
|
||||
target_label = self.display_label(target["display_name"])
|
||||
|
||||
def boost(account: dict[str, Any]) -> str:
|
||||
normalize_player_account(account)
|
||||
if account["scan_boost_tickets"] > 0:
|
||||
account["scan_boost_tickets"] -= 1
|
||||
cost_text = "消耗扫码概率券 1 张"
|
||||
elif account["primogems"] > 0:
|
||||
spend_primogems(account, 1, f"扫码加权给 {target_label}")
|
||||
cost_text = "消耗原石 1 个"
|
||||
else:
|
||||
return (
|
||||
f"{player_label}:没有扫码概率券,原石也不足;"
|
||||
f"当前原石 {account['primogems']} 个"
|
||||
)
|
||||
boosts = [
|
||||
item
|
||||
for item in account["scan_boosts"]
|
||||
if item["target_person_key"] != target["person_key"]
|
||||
]
|
||||
boosts.append(
|
||||
{
|
||||
"target_person_key": str(target["person_key"]),
|
||||
"target_display_name": target_label,
|
||||
"remaining": SCAN_BOOST_USES,
|
||||
"weight": SCAN_BOOST_WEIGHT,
|
||||
"created_at": time.time(),
|
||||
}
|
||||
)
|
||||
account["scan_boosts"] = boosts[-20:]
|
||||
return (
|
||||
f"{player_label}:已给 {target_label} 增加整点扫码权重;"
|
||||
f"{cost_text};未来 {SCAN_BOOST_USES} 次送达扫码播报中,"
|
||||
f"{target_label} 额外 +{SCAN_BOOST_WEIGHT} 权重"
|
||||
)
|
||||
|
||||
return self.ledger.apply_player_action(
|
||||
group,
|
||||
sender,
|
||||
event_id,
|
||||
"scan_boost:" + arguments,
|
||||
boost,
|
||||
)
|
||||
|
||||
if kind == "taffy_say":
|
||||
parsed = self.parse_taffy_say(arguments)
|
||||
if parsed is None:
|
||||
return (
|
||||
"用法:/Taffy发话 内容;"
|
||||
"/Taffy发话 定时 HH:MM 内容。1原石兑换2次发话额度"
|
||||
)
|
||||
content, scheduled_at = parsed
|
||||
return self.ledger.player_taffy_say(
|
||||
group,
|
||||
sender,
|
||||
event_id,
|
||||
"taffy_say:" + arguments,
|
||||
content,
|
||||
scheduled_at,
|
||||
lambda data: self.render_taffy_say(data, player_label),
|
||||
)
|
||||
|
||||
raise ValueError(f"unsupported primogem command: {kind}")
|
||||
|
||||
@staticmethod
|
||||
def parse_taffy_say(arguments: str) -> tuple[str, float | None] | None:
|
||||
value = re.sub(r"\s+", " ", arguments).strip()
|
||||
if not value:
|
||||
return None
|
||||
scheduled_at: float | None = None
|
||||
match = TAFFY_SAY_SCHEDULE.fullmatch(value)
|
||||
if match is not None:
|
||||
content = match["content"].strip()
|
||||
try:
|
||||
scheduled_at = PlayerGames.parse_taffy_time(match["time"])
|
||||
except ValueError:
|
||||
return None
|
||||
else:
|
||||
content = value
|
||||
if not content or len(content) > TAFFY_SAY_MAX_CHARS:
|
||||
return None
|
||||
return content, scheduled_at
|
||||
|
||||
@staticmethod
|
||||
def parse_taffy_time(value: str) -> float:
|
||||
cleaned = re.sub(r"\s+", "", value)
|
||||
day_offset = 0
|
||||
if cleaned.startswith("明天"):
|
||||
day_offset = 1
|
||||
cleaned = cleaned[2:]
|
||||
elif cleaned.startswith("今天"):
|
||||
cleaned = cleaned[2:]
|
||||
hour_text, minute_text = cleaned.split(":", 1)
|
||||
hour = int(hour_text)
|
||||
minute = int(minute_text)
|
||||
if not 0 <= hour <= 23 or not 0 <= minute <= 59:
|
||||
raise ValueError("Taffy发话定时必须是 HH:MM")
|
||||
from datetime import datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
timezone = ZoneInfo("Asia/Shanghai")
|
||||
now = datetime.now(timezone)
|
||||
scheduled = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
||||
if day_offset:
|
||||
scheduled += timedelta(days=day_offset)
|
||||
elif scheduled <= now:
|
||||
scheduled += timedelta(days=1)
|
||||
return scheduled.timestamp()
|
||||
|
||||
@staticmethod
|
||||
def render_taffy_say(data: dict[str, Any], player_label: str) -> str:
|
||||
if data["status"] == "insufficient":
|
||||
return f"{player_label}:原石不足,无法兑换Taffy发话额度"
|
||||
if data["status"] == "scheduled":
|
||||
return (
|
||||
f"{player_label}:已安排Taffy在 {data['scheduled_text']} 发话;"
|
||||
f"剩余额度 {data['taffy_say_credits']} 次,"
|
||||
f"原石 {data['primogems']} 个"
|
||||
)
|
||||
return f"Taffy:{data['content']}"
|
||||
|
||||
def coin_ranking(self, row: dict[str, Any], arguments: str) -> str:
|
||||
value = arguments.strip()
|
||||
if value and not re.fullmatch(r"\d{1,2}", value):
|
||||
@@ -940,6 +1442,7 @@ class PlayerGames:
|
||||
f"下注 {bet},返还 {payout},净变化 {payout - bet:+d};"
|
||||
f"摩拉 {player['balance']}"
|
||||
)
|
||||
lines.extend(player.get("achievements") or [])
|
||||
return "\n".join(lines)
|
||||
|
||||
def render_blackjack_table_cancel(self, data: dict[str, Any]) -> str:
|
||||
@@ -1234,6 +1737,7 @@ class PlayerGames:
|
||||
f"{entry['payout'] - entry['stake']:+d};"
|
||||
f"摩拉 {entry['balance']}"
|
||||
)
|
||||
lines.extend(entry.get("achievements") or [])
|
||||
return "\n".join(lines)
|
||||
|
||||
return self.ledger.draw_group_lottery(
|
||||
@@ -1362,10 +1866,15 @@ class PlayerGames:
|
||||
)
|
||||
account["game"] = None
|
||||
account["losses"] += 1
|
||||
account["blackjack_taffy_win_streak"] = 0
|
||||
return f"本人的21点已认输结束,不退下注;摩拉 {account['balance']}"
|
||||
game["updated_at"] = time.time()
|
||||
if action == "hit":
|
||||
game["player"].append(game["deck"].pop())
|
||||
before = hand_value(game["player"])
|
||||
drawn = game["deck"].pop()
|
||||
game["player"].append(drawn)
|
||||
if before == 20 and hand_value(game["player"]) == 21:
|
||||
game["extreme_21"] = True
|
||||
if hand_value(game["player"]) > 21:
|
||||
return self.finish_blackjack(account)
|
||||
return self.show_hand(game)
|
||||
@@ -1409,7 +1918,19 @@ class PlayerGames:
|
||||
stake=int(bet),
|
||||
payout=int(payout),
|
||||
)
|
||||
achievements = blackjack_achievement_lines(
|
||||
account,
|
||||
outcome=outcome,
|
||||
natural_player_21=bool(natural and player == 21),
|
||||
extreme_21=bool(game.get("extreme_21") and player == 21),
|
||||
)
|
||||
account["game"] = None
|
||||
return (f"21点结算:{outcome};你的牌 {' '.join(map(card_label, game['player']))}({player}点),"
|
||||
f"庄家 {' '.join(map(card_label, game['dealer']))}({dealer}点);"
|
||||
f"下注 {bet},返还 {payout},净变化 {payout - bet:+d};摩拉 {account['balance']}")
|
||||
result = (
|
||||
f"21点结算:{outcome};你的牌 {' '.join(map(card_label, game['player']))}"
|
||||
f"({player}点),庄家 {' '.join(map(card_label, game['dealer']))}"
|
||||
f"({dealer}点);下注 {bet},返还 {payout},"
|
||||
f"净变化 {payout - bet:+d};摩拉 {account['balance']}"
|
||||
)
|
||||
if achievements:
|
||||
result += "\n" + "\n".join(achievements)
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user