This commit is contained in:
@@ -1,5 +1,22 @@
|
||||
# Changelog
|
||||
|
||||
## alpha 0.6.9.1015
|
||||
|
||||
- Move CS Major autochess public game data out of `cs_autochess.py` and into
|
||||
package data files under `jwebot_official_extensions/data/cs_autochess/`.
|
||||
Player cards, coach cards, narrative fragments, shop/economy odds, draft
|
||||
pacing, synergy thresholds, match formats, MR15/overtime settings, and
|
||||
special event rewards are now data driven.
|
||||
- Add startup validation for the externalized data so missing fields,
|
||||
duplicate card IDs, invalid schema versions, or an undersized narrative
|
||||
library fail fast before the command engine runs.
|
||||
- Add regression coverage that verifies the JSON files are packaged, readable,
|
||||
and consistent with the runtime card, coach, match-format, and narrative
|
||||
inventories.
|
||||
- Add virtual coin transfers with `/转账 对方 金额` and `/transfer name amount`.
|
||||
Transfers are single-transaction ledger updates, require the receiver to
|
||||
initialize `/金币` first, reject self-transfers, and replay safely by event ID.
|
||||
|
||||
## alpha 0.6.8.1015
|
||||
|
||||
- Add an atomic compatibility path for old active CS autochess runs. When a
|
||||
|
||||
@@ -8,7 +8,7 @@ JWeBot 是运行于 Linux 的单群微信自动回复系统。它通过本地 OC
|
||||
观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件,
|
||||
并通过带视觉确认的 GUI 发送链路回复。
|
||||
|
||||
当前版本:`alpha 0.6.8.1015`。
|
||||
当前版本:`alpha 0.6.9.1015`。
|
||||
|
||||
0.6.1 新增本地上下文总结、人物与别名索引、单人虚拟金币游戏,并增加
|
||||
两秒画面复核以补抓遗漏的消息变化。命令和游戏管理规则见
|
||||
@@ -36,6 +36,11 @@ D 牌、锁店、调阵容和战术窗口会在轮次之间自然提示。
|
||||
0.6.8 增加旧版自走棋 active 状态兼容:如果群里残留 0.6.6/0.6.7
|
||||
之前创建的未完成赛事,新的 `/自走棋开始` 会原子归档旧档案并开启新版
|
||||
赛季,不需要维护人员手工改库。
|
||||
0.6.9 将 CS Major 自走棋的公开游戏数据拆出到包内 JSON:选手卡池、
|
||||
教练池、叙事素材、商店概率、经济奖励、选秀节奏、羁绊阈值、赛制和
|
||||
MR15/加时参数都由数据文件驱动,启动时会校验字段、ID 和素材数量。
|
||||
同版新增虚拟金币转账:`/转账 对方 金额` 会在同一事务内扣款入账,
|
||||
收款人需先发送 `/金币` 初始化账户。
|
||||
|
||||
### 运行流程
|
||||
|
||||
@@ -102,6 +107,7 @@ PATH="$PWD/.venv/bin:$PATH" ./tools/check-local.sh
|
||||
/上下文 [100|200|300](/context)
|
||||
/人物列表 [页码](/people);/人物 [名称或别名](/person)
|
||||
/金币(/wallet);/抽奖 [金额](/draw)
|
||||
/转账 对方 金额(/transfer name amount)
|
||||
/彩票 开始|报名|当前状态|开奖|取消(/lottery)
|
||||
/21点 开始 [1~500];开局后 /21点 要牌|停牌|状态|结束
|
||||
/游戏记录 [页码](/gamehistory)
|
||||
@@ -278,7 +284,7 @@ visually confirmed GUI bridge.
|
||||
Current version:
|
||||
|
||||
```text
|
||||
alpha 0.6.8.1015
|
||||
alpha 0.6.9.1015
|
||||
```
|
||||
|
||||
## Runtime Flow
|
||||
@@ -379,8 +385,9 @@ Version 0.6.1 adds local `/context 100|200|300` extractive summaries,
|
||||
`/people` and `/person` lookup, and `/wallet`, `/draw`, `/lottery`, `/blackjack`
|
||||
single-player virtual coin games. Chinese aliases share the same implementations.
|
||||
Accounts receive 1000 virtual coins once; funds, game state and event replay
|
||||
results commit together. OCR display aliases identify group-scoped people;
|
||||
native WeChat account IDs are unavailable. See
|
||||
results commit together. `/transfer name amount` moves virtual coins between
|
||||
initialized group-scoped accounts in one ledger transaction. OCR display aliases
|
||||
identify group-scoped people; native WeChat account IDs are unavailable. See
|
||||
[plugin rules and capture reconciliation](docs/local-plugins-0.6.1.md).
|
||||
Version 0.6.2 adds a 30-second total deadline to direct DeepSeek streams so an
|
||||
unfinished upstream response cannot occupy both generation workers for about
|
||||
|
||||
@@ -81,6 +81,12 @@
|
||||
选手数据不是对真实比赛数据的一比一复刻,而是基于公众认知做游戏化映射。
|
||||
0.6.7 起本地维护 `100+` 名棋子和 `18` 名教练元数据;运行期不在线抓取
|
||||
或改写数值。首轮教练选秀仍固定展示 10 个候选,保证聊天窗口可读。
|
||||
0.6.9 起,公开游戏内容不再内联在 `cs_autochess.py` 中,而是维护在
|
||||
`packages/jwebot-official-extensions/src/jwebot_official_extensions/data/cs_autochess/`:
|
||||
`players.json` 保存选手卡,`coaches.json` 保存教练卡,
|
||||
`narratives.json` 保存战报和剧情素材,`rules.json` 保存经济、商店、
|
||||
选秀、羁绊、赛制、MR15/加时和特殊事件规则。运行时启动会校验 schema、
|
||||
重复 ID、必要字段和叙事素材数量。
|
||||
|
||||
### 4.2 费用层级
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ OpenClaw;中文、英文命令对应同一实现,英文不区分大小写,
|
||||
|---|---|---|
|
||||
| context-summary | `/上下文 [100|200|300]` | `/context [100|200|300]` |
|
||||
| single-player-games | `/金币` | `/wallet` |
|
||||
| coin-transfer | `/转账 对方 金额` | `/transfer name amount` |
|
||||
| single-player-games | `/抽奖 [金额]` | `/draw [金额]` |
|
||||
| single-player-games | `/彩票 开始/报名/当前状态/开奖/取消` | `/lottery start/join/status/draw/cancel` |
|
||||
| single-player-games | `/21点 开始 [金额]`(空格可省略) | `/blackjack start [金额]`(空格可省略) |
|
||||
@@ -49,8 +50,9 @@ person_key。相同人物的已配置别名共用金币和牌局,其他群使
|
||||
仓库没有生产人物或映射文件。
|
||||
|
||||
昵称识别不是账号身份认证。修改 canonical_name 会改变当前人物作用域;
|
||||
应优先追加别名而不是重命名。用户不能通过游戏命令指定他人的账户,
|
||||
不能充值、转账、赠币或兑换真实资产。
|
||||
应优先追加别名而不是重命名。除 `/转账` 外,用户不能通过游戏命令
|
||||
指定他人的账户;转账只支持同群虚拟金币账户,收款人需先发送 `/金币`
|
||||
初始化。虚拟金币不能充值、赠币或兑换真实资产。
|
||||
|
||||
## 虚拟金币与游戏管理
|
||||
|
||||
@@ -58,6 +60,11 @@ person_key。相同人物的已配置别名共用金币和牌局,其他群使
|
||||
重启服务或切换已配置别名都不会重复领取。抽奖下注为 2~100,彩票和
|
||||
21 点下注为 1~500;余额不足或参数非法不扣款。
|
||||
|
||||
`/转账 对方 金额` 会把本人的虚拟金币转给同群已初始化账户,英文为
|
||||
`/transfer name amount`。金额范围 1~100000;不允许给自己转账。
|
||||
转账、响应记录和重试去重写在同一个 ledger 事务内,同一 event_id
|
||||
不会重复扣款。
|
||||
|
||||
抽奖默认下注 10:5% 概率返还五倍下注,20% 返还两倍,其余无返还。
|
||||
alpha 0.6.3 将旧的个人即时彩票替换为群内多人轮次。三位数字顺序无关,
|
||||
下注范围 1~500,命中 1/2/3 个分别返还 50%/120%/300%。完整流程见
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# coin-transfer
|
||||
|
||||
Author: `jwebot`
|
||||
|
||||
金币转账
|
||||
|
||||
Help:
|
||||
|
||||
- `/转账 对方 金额;/transfer`
|
||||
|
||||
Registered commands:
|
||||
|
||||
- `/转账`
|
||||
- `/transfer`
|
||||
|
||||
The adapter has no direct Ledger, GUI, environment, shell, or network access.
|
||||
It delegates to the reviewed `coin-transfer` Kernel service profile
|
||||
and returns a structured `ReplyPlan`.
|
||||
@@ -0,0 +1,14 @@
|
||||
id = "coin-transfer"
|
||||
author = "jwebot"
|
||||
description = "金币转账"
|
||||
help = ["/转账 对方 金额;/transfer"]
|
||||
version = "1.0.0"
|
||||
api_version = "1"
|
||||
entrypoint = "coin_transfer:plugin"
|
||||
type = "command"
|
||||
|
||||
[capabilities]
|
||||
commands = ["/转账", "/transfer"]
|
||||
storage = false
|
||||
network_profiles = []
|
||||
service_profiles = ["coin-transfer"]
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Generated reviewed adapter for coin-transfer."""
|
||||
|
||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
|
||||
|
||||
PROFILE = 'coin-transfer'
|
||||
AUTHOR = 'jwebot'
|
||||
DESCRIPTION = '金币转账'
|
||||
HELP_ENTRIES = ('/转账 对方 金额;/transfer',)
|
||||
COMMANDS = ('/转账', '/transfer')
|
||||
|
||||
|
||||
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))
|
||||
@@ -0,0 +1,29 @@
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
|
||||
SOURCE = Path(__file__).resolve().parents[1] / "src"
|
||||
sys.path.insert(0, str(SOURCE))
|
||||
|
||||
from coin_transfer import AUTHOR, DESCRIPTION, HELP_ENTRIES, plugin
|
||||
|
||||
|
||||
class PluginTests(unittest.TestCase):
|
||||
def test_registers_declared_commands_and_author(self):
|
||||
context = mock.Mock()
|
||||
plugin(context)
|
||||
registered = [
|
||||
call.args[0]
|
||||
for call in context.commands.register.call_args_list
|
||||
]
|
||||
self.assertEqual(AUTHOR, 'jwebot')
|
||||
self.assertEqual(DESCRIPTION, '金币转账')
|
||||
self.assertEqual(HELP_ENTRIES, ('/转账 对方 金额;/transfer',))
|
||||
self.assertIn('/转账', registered)
|
||||
self.assertEqual(len(registered), 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
Author: `jwebot`
|
||||
|
||||
虚拟金币账户、抽奖、群内多人彩票和单人21点
|
||||
金币游戏
|
||||
|
||||
Help:
|
||||
|
||||
- `/金币 /wallet;/抽奖 [金额] /draw;/游戏记录 /gamehistory`
|
||||
- `/彩票 开始|报名|当前状态|开奖|取消;/lottery`
|
||||
- `/21点 开始 [1~500](金额=本局下注,默认50,空格可省略)`
|
||||
- `/21点 要牌|停牌|状态|结束;/blackjack hit|stand|status|end`
|
||||
- `/金币 /wallet;/游戏记录 /gamehistory`
|
||||
- `/抽奖 [金额];/draw`
|
||||
- `/彩票 开始|报名|状态|开奖|取消;/lottery`
|
||||
- `/21点 开始 [1~500];/blackjack start`
|
||||
- `/21点 要牌|停牌|状态|结束;/blackjack`
|
||||
- `/游戏帮助;/games`
|
||||
|
||||
Registered commands:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "single-player-games"
|
||||
author = "jwebot"
|
||||
description = "虚拟金币账户、抽奖、群内多人彩票和单人21点"
|
||||
help = ["/金币 /wallet;/抽奖 [金额] /draw;/游戏记录 /gamehistory", "/彩票 开始|报名|当前状态|开奖|取消;/lottery", "/21点 开始 [1~500](金额=本局下注,默认50,空格可省略)", "/21点 要牌|停牌|状态|结束;/blackjack hit|stand|status|end", "/游戏帮助;/games"]
|
||||
description = "金币游戏"
|
||||
help = ["/金币 /wallet;/游戏记录 /gamehistory", "/抽奖 [金额];/draw", "/彩票 开始|报名|状态|开奖|取消;/lottery", "/21点 开始 [1~500];/blackjack start", "/21点 要牌|停牌|状态|结束;/blackjack", "/游戏帮助;/games"]
|
||||
version = "1.0.0"
|
||||
api_version = "1"
|
||||
entrypoint = "single_player_games:plugin"
|
||||
|
||||
@@ -5,8 +5,8 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
|
||||
PROFILE = 'single-player-games'
|
||||
AUTHOR = 'jwebot'
|
||||
DESCRIPTION = '虚拟金币账户、抽奖、群内多人彩票和单人21点'
|
||||
HELP_ENTRIES = ('/金币 /wallet;/抽奖 [金额] /draw;/游戏记录 /gamehistory', '/彩票 开始|报名|当前状态|开奖|取消;/lottery', '/21点 开始 [1~500](金额=本局下注,默认50,空格可省略)', '/21点 要牌|停牌|状态|结束;/blackjack hit|stand|status|end', '/游戏帮助;/games')
|
||||
DESCRIPTION = '金币游戏'
|
||||
HELP_ENTRIES = ('/金币 /wallet;/游戏记录 /gamehistory', '/抽奖 [金额];/draw', '/彩票 开始|报名|状态|开奖|取消;/lottery', '/21点 开始 [1~500];/blackjack start', '/21点 要牌|停牌|状态|结束;/blackjack', '/游戏帮助;/games')
|
||||
COMMANDS = ('/金币', '/wallet', '/抽奖', '/draw', '/彩票', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏记录', '/个人记录', '/gamehistory', '/游戏帮助', '/games')
|
||||
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ class PluginTests(unittest.TestCase):
|
||||
for call in context.commands.register.call_args_list
|
||||
]
|
||||
self.assertEqual(AUTHOR, 'jwebot')
|
||||
self.assertEqual(DESCRIPTION, '虚拟金币账户、抽奖、群内多人彩票和单人21点')
|
||||
self.assertEqual(HELP_ENTRIES, ('/金币 /wallet;/抽奖 [金额] /draw;/游戏记录 /gamehistory', '/彩票 开始|报名|当前状态|开奖|取消;/lottery', '/21点 开始 [1~500](金额=本局下注,默认50,空格可省略)', '/21点 要牌|停牌|状态|结束;/blackjack hit|stand|status|end', '/游戏帮助;/games'))
|
||||
self.assertEqual(DESCRIPTION, '金币游戏')
|
||||
self.assertEqual(HELP_ENTRIES, ('/金币 /wallet;/游戏记录 /gamehistory', '/抽奖 [金额];/draw', '/彩票 开始|报名|状态|开奖|取消;/lottery', '/21点 开始 [1~500];/blackjack start', '/21点 要牌|停牌|状态|结束;/blackjack', '/游戏帮助;/games'))
|
||||
self.assertIn('/金币', registered)
|
||||
self.assertEqual(len(registered), 15)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-config"
|
||||
version = "0.6.8a0+1015"
|
||||
version = "0.6.9a0+1015"
|
||||
description = "Profile-based configuration compiler for JWeBot"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
|
||||
@@ -36,4 +36,4 @@ __all__ = [
|
||||
"write_default_profile",
|
||||
]
|
||||
|
||||
__version__ = "0.6.8a0+1015"
|
||||
__version__ = "0.6.9a0+1015"
|
||||
|
||||
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-extension-host"
|
||||
version = "0.6.8a0+1015"
|
||||
version = "0.6.9a0+1015"
|
||||
description = "Single-release, single-invocation process host for JWeBot extensions"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"jwebot-sdk==0.6.8a0+1015",
|
||||
"jwebot-kernel==0.6.8a0+1015",
|
||||
"jwebot-sdk==0.6.9a0+1015",
|
||||
"jwebot-kernel==0.6.9a0+1015",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -16,4 +16,4 @@ __all__ = [
|
||||
"process_request",
|
||||
]
|
||||
|
||||
__version__ = "0.6.8a0+1015"
|
||||
__version__ = "0.6.9a0+1015"
|
||||
|
||||
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-kernel"
|
||||
version = "0.6.8a0+1015"
|
||||
version = "0.6.9a0+1015"
|
||||
description = "Stable routing and extension authority for JWeBot"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"jwebot-config==0.6.8a0+1015",
|
||||
"jwebot-sdk==0.6.8a0+1015",
|
||||
"jwebot-official-extensions==0.6.8a0+1015",
|
||||
"jwebot-config==0.6.9a0+1015",
|
||||
"jwebot-sdk==0.6.9a0+1015",
|
||||
"jwebot-official-extensions==0.6.9a0+1015",
|
||||
"tomli>=2; python_version < '3.11'",
|
||||
]
|
||||
|
||||
|
||||
@@ -107,4 +107,4 @@ __all__ = [
|
||||
"verify_installed_release_signature",
|
||||
]
|
||||
|
||||
__version__ = "0.6.8a0+1015"
|
||||
__version__ = "0.6.9a0+1015"
|
||||
|
||||
@@ -3665,6 +3665,98 @@ class Ledger:
|
||||
"game_counts": dict(account["game_counts"]),
|
||||
}
|
||||
|
||||
def transfer_player_coins(
|
||||
self,
|
||||
group: str,
|
||||
sender: str,
|
||||
event_id: str,
|
||||
command: str,
|
||||
target_label: str,
|
||||
amount: int,
|
||||
render: Callable[[dict[str, Any]], str],
|
||||
) -> str:
|
||||
"""Move virtual coins between two group-scoped player accounts."""
|
||||
source = self._player_identity(group, sender)
|
||||
source_key = str(source["person_key"])
|
||||
target = self.resolve_person(group, target_label)
|
||||
target_key = str(target["person_key"])
|
||||
target_display_name = person_display_label(target["display_name"])
|
||||
with self.lock:
|
||||
self.connection.execute("BEGIN IMMEDIATE")
|
||||
try:
|
||||
replay = self._player_action_replay(
|
||||
group, event_id, source_key, command
|
||||
)
|
||||
if replay is not None:
|
||||
self.connection.execute("COMMIT")
|
||||
return replay
|
||||
if source_key == target_key:
|
||||
data: dict[str, Any] = {
|
||||
"status": "self",
|
||||
"source_display_name": source["display_name"],
|
||||
"target_display_name": target_display_name,
|
||||
"amount": amount,
|
||||
}
|
||||
else:
|
||||
target_exists = self.connection.execute(
|
||||
"""
|
||||
SELECT 1 FROM player_accounts
|
||||
WHERE group_fingerprint = ? AND person_key = ?
|
||||
LIMIT 1
|
||||
""",
|
||||
(group, target_key),
|
||||
).fetchone() is not None
|
||||
if not target_exists:
|
||||
data = {
|
||||
"status": "target_missing",
|
||||
"source_display_name": source["display_name"],
|
||||
"target_display_name": target_display_name,
|
||||
"amount": amount,
|
||||
}
|
||||
else:
|
||||
source_account = self._load_player_account(
|
||||
group, source_key
|
||||
)
|
||||
if amount > int(source_account["balance"]):
|
||||
data = {
|
||||
"status": "insufficient",
|
||||
"source_display_name": source["display_name"],
|
||||
"target_display_name": target_display_name,
|
||||
"amount": amount,
|
||||
"source_balance": int(source_account["balance"]),
|
||||
}
|
||||
else:
|
||||
target_account = self._load_player_account(
|
||||
group, target_key
|
||||
)
|
||||
source_account["balance"] -= amount
|
||||
target_account["balance"] += amount
|
||||
timestamp = utc_now()
|
||||
self._save_player_account(
|
||||
group, source_key, source_account, timestamp
|
||||
)
|
||||
self._save_player_account(
|
||||
group, target_key, target_account, timestamp
|
||||
)
|
||||
data = {
|
||||
"status": "transferred",
|
||||
"source_display_name": source["display_name"],
|
||||
"target_display_name": target_display_name,
|
||||
"amount": amount,
|
||||
"source_balance": int(source_account["balance"]),
|
||||
"target_balance": int(target_account["balance"]),
|
||||
}
|
||||
response = render(data)
|
||||
timestamp = utc_now()
|
||||
self._store_player_action(
|
||||
group, event_id, source_key, command, response, timestamp
|
||||
)
|
||||
self.connection.execute("COMMIT")
|
||||
return response
|
||||
except BaseException:
|
||||
self.connection.execute("ROLLBACK")
|
||||
raise
|
||||
|
||||
def apply_player_action(
|
||||
self, group: str, sender: str, event_id: str, command: str,
|
||||
action: Callable[[dict[str, Any]], str],
|
||||
|
||||
@@ -4,12 +4,15 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-official-extensions"
|
||||
version = "0.6.8a0+1015"
|
||||
version = "0.6.9a0+1015"
|
||||
description = "Reviewed local command adapters and broker implementation for JWeBot"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"jwebot-sdk==0.6.8a0+1015",
|
||||
"jwebot-sdk==0.6.9a0+1015",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
jwebot_official_extensions = ["data/cs_autochess/*.json"]
|
||||
|
||||
@@ -90,4 +90,4 @@ __all__ = [
|
||||
"render_feature_list",
|
||||
]
|
||||
|
||||
__version__ = "0.6.8a0+1015"
|
||||
__version__ = "0.6.9a0+1015"
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
"""Generated reviewed adapter for coin-transfer."""
|
||||
|
||||
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
|
||||
|
||||
PROFILE = 'coin-transfer'
|
||||
AUTHOR = 'jwebot'
|
||||
DESCRIPTION = '金币转账'
|
||||
HELP_ENTRIES = ('/转账 对方 金额;/transfer',)
|
||||
COMMANDS = ('/转账', '/transfer')
|
||||
|
||||
|
||||
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
@@ -5,8 +5,8 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
|
||||
PROFILE = 'single-player-games'
|
||||
AUTHOR = 'jwebot'
|
||||
DESCRIPTION = '虚拟金币账户、抽奖、群内多人彩票和单人21点'
|
||||
HELP_ENTRIES = ('/金币 /wallet;/抽奖 [金额] /draw;/游戏记录 /gamehistory', '/彩票 开始|报名|当前状态|开奖|取消;/lottery', '/21点 开始 [1~500](金额=本局下注,默认50,空格可省略)', '/21点 要牌|停牌|状态|结束;/blackjack hit|stand|status|end', '/游戏帮助;/games')
|
||||
DESCRIPTION = '金币游戏'
|
||||
HELP_ENTRIES = ('/金币 /wallet;/游戏记录 /gamehistory', '/抽奖 [金额];/draw', '/彩票 开始|报名|状态|开奖|取消;/lottery', '/21点 开始 [1~500];/blackjack start', '/21点 要牌|停牌|状态|结束;/blackjack', '/游戏帮助;/games')
|
||||
COMMANDS = ('/金币', '/wallet', '/抽奖', '/draw', '/彩票', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏记录', '/个人记录', '/gamehistory', '/游戏帮助', '/games')
|
||||
|
||||
|
||||
|
||||
+573
-481
File diff suppressed because it is too large
Load Diff
+185
@@ -0,0 +1,185 @@
|
||||
{
|
||||
"coaches": [
|
||||
{
|
||||
"bonus": 9,
|
||||
"clutch": 88,
|
||||
"firepower": 78,
|
||||
"id": "zonic",
|
||||
"level": 5,
|
||||
"name": "zonic",
|
||||
"stability": 92,
|
||||
"style": "冠军体系"
|
||||
},
|
||||
{
|
||||
"bonus": 9,
|
||||
"clutch": 84,
|
||||
"firepower": 72,
|
||||
"id": "blade",
|
||||
"level": 5,
|
||||
"name": "B1ad3",
|
||||
"stability": 96,
|
||||
"style": "结构控图"
|
||||
},
|
||||
{
|
||||
"bonus": 8,
|
||||
"clutch": 80,
|
||||
"firepower": 86,
|
||||
"id": "xtqzzz",
|
||||
"level": 4,
|
||||
"name": "XTQZZZ",
|
||||
"stability": 78,
|
||||
"style": "进攻提速"
|
||||
},
|
||||
{
|
||||
"bonus": 8,
|
||||
"clutch": 90,
|
||||
"firepower": 76,
|
||||
"id": "neo",
|
||||
"level": 4,
|
||||
"name": "NEO",
|
||||
"stability": 82,
|
||||
"style": "残局纪律"
|
||||
},
|
||||
{
|
||||
"bonus": 7,
|
||||
"clutch": 82,
|
||||
"firepower": 80,
|
||||
"id": "saw",
|
||||
"level": 3,
|
||||
"name": "sAw",
|
||||
"stability": 80,
|
||||
"style": "临场调整"
|
||||
},
|
||||
{
|
||||
"bonus": 7,
|
||||
"clutch": 78,
|
||||
"firepower": 74,
|
||||
"id": "robban",
|
||||
"level": 3,
|
||||
"name": "RobbaN",
|
||||
"stability": 86,
|
||||
"style": "更衣室管理"
|
||||
},
|
||||
{
|
||||
"bonus": 6,
|
||||
"clutch": 76,
|
||||
"firepower": 84,
|
||||
"id": "kane",
|
||||
"level": 2,
|
||||
"name": "kane",
|
||||
"stability": 72,
|
||||
"style": "明星激活"
|
||||
},
|
||||
{
|
||||
"bonus": 6,
|
||||
"clutch": 74,
|
||||
"firepower": 70,
|
||||
"id": "trace",
|
||||
"level": 2,
|
||||
"name": "trace",
|
||||
"stability": 86,
|
||||
"style": "防守复盘"
|
||||
},
|
||||
{
|
||||
"bonus": 5,
|
||||
"clutch": 72,
|
||||
"firepower": 76,
|
||||
"id": "ash",
|
||||
"level": 1,
|
||||
"name": "ash",
|
||||
"stability": 74,
|
||||
"style": "新人培养"
|
||||
},
|
||||
{
|
||||
"bonus": 5,
|
||||
"clutch": 70,
|
||||
"firepower": 70,
|
||||
"id": "mithr",
|
||||
"level": 1,
|
||||
"name": "mithR",
|
||||
"stability": 78,
|
||||
"style": "基础纪律"
|
||||
},
|
||||
{
|
||||
"bonus": 8,
|
||||
"clutch": 82,
|
||||
"firepower": 74,
|
||||
"id": "kuben",
|
||||
"level": 4,
|
||||
"name": "kuben",
|
||||
"stability": 88,
|
||||
"style": "慢控默认"
|
||||
},
|
||||
{
|
||||
"bonus": 7,
|
||||
"clutch": 78,
|
||||
"firepower": 72,
|
||||
"id": "groove",
|
||||
"level": 3,
|
||||
"name": "groove",
|
||||
"stability": 84,
|
||||
"style": "学院体系"
|
||||
},
|
||||
{
|
||||
"bonus": 8,
|
||||
"clutch": 80,
|
||||
"firepower": 86,
|
||||
"id": "guerri",
|
||||
"level": 4,
|
||||
"name": "guerri",
|
||||
"stability": 76,
|
||||
"style": "巴西爆弹"
|
||||
},
|
||||
{
|
||||
"bonus": 8,
|
||||
"clutch": 82,
|
||||
"firepower": 76,
|
||||
"id": "xizt",
|
||||
"level": 4,
|
||||
"name": "Xizt",
|
||||
"stability": 86,
|
||||
"style": "默认纪律"
|
||||
},
|
||||
{
|
||||
"bonus": 7,
|
||||
"clutch": 78,
|
||||
"firepower": 80,
|
||||
"id": "sycrone",
|
||||
"level": 3,
|
||||
"name": "sycrone",
|
||||
"stability": 80,
|
||||
"style": "青年提拔"
|
||||
},
|
||||
{
|
||||
"bonus": 7,
|
||||
"clutch": 80,
|
||||
"firepower": 82,
|
||||
"id": "malek",
|
||||
"level": 3,
|
||||
"name": "maLeK",
|
||||
"stability": 78,
|
||||
"style": "明星平衡"
|
||||
},
|
||||
{
|
||||
"bonus": 6,
|
||||
"clutch": 76,
|
||||
"firepower": 78,
|
||||
"id": "peacemaker",
|
||||
"level": 2,
|
||||
"name": "peacemaker",
|
||||
"stability": 74,
|
||||
"style": "临时救火"
|
||||
},
|
||||
{
|
||||
"bonus": 6,
|
||||
"clutch": 74,
|
||||
"firepower": 72,
|
||||
"id": "kassad",
|
||||
"level": 2,
|
||||
"name": "kassad",
|
||||
"stability": 82,
|
||||
"style": "纪律重建"
|
||||
}
|
||||
],
|
||||
"schema_version": 1
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
{
|
||||
"fragments": {
|
||||
"challenges": [
|
||||
"媒体日问题很尖锐,队伍需要在镜头前保持状态。",
|
||||
"赞助商临时加码,要求队伍用指定战术打出观赏性。",
|
||||
"主办方安排了签名会,状态管理成了额外考验。",
|
||||
"训练馆网络短暂波动,队伍必须快速适应备用机器。",
|
||||
"赛前采访拖长,指挥需要把注意力重新拉回比赛。",
|
||||
"赞助商想看激进打法,但稳住节奏的人会拿到更高回报。",
|
||||
"媒体日不是战斗回合,却会影响队伍的运营资源。",
|
||||
"队伍如果阵容完整,就更容易把这次插曲处理成奖金。",
|
||||
"这次挑战只出现一次,处理完就回到小组赛主线。",
|
||||
"主舞台彩排占用了热身时间,教练的管理能力开始生效。",
|
||||
"赞助商要求展示新战术,选错方向也不会淘汰,只会少拿金币。",
|
||||
"现场采访提到了前一场失利,队员心态需要稳住。",
|
||||
"这类挑战不扣生命,目的只是给运营阶段增加变数。",
|
||||
"处理好这次插曲的队伍,会在下一场前多一点经济余地。"
|
||||
],
|
||||
"closers": [
|
||||
"{team} 没有给翻盘窗口,最后一波回防处理得很完整。",
|
||||
"{team} 在赛点局选择最熟悉的默认,用纪律结束地图。",
|
||||
"{team} 最后一回合没有贪枪,稳稳把人数优势转成胜势。",
|
||||
"{team} 的收官很干净,地图尾声没有出现多余失误。",
|
||||
"{team} 用一波双拉打开包点,最后比分定格。",
|
||||
"{team} 把比赛拖进自己舒服的节奏,收尾阶段非常稳。",
|
||||
"{team} 赛点局的道具层次很清楚,对手没有舒服的回防路线。",
|
||||
"{team} 终于等到对手前压,抓住这个窗口终结地图。",
|
||||
"{team} 收下最后一分时没有冒进,这就是强队式结尾。",
|
||||
"{team} 最后一枪来得很快,但前面的信息铺垫已经足够。",
|
||||
"{team} 在关键经济局之后顺势收掉地图。",
|
||||
"{team} 先控退路再进包点,结尾没有留下残局悬念。",
|
||||
"{team} 把上半场积累的优势守到最后。",
|
||||
"{team} 用一波慢夹收官,比分看起来合理,过程更像真实比赛。",
|
||||
"{team} 没让对手连追,赛点局直接把节奏抢回来。",
|
||||
"{team} 的最后一分赢在补枪,枪线没有断。",
|
||||
"{team} 收官回合把每个角落都清干净,耐心给了回报。",
|
||||
"{team} 把悬念留到最后一刻,但关键枪没有手软。"
|
||||
],
|
||||
"clutches": [
|
||||
"{player} 留到最后一个残局,靠一颗烟把拆包时间偷了出来。",
|
||||
"{player} 没有急着补枪,等脚步声靠近后才横拉收掉残局。",
|
||||
"{player} 在 1v2 里先断退路,再回头处理包点。",
|
||||
"{player} 的残局处理很冷,假拆骗出第一枪后直接反清。",
|
||||
"{player} 被逼到死角,但一颗反弹闪把局面重新打开。",
|
||||
"{player} 没有追击残血,而是先保住雷包,判断非常老练。",
|
||||
"{player} 关键回合双杀,把本来要崩的经济线救回来。",
|
||||
"{player} 在读秒阶段完成补枪,队友的拉扯给得刚刚好。",
|
||||
"{player} 先拿信息再开枪,这个残局赢在耐心。",
|
||||
"{player} 硬吃白光完成换人,比分没有被对手咬住。",
|
||||
"{player} 从连接位摸出,正好打掉回防节奏。",
|
||||
"{player} 这次不追求集锦,只把枪线封死等队友到位。",
|
||||
"{player} 的狙击枪没有空枪,关键首杀让整回合变得简单。",
|
||||
"{player} 残血仍然敢看第一枪位,队伍因此拿到赛点。",
|
||||
"{player} 拿下残局后没有庆祝,马上提醒队友保枪经济。",
|
||||
"{player} 靠脚步假信息骗出转点,最后一枪很稳。",
|
||||
"{player} 在包点里连续换位,对手始终没能定位。",
|
||||
"{player} 把闪光背身吃完后仍然补到第二枪,这是关键转折。",
|
||||
"{player} 一个人拖住整条回防路线,为队伍争到拆包窗口。",
|
||||
"{player} 最后选择不 peek,让时间替自己赢下这一分。"
|
||||
],
|
||||
"economy": [
|
||||
"{team} 赢下这一分后可以完整起枪,下一回合选择会更多。",
|
||||
"{team} 输掉长枪局后经济见底,可能要交出一回合调整空间。",
|
||||
"{team} 没有强行追梦,选择保枪把下一回合留住。",
|
||||
"{team} 的连败补偿到位,下一回合有机会靠道具质量反打。",
|
||||
"{team} 这回合缴下两把枪,经济收益比比分更重要。",
|
||||
"{team} 虽然丢分,但保下主狙,下一回合还有破局点。",
|
||||
"{team} 的半甲手枪没有打成,但至少逼出了对手不少道具。",
|
||||
"{team} 经济优势开始显现,可以更大胆地争夺第一枪位。",
|
||||
"{team} 如果下一分再丢,就必须进入半起调整。",
|
||||
"{team} 这一波选择全甲全弹,明显是要在中期一次打穿。",
|
||||
"{team} 保下来的两把枪让下回合不至于完全被动。",
|
||||
"{team} 连续换枪成功,把对手的存款一点点磨掉。",
|
||||
"{team} 用低成本回合换到下包,经济线还没有断。",
|
||||
"{team} 这波赢的不只是分数,还有下一回合的主动权。",
|
||||
"{team} 被重置经济后节奏放慢,先求稳住半场尾声。",
|
||||
"{team} 靠残局胜利拿到额外空间,可以考虑继续 D 牌补强。"
|
||||
],
|
||||
"openings": [
|
||||
"{team} 开局没有急着提速,先把默认控图铺开。",
|
||||
"{team} 第一波提速很坚决,外场道具落点干净。",
|
||||
"{team} 在手枪局后选择慢摸,试图逼出对手的前压信息。",
|
||||
"{team} 的狙击手早早架住第一交火点,节奏被压得很低。",
|
||||
"{team} 连续用两颗闪光清掉近点,进攻面一下打开。",
|
||||
"{team} 防守开局站位很深,把回防路线留得很完整。",
|
||||
"{team} 先拿地图控制权,再把包点压力一点点推上去。",
|
||||
"{team} 开局没有赌点,靠信息互换慢慢读阵型。",
|
||||
"{team} 在第一枪位赢下后没有贪,马上回收人数优势。",
|
||||
"{team} 用一波假打骗出轮转,第二时间才开始真正爆弹。",
|
||||
"{team} 选择保守开局,宁愿少拿信息也不送首杀。",
|
||||
"{team} 的指挥把默认拉得很散,让对手很难判断重心。",
|
||||
"{team} 手枪局道具很少,但站位互补做得很扎实。",
|
||||
"{team} 没有被前几回合影响,依旧按自己的节奏展开。",
|
||||
"{team} 的第一套进攻明显针对了对手弱侧。",
|
||||
"{team} 在经济局也保留了两颗关键闪,准备打后点反清。",
|
||||
"{team} 上来就抢中路,想用主动权换后续转点空间。",
|
||||
"{team} 先把狙击枪放在安全位,围绕它慢慢组织推进。"
|
||||
],
|
||||
"pauses": [
|
||||
"比分被拉开后,双方都慢了半拍,像是在重新读这张图。",
|
||||
"这里适合停一下,刚才几回合的经济差已经开始影响下一枪。",
|
||||
"暂停回来之后,防守方明显把第一接触点后撤了半格。",
|
||||
"连续交火后节奏突然变慢,双方都在等对方先犯错。",
|
||||
"这一段没有夸张集锦,但每一颗道具都在改变下一回合。",
|
||||
"场面安静了几秒,真正决定胜负的是下一次首杀。",
|
||||
"队伍语音应该都在压节奏,没人愿意在这里送掉经济。",
|
||||
"这不是简单的比分变化,战术重心已经从爆弹转到控图。",
|
||||
"现场进入短暂拉扯,地图控制权一点点被重新分配。",
|
||||
"两边都知道下一分很贵,所以这回合开局格外谨慎。",
|
||||
"这一小段空白很重要,强队往往就是在这里调整默认。",
|
||||
"比赛没有马上进入下一波交火,指挥正在重新排优先级。",
|
||||
"刚才的连分让对手不得不换站位,防线开始出现试探。",
|
||||
"经济压力让双方都少了一点容错,下一波道具会很关键。",
|
||||
"这里没有急躁的前压,只有一点点互相试探。",
|
||||
"暂停不是为了拖时间,而是在给下一套进攻换答案。",
|
||||
"双方都把节奏压住,地图像被重新归零。",
|
||||
"这一分之前的几秒钟很长,所有人都在等第一个脚步声。",
|
||||
"场上没有无意义的 peek,每个人都在守自己的触发条件。",
|
||||
"这段过渡让比赛更像真正的 BO 系列赛,而不是单纯掷骰子。"
|
||||
],
|
||||
"runs": [
|
||||
"{team} 连拿 {count} 分之后,现场短暂安静下来。",
|
||||
"{team} 把经济滚起来后,连续 {count} 个回合没有给对手完整开枪空间。",
|
||||
"{team} 这一波 {count} 连分不是纯枪法碾压,而是每回合都换到正确信息。",
|
||||
"{team} 的节奏忽快忽慢,{count} 个回合里每次进点方式都不一样。",
|
||||
"{team} 靠补枪链吃下 {count} 分,队伍语音应该已经很热。",
|
||||
"{team} 的中期处理开始成型,连续 {count} 分把对手钱打空。",
|
||||
"{team} 没有急着结束比赛,而是用 {count} 连分把地图控制牢牢握住。",
|
||||
"{team} 这段 {count} 连分很关键,尤其是第二个残局稳定了军心。",
|
||||
"{team} 在暂停之后突然提速,直接打出 {count} 连分。",
|
||||
"{team} 先抓前压,再抓回防,{count} 个回合把读秒处理做满。",
|
||||
"{team} 的防守交叉火力越站越舒服,{count} 连分让比分拉开。",
|
||||
"{team} 把对手的道具节奏读透,连续 {count} 分都避开了主防区。",
|
||||
"{team} 经济起飞后没有乱买,{count} 连分中枪线一直保持完整。",
|
||||
"{team} 在被首杀的回合仍然能翻,{count} 连分含金量很高。",
|
||||
"{team} 的默认推进开始收利息,{count} 个回合每次都能换到便宜。",
|
||||
"{team} 靠一波小连胜把压力交回去,{count} 分之后对手必须叫停。",
|
||||
"{team} 的二楼夹击反复奏效,{count} 连分让对手开始犹豫。",
|
||||
"{team} 连续 {count} 回合把残局拖到优势人数,后点纪律很稳。",
|
||||
"{team} 打出 {count} 连分后节奏放慢,像是在等对手自己露破绽。",
|
||||
"{team} 这波 {count} 连分让地图气质变了,所有枪位都开始往前压。"
|
||||
],
|
||||
"training_hooks": [
|
||||
"训练赛不计入小组胜负,但会发金币,方便后续刷新商店。",
|
||||
"这轮训练更像实战热身,每队都会遇到同场参赛对手。",
|
||||
"训练赛重点不是淘汰谁,而是检验当前阵容能不能打完整张图。",
|
||||
"这轮结束后可以 D 牌、锁店或调首发,下一轮再检验成果。",
|
||||
"AI 战队也会参与训练,对局结果会写入档案。",
|
||||
"如果阵容缺少指挥,训练赛的中期回合会更容易失控。",
|
||||
"如果你想追羁绊,现在是最适合刷新商店的窗口。",
|
||||
"训练赛的金币很重要,别把每次刷新都当成免费试错。",
|
||||
"这一阶段没有生命扣除,重点是运营和阵容成型。",
|
||||
"训练赛的地图会轮换,不会一直用同一个固定比分模板。",
|
||||
"每轮训练结束后,真人都需要发送 /结束回合 才会进入下一轮。",
|
||||
"训练赛会按轮次播报,避免一次性刷出一大串结果。",
|
||||
"如果出现轮空,会安排本地陪练队补一张 BO1。",
|
||||
"训练阶段适合观察战术克制,火力、稳定、残局都会影响回合走向。",
|
||||
"连胜会带来金币,但失败也有保底,保证测试局能继续运营。",
|
||||
"这部分是纯文字自走棋的节奏层,不再是单条目标数值判定。"
|
||||
]
|
||||
},
|
||||
"schema_version": 1
|
||||
}
|
||||
+1852
File diff suppressed because it is too large
Load Diff
+309
@@ -0,0 +1,309 @@
|
||||
{
|
||||
"ai": {
|
||||
"power_multiplier": 0.82,
|
||||
"primary_display_name": "AI 战队",
|
||||
"primary_person_key": "__local_ai_team__",
|
||||
"secondary_display_name": "AI 战队 B",
|
||||
"secondary_person_key": "__local_ai_team_2__"
|
||||
},
|
||||
"battle": {
|
||||
"damage": {
|
||||
"bo5_sweep": 18,
|
||||
"non_sweep": 10,
|
||||
"sweep": 15
|
||||
},
|
||||
"maps": [
|
||||
"Mirage",
|
||||
"Inferno",
|
||||
"Nuke",
|
||||
"Ancient",
|
||||
"Dust2",
|
||||
"Anubis",
|
||||
"Overpass"
|
||||
],
|
||||
"match_best_of": {
|
||||
"final": 5,
|
||||
"group": 3,
|
||||
"semifinal": 5,
|
||||
"training": 1
|
||||
},
|
||||
"mr_rounds": 30,
|
||||
"overtime_safety_cap": 72,
|
||||
"overtime_segment_rounds": 6,
|
||||
"overtime_start_score": 15,
|
||||
"overtime_target_score": 19,
|
||||
"overtime_win_by": 2,
|
||||
"round_probability": {
|
||||
"base": 0.5,
|
||||
"first_half_side_swing": 0.018,
|
||||
"jitter_center": 350,
|
||||
"jitter_divisor": 10000,
|
||||
"jitter_mod": 701,
|
||||
"max": 0.68,
|
||||
"min": 0.32,
|
||||
"power_edge_cap": 0.16,
|
||||
"power_edge_scale": 0.3,
|
||||
"pressure_cap": 0.04,
|
||||
"pressure_scale": 0.004,
|
||||
"second_half_side_swing": -0.012
|
||||
},
|
||||
"score_bands": [
|
||||
{
|
||||
"label": "压制",
|
||||
"max_loser_score": 6
|
||||
},
|
||||
{
|
||||
"label": "优势",
|
||||
"max_loser_score": 10
|
||||
},
|
||||
{
|
||||
"label": "小胜",
|
||||
"max_loser_score": 13
|
||||
},
|
||||
{
|
||||
"label": "险胜",
|
||||
"max_loser_score": 999
|
||||
}
|
||||
],
|
||||
"target_score": 16
|
||||
},
|
||||
"coach": {
|
||||
"fallback_bonuses": {
|
||||
"1": 2,
|
||||
"2": 4,
|
||||
"3": 6,
|
||||
"4": 8,
|
||||
"5": 10
|
||||
},
|
||||
"upgrade_costs": {
|
||||
"1": 6,
|
||||
"2": 10,
|
||||
"3": 16,
|
||||
"4": 24
|
||||
}
|
||||
},
|
||||
"draft": {
|
||||
"coach_choice_count": 10,
|
||||
"coach_round": 1,
|
||||
"later_round_preferred_costs": [
|
||||
3,
|
||||
2,
|
||||
4,
|
||||
1,
|
||||
5
|
||||
],
|
||||
"player_choice_count": 6,
|
||||
"player_copy_count": 3,
|
||||
"round_two_preferred_costs": [
|
||||
2,
|
||||
3,
|
||||
1,
|
||||
4,
|
||||
5
|
||||
],
|
||||
"rounds": 3
|
||||
},
|
||||
"economy": {
|
||||
"round_income": {
|
||||
"base": 5,
|
||||
"interest_cap": 5,
|
||||
"interest_divisor": 10
|
||||
},
|
||||
"series_rewards": {
|
||||
"group_win_gold": 5,
|
||||
"playoff_win_gold": 6
|
||||
},
|
||||
"starting_gold": 10,
|
||||
"starting_hp": 100,
|
||||
"training_rewards": {
|
||||
"bye_gold": 2,
|
||||
"loss_gold": 1,
|
||||
"win_gold": 3
|
||||
}
|
||||
},
|
||||
"pve_events": {
|
||||
"pve_media": {
|
||||
"loss_gold": 3,
|
||||
"name": "媒体日挑战",
|
||||
"target": 335,
|
||||
"win_gold": 6
|
||||
},
|
||||
"pve_sponsor": {
|
||||
"loss_gold": 4,
|
||||
"name": "赞助挑战",
|
||||
"target": 395,
|
||||
"win_gold": 8
|
||||
},
|
||||
"pve_training": {
|
||||
"loss_gold": 2,
|
||||
"name": "训练赛",
|
||||
"target": 275,
|
||||
"win_gold": 4
|
||||
}
|
||||
},
|
||||
"roster": {
|
||||
"bench_limit": 3,
|
||||
"nation_synergy": {
|
||||
"max_sources": 1,
|
||||
"thresholds": [
|
||||
{
|
||||
"bonus": 18,
|
||||
"min_count": 5
|
||||
},
|
||||
{
|
||||
"bonus": 12,
|
||||
"min_count": 4
|
||||
},
|
||||
{
|
||||
"bonus": 7,
|
||||
"min_count": 3
|
||||
},
|
||||
{
|
||||
"bonus": 3,
|
||||
"min_count": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"star_multipliers": {
|
||||
"1": 1.0,
|
||||
"2": 1.35,
|
||||
"3": 1.8
|
||||
},
|
||||
"starter_count": 5,
|
||||
"team_synergy": {
|
||||
"max_sources": 2,
|
||||
"thresholds": [
|
||||
{
|
||||
"bonus": 22,
|
||||
"min_count": 5
|
||||
},
|
||||
{
|
||||
"bonus": 15,
|
||||
"min_count": 4
|
||||
},
|
||||
{
|
||||
"bonus": 9,
|
||||
"min_count": 3
|
||||
},
|
||||
{
|
||||
"bonus": 4,
|
||||
"min_count": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit_power_weights": {
|
||||
"clutch": 0.25,
|
||||
"firepower": 0.45,
|
||||
"leadership_scale": 0.12,
|
||||
"leadership_threshold": 70,
|
||||
"stability": 0.3
|
||||
}
|
||||
},
|
||||
"schema_version": 1,
|
||||
"shop": {
|
||||
"cost_pool_copies": {
|
||||
"1": 18,
|
||||
"2": 15,
|
||||
"3": 12,
|
||||
"4": 9,
|
||||
"5": 7
|
||||
},
|
||||
"odds": {
|
||||
"1": {
|
||||
"1": 70,
|
||||
"2": 25,
|
||||
"3": 5,
|
||||
"4": 0,
|
||||
"5": 0
|
||||
},
|
||||
"2": {
|
||||
"1": 50,
|
||||
"2": 30,
|
||||
"3": 18,
|
||||
"4": 2,
|
||||
"5": 0
|
||||
},
|
||||
"3": {
|
||||
"1": 30,
|
||||
"2": 35,
|
||||
"3": 25,
|
||||
"4": 9,
|
||||
"5": 1
|
||||
},
|
||||
"4": {
|
||||
"1": 15,
|
||||
"2": 30,
|
||||
"3": 30,
|
||||
"4": 20,
|
||||
"5": 5
|
||||
},
|
||||
"5": {
|
||||
"1": 5,
|
||||
"2": 20,
|
||||
"3": 30,
|
||||
"4": 30,
|
||||
"5": 15
|
||||
}
|
||||
},
|
||||
"refresh_cost": 2,
|
||||
"size": 6
|
||||
},
|
||||
"special_events": {
|
||||
"media": {
|
||||
"loss_gold": 1,
|
||||
"name": "媒体日挑战",
|
||||
"target": 320,
|
||||
"win_gold": 4
|
||||
},
|
||||
"sponsor": {
|
||||
"loss_gold": 1,
|
||||
"name": "赞助挑战",
|
||||
"target": 340,
|
||||
"win_gold": 4
|
||||
}
|
||||
},
|
||||
"tactics": {
|
||||
"attribute_counters": [
|
||||
[
|
||||
"稳定",
|
||||
"火力"
|
||||
],
|
||||
[
|
||||
"火力",
|
||||
"残局"
|
||||
],
|
||||
[
|
||||
"残局",
|
||||
"稳定"
|
||||
]
|
||||
],
|
||||
"axis_percent_cap": 8.0,
|
||||
"axis_percent_divisor": 12.5,
|
||||
"counters": {
|
||||
"残局": "稳定",
|
||||
"火力": "残局",
|
||||
"稳定": "火力"
|
||||
},
|
||||
"names": [
|
||||
"火力",
|
||||
"稳定",
|
||||
"残局"
|
||||
],
|
||||
"role_counters": {
|
||||
"指挥": "狙击手",
|
||||
"步枪手": "指挥",
|
||||
"狙击手": "步枪手"
|
||||
},
|
||||
"role_percent_cap": 5.0,
|
||||
"role_percent_scale": 0.8,
|
||||
"selected_axis_multiplier": 1.08,
|
||||
"tactic_counter_percent": 6.0,
|
||||
"total_percent_cap": 15.0
|
||||
},
|
||||
"tournament": {
|
||||
"event_title_template": "第 {season} 届 IEM 盐田 Major",
|
||||
"host_name": "Taffy",
|
||||
"max_players": 8,
|
||||
"min_players": 2
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,8 @@ LOCAL_COMMAND_ALIASES = {
|
||||
"context": "context_summary",
|
||||
"金币": "wallet",
|
||||
"wallet": "wallet",
|
||||
"转账": "transfer",
|
||||
"transfer": "transfer",
|
||||
"抽奖": "prize_draw",
|
||||
"draw": "prize_draw",
|
||||
"彩票": "lottery",
|
||||
@@ -138,12 +140,13 @@ LOCAL_EXTENSIONS = (
|
||||
LocalExtensionDescriptor(
|
||||
extension_id="single-player-games",
|
||||
author=JWEBOT_AUTHOR,
|
||||
description="虚拟金币账户、抽奖、群内多人彩票和单人21点",
|
||||
description="金币游戏",
|
||||
help_entries=(
|
||||
"/金币 /wallet;/抽奖 [金额] /draw;/游戏记录 /gamehistory",
|
||||
"/彩票 开始|报名|当前状态|开奖|取消;/lottery",
|
||||
"/21点 开始 [1~500](金额=本局下注,默认50,空格可省略)",
|
||||
"/21点 要牌|停牌|状态|结束;/blackjack hit|stand|status|end",
|
||||
"/金币 /wallet;/游戏记录 /gamehistory",
|
||||
"/抽奖 [金额];/draw",
|
||||
"/彩票 开始|报名|状态|开奖|取消;/lottery",
|
||||
"/21点 开始 [1~500];/blackjack start",
|
||||
"/21点 要牌|停牌|状态|结束;/blackjack",
|
||||
"/游戏帮助;/games",
|
||||
),
|
||||
command_kinds=(
|
||||
@@ -155,6 +158,13 @@ LOCAL_EXTENSIONS = (
|
||||
"game_rules",
|
||||
),
|
||||
),
|
||||
LocalExtensionDescriptor(
|
||||
extension_id="coin-transfer",
|
||||
author=JWEBOT_AUTHOR,
|
||||
description="金币转账",
|
||||
help_entries=("/转账 对方 金额;/transfer",),
|
||||
command_kinds=("transfer",),
|
||||
),
|
||||
LocalExtensionDescriptor(
|
||||
extension_id="cs-major-autochess",
|
||||
author=JWEBOT_AUTHOR,
|
||||
|
||||
@@ -154,6 +154,7 @@ class LocalCommandEngine:
|
||||
return self.reply_cleaner(self.person_index(row, kind, arguments))
|
||||
if kind in {
|
||||
"wallet",
|
||||
"transfer",
|
||||
"prize_draw",
|
||||
"lottery",
|
||||
"blackjack",
|
||||
|
||||
@@ -16,6 +16,8 @@ MAX_LOTTERY_BET = 500
|
||||
DEFAULT_LOTTERY_BET = 10
|
||||
MIN_BLACKJACK_BET = 1
|
||||
MAX_BLACKJACK_BET = 500
|
||||
MIN_TRANSFER_AMOUNT = 1
|
||||
MAX_TRANSFER_AMOUNT = 100_000
|
||||
GAME_TIMEOUT_SECONDS = 600
|
||||
GAME_HISTORY_LIMIT = 50
|
||||
GAME_HISTORY_PAGE_SIZE = 5
|
||||
@@ -41,6 +43,11 @@ BLACKJACK_COMBINED_ACTION = re.compile(
|
||||
r"^(?:开始|start)(?P<bet>\d{1,3})?"
|
||||
r"(?:要牌|hit|停牌|stand)$"
|
||||
)
|
||||
TRANSFER_MARKED = re.compile(
|
||||
r"^(?:给|to)?\s*(?P<target>.+?)\s*"
|
||||
r"(?:金额|金币|coin|coins|:|:)\s*(?P<amount>\d{1,6})$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def normalize_player_account(account: dict[str, Any]) -> dict[str, Any]:
|
||||
@@ -128,6 +135,8 @@ class PlayerGames:
|
||||
def rules() -> str:
|
||||
return (
|
||||
"每人首次使用获 1000 虚拟金币;/金币 查看余额;/游戏记录 查看本人记录。\n"
|
||||
"转账:/转账 对方 金额,或 /transfer name amount;"
|
||||
"对方需先用 /金币 初始化账户。\n"
|
||||
"抽奖:/抽奖 [2~100],5%返5倍、20%返2倍,其余无返还。\n"
|
||||
"多人彩票:/彩票 开始;/彩票 报名 3 4 7 [1~500];"
|
||||
"/彩票 当前状态;/彩票 开奖。三位号码顺序无关,省略金额默认10;"
|
||||
@@ -137,7 +146,7 @@ class PlayerGames:
|
||||
"(/blackjack hit|stand|status|end);"
|
||||
"每人最多一局,10分钟无操作视为认输,结束不退下注。\n"
|
||||
"21点获胜返2倍、平局返下注、自然21点返2.5倍向下取整,庄家软17停牌。"
|
||||
"以上均含本金;虚拟金币不能充值、转账或兑换。"
|
||||
"以上均含本金;虚拟金币不能充值或兑换。"
|
||||
)
|
||||
|
||||
def handle(self, row: dict[str, Any], kind: str, arguments: str) -> str:
|
||||
@@ -150,6 +159,8 @@ class PlayerGames:
|
||||
return self.game_history(row, arguments)
|
||||
if kind == "lottery":
|
||||
return self.group_lottery(row, arguments)
|
||||
if kind == "transfer":
|
||||
return self.transfer(row, arguments)
|
||||
if kind == "wallet" and arguments:
|
||||
return "用法:/金币(/wallet),查看本人的虚拟金币;/游戏帮助 查看规则"
|
||||
if kind == "prize_draw":
|
||||
@@ -283,6 +294,78 @@ class PlayerGames:
|
||||
str(row["group_fingerprint"]), str(row["sender"]),
|
||||
str(row["event_id"]), kind + ":" + arguments, execute)
|
||||
|
||||
@staticmethod
|
||||
def parse_transfer(
|
||||
arguments: str,
|
||||
) -> tuple[str, int] | None:
|
||||
value = arguments.strip()
|
||||
if not value:
|
||||
return None
|
||||
marked = TRANSFER_MARKED.fullmatch(value)
|
||||
if marked is not None:
|
||||
target = marked["target"]
|
||||
amount = int(marked["amount"])
|
||||
else:
|
||||
tokens = value.split()
|
||||
if len(tokens) < 2:
|
||||
return None
|
||||
if re.fullmatch(r"\d{1,6}", tokens[0]):
|
||||
amount = int(tokens[0])
|
||||
target = " ".join(tokens[1:])
|
||||
elif re.fullmatch(r"\d{1,6}", tokens[-1]):
|
||||
amount = int(tokens[-1])
|
||||
target = " ".join(tokens[:-1])
|
||||
else:
|
||||
return None
|
||||
target = re.sub(r"^(?:给|to)\s*", "", target.strip(), flags=re.IGNORECASE)
|
||||
target = target.lstrip("@@").strip()
|
||||
if not target:
|
||||
return None
|
||||
return target, amount
|
||||
|
||||
def transfer(self, row: dict[str, Any], arguments: str) -> str:
|
||||
parsed = self.parse_transfer(arguments)
|
||||
if parsed is None:
|
||||
return (
|
||||
"用法:/转账 对方 金额,例如 /转账 Bob 100;"
|
||||
"英文 /transfer Bob 100。金额范围 1~100000,"
|
||||
"对方需先发送 /金币 初始化账户"
|
||||
)
|
||||
target, amount = parsed
|
||||
if not MIN_TRANSFER_AMOUNT <= amount <= MAX_TRANSFER_AMOUNT:
|
||||
return "转账金额必须为 1~100000 的整数金币"
|
||||
|
||||
def render(data: dict[str, Any]) -> str:
|
||||
source = data["source_display_name"]
|
||||
target_name = data["target_display_name"]
|
||||
if data["status"] == "self":
|
||||
return f"{source}:不能给自己转账"
|
||||
if data["status"] == "target_missing":
|
||||
return (
|
||||
f"{source}:{target_name} 还没有金币账户,"
|
||||
"请先让对方发送 /金币 初始化"
|
||||
)
|
||||
if data["status"] == "insufficient":
|
||||
return (
|
||||
f"{source}:金币不足,余额 {data['source_balance']},"
|
||||
f"本次需要 {data['amount']}"
|
||||
)
|
||||
return (
|
||||
f"{source}:已向 {target_name} 转账 {data['amount']} 金币;"
|
||||
f"本人金币 {data['source_balance']};"
|
||||
f"{target_name} 金币 {data['target_balance']}"
|
||||
)
|
||||
|
||||
return self.ledger.transfer_player_coins(
|
||||
str(row["group_fingerprint"]),
|
||||
str(row["sender"]),
|
||||
str(row["event_id"]),
|
||||
"transfer:" + arguments,
|
||||
target,
|
||||
amount,
|
||||
render,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_lottery_entry(
|
||||
arguments: str,
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-sdk"
|
||||
version = "0.6.8a0+1015"
|
||||
version = "0.6.9a0+1015"
|
||||
description = "Typed, capability-limited extension API for JWeBot plugins"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = []
|
||||
|
||||
@@ -38,4 +38,4 @@ __all__ = [
|
||||
"SingleGroupContext",
|
||||
]
|
||||
|
||||
__version__ = "0.6.8a0+1015"
|
||||
__version__ = "0.6.9a0+1015"
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
[tool.jwebot]
|
||||
release = "alpha 0.6.8.1015"
|
||||
release = "alpha 0.6.9.1015"
|
||||
production_deployments_enabled_by_default = false
|
||||
|
||||
[tool.jwebot.workspace]
|
||||
|
||||
@@ -90,6 +90,7 @@ class LocalExtensionInventoryTests(unittest.TestCase):
|
||||
"debug-telemetry": OFFICIAL_AUTHOR,
|
||||
"dice": JWEBOT_AUTHOR,
|
||||
"coin": JWEBOT_AUTHOR,
|
||||
"coin-transfer": JWEBOT_AUTHOR,
|
||||
"random-number": JWEBOT_AUTHOR,
|
||||
"draw-lot": JWEBOT_AUTHOR,
|
||||
"daily-fortune": JWEBOT_AUTHOR,
|
||||
|
||||
@@ -191,6 +191,34 @@ class Release063GameTests(unittest.TestCase):
|
||||
self.assertIn("alice #2:抽奖 未中奖", history)
|
||||
self.assertIn("alice #1:抽奖 未中奖", history)
|
||||
|
||||
def test_transfer_requires_initialized_target_and_replays_once(self):
|
||||
self.assertIn("金币 1000", self.command("/金币"))
|
||||
missing = self.command("/转账 Bob 100", event_id="missing-target")
|
||||
self.assertIn("bob 还没有金币账户", missing)
|
||||
self.assertEqual(self.state()["balance"], 1000)
|
||||
|
||||
self.assertIn("金币 1000", self.command("/金币", sender="Bob"))
|
||||
first = self.command("/转账 Bob 250", event_id="transfer-1")
|
||||
replay = self.command(
|
||||
"/转账 Bob 250",
|
||||
sender="a1",
|
||||
event_id="transfer-1",
|
||||
)
|
||||
|
||||
self.assertEqual(replay, first)
|
||||
self.assertIn("alice:已向 bob 转账 250 金币", first)
|
||||
self.assertEqual(self.state()["balance"], 750)
|
||||
self.assertEqual(self.state("Bob")["balance"], 1250)
|
||||
|
||||
def test_transfer_rejects_invalid_self_and_insufficient_cases(self):
|
||||
self.assertIn("金币 1000", self.command("/金币"))
|
||||
self.assertIn("转账金额必须为 1~100000", self.command("/转账 Bob 0"))
|
||||
self.assertIn("不能给自己转账", self.command("/转账 a1 100"))
|
||||
self.assertIn("金币 1000", self.command("/金币", sender="Bob"))
|
||||
self.assertIn("金币不足", self.command("/transfer 1001 Bob"))
|
||||
self.assertEqual(self.state()["balance"], 1000)
|
||||
self.assertEqual(self.state("Bob")["balance"], 1000)
|
||||
|
||||
def test_open_round_and_history_survive_restart(self):
|
||||
root = Path(self.temp.name)
|
||||
self.command("/彩票开始")
|
||||
|
||||
@@ -6,6 +6,7 @@ import json
|
||||
import re
|
||||
import tempfile
|
||||
import unittest
|
||||
from importlib import resources
|
||||
from pathlib import Path
|
||||
|
||||
from test_jwebot_edge import MODULE, config
|
||||
@@ -161,6 +162,35 @@ class Release067AutochessTests(unittest.TestCase):
|
||||
self.assertEqual(new_state["season_number"], 2)
|
||||
self.assertEqual(new_state["status"], "registration")
|
||||
|
||||
def test_autochess_data_files_are_externalized_and_loaded(self):
|
||||
data_root = resources.files(AUTOCHESS.__package__).joinpath(
|
||||
AUTOCHESS.DATA_ROOT
|
||||
)
|
||||
payloads = {}
|
||||
for filename in AUTOCHESS.DATA_FILES:
|
||||
path = data_root.joinpath(filename)
|
||||
self.assertTrue(path.is_file(), filename)
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(payload["schema_version"], 1)
|
||||
payloads[filename] = payload
|
||||
|
||||
self.assertEqual(
|
||||
len(payloads["players.json"]["players"]),
|
||||
len(AUTOCHESS.PLAYER_POOL),
|
||||
)
|
||||
self.assertEqual(
|
||||
len(payloads["coaches.json"]["coaches"]),
|
||||
len(AUTOCHESS.COACH_POOL),
|
||||
)
|
||||
narrative_count = sum(
|
||||
len(items)
|
||||
for items in payloads["narratives.json"]["fragments"].values()
|
||||
)
|
||||
self.assertEqual(narrative_count, AUTOCHESS._narrative_inventory_size())
|
||||
self.assertGreaterEqual(narrative_count, 120)
|
||||
self.assertEqual(AUTOCHESS.MATCH_BEST_OF["group"], 3)
|
||||
self.assertEqual(AUTOCHESS.MATCH_BEST_OF["final"], 5)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user