This commit is contained in:
@@ -1,5 +1,24 @@
|
||||
# Changelog
|
||||
|
||||
## alpha 0.6.7.1015
|
||||
|
||||
- Expand CS Major autochess content to 100+ player cards and an 18-coach
|
||||
metadata pool while keeping the opening coach draft to 10 visible choices.
|
||||
- Replace abstract training targets with real round-robin training rounds.
|
||||
Each round resolves participant-vs-participant or participant-vs-AI BO1
|
||||
scrims, then opens an operation window for D rolling, locking, lineup swaps,
|
||||
tactics, and readiness.
|
||||
- Replace fixed score bands with deterministic MR15 round simulation. Map
|
||||
scores now come from individual round outcomes, including 15-15 overtime,
|
||||
instead of direct mappings such as 16-6 or 16-9.
|
||||
- Add a 120+ line local narrative library for openings, streaks, pauses,
|
||||
clutches, economy beats, closes, training hooks, and media/sponsor
|
||||
challenges so match reports vary across maps.
|
||||
- Hide raw target/performance calculation text from training, match, and
|
||||
challenge reports while preserving structured battle facts in the archive.
|
||||
- Start drafted players as two-star素材 and rebalance roster completion toward
|
||||
low/mid-cost cards, leaving upgrades and synergies to the shop loop.
|
||||
|
||||
## alpha 0.6.6.1015
|
||||
|
||||
- Rebuild CS Major autochess around multi-message hosting: tournament
|
||||
|
||||
@@ -8,7 +8,7 @@ JWeBot 是运行于 Linux 的单群微信自动回复系统。它通过本地 OC
|
||||
观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件,
|
||||
并通过带视觉确认的 GUI 发送链路回复。
|
||||
|
||||
当前版本:`alpha 0.6.6.1015`。
|
||||
当前版本:`alpha 0.6.7.1015`。
|
||||
|
||||
0.6.1 新增本地上下文总结、人物与别名索引、单人虚拟金币游戏,并增加
|
||||
两秒画面复核以补抓遗漏的消息变化。命令和游戏管理规则见
|
||||
@@ -26,10 +26,13 @@ JWeBot 是运行于 Linux 的单群微信自动回复系统。它通过本地 OC
|
||||
0.6.5 将 AI 测试模式扩展到双人:两名真人可一键补入 AI 战队 A、B,
|
||||
两支 AI 独立运营。
|
||||
0.6.6 重构自走棋流程:开赛后三轮选秀,第一轮从固定 10 人教练池中
|
||||
选教练,第二、三轮选选手;随后进入三场训练赛、BO3 单循环小组赛、
|
||||
一次媒体日或赞助挑战、BO5 半决赛和总决赛。主持消息会拆分为多条
|
||||
回复;所有需要推进的阶段由相关真人发送 `/结束回合` 自动进入下一步,
|
||||
已淘汰玩家不会再收到赛前准备提示。
|
||||
选教练,第二、三轮选选手;随后进入训练赛、小组赛、挑战、淘汰赛和
|
||||
总决赛。主持消息会拆分为多条回复;所有需要推进的阶段由相关真人发送
|
||||
`/结束回合` 自动进入下一步,已淘汰玩家不会再收到赛前准备提示。
|
||||
0.6.7 继续强化文字自走棋体验:卡池扩充到 100+ 人、教练库扩充到
|
||||
18 人;训练赛改为按参赛人数生成的真实对阵轮次;单图比分由 MR15
|
||||
逐回合模拟产生,不再使用固定比分档;赛事叙事素材扩充到 120+ 条,
|
||||
D 牌、锁店、调阵容和战术窗口会在轮次之间自然提示。
|
||||
|
||||
### 运行流程
|
||||
|
||||
@@ -272,7 +275,7 @@ visually confirmed GUI bridge.
|
||||
Current version:
|
||||
|
||||
```text
|
||||
alpha 0.6.6.1015
|
||||
alpha 0.6.7.1015
|
||||
```
|
||||
|
||||
## Runtime Flow
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
1. 做成一个多人报名、多人运营、多人对战的群聊自走棋。
|
||||
2. 玩法重心放在每轮选秀和阵容运营,而不是复杂微操。
|
||||
3. 用 Major 赛事包装 PvE、小组单循环、淘汰赛和终局剧情。
|
||||
3. 用 Major 赛事包装训练赛、小组单循环、淘汰赛和终局剧情。
|
||||
4. 结算逻辑必须完全本地、可重放、可持久化、可回档验证。
|
||||
5. 每一段剧情都要落档,且剧情只能在数值结算之后生成。
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
一句话定义:
|
||||
|
||||
`三轮赛前选秀 -> 训练赛拿资源 -> D 牌与布阵 -> BO3 小组单循环 -> BO5 淘汰赛 -> 保存每轮赛报与剧情`
|
||||
`三轮赛前选秀 -> 轮次训练赛拿资源 -> D 牌与布阵 -> BO3 小组单循环 -> BO5 淘汰赛 -> 保存每轮赛报与剧情`
|
||||
|
||||
玩家感受到的乐趣应当主要来自:
|
||||
|
||||
@@ -79,7 +79,8 @@
|
||||
- Eternal Fire
|
||||
|
||||
选手数据不是对真实比赛数据的一比一复刻,而是基于公众认知做游戏化映射。
|
||||
首发版本固定维护 `60` 名棋子,运行期不在线抓取或改写数值。
|
||||
0.6.7 起本地维护 `100+` 名棋子和 `18` 名教练元数据;运行期不在线抓取
|
||||
或改写数值。首轮教练选秀仍固定展示 10 个候选,保证聊天窗口可读。
|
||||
|
||||
### 4.2 费用层级
|
||||
|
||||
@@ -280,41 +281,44 @@
|
||||
|
||||
1. `回合开始播报`
|
||||
2. `6 人选秀 / 刷新 / 升级 / 调阵`
|
||||
3. `PvE 或 PvP 结算`
|
||||
3. `训练赛 / 赛事挑战 / PvP 结算`
|
||||
4. `回合结算与经济发放`
|
||||
5. `剧情归档`
|
||||
|
||||
## 10. PvE 设计
|
||||
## 10. 训练赛与赛事挑战
|
||||
|
||||
PvE 不叫“打野怪”,统一包装成赛事内活动。
|
||||
训练赛不再是阈值型 PvE。它是正式小组赛前的多轮真实对阵,用来给玩家
|
||||
检验阵容、获取金币并自然插入 D 牌窗口。媒体日和赞助挑战仍作为赛事内
|
||||
插曲出现,但只提供资源变动,不淘汰玩家。
|
||||
|
||||
### 10.1 PvE 类型
|
||||
### 10.1 活动类型
|
||||
|
||||
- 训练赛
|
||||
- 媒体日
|
||||
- 热身赛
|
||||
- 赞助挑战
|
||||
- 地图专项训练
|
||||
- 本地陪练队热身
|
||||
|
||||
### 10.2 PvE 作用
|
||||
### 10.2 训练赛作用
|
||||
|
||||
PvE 的目标不是淘汰人,而是给资源:
|
||||
训练赛按参赛队伍生成 round-robin 轮次。四队测试局会有三轮训练赛,
|
||||
每队在训练阶段遇到另外三支队伍;奇数队伍时轮空队会与本地陪练队热身。
|
||||
训练赛只发放金币,不记录小组胜负,也不扣生命。
|
||||
|
||||
- 金币
|
||||
- 装备
|
||||
- 战术卡
|
||||
- 教练经验
|
||||
- 临时 buff
|
||||
每轮训练赛结束后进入运营窗口:
|
||||
|
||||
### 10.3 PvE 结算逻辑
|
||||
- `/刷新` 即 D 牌,消耗金币换一组 6 人商店;
|
||||
- `/选1~/选6` 买入商店选手,重复卡自动累积星级;
|
||||
- `/锁定` 保留当前商店;
|
||||
- `/上阵` 调整 5 人首发;
|
||||
- `/战术 火力|稳定|残局` 改变下一场的回合倾向。
|
||||
|
||||
PvE 对手是模板化敌方阵容或阈值型事件,不需要复杂 AI。
|
||||
### 10.3 训练赛结算逻辑
|
||||
|
||||
示例:
|
||||
训练赛使用与正式比赛相同的单图 MR15 逐回合模拟,只是赛制为 BO1。
|
||||
胜方获得更多金币,负方也有保底金币,保证测试局能继续运营。
|
||||
|
||||
`玩家总战力 >= 目标战力` 则获胜,否则失败。
|
||||
|
||||
这样实现简单,也利于稳定复盘。
|
||||
媒体日和赞助挑战根据阵容完整度、教练、战术和本地确定性随机因子发放
|
||||
金币,但播报不展示“目标/发挥”这类计算日志,只写剧情和奖励。
|
||||
|
||||
## 11. PvP 赛制
|
||||
|
||||
@@ -430,7 +434,7 @@ PvE 对手是模板化敌方阵容或阈值型事件,不需要复杂 AI。
|
||||
- 开局报名完成
|
||||
- 每轮 6 人选秀结果
|
||||
- 每次刷新和关键升星
|
||||
- 每次 PvE 事件结果
|
||||
- 每轮训练赛和赛事挑战结果
|
||||
- 每场 PvP 结果
|
||||
- 小组赛排名、晋级和淘汰节点
|
||||
- 淘汰赛对阵与结果
|
||||
@@ -597,8 +601,9 @@ PvE 对手是模板化敌方阵容或阈值型事件,不需要复杂 AI。
|
||||
Jacky 选择了 zonic!
|
||||
现在轮到 @Alice 做出选择,使用 /选1~/选10。
|
||||
|
||||
训练赛阶段结束。可以 /刷新、/选1~/选6、/上阵、/战术;
|
||||
所有真人发送 /结束回合 后进入小组赛。
|
||||
训练赛第 1/3 轮对阵:Team Jacky vs Team Alayak;AI 战队 A vs AI 战队 B
|
||||
本轮训练结束。现在是运营窗口:/刷新 是 D 牌,/选1~/选6 是买店内选手;
|
||||
所有真人发送 /结束回合 后进入下一轮训练。
|
||||
```
|
||||
|
||||
以下节点都必须由 Taffy 主持并落档:
|
||||
@@ -607,7 +612,7 @@ Jacky 选择了 zonic!
|
||||
- AI 测试战队加入
|
||||
- 赛制与种子公布
|
||||
- 三轮选秀候选池、选择结果和选秀完成
|
||||
- 三场训练赛结果
|
||||
- 每轮训练赛对阵、比分、金币和运营提示
|
||||
- 媒体日或赞助挑战
|
||||
- 每场 BO3/BO5 对阵与结果
|
||||
- 每张加时地图
|
||||
@@ -625,7 +630,7 @@ Jacky 选择了 zonic!
|
||||
- 每轮 6 人选秀
|
||||
- 买卡、卖卡、升星
|
||||
- 教练等级
|
||||
- 简化 PvE
|
||||
- 简化训练赛
|
||||
- 简化 PvP
|
||||
- 基础剧情归档
|
||||
|
||||
@@ -638,7 +643,7 @@ Jacky 选择了 zonic!
|
||||
|
||||
### 第三阶段:内容扩展
|
||||
|
||||
- 在固定 60 人首发池基础上增加新赛季棋子
|
||||
- 在 100+ 人本地卡池基础上增加新赛季棋子
|
||||
- 更多俱乐部和国家
|
||||
- 特殊赛事事件
|
||||
- 更细的剧情风格与本届 Major 年鉴
|
||||
@@ -660,7 +665,7 @@ Jacky 选择了 zonic!
|
||||
最重要的三个锚点已经确定:
|
||||
|
||||
1. `每轮固定 6 人公开选秀`
|
||||
2. `PvE 负责资源,PvP 负责小组单循环与淘汰赛`
|
||||
2. `训练赛与挑战负责资源,PvP 负责小组单循环与淘汰赛`
|
||||
3. `每一段剧情都必须落档`
|
||||
|
||||
如果按这个设计推进,实现出来的成品会更像一场可回放、可复盘、
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
Author: `jwebot`
|
||||
|
||||
CS自走棋
|
||||
自走棋
|
||||
|
||||
Help:
|
||||
|
||||
- `/自走棋开始|报名`
|
||||
- `/自走棋 开始|报名|选`
|
||||
|
||||
Registered commands:
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "cs-major-autochess"
|
||||
author = "jwebot"
|
||||
description = "CS自走棋"
|
||||
help = ["/自走棋开始|报名"]
|
||||
description = "自走棋"
|
||||
help = ["/自走棋 开始|报名|选"]
|
||||
version = "1.0.0"
|
||||
api_version = "1"
|
||||
entrypoint = "cs_major_autochess:plugin"
|
||||
|
||||
@@ -5,8 +5,8 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
|
||||
PROFILE = 'cs-major-autochess'
|
||||
AUTHOR = 'jwebot'
|
||||
DESCRIPTION = 'CS自走棋'
|
||||
HELP_ENTRIES = ('/自走棋开始|报名',)
|
||||
DESCRIPTION = '自走棋'
|
||||
HELP_ENTRIES = ('/自走棋 开始|报名|选',)
|
||||
COMMANDS = ('/自走棋', '/cs自走棋', '/autochess', '/选', '/刷新', '/锁定', '/战术', '/升级教练', '/上阵', '/下阵', '/出售', '/阵容', '/赛程', '/结束回合')
|
||||
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ class PluginTests(unittest.TestCase):
|
||||
for call in context.commands.register.call_args_list
|
||||
]
|
||||
self.assertEqual(AUTHOR, 'jwebot')
|
||||
self.assertEqual(DESCRIPTION, 'CS自走棋')
|
||||
self.assertEqual(HELP_ENTRIES, ('/自走棋开始|报名',))
|
||||
self.assertEqual(DESCRIPTION, '自走棋')
|
||||
self.assertEqual(HELP_ENTRIES, ('/自走棋 开始|报名|选',))
|
||||
self.assertIn('/自走棋', registered)
|
||||
self.assertEqual(len(registered), 14)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-config"
|
||||
version = "0.6.6a0+1015"
|
||||
version = "0.6.7a0+1015"
|
||||
description = "Profile-based configuration compiler for JWeBot"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
|
||||
@@ -36,4 +36,4 @@ __all__ = [
|
||||
"write_default_profile",
|
||||
]
|
||||
|
||||
__version__ = "0.6.6a0+1015"
|
||||
__version__ = "0.6.7a0+1015"
|
||||
|
||||
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-extension-host"
|
||||
version = "0.6.6a0+1015"
|
||||
version = "0.6.7a0+1015"
|
||||
description = "Single-release, single-invocation process host for JWeBot extensions"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"jwebot-sdk==0.6.6a0+1015",
|
||||
"jwebot-kernel==0.6.6a0+1015",
|
||||
"jwebot-sdk==0.6.7a0+1015",
|
||||
"jwebot-kernel==0.6.7a0+1015",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -16,4 +16,4 @@ __all__ = [
|
||||
"process_request",
|
||||
]
|
||||
|
||||
__version__ = "0.6.6a0+1015"
|
||||
__version__ = "0.6.7a0+1015"
|
||||
|
||||
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-kernel"
|
||||
version = "0.6.6a0+1015"
|
||||
version = "0.6.7a0+1015"
|
||||
description = "Stable routing and extension authority for JWeBot"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"jwebot-config==0.6.6a0+1015",
|
||||
"jwebot-sdk==0.6.6a0+1015",
|
||||
"jwebot-official-extensions==0.6.6a0+1015",
|
||||
"jwebot-config==0.6.7a0+1015",
|
||||
"jwebot-sdk==0.6.7a0+1015",
|
||||
"jwebot-official-extensions==0.6.7a0+1015",
|
||||
"tomli>=2; python_version < '3.11'",
|
||||
]
|
||||
|
||||
|
||||
@@ -107,4 +107,4 @@ __all__ = [
|
||||
"verify_installed_release_signature",
|
||||
]
|
||||
|
||||
__version__ = "0.6.6a0+1015"
|
||||
__version__ = "0.6.7a0+1015"
|
||||
|
||||
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-official-extensions"
|
||||
version = "0.6.6a0+1015"
|
||||
version = "0.6.7a0+1015"
|
||||
description = "Reviewed local command adapters and broker implementation for JWeBot"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"jwebot-sdk==0.6.6a0+1015",
|
||||
"jwebot-sdk==0.6.7a0+1015",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
|
||||
@@ -90,4 +90,4 @@ __all__ = [
|
||||
"render_feature_list",
|
||||
]
|
||||
|
||||
__version__ = "0.6.6a0+1015"
|
||||
__version__ = "0.6.7a0+1015"
|
||||
|
||||
+2
-2
@@ -5,8 +5,8 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||
|
||||
PROFILE = 'cs-major-autochess'
|
||||
AUTHOR = 'jwebot'
|
||||
DESCRIPTION = 'CS自走棋'
|
||||
HELP_ENTRIES = ('/自走棋开始|报名',)
|
||||
DESCRIPTION = '自走棋'
|
||||
HELP_ENTRIES = ('/自走棋 开始|报名|选',)
|
||||
COMMANDS = ('/自走棋', '/cs自走棋', '/autochess', '/选', '/刷新', '/锁定', '/战术', '/升级教练', '/上阵', '/下阵', '/出售', '/阵容', '/赛程', '/结束回合')
|
||||
|
||||
|
||||
|
||||
+612
-142
@@ -145,6 +145,62 @@ PLAYER_POOL = (
|
||||
_card("maj3r", "MAJ3R", 2, "指挥", "土耳其", "Eternal Fire", ("Space Soldiers",), 68, 74, 71, 93),
|
||||
_card("wicadia", "Wicadia", 2, "步枪手", "土耳其", "Eternal Fire", (), 76, 70, 68),
|
||||
_card("jottaaa", "jottAAA", 1, "步枪手", "土耳其", "Eternal Fire", ("Sangal",), 67, 64, 63),
|
||||
_card("teses_heroic", "TeSeS", 2, "步枪手", "丹麦", "HEROIC", ("Falcons",), 76, 73, 72),
|
||||
_card("sjuush", "sjuush", 2, "步枪手", "丹麦", "HEROIC", (), 74, 78, 77),
|
||||
_card("degster", "degster", 3, "狙击手", "俄罗斯", "HEROIC", ("OG", "Spirit"), 82, 76, 82),
|
||||
_card("kyxsan", "kyxsan", 2, "指挥", "北马其顿", "HEROIC", ("Apeks",), 64, 75, 70, 90),
|
||||
_card("nertz_heroic", "NertZ", 3, "步枪手", "以色列", "HEROIC", ("ENCE", "Liquid"), 85, 79, 82),
|
||||
_card("torzi_mouz", "torzsi", 2, "狙击手", "匈牙利", "MOUZ", (), 78, 78, 80),
|
||||
_card("spinx", "Spinx", 4, "步枪手", "以色列", "Vitality", ("ENCE",), 88, 87, 86),
|
||||
_card("electronic", "electroNic", 4, "步枪手", "俄罗斯", "Virtus.pro", ("NAVI", "Cloud9"), 87, 84, 84),
|
||||
_card("fame", "fame", 3, "步枪手", "俄罗斯", "Virtus.pro", (), 82, 83, 82),
|
||||
_card("fl1t", "FL1T", 3, "步枪手", "俄罗斯", "Virtus.pro", ("forZe",), 81, 82, 80),
|
||||
_card("jame", "Jame", 3, "指挥", "俄罗斯", "Virtus.pro", ("AVANGAR",), 72, 87, 86, 96),
|
||||
_card("yekindar_vp", "YEKINDAR", 4, "步枪手", "拉脱维亚", "Virtus.pro", ("Liquid", "FURIA"), 90, 78, 76),
|
||||
_card("hallzerk", "hallzerk", 2, "狙击手", "挪威", "Complexity", ("Dignitas",), 75, 72, 75),
|
||||
_card("grim", "Grim", 2, "步枪手", "美国", "Complexity", ("Liquid",), 74, 74, 73),
|
||||
_card("jt", "JT", 2, "指挥", "南非", "Complexity", ("Cloud9",), 62, 73, 69, 89),
|
||||
_card("floppy", "floppy", 1, "步枪手", "美国", "Complexity", ("Cloud9",), 68, 67, 65),
|
||||
_card("junior", "junior", 1, "狙击手", "美国", "NRG", ("FURIA", "Complexity"), 68, 64, 66),
|
||||
_card("art", "arT", 2, "指挥", "巴西", "Fluxo", ("FURIA",), 69, 68, 74, 92),
|
||||
_card("biguzera", "biguzera", 3, "指挥", "巴西", "paiN", (), 75, 80, 80, 92),
|
||||
_card("nqz", "nqz", 2, "狙击手", "巴西", "paiN", ("00Nation",), 76, 72, 74),
|
||||
_card("snow", "snow", 1, "步枪手", "巴西", "paiN", (), 67, 65, 63),
|
||||
_card("brnz4n", "brnz4n", 1, "步枪手", "巴西", "MIBR", (), 66, 66, 64),
|
||||
_card("exit", "exit", 2, "指挥", "巴西", "MIBR", (), 67, 75, 71, 88),
|
||||
_card("insani", "insani", 3, "步枪手", "巴西", "MIBR", (), 83, 75, 77),
|
||||
_card("saffee", "saffee", 2, "狙击手", "巴西", "MIBR", ("FURIA",), 76, 72, 74),
|
||||
_card("slaxz", "slaxz-", 2, "狙击手", "德国", "GamerLegion", ("ALTERNATE",), 75, 72, 74),
|
||||
_card("volt", "volt", 2, "步枪手", "英国", "GamerLegion", ("Into the Breach",), 74, 73, 72),
|
||||
_card("acoR", "acoR", 2, "狙击手", "丹麦", "GamerLegion", ("MOUZ",), 75, 73, 76),
|
||||
_card("keoz", "Keoz", 1, "步枪手", "比利时", "GamerLegion", (), 65, 66, 64),
|
||||
_card("isak", "isak", 1, "步枪手", "瑞典", "GamerLegion", (), 66, 67, 65),
|
||||
_card("tabseN", "tabseN", 3, "指挥", "德国", "BIG", (), 77, 82, 79, 94),
|
||||
_card("krimbo", "Krimbo", 3, "步枪手", "德国", "BIG", (), 82, 82, 81),
|
||||
_card("syrson", "syrsoN", 2, "狙击手", "德国", "BIG", (), 76, 73, 76),
|
||||
_card("mantuu", "mantuu", 2, "狙击手", "波兰", "BIG", ("OG",), 76, 74, 75),
|
||||
_card("blamef", "blameF", 4, "步枪手", "丹麦", "fnatic", ("Astralis", "Complexity"), 88, 88, 84),
|
||||
_card("bodyy", "bodyy", 2, "指挥", "法国", "fnatic", ("G2",), 66, 75, 72, 89),
|
||||
_card("matys", "MATYS", 2, "步枪手", "斯洛伐克", "fnatic", (), 76, 72, 72),
|
||||
_card("afro", "afro", 2, "狙击手", "法国", "fnatic", ("LDLC",), 75, 72, 74),
|
||||
_card("kairon", "KaiR0N-", 2, "步枪手", "俄罗斯", "BetBoom", ("Virtus.pro",), 76, 70, 72),
|
||||
_card("zorte", "zorte", 2, "狙击手", "俄罗斯", "BetBoom", ("forZe",), 77, 73, 75),
|
||||
_card("nafany", "nafany", 2, "指挥", "俄罗斯", "BetBoom", ("Cloud9", "Gambit"), 65, 74, 70, 91),
|
||||
_card("s1ren", "s1ren", 2, "步枪手", "俄罗斯", "BetBoom", ("Spirit",), 75, 73, 72),
|
||||
_card("rigoN", "rigoN", 2, "步枪手", "瑞士", "Bad News Eagles", ("Guild Eagles",), 77, 70, 72),
|
||||
_card("juanflatroo", "juanflatroo", 2, "步枪手", "科索沃", "Bad News Eagles", ("Guild Eagles",), 76, 71, 72),
|
||||
_card("calyx", "Calyx", 2, "步枪手", "土耳其", "Eternal Fire", ("Space Soldiers",), 75, 73, 74),
|
||||
_card("issaa", "ISSAA", 1, "步枪手", "约旦", "JiJieHao", ("OG",), 67, 64, 66),
|
||||
_card("boros", "BOROS", 2, "步枪手", "约旦", "Into the Breach", ("Falcons", "Monte"), 78, 68, 70),
|
||||
_card("woro2k", "Woro2k", 3, "狙击手", "乌克兰", "Monte", (), 82, 76, 80),
|
||||
_card("demqq", "DemQQ", 2, "步枪手", "乌克兰", "Monte", (), 76, 74, 73),
|
||||
_card("dycha", "dycha", 2, "步枪手", "波兰", "ENCE", (), 75, 74, 72),
|
||||
_card("gla1ve", "gla1ve", 3, "指挥", "丹麦", "ENCE", ("Astralis",), 72, 84, 79, 98),
|
||||
_card("hades", "hades", 2, "狙击手", "波兰", "ENCE", ("9INE",), 76, 73, 75),
|
||||
_card("goofy", "Goofy", 1, "步枪手", "波兰", "ENCE", ("9INE",), 67, 66, 64),
|
||||
_card("sausol", "sausol", 1, "步枪手", "西班牙", "Movistar Riders", (), 66, 65, 63),
|
||||
_card("alex_movistar", "alex", 2, "指挥", "西班牙", "Movistar Riders", ("NiP",), 64, 74, 70, 88),
|
||||
_card("dav1g", "dav1g", 1, "步枪手", "西班牙", "KOI", ("Movistar Riders",), 65, 65, 64),
|
||||
)
|
||||
CARD_BY_ID = {card["id"]: card for card in PLAYER_POOL}
|
||||
|
||||
@@ -182,16 +238,180 @@ COACH_POOL = (
|
||||
_coach("trace", "trace", 2, "防守复盘", 70, 86, 74, 6),
|
||||
_coach("ash", "ash", 1, "新人培养", 76, 74, 72, 5),
|
||||
_coach("mithr", "mithR", 1, "基础纪律", 70, 78, 70, 5),
|
||||
_coach("kuben", "kuben", 4, "慢控默认", 74, 88, 82, 8),
|
||||
_coach("groove", "groove", 3, "学院体系", 72, 84, 78, 7),
|
||||
_coach("guerri", "guerri", 4, "巴西爆弹", 86, 76, 80, 8),
|
||||
_coach("xizt", "Xizt", 4, "默认纪律", 76, 86, 82, 8),
|
||||
_coach("sycrone", "sycrone", 3, "青年提拔", 80, 80, 78, 7),
|
||||
_coach("malek", "maLeK", 3, "明星平衡", 82, 78, 80, 7),
|
||||
_coach("peacemaker", "peacemaker", 2, "临时救火", 78, 74, 76, 6),
|
||||
_coach("kassad", "kassad", 2, "纪律重建", 72, 82, 74, 6),
|
||||
)
|
||||
COACH_BY_ID = {coach["id"]: coach for coach in COACH_POOL}
|
||||
TRAINING_OPPONENTS = (
|
||||
("Team Falcons", 292),
|
||||
("Natus Vincere", 306),
|
||||
("Team Vitality", 316),
|
||||
("FaZe Clan", 300),
|
||||
("G2 Esports", 286),
|
||||
("The MongolZ", 278),
|
||||
)
|
||||
TRAINING_WIN_GOLD = 3
|
||||
TRAINING_LOSS_GOLD = 1
|
||||
TRAINING_BYE_GOLD = 2
|
||||
|
||||
NARRATIVE_FRAGMENTS = {
|
||||
"openings": (
|
||||
"{team} 开局没有急着提速,先把默认控图铺开。",
|
||||
"{team} 第一波提速很坚决,外场道具落点干净。",
|
||||
"{team} 在手枪局后选择慢摸,试图逼出对手的前压信息。",
|
||||
"{team} 的狙击手早早架住第一交火点,节奏被压得很低。",
|
||||
"{team} 连续用两颗闪光清掉近点,进攻面一下打开。",
|
||||
"{team} 防守开局站位很深,把回防路线留得很完整。",
|
||||
"{team} 先拿地图控制权,再把包点压力一点点推上去。",
|
||||
"{team} 开局没有赌点,靠信息互换慢慢读阵型。",
|
||||
"{team} 在第一枪位赢下后没有贪,马上回收人数优势。",
|
||||
"{team} 用一波假打骗出轮转,第二时间才开始真正爆弹。",
|
||||
"{team} 选择保守开局,宁愿少拿信息也不送首杀。",
|
||||
"{team} 的指挥把默认拉得很散,让对手很难判断重心。",
|
||||
"{team} 手枪局道具很少,但站位互补做得很扎实。",
|
||||
"{team} 没有被前几回合影响,依旧按自己的节奏展开。",
|
||||
"{team} 的第一套进攻明显针对了对手弱侧。",
|
||||
"{team} 在经济局也保留了两颗关键闪,准备打后点反清。",
|
||||
"{team} 上来就抢中路,想用主动权换后续转点空间。",
|
||||
"{team} 先把狙击枪放在安全位,围绕它慢慢组织推进。",
|
||||
),
|
||||
"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} 连分让地图气质变了,所有枪位都开始往前压。",
|
||||
),
|
||||
"clutches": (
|
||||
"{player} 留到最后一个残局,靠一颗烟把拆包时间偷了出来。",
|
||||
"{player} 没有急着补枪,等脚步声靠近后才横拉收掉残局。",
|
||||
"{player} 在 1v2 里先断退路,再回头处理包点。",
|
||||
"{player} 的残局处理很冷,假拆骗出第一枪后直接反清。",
|
||||
"{player} 被逼到死角,但一颗反弹闪把局面重新打开。",
|
||||
"{player} 没有追击残血,而是先保住雷包,判断非常老练。",
|
||||
"{player} 关键回合双杀,把本来要崩的经济线救回来。",
|
||||
"{player} 在读秒阶段完成补枪,队友的拉扯给得刚刚好。",
|
||||
"{player} 先拿信息再开枪,这个残局赢在耐心。",
|
||||
"{player} 硬吃白光完成换人,比分没有被对手咬住。",
|
||||
"{player} 从连接位摸出,正好打掉回防节奏。",
|
||||
"{player} 这次不追求集锦,只把枪线封死等队友到位。",
|
||||
"{player} 的狙击枪没有空枪,关键首杀让整回合变得简单。",
|
||||
"{player} 残血仍然敢看第一枪位,队伍因此拿到赛点。",
|
||||
"{player} 拿下残局后没有庆祝,马上提醒队友保枪经济。",
|
||||
"{player} 靠脚步假信息骗出转点,最后一枪很稳。",
|
||||
"{player} 在包点里连续换位,对手始终没能定位。",
|
||||
"{player} 把闪光背身吃完后仍然补到第二枪,这是关键转折。",
|
||||
"{player} 一个人拖住整条回防路线,为队伍争到拆包窗口。",
|
||||
"{player} 最后选择不 peek,让时间替自己赢下这一分。",
|
||||
),
|
||||
"pauses": (
|
||||
"比分被拉开后,双方都慢了半拍,像是在重新读这张图。",
|
||||
"这里适合停一下,刚才几回合的经济差已经开始影响下一枪。",
|
||||
"暂停回来之后,防守方明显把第一接触点后撤了半格。",
|
||||
"连续交火后节奏突然变慢,双方都在等对方先犯错。",
|
||||
"这一段没有夸张集锦,但每一颗道具都在改变下一回合。",
|
||||
"场面安静了几秒,真正决定胜负的是下一次首杀。",
|
||||
"队伍语音应该都在压节奏,没人愿意在这里送掉经济。",
|
||||
"这不是简单的比分变化,战术重心已经从爆弹转到控图。",
|
||||
"现场进入短暂拉扯,地图控制权一点点被重新分配。",
|
||||
"两边都知道下一分很贵,所以这回合开局格外谨慎。",
|
||||
"这一小段空白很重要,强队往往就是在这里调整默认。",
|
||||
"比赛没有马上进入下一波交火,指挥正在重新排优先级。",
|
||||
"刚才的连分让对手不得不换站位,防线开始出现试探。",
|
||||
"经济压力让双方都少了一点容错,下一波道具会很关键。",
|
||||
"这里没有急躁的前压,只有一点点互相试探。",
|
||||
"暂停不是为了拖时间,而是在给下一套进攻换答案。",
|
||||
"双方都把节奏压住,地图像被重新归零。",
|
||||
"这一分之前的几秒钟很长,所有人都在等第一个脚步声。",
|
||||
"场上没有无意义的 peek,每个人都在守自己的触发条件。",
|
||||
"这段过渡让比赛更像真正的 BO 系列赛,而不是单纯掷骰子。",
|
||||
),
|
||||
"economy": (
|
||||
"{team} 赢下这一分后可以完整起枪,下一回合选择会更多。",
|
||||
"{team} 输掉长枪局后经济见底,可能要交出一回合调整空间。",
|
||||
"{team} 没有强行追梦,选择保枪把下一回合留住。",
|
||||
"{team} 的连败补偿到位,下一回合有机会靠道具质量反打。",
|
||||
"{team} 这回合缴下两把枪,经济收益比比分更重要。",
|
||||
"{team} 虽然丢分,但保下主狙,下一回合还有破局点。",
|
||||
"{team} 的半甲手枪没有打成,但至少逼出了对手不少道具。",
|
||||
"{team} 经济优势开始显现,可以更大胆地争夺第一枪位。",
|
||||
"{team} 如果下一分再丢,就必须进入半起调整。",
|
||||
"{team} 这一波选择全甲全弹,明显是要在中期一次打穿。",
|
||||
"{team} 保下来的两把枪让下回合不至于完全被动。",
|
||||
"{team} 连续换枪成功,把对手的存款一点点磨掉。",
|
||||
"{team} 用低成本回合换到下包,经济线还没有断。",
|
||||
"{team} 这波赢的不只是分数,还有下一回合的主动权。",
|
||||
"{team} 被重置经济后节奏放慢,先求稳住半场尾声。",
|
||||
"{team} 靠残局胜利拿到额外空间,可以考虑继续 D 牌补强。",
|
||||
),
|
||||
"closers": (
|
||||
"{team} 没有给翻盘窗口,最后一波回防处理得很完整。",
|
||||
"{team} 在赛点局选择最熟悉的默认,用纪律结束地图。",
|
||||
"{team} 最后一回合没有贪枪,稳稳把人数优势转成胜势。",
|
||||
"{team} 的收官很干净,地图尾声没有出现多余失误。",
|
||||
"{team} 用一波双拉打开包点,最后比分定格。",
|
||||
"{team} 把比赛拖进自己舒服的节奏,收尾阶段非常稳。",
|
||||
"{team} 赛点局的道具层次很清楚,对手没有舒服的回防路线。",
|
||||
"{team} 终于等到对手前压,抓住这个窗口终结地图。",
|
||||
"{team} 收下最后一分时没有冒进,这就是强队式结尾。",
|
||||
"{team} 最后一枪来得很快,但前面的信息铺垫已经足够。",
|
||||
"{team} 在关键经济局之后顺势收掉地图。",
|
||||
"{team} 先控退路再进包点,结尾没有留下残局悬念。",
|
||||
"{team} 把上半场积累的优势守到最后。",
|
||||
"{team} 用一波慢夹收官,比分看起来合理,过程更像真实比赛。",
|
||||
"{team} 没让对手连追,赛点局直接把节奏抢回来。",
|
||||
"{team} 的最后一分赢在补枪,枪线没有断。",
|
||||
"{team} 收官回合把每个角落都清干净,耐心给了回报。",
|
||||
"{team} 把悬念留到最后一刻,但关键枪没有手软。",
|
||||
),
|
||||
"training_hooks": (
|
||||
"训练赛不计入小组胜负,但会发金币,方便后续刷新商店。",
|
||||
"这轮训练更像实战热身,每队都会遇到同场参赛对手。",
|
||||
"训练赛重点不是淘汰谁,而是检验当前阵容能不能打完整张图。",
|
||||
"这轮结束后可以 D 牌、锁店或调首发,下一轮再检验成果。",
|
||||
"AI 战队也会参与训练,对局结果会写入档案。",
|
||||
"如果阵容缺少指挥,训练赛的中期回合会更容易失控。",
|
||||
"如果你想追羁绊,现在是最适合刷新商店的窗口。",
|
||||
"训练赛的金币很重要,别把每次刷新都当成免费试错。",
|
||||
"这一阶段没有生命扣除,重点是运营和阵容成型。",
|
||||
"训练赛的地图会轮换,不会一直用同一个固定比分模板。",
|
||||
"每轮训练结束后,真人都需要发送 /结束回合 才会进入下一轮。",
|
||||
"训练赛会按轮次播报,避免一次性刷出一大串结果。",
|
||||
"如果出现轮空,会安排本地陪练队补一张 BO1。",
|
||||
"训练阶段适合观察战术克制,火力、稳定、残局都会影响回合走向。",
|
||||
"连胜会带来金币,但失败也有保底,保证测试局能继续运营。",
|
||||
"这部分是纯文字自走棋的节奏层,不再是单条目标数值判定。",
|
||||
),
|
||||
"challenges": (
|
||||
"媒体日问题很尖锐,队伍需要在镜头前保持状态。",
|
||||
"赞助商临时加码,要求队伍用指定战术打出观赏性。",
|
||||
"主办方安排了签名会,状态管理成了额外考验。",
|
||||
"训练馆网络短暂波动,队伍必须快速适应备用机器。",
|
||||
"赛前采访拖长,指挥需要把注意力重新拉回比赛。",
|
||||
"赞助商想看激进打法,但稳住节奏的人会拿到更高回报。",
|
||||
"媒体日不是战斗回合,却会影响队伍的运营资源。",
|
||||
"队伍如果阵容完整,就更容易把这次插曲处理成奖金。",
|
||||
"这次挑战只出现一次,处理完就回到小组赛主线。",
|
||||
"主舞台彩排占用了热身时间,教练的管理能力开始生效。",
|
||||
"赞助商要求展示新战术,选错方向也不会淘汰,只会少拿金币。",
|
||||
"现场采访提到了前一场失利,队员心态需要稳住。",
|
||||
"这类挑战不扣生命,目的只是给运营阶段增加变数。",
|
||||
"处理好这次插曲的队伍,会在下一场前多一点经济余地。",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _hash_int(seed: str) -> int:
|
||||
@@ -201,6 +421,18 @@ def _hash_int(seed: str) -> int:
|
||||
)
|
||||
|
||||
|
||||
def _pick_fragment(category: str, seed: str, **values: Any) -> str:
|
||||
fragments = tuple(NARRATIVE_FRAGMENTS.get(category, ()))
|
||||
if not fragments:
|
||||
return ""
|
||||
template = fragments[_hash_int(f"{category}|{seed}") % len(fragments)]
|
||||
return template.format(**values)
|
||||
|
||||
|
||||
def _narrative_inventory_size() -> int:
|
||||
return sum(len(items) for items in NARRATIVE_FRAGMENTS.values())
|
||||
|
||||
|
||||
def _pack_response_parts(parts: list[str]) -> str:
|
||||
cleaned = [part.strip() for part in parts if part.strip()]
|
||||
if len(cleaned) <= 1:
|
||||
@@ -918,21 +1150,152 @@ def _auto_manage_ai(
|
||||
return stories
|
||||
|
||||
|
||||
MAP_POOL = ("Mirage", "Inferno", "Nuke", "Ancient", "Dust2")
|
||||
MAP_POOL = ("Mirage", "Inferno", "Nuke", "Ancient", "Dust2", "Anubis", "Overpass")
|
||||
|
||||
|
||||
def _map_score(margin: float, seed: str) -> tuple[str, str, int]:
|
||||
if margin >= 0.18:
|
||||
return "16-6", "碾压", 0
|
||||
if margin >= 0.11:
|
||||
return "16-9", "明显优势", 0
|
||||
if margin >= 0.055:
|
||||
return "16-12", "小胜", 0
|
||||
if margin >= 0.025:
|
||||
return "16-14", "险胜", 0
|
||||
overtime_count = 1 + _hash_int(seed + "|overtime") % 2
|
||||
winner_score = 16 + overtime_count * 3
|
||||
return f"{winner_score}-{winner_score - 2}", "加时险胜", overtime_count
|
||||
def _round_probability(
|
||||
adjusted_a: float,
|
||||
adjusted_b: float,
|
||||
seed: str,
|
||||
round_index: int,
|
||||
score_a: int,
|
||||
score_b: int,
|
||||
) -> float:
|
||||
average = max(1.0, (adjusted_a + adjusted_b) / 2)
|
||||
power_edge = max(-0.16, min(0.16, (adjusted_a - adjusted_b) / average * 0.30))
|
||||
pressure = max(-0.04, min(0.04, (score_b - score_a) * 0.004))
|
||||
side_swing = 0.018 if round_index <= 15 else -0.012
|
||||
if round_index > 30:
|
||||
side_swing = 0.0
|
||||
jitter = ((_hash_int(f"{seed}|prob|{round_index}") % 701) - 350) / 10_000
|
||||
return max(0.32, min(0.68, 0.5 + power_edge + pressure + side_swing + jitter))
|
||||
|
||||
|
||||
def _simulate_map(
|
||||
state: dict[str, Any],
|
||||
key_a: str,
|
||||
key_b: str,
|
||||
seed: str,
|
||||
map_index: int,
|
||||
adjusted_a: float,
|
||||
adjusted_b: float,
|
||||
) -> dict[str, Any]:
|
||||
score_a = 0
|
||||
score_b = 0
|
||||
rounds: list[str] = []
|
||||
current_key = ""
|
||||
current_run = 0
|
||||
best_run_key = ""
|
||||
best_run = 0
|
||||
|
||||
def record_round(round_index: int) -> None:
|
||||
nonlocal score_a, score_b, current_key, current_run
|
||||
nonlocal best_run_key, best_run
|
||||
probability_a = _round_probability(
|
||||
adjusted_a,
|
||||
adjusted_b,
|
||||
seed,
|
||||
round_index,
|
||||
score_a,
|
||||
score_b,
|
||||
)
|
||||
roll = (_hash_int(f"{seed}|round|{round_index}") % 10_000) / 10_000
|
||||
winner_key = key_a if roll < probability_a else key_b
|
||||
if winner_key == key_a:
|
||||
score_a += 1
|
||||
else:
|
||||
score_b += 1
|
||||
rounds.append(winner_key)
|
||||
if winner_key == current_key:
|
||||
current_run += 1
|
||||
else:
|
||||
current_key = winner_key
|
||||
current_run = 1
|
||||
if current_run > best_run:
|
||||
best_run = current_run
|
||||
best_run_key = winner_key
|
||||
|
||||
for round_index in range(1, 31):
|
||||
record_round(round_index)
|
||||
if score_a >= 16 or score_b >= 16:
|
||||
break
|
||||
round_index = len(rounds)
|
||||
if score_a == 15 and score_b == 15:
|
||||
while max(score_a, score_b) < 19 or abs(score_a - score_b) < 2:
|
||||
round_index += 1
|
||||
record_round(round_index)
|
||||
if round_index > 72:
|
||||
break
|
||||
|
||||
winner_key = key_a if score_a > score_b else key_b
|
||||
loser_key = key_b if winner_key == key_a else key_a
|
||||
winner_score = score_a if winner_key == key_a else score_b
|
||||
loser_score = score_b if winner_key == key_a else score_a
|
||||
overtime_rounds = max(0, len(rounds) - 30)
|
||||
overtime_count = (overtime_rounds + 5) // 6
|
||||
if overtime_count:
|
||||
band = "加时险胜"
|
||||
elif loser_score <= 6:
|
||||
band = "压制"
|
||||
elif loser_score <= 10:
|
||||
band = "优势"
|
||||
elif loser_score <= 13:
|
||||
band = "小胜"
|
||||
else:
|
||||
band = "险胜"
|
||||
map_name = MAP_POOL[(map_index - 1) % len(MAP_POOL)]
|
||||
winner_team = _team_name(state["players"][winner_key])
|
||||
loser_team = _team_name(state["players"][loser_key])
|
||||
best_run_team = (
|
||||
_team_name(state["players"][best_run_key]) if best_run_key else winner_team
|
||||
)
|
||||
opening_team = _team_name(
|
||||
state["players"][key_a if _hash_int(seed + "|open") % 2 == 0 else key_b]
|
||||
)
|
||||
actor = _key_player_name(state, winner_key, map_index)
|
||||
pause = _pick_fragment("pauses", f"{seed}|pause")
|
||||
run_text = (
|
||||
_pick_fragment(
|
||||
"runs",
|
||||
f"{seed}|run",
|
||||
team=best_run_team,
|
||||
count=best_run,
|
||||
)
|
||||
if best_run >= 4
|
||||
else pause
|
||||
)
|
||||
overtime_story = ""
|
||||
if overtime_count:
|
||||
overtime_story = (
|
||||
f"{map_name} 常规时间战成 15-15。{winner_team} 在加时第 "
|
||||
f"{overtime_count} 段抓住 {loser_team} 的转点空隙,以 "
|
||||
f"{winner_score}-{loser_score} 收下地图。"
|
||||
)
|
||||
return {
|
||||
"map_index": map_index,
|
||||
"map_name": map_name,
|
||||
"winner_key": winner_key,
|
||||
"winner": state["players"][winner_key]["display_name"],
|
||||
"loser_key": loser_key,
|
||||
"loser": state["players"][loser_key]["display_name"],
|
||||
"score": f"{winner_score}-{loser_score}",
|
||||
"score_a": score_a,
|
||||
"score_b": score_b,
|
||||
"band": band,
|
||||
"overtime_count": overtime_count,
|
||||
"overtime_story": overtime_story,
|
||||
"rounds_played": len(rounds),
|
||||
"best_run_key": best_run_key,
|
||||
"best_run": best_run,
|
||||
"opening_text": _pick_fragment("openings", f"{seed}|opening", team=opening_team),
|
||||
"run_text": run_text,
|
||||
"pause_text": pause,
|
||||
"clutch_text": _pick_fragment("clutches", f"{seed}|clutch", player=actor),
|
||||
"economy_text": _pick_fragment("economy", f"{seed}|economy", team=loser_team),
|
||||
"closer_text": _pick_fragment("closers", f"{seed}|closer", team=winner_team),
|
||||
"adjusted_a": round(adjusted_a, 2),
|
||||
"adjusted_b": round(adjusted_b, 2),
|
||||
}
|
||||
|
||||
|
||||
def _battle(
|
||||
@@ -969,51 +1332,23 @@ def _battle(
|
||||
* (10_000 + fluct_b)
|
||||
/ 10_000
|
||||
)
|
||||
if adjusted_a == adjusted_b:
|
||||
adjusted_a += (
|
||||
0.01 if _hash_int(f"{seed}|map|{map_index}|tie") % 2 == 0 else -0.01
|
||||
)
|
||||
winner_key = key_a if adjusted_a > adjusted_b else key_b
|
||||
item = _simulate_map(
|
||||
state,
|
||||
key_a,
|
||||
key_b,
|
||||
f"{seed}|map|{map_index}",
|
||||
map_index,
|
||||
adjusted_a,
|
||||
adjusted_b,
|
||||
)
|
||||
winner_key = str(item["winner_key"])
|
||||
if winner_key == key_a:
|
||||
wins_a += 1
|
||||
else:
|
||||
wins_b += 1
|
||||
high = max(adjusted_a, adjusted_b)
|
||||
low = min(adjusted_a, adjusted_b)
|
||||
margin = (high - low) / max(high, 1)
|
||||
score, band, overtime_count = _map_score(
|
||||
margin,
|
||||
f"{seed}|map|{map_index}",
|
||||
)
|
||||
winner_name = state["players"][winner_key]["display_name"]
|
||||
loser_key = key_b if winner_key == key_a else key_a
|
||||
loser_name = state["players"][loser_key]["display_name"]
|
||||
map_name = MAP_POOL[map_index - 1]
|
||||
overtime_story = ""
|
||||
if overtime_count:
|
||||
overtime_story = (
|
||||
f"{map_name} 常规时间战成 15-15。"
|
||||
f"{winner_name} 在第 {overtime_count} 轮加时抓住"
|
||||
f"{loser_name} 的转点空隙,以 {score} 收下地图。"
|
||||
)
|
||||
maps.append(
|
||||
{
|
||||
"map_index": map_index,
|
||||
"map_name": map_name,
|
||||
"winner_key": winner_key,
|
||||
"winner": winner_name,
|
||||
"loser_key": loser_key,
|
||||
"loser": loser_name,
|
||||
"score": score,
|
||||
"band": band,
|
||||
"overtime_count": overtime_count,
|
||||
"overtime_story": overtime_story,
|
||||
"adjusted_a": round(adjusted_a, 2),
|
||||
"adjusted_b": round(adjusted_b, 2),
|
||||
"fluctuation_a_bps": fluct_a,
|
||||
"fluctuation_b_bps": fluct_b,
|
||||
}
|
||||
)
|
||||
item["fluctuation_a_bps"] = fluct_a
|
||||
item["fluctuation_b_bps"] = fluct_b
|
||||
maps.append(item)
|
||||
if wins_a == target_maps or wins_b == target_maps:
|
||||
break
|
||||
winner_key, loser_key = (
|
||||
@@ -1113,11 +1448,15 @@ def _battle_story(result: dict[str, Any]) -> str:
|
||||
for item in result["maps"]
|
||||
)
|
||||
matchup = result["matchup"]
|
||||
tactic_note = (
|
||||
"战术克制明显"
|
||||
if abs(float(matchup["a_total_percent"])) >= 5.5
|
||||
else "战术拉扯接近"
|
||||
)
|
||||
return _host_text(
|
||||
f"{result['winner']} 在 {result['series_format']} 以 {result['score']} "
|
||||
f"击败 {result['loser']}({map_scores});"
|
||||
f"战术 {matchup['tactic_a']} 对 {matchup['tactic_b']},"
|
||||
f"{result['player_a']} 克制修正 {matchup['a_total_percent']:+.2f}%。"
|
||||
f"击败 {result['loser']}({map_scores})。"
|
||||
f"战术面是 {matchup['tactic_a']} 对 {matchup['tactic_b']},{tactic_note}。"
|
||||
)
|
||||
|
||||
|
||||
@@ -1684,7 +2023,7 @@ def _draft_candidate_cards(state: dict[str, Any], round_number: int) -> list[str
|
||||
def _start_draft_round(state: dict[str, Any], round_number: int) -> None:
|
||||
draft_type = "coach" if round_number == 1 else "player"
|
||||
candidates = (
|
||||
[coach["id"] for coach in COACH_POOL]
|
||||
[coach["id"] for coach in COACH_POOL[:10]]
|
||||
if draft_type == "coach"
|
||||
else _draft_candidate_cards(state, round_number)
|
||||
)
|
||||
@@ -1740,8 +2079,13 @@ def _draft_prompt_part(state: dict[str, Any]) -> str:
|
||||
return f"现在轮到 @{name} 做出选择,使用 {command}。"
|
||||
|
||||
|
||||
def _add_unit_to_roster(player: dict[str, Any], card_id: str) -> None:
|
||||
unit = _new_unit(card_id)
|
||||
def _add_unit_to_roster(
|
||||
player: dict[str, Any],
|
||||
card_id: str,
|
||||
*,
|
||||
copies: int = 1,
|
||||
) -> None:
|
||||
unit = _new_unit(card_id, copies=copies)
|
||||
if len(player["board"]) < STARTER_COUNT:
|
||||
player["board"].append(unit)
|
||||
else:
|
||||
@@ -1771,13 +2115,15 @@ def _apply_draft_choice(
|
||||
)
|
||||
else:
|
||||
card = CARD_BY_ID[choice_id]
|
||||
if int(state["pool"].get(choice_id, 0)) <= 0:
|
||||
draft_copies = 3
|
||||
if int(state["pool"].get(choice_id, 0)) < draft_copies:
|
||||
raise ValueError("这名选手已经被选走")
|
||||
state["pool"][choice_id] -= 1
|
||||
_add_unit_to_roster(player, choice_id)
|
||||
state["pool"][choice_id] -= draft_copies
|
||||
_add_unit_to_roster(player, choice_id, copies=draft_copies)
|
||||
text = (
|
||||
f"{player['display_name']} 选择了 {card['name']}!"
|
||||
f"{card['cost']}费 {card['role']},{card['team']}/{card['nation']}。"
|
||||
f"{card['cost']}费 {card['role']},{card['team']}/{card['nation']},"
|
||||
"以 2 星素材加入阵容。"
|
||||
)
|
||||
candidates.pop(slot - 1)
|
||||
draft["candidates"] = candidates
|
||||
@@ -1876,7 +2222,10 @@ def _ensure_full_rosters(state: dict[str, Any]) -> None:
|
||||
]
|
||||
candidates.sort(
|
||||
key=lambda card_id: (
|
||||
CARD_BY_ID[card_id]["cost"] if player.get("is_ai") else -CARD_BY_ID[card_id]["cost"],
|
||||
CARD_BY_ID[card_id]["cost"]
|
||||
if player.get("is_ai")
|
||||
else abs(int(CARD_BY_ID[card_id]["cost"]) - 2),
|
||||
int(CARD_BY_ID[card_id]["cost"]),
|
||||
_hash_int(f"{state['run_id']}|fill|{key}|{card_id}"),
|
||||
)
|
||||
)
|
||||
@@ -1954,62 +2303,172 @@ def _shop_status_text(state: dict[str, Any], player: dict[str, Any]) -> str:
|
||||
) + "\n操作:/选1~/选6;/刷新;/锁定;/阵容;/战术 火力|稳定|残局;/结束回合"
|
||||
|
||||
|
||||
def _training_parts(state: dict[str, Any], event_id: str) -> tuple[list[str], list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
parts = [
|
||||
f"欢迎来到{state['event_title']}!你们已经到达酒店宾馆,即将开始训练赛任务。每队会连续完成三场训练赛,结束后可以 D 牌、调阵容,再用 /结束回合 进入小组赛。"
|
||||
def _training_schedule(state: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
rounds: list[dict[str, Any]] = []
|
||||
keys = list(state["seed_order"])
|
||||
for round_index, pairs in enumerate(_round_robin_rounds(keys), start=1):
|
||||
paired = {key for pair in pairs for key in pair}
|
||||
bye = next((key for key in keys if key not in paired), "")
|
||||
rounds.append(
|
||||
{
|
||||
"round": round_index,
|
||||
"pairs": [[str(pair[0]), str(pair[1])] for pair in pairs],
|
||||
"bye": bye,
|
||||
}
|
||||
)
|
||||
return rounds
|
||||
|
||||
|
||||
def _ensure_training_schedule(state: dict[str, Any]) -> None:
|
||||
if state.get("training_rounds"):
|
||||
return
|
||||
state["training_rounds"] = _training_schedule(state)
|
||||
state["training_round_cursor"] = 0
|
||||
|
||||
|
||||
def _training_pairing_line(state: dict[str, Any], round_item: dict[str, Any]) -> str:
|
||||
rendered = [
|
||||
f"{_team_name(state['players'][pair[0]])} vs {_team_name(state['players'][pair[1]])}"
|
||||
for pair in round_item.get("pairs", [])
|
||||
]
|
||||
bye = str(round_item.get("bye") or "")
|
||||
if bye:
|
||||
rendered.append(f"{_team_name(state['players'][bye])} vs 本地陪练队")
|
||||
return ";".join(rendered)
|
||||
|
||||
|
||||
def _training_match_part(
|
||||
state: dict[str, Any],
|
||||
result: dict[str, Any],
|
||||
round_number: int,
|
||||
) -> str:
|
||||
item = result["maps"][0]
|
||||
winner_key = str(result["winner_key"])
|
||||
loser_key = str(result["loser_key"])
|
||||
winner = state["players"][winner_key]
|
||||
loser = state["players"][loser_key]
|
||||
return "\n".join(
|
||||
[
|
||||
(
|
||||
f"训练赛第 {round_number} 轮:{_team_name(winner)} "
|
||||
f"vs {_team_name(loser)}"
|
||||
),
|
||||
(
|
||||
f"{item['map_name']}:{item['opening_text']}"
|
||||
f"{item['run_text']}"
|
||||
),
|
||||
(
|
||||
f"{item['clutch_text']}{item['closer_text']}"
|
||||
f"{_team_name(winner)} 以 {item['score']} 拿下本图。"
|
||||
),
|
||||
(
|
||||
f"金币:{_team_name(winner)} +{TRAINING_WIN_GOLD},"
|
||||
f"{_team_name(loser)} +{TRAINING_LOSS_GOLD}。"
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _training_bye_part(state: dict[str, Any], key: str, round_number: int) -> str:
|
||||
player = state["players"][key]
|
||||
player["gold"] = int(player["gold"]) + TRAINING_BYE_GOLD
|
||||
hook = _pick_fragment(
|
||||
"training_hooks",
|
||||
f"{state['run_id']}|training|bye|{round_number}|{key}",
|
||||
)
|
||||
return (
|
||||
f"训练赛第 {round_number} 轮:{_team_name(player)} 与本地陪练队完成热身。\n"
|
||||
f"{hook}\n金币:{_team_name(player)} +{TRAINING_BYE_GOLD}。"
|
||||
)
|
||||
|
||||
|
||||
def _settle_training_round(
|
||||
state: dict[str, Any],
|
||||
event_id: str,
|
||||
) -> tuple[list[str], list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
_ensure_training_schedule(state)
|
||||
rounds = list(state.get("training_rounds", []))
|
||||
cursor = int(state.get("training_round_cursor", 0))
|
||||
if cursor >= len(rounds):
|
||||
state["phase"] = "training_complete_manage"
|
||||
_prepare_new_shops(state, "training-complete")
|
||||
_reset_ready(state, _human_keys(state))
|
||||
return [
|
||||
"训练赛已经全部结束。可以最后 D 牌、调阵容;所有真人发送 /结束回合 后进入小组赛。"
|
||||
], [], []
|
||||
|
||||
round_item = rounds[cursor]
|
||||
round_number = int(round_item["round"])
|
||||
state["round_number"] = round_number
|
||||
parts: list[str] = []
|
||||
stories: list[dict[str, Any]] = []
|
||||
battles: list[dict[str, Any]] = []
|
||||
for key in state["join_order"]:
|
||||
player = state["players"][key]
|
||||
power = lineup_power(player)["power"]
|
||||
lines = [f"{_team_name(player)} 训练赛:"]
|
||||
total_reward = 0
|
||||
for index in range(3):
|
||||
opponent, target = TRAINING_OPPONENTS[
|
||||
(_hash_int(f"{state['run_id']}|train|{key}|{index}") + index)
|
||||
% len(TRAINING_OPPONENTS)
|
||||
]
|
||||
fluctuation = _hash_int(
|
||||
f"{state['run_id']}|{event_id}|train|{key}|{index}"
|
||||
) % 401 - 200
|
||||
adjusted = power * (10_000 + fluctuation) / 10_000
|
||||
won = adjusted >= target
|
||||
reward = 3 if won else 1
|
||||
total_reward += reward
|
||||
score = "16-12" if won else "13-16"
|
||||
if abs(adjusted - target) < 12:
|
||||
score = "19-17" if won else "17-19"
|
||||
lines.append(
|
||||
f"{index + 1}. vs {opponent}(目标 {target},发挥 {adjusted:.1f}):"
|
||||
f"{'胜' if won else '负'} {score},获得 {reward} 金币"
|
||||
)
|
||||
result = {
|
||||
"opponent": opponent,
|
||||
"target_power": target,
|
||||
"player_power": round(adjusted, 1),
|
||||
"score": score,
|
||||
"won": won,
|
||||
"reward": reward,
|
||||
}
|
||||
battles.append(
|
||||
_battle_record(
|
||||
state,
|
||||
battle_type="training",
|
||||
player_a=key,
|
||||
player_b="training",
|
||||
result=result,
|
||||
)
|
||||
)
|
||||
player["gold"] = int(player["gold"]) + total_reward
|
||||
lines.append(f"训练赛合计收入 {total_reward} 金币;当前金币 {player['gold']}。")
|
||||
text = "\n".join(lines)
|
||||
if cursor == 0:
|
||||
intro = (
|
||||
f"欢迎来到{state['event_title']}!你们已经到达酒店宾馆,"
|
||||
f"训练赛共 {len(rounds)} 轮。训练赛不扣生命,只产出运营金币。"
|
||||
)
|
||||
parts.append(intro)
|
||||
stories.append(_story(state, "training_open", intro))
|
||||
hook = _pick_fragment(
|
||||
"training_hooks",
|
||||
f"{state['run_id']}|training|hook|{round_number}",
|
||||
)
|
||||
parts.append(
|
||||
f"训练赛第 {round_number}/{len(rounds)} 轮对阵:"
|
||||
f"{_training_pairing_line(state, round_item)}\n{hook}"
|
||||
)
|
||||
for match_index, pair in enumerate(round_item.get("pairs", []), start=1):
|
||||
key_a, key_b = str(pair[0]), str(pair[1])
|
||||
result = _battle(
|
||||
state,
|
||||
key_a,
|
||||
key_b,
|
||||
f"{state['run_id']}|training|{round_number}|{match_index}|{event_id}",
|
||||
best_of=1,
|
||||
)
|
||||
winner_key = str(result["winner_key"])
|
||||
loser_key = str(result["loser_key"])
|
||||
state["players"][winner_key]["gold"] = (
|
||||
int(state["players"][winner_key]["gold"]) + TRAINING_WIN_GOLD
|
||||
)
|
||||
state["players"][loser_key]["gold"] = (
|
||||
int(state["players"][loser_key]["gold"]) + TRAINING_LOSS_GOLD
|
||||
)
|
||||
result["training_rewards"] = {
|
||||
winner_key: TRAINING_WIN_GOLD,
|
||||
loser_key: TRAINING_LOSS_GOLD,
|
||||
}
|
||||
battle = _battle_record(
|
||||
state,
|
||||
battle_type="training_bo1",
|
||||
player_a=key_a,
|
||||
player_b=key_b,
|
||||
result=result,
|
||||
)
|
||||
text = _training_match_part(state, result, round_number)
|
||||
parts.append(text)
|
||||
stories.append(_story(state, "training_result", text, {"person_key": key}))
|
||||
state["phase"] = "training_manage"
|
||||
_prepare_new_shops(state, "training")
|
||||
stories.append(_story(state, "training_result", text, result))
|
||||
battles.append(battle)
|
||||
bye = str(round_item.get("bye") or "")
|
||||
if bye:
|
||||
text = _training_bye_part(state, bye, round_number)
|
||||
parts.append(text)
|
||||
stories.append(_story(state, "training_bye", text, {"person_key": bye}))
|
||||
state["training_round_cursor"] = cursor + 1
|
||||
_prepare_new_shops(state, f"training-{round_number}")
|
||||
_reset_ready(state, _human_keys(state))
|
||||
parts.append("训练赛阶段结束。可以 /刷新、/选1~/选6、/上阵、/战术;所有真人发送 /结束回合 后进入小组赛。")
|
||||
if cursor + 1 < len(rounds):
|
||||
state["phase"] = "training_manage"
|
||||
parts.append(
|
||||
"本轮训练结束。现在是运营窗口:/刷新 是 D 牌,/选1~/选6 是买店内选手,"
|
||||
"/锁定 可保留商店,/上阵 可调整首发;所有真人发送 /结束回合 后进入下一轮训练。"
|
||||
)
|
||||
else:
|
||||
state["phase"] = "training_complete_manage"
|
||||
parts.append(
|
||||
"训练赛全部结束。可以最后 D 牌、调阵容;所有真人发送 /结束回合 后进入小组赛。"
|
||||
)
|
||||
return parts, stories, battles
|
||||
|
||||
|
||||
@@ -2120,38 +2579,42 @@ def _series_report_parts(
|
||||
result: dict[str, Any],
|
||||
) -> list[str]:
|
||||
key_a, key_b = match["pair"]
|
||||
team_a = _team_name(state["players"][key_a])
|
||||
team_b = _team_name(state["players"][key_b])
|
||||
matchup = result["matchup"]
|
||||
parts = [
|
||||
f"{_match_title(state, match)} 开始。{team_a} 战力 {result['power_a']:.1f},{team_b} 战力 {result['power_b']:.1f}。"
|
||||
(
|
||||
f"{_match_title(state, match)} 开始。"
|
||||
f"战术面是 {matchup['tactic_a']} 对 {matchup['tactic_b']},"
|
||||
"本场按逐回合模拟推进。"
|
||||
)
|
||||
]
|
||||
wins: dict[str, int] = {key_a: 0, key_b: 0}
|
||||
for index, item in enumerate(result["maps"], start=1):
|
||||
winner_key = str(item["winner_key"])
|
||||
loser_key = str(item["loser_key"])
|
||||
wins[winner_key] += 1
|
||||
actor = _key_player_name(state, winner_key, index)
|
||||
closer = _key_player_name(state, loser_key, index + 2)
|
||||
lead_text = (
|
||||
f"{_team_name(state['players'][winner_key])} 在系列赛中 "
|
||||
f"{wins[winner_key]}-{wins[loser_key]} {'领先' if wins[winner_key] > wins[loser_key] else '追平'}"
|
||||
)
|
||||
overtime = "常规时间 15-15 后进入加时," if int(item["overtime_count"]) else ""
|
||||
parts.append(
|
||||
f"图{index} {item['map_name']}:{overtime}{actor} 在关键回合打开局面,"
|
||||
f"{closer} 的残局反扑没能改写结果。"
|
||||
f"图{index} {item['map_name']}:{item['opening_text']}"
|
||||
f"{item['run_text']}{item['clutch_text']}"
|
||||
f"{item['economy_text']}{item['closer_text']}"
|
||||
f"{_team_name(state['players'][winner_key])} 以 {item['score']} 拿下地图;{lead_text}。"
|
||||
)
|
||||
winner_key = str(result["winner_key"])
|
||||
loser_key = str(result["loser_key"])
|
||||
reward = 5 if match["stage"] == "group" else 6
|
||||
state["players"][winner_key]["gold"] = int(state["players"][winner_key]["gold"]) + reward
|
||||
parts.append(
|
||||
final_text = (
|
||||
f"系列赛结束,{_team_name(state['players'][winner_key])} 以 {result['score']} 击败 "
|
||||
f"{_team_name(state['players'][loser_key])},获得 {reward} 金币。"
|
||||
"可用于 /刷新 和 /选1~/选6。"
|
||||
"这笔钱可以马上用于 /刷新、/选1~/选6 或锁店等下一轮运营。"
|
||||
)
|
||||
return parts[:5]
|
||||
parts[-1] = parts[-1] + "\n" + final_text
|
||||
if len(parts) <= 5:
|
||||
return parts
|
||||
return [parts[0], parts[1], parts[2], "\n".join(parts[3:-1]), parts[-1]]
|
||||
|
||||
|
||||
def _settle_current_match(
|
||||
@@ -2214,9 +2677,13 @@ def _special_event_parts(
|
||||
won = power >= target
|
||||
reward = 4 if won else 1
|
||||
player["gold"] = int(player["gold"]) + reward
|
||||
hook = _pick_fragment(
|
||||
"challenges",
|
||||
f"{state['run_id']}|{event_kind}|{event_id}|{key}",
|
||||
)
|
||||
text = (
|
||||
f"{_team_name(player)} {'完成' if won else '勉强应付'}{event_kind}:"
|
||||
f"表现 {power:.1f}/{target},获得 {reward} 金币。"
|
||||
f"{hook}获得 {reward} 金币。"
|
||||
)
|
||||
parts.append(text)
|
||||
stories.append(_story(state, "special_event", text, {"person_key": key}))
|
||||
@@ -2289,7 +2756,8 @@ class CsMajorAutochess:
|
||||
"选秀完成后每队 5 名首发、1 名教练、3 个备战席。\n"
|
||||
"/选1~/选6;/阵容;/刷新;/锁定;/上阵 备战编号 [首发位];/出售 备战编号。"
|
||||
"/战术 火力|稳定|残局,稳定克火力、火力克残局、残局克稳定。\n"
|
||||
"三场训练赛后进入 BO3 单循环小组赛;半决赛/决赛 BO5。"
|
||||
"训练赛按参赛人数生成多轮真实对阵,每轮结束后可 D 牌和调阵容;"
|
||||
"随后进入 BO3 单循环小组赛,半决赛/决赛 BO5。"
|
||||
"所有需要推进的阶段由相关真人发送 /结束回合,满足条件后自动进入下一步。"
|
||||
)
|
||||
|
||||
@@ -2373,7 +2841,7 @@ class CsMajorAutochess:
|
||||
)
|
||||
season = next_season
|
||||
state = {
|
||||
"schema_version": 2,
|
||||
"schema_version": 3,
|
||||
"run_id": f"major_{season:04d}",
|
||||
"season_number": season,
|
||||
"event_title": f"第 {season} 届 IEM 盐田 Major",
|
||||
@@ -2403,6 +2871,8 @@ class CsMajorAutochess:
|
||||
"story_sequence": 0,
|
||||
"battle_sequence": 0,
|
||||
"special_event_done": False,
|
||||
"training_rounds": [],
|
||||
"training_round_cursor": 0,
|
||||
}
|
||||
text = (
|
||||
f"{state['event_title']} 开放报名;主持人 {display_name} 已自动加入。"
|
||||
@@ -2485,7 +2955,7 @@ class CsMajorAutochess:
|
||||
f"参赛者:{names}。",
|
||||
(
|
||||
f"种子顺序:{_seed_lines(state)}。本届采用 {mode}:"
|
||||
"先进行三轮选秀,随后训练赛、小组赛 BO3 单循环,"
|
||||
"先进行三轮选秀,随后按轮次训练赛、小组赛 BO3 单循环,"
|
||||
"淘汰赛 BO5;单图 MR15,常规时间 15-15 进入加时。"
|
||||
),
|
||||
_draft_board_part(state),
|
||||
@@ -2667,10 +3137,10 @@ class CsMajorAutochess:
|
||||
phase = str(state.get("phase"))
|
||||
stories: list[dict[str, Any]] = []
|
||||
battles: list[dict[str, Any]] = []
|
||||
if phase == "lineup_prep":
|
||||
parts, stories, battles = _training_parts(state, event_id)
|
||||
if phase in {"lineup_prep", "training_manage"}:
|
||||
parts, stories, battles = _settle_training_round(state, event_id)
|
||||
return respond(state, parts, stories=stories, battles=battles)
|
||||
if phase == "training_manage":
|
||||
if phase == "training_complete_manage":
|
||||
parts = _start_group_parts(state)
|
||||
return respond(state, parts)
|
||||
if phase == "group_ready":
|
||||
|
||||
@@ -158,8 +158,10 @@ LOCAL_EXTENSIONS = (
|
||||
LocalExtensionDescriptor(
|
||||
extension_id="cs-major-autochess",
|
||||
author=JWEBOT_AUTHOR,
|
||||
description="CS自走棋",
|
||||
help_entries=("/自走棋开始|报名",),
|
||||
description="自走棋",
|
||||
help_entries=(
|
||||
"/自走棋 开始|报名|选",
|
||||
),
|
||||
command_kinds=("cs_autochess",),
|
||||
),
|
||||
LocalExtensionDescriptor(
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-sdk"
|
||||
version = "0.6.6a0+1015"
|
||||
version = "0.6.7a0+1015"
|
||||
description = "Typed, capability-limited extension API for JWeBot plugins"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = []
|
||||
|
||||
@@ -38,4 +38,4 @@ __all__ = [
|
||||
"SingleGroupContext",
|
||||
]
|
||||
|
||||
__version__ = "0.6.6a0+1015"
|
||||
__version__ = "0.6.7a0+1015"
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
[tool.jwebot]
|
||||
release = "alpha 0.6.6.1015"
|
||||
release = "alpha 0.6.7.1015"
|
||||
production_deployments_enabled_by_default = false
|
||||
|
||||
[tool.jwebot.workspace]
|
||||
|
||||
+21
-11
@@ -77,12 +77,15 @@ class Release064AutochessTests(unittest.TestCase):
|
||||
return replies
|
||||
|
||||
def test_static_pool_parser_and_coach_pool(self):
|
||||
self.assertEqual(len(AUTOCHESS.PLAYER_POOL), 60)
|
||||
self.assertEqual(len({card["id"] for card in AUTOCHESS.PLAYER_POOL}), 60)
|
||||
self.assertEqual(len(AUTOCHESS.COACH_POOL), 10)
|
||||
self.assertGreaterEqual(len(AUTOCHESS.PLAYER_POOL), 100)
|
||||
self.assertEqual(
|
||||
len({card["id"] for card in AUTOCHESS.PLAYER_POOL}),
|
||||
len(AUTOCHESS.PLAYER_POOL),
|
||||
)
|
||||
self.assertGreaterEqual(len(AUTOCHESS.COACH_POOL), 18)
|
||||
self.assertEqual(
|
||||
len({coach["id"] for coach in AUTOCHESS.COACH_POOL}),
|
||||
10,
|
||||
len(AUTOCHESS.COACH_POOL),
|
||||
)
|
||||
self.assertEqual(
|
||||
AUTOCHESS.parse_autochess_action("/自走棋开始"),
|
||||
@@ -114,7 +117,7 @@ class Release064AutochessTests(unittest.TestCase):
|
||||
self.assertEqual(player["board"], [])
|
||||
self.assertEqual(player["coach_id"], "")
|
||||
|
||||
def test_three_round_draft_training_and_group_intro(self):
|
||||
def test_three_round_draft_training_rounds_and_group_intro(self):
|
||||
self.start_two_human_two_ai()
|
||||
final_draft = self.finish_draft()
|
||||
state = self.state()
|
||||
@@ -126,14 +129,21 @@ class Release064AutochessTests(unittest.TestCase):
|
||||
self.assertEqual(len(player["bench"]), 3)
|
||||
self.assertTrue(player["coach_id"])
|
||||
|
||||
self.command("/结束回合")
|
||||
training = self.command("/结束回合", sender="Bob")
|
||||
self.assertIn("Team Falcons", "\n".join(training))
|
||||
self.assertIn("Natus Vincere", "\n".join(training))
|
||||
training = self.ready_all()
|
||||
training_text = "\n".join(training)
|
||||
self.assertIn("训练赛第 1/3 轮", training_text)
|
||||
self.assertIn("Team", training_text)
|
||||
self.assertNotIn("目标", training_text)
|
||||
self.assertNotIn("发挥", training_text)
|
||||
self.assertEqual(self.state()["phase"], "training_manage")
|
||||
|
||||
self.command("/结束回合")
|
||||
group = self.command("/结束回合", sender="Bob")
|
||||
round_two = "\n".join(self.ready_all())
|
||||
self.assertIn("训练赛第 2/3 轮", round_two)
|
||||
round_three = "\n".join(self.ready_all())
|
||||
self.assertIn("训练赛第 3/3 轮", round_three)
|
||||
self.assertEqual(self.state()["phase"], "training_complete_manage")
|
||||
|
||||
group = self.ready_all()
|
||||
self.assertIn("正赛小组赛", "\n".join(group))
|
||||
state = self.state()
|
||||
self.assertEqual(state["phase"], "group_ready")
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
"""Behavioral coverage for the CS Major autochess 0.6.7 pacing rewrite."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from test_jwebot_edge import MODULE, config
|
||||
from jwebot_official_extensions import cs_autochess as AUTOCHESS
|
||||
|
||||
|
||||
class Release067AutochessTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.engine = MODULE.EdgeApplication(config(Path(self.temp.name)))
|
||||
self.counter = 0
|
||||
|
||||
def tearDown(self):
|
||||
self.engine.close()
|
||||
self.temp.cleanup()
|
||||
|
||||
def command(self, question, *, sender="Alice", event_id=None):
|
||||
self.counter += 1
|
||||
response = self.engine.accept(
|
||||
{
|
||||
"event_id": event_id or f"autochess-067-{self.counter}",
|
||||
"group_fingerprint": "group-a",
|
||||
"sender": sender,
|
||||
"text": "@JWeBot " + question,
|
||||
"mention_bot": True,
|
||||
"confidence": 1.0,
|
||||
"sender_confidence": 1.0,
|
||||
"body_confidence": 1.0,
|
||||
}
|
||||
)
|
||||
self.assertEqual(response["status"], "reply")
|
||||
return list(response.get("texts") or [response["text"]])
|
||||
|
||||
def state(self):
|
||||
row = self.engine.ledger.connection.execute(
|
||||
"""
|
||||
SELECT state_json FROM autochess_runs
|
||||
WHERE group_fingerprint = 'group-a'
|
||||
ORDER BY season_number DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
).fetchone()
|
||||
self.assertIsNotNone(row)
|
||||
return json.loads(row[0])
|
||||
|
||||
def ready_all(self):
|
||||
state = self.state()
|
||||
replies = []
|
||||
for key in list(state.get("ready_keys") or []):
|
||||
replies.extend(
|
||||
self.command(
|
||||
"/结束回合",
|
||||
sender=state["players"][key]["display_name"],
|
||||
)
|
||||
)
|
||||
return replies
|
||||
|
||||
def start_two_human_two_ai(self):
|
||||
self.command("/自走棋开始")
|
||||
self.command("/自走棋报名", sender="Bob")
|
||||
self.command("/自走棋加AI战队")
|
||||
self.command("/自走棋开赛")
|
||||
|
||||
def finish_draft(self):
|
||||
self.command("/选1")
|
||||
self.command("/选1", sender="Bob")
|
||||
self.command("/选1", sender="Bob")
|
||||
self.command("/选1")
|
||||
self.command("/选1")
|
||||
self.command("/选1", sender="Bob")
|
||||
|
||||
def test_narrative_inventory_and_training_round_robin(self):
|
||||
self.assertGreaterEqual(AUTOCHESS._narrative_inventory_size(), 120)
|
||||
|
||||
self.start_two_human_two_ai()
|
||||
self.finish_draft()
|
||||
training = "\n".join(self.ready_all())
|
||||
state = self.state()
|
||||
|
||||
self.assertEqual(len(state["training_rounds"]), 3)
|
||||
self.assertEqual(state["training_round_cursor"], 1)
|
||||
self.assertIn("训练赛第 1/3 轮对阵", training)
|
||||
self.assertIn("Team", training)
|
||||
self.assertIn("AI 战队", training)
|
||||
self.assertNotIn("目标", training)
|
||||
self.assertNotIn("发挥", training)
|
||||
|
||||
def test_round_simulation_produces_natural_score_variety(self):
|
||||
self.start_two_human_two_ai()
|
||||
self.finish_draft()
|
||||
state = self.state()
|
||||
key_a, key_b = state["seed_order"][:2]
|
||||
|
||||
scores = set()
|
||||
for index in range(12):
|
||||
result = AUTOCHESS._battle(
|
||||
state,
|
||||
key_a,
|
||||
key_b,
|
||||
f"score-variety-{index}",
|
||||
best_of=1,
|
||||
)
|
||||
score = result["maps"][0]["score"]
|
||||
self.assertRegex(score, r"^\d+-\d+$")
|
||||
self.assertGreaterEqual(result["maps"][0]["rounds_played"], 16)
|
||||
scores.add(score)
|
||||
|
||||
old_fixed = {"16-6", "16-9", "16-12", "16-14", "19-17"}
|
||||
self.assertGreaterEqual(len(scores), 3)
|
||||
self.assertTrue(scores - old_fixed)
|
||||
|
||||
def test_group_report_is_paced_and_hides_raw_calculation_terms(self):
|
||||
self.start_two_human_two_ai()
|
||||
self.finish_draft()
|
||||
self.ready_all()
|
||||
self.ready_all()
|
||||
self.ready_all()
|
||||
self.ready_all()
|
||||
|
||||
report = "\n".join(self.ready_all())
|
||||
self.assertIn("图1", report)
|
||||
self.assertIn("系列赛结束", report)
|
||||
self.assertNotIn("目标", report)
|
||||
self.assertNotIn("发挥", report)
|
||||
self.assertNotIn("克制修正", report)
|
||||
self.assertIsNotNone(re.search(r"\b\d{2}-\d{1,2}\b", report))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user