This commit is contained in:
@@ -1,5 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## alpha 0.6.5.1015
|
||||
|
||||
- Extend the AI test path for CS Major autochess to support two-human games:
|
||||
`/自走棋加AI战队` now adds AI 战队 A and AI 战队 B when exactly two
|
||||
real players have registered.
|
||||
- Keep the same command for two- and three-human test modes. The tournament
|
||||
engine selects 2 人+2 AI or 3 人+1 AI from the current registration count.
|
||||
- Lock the roster after AI teams join, seed the four-team bracket as 1v4 and
|
||||
2v3, and keep AI teams out of the human preparation order while preserving
|
||||
independent AI operations and story archives.
|
||||
|
||||
## alpha 0.6.4.1015
|
||||
|
||||
- Add a reviewed 2-8 player CS Major autochess extension with a fixed
|
||||
|
||||
@@ -8,7 +8,7 @@ JWeBot 是运行于 Linux 的单群微信自动回复系统。它通过本地 OC
|
||||
观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件,
|
||||
并通过带视觉确认的 GUI 发送链路回复。
|
||||
|
||||
当前版本:`alpha 0.6.4.1015`。
|
||||
当前版本:`alpha 0.6.5.1015`。
|
||||
|
||||
0.6.1 新增本地上下文总结、人物与别名索引、单人虚拟金币游戏,并增加
|
||||
两秒画面复核以补抓遗漏的消息变化。命令和游戏管理规则见
|
||||
@@ -26,6 +26,8 @@ BO3/BO5 的 MR15 加时赛。支持三名真人加入一支确定性 AI 测试
|
||||
Taffy 负责赛制播报、顺序准备、交棒和全程剧情主持。所有选秀、战斗和
|
||||
加时剧情均在本地事务内落档。
|
||||
完整规则见[CS Major 自走棋设计稿](docs/cs-major-autochess-design.md)。
|
||||
0.6.5 将 AI 测试模式扩展到双人:两名真人可一键补入 AI 战队 A、B,
|
||||
按四队种子运行 1v4、2v3 半决赛;两支 AI 独立运营且不占真人准备顺序。
|
||||
|
||||
### 运行流程
|
||||
|
||||
@@ -97,7 +99,7 @@ PATH="$PWD/.venv/bin:$PATH" ./tools/check-local.sh
|
||||
/游戏记录 [页码](/gamehistory)
|
||||
/游戏帮助(/games)
|
||||
/自走棋开始|报名|开赛|状态|结算|档案
|
||||
/自走棋加AI战队(三名真人测试模式)
|
||||
/自走棋加AI战队(两名或三名真人测试模式)
|
||||
/选1~6;/战术 火力|稳定|残局;/阵容;/结束回合
|
||||
/丢骰子 [NdM]
|
||||
/猜数字 开始 [上限]
|
||||
@@ -267,7 +269,7 @@ visually confirmed GUI bridge.
|
||||
Current version:
|
||||
|
||||
```text
|
||||
alpha 0.6.4.1015
|
||||
alpha 0.6.5.1015
|
||||
```
|
||||
|
||||
## Runtime Flow
|
||||
|
||||
@@ -33,8 +33,10 @@
|
||||
- 开局命令由任意玩家发起。
|
||||
- 其他玩家在报名窗口内加入。
|
||||
- 建议一局支持 `2~8` 名真人玩家。
|
||||
- 主持人可在报名阶段发送 `/自走棋加AI战队`,最多加入一支 AI 战队。
|
||||
- 三名真人加一支 AI 时,按四队赛制直接生成 `1v4`、`2v3` 半决赛。
|
||||
- 主持人可在报名阶段发送 `/自走棋加AI战队`:
|
||||
- 两名真人时加入 AI 战队 A、B;
|
||||
- 三名真人时加入一支 AI 战队。
|
||||
- 两种测试模式都补满四队,直接生成 `1v4`、`2v3` 半决赛。
|
||||
- AI 占一个参赛席位,但不对应群成员,也不调用在线模型。
|
||||
- AI 每回合以确定性规则买牌、升级教练、调整首发和选择克制战术。
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-config"
|
||||
version = "0.6.4a0+1015"
|
||||
version = "0.6.5a0+1015"
|
||||
description = "Profile-based configuration compiler for JWeBot"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
|
||||
@@ -36,4 +36,4 @@ __all__ = [
|
||||
"write_default_profile",
|
||||
]
|
||||
|
||||
__version__ = "0.6.4a0+1015"
|
||||
__version__ = "0.6.5a0+1015"
|
||||
|
||||
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-extension-host"
|
||||
version = "0.6.4a0+1015"
|
||||
version = "0.6.5a0+1015"
|
||||
description = "Single-release, single-invocation process host for JWeBot extensions"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"jwebot-sdk==0.6.4a0+1015",
|
||||
"jwebot-kernel==0.6.4a0+1015",
|
||||
"jwebot-sdk==0.6.5a0+1015",
|
||||
"jwebot-kernel==0.6.5a0+1015",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -16,4 +16,4 @@ __all__ = [
|
||||
"process_request",
|
||||
]
|
||||
|
||||
__version__ = "0.6.4a0+1015"
|
||||
__version__ = "0.6.5a0+1015"
|
||||
|
||||
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-kernel"
|
||||
version = "0.6.4a0+1015"
|
||||
version = "0.6.5a0+1015"
|
||||
description = "Stable routing and extension authority for JWeBot"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"jwebot-config==0.6.4a0+1015",
|
||||
"jwebot-sdk==0.6.4a0+1015",
|
||||
"jwebot-official-extensions==0.6.4a0+1015",
|
||||
"jwebot-config==0.6.5a0+1015",
|
||||
"jwebot-sdk==0.6.5a0+1015",
|
||||
"jwebot-official-extensions==0.6.5a0+1015",
|
||||
"tomli>=2; python_version < '3.11'",
|
||||
]
|
||||
|
||||
|
||||
@@ -107,4 +107,4 @@ __all__ = [
|
||||
"verify_installed_release_signature",
|
||||
]
|
||||
|
||||
__version__ = "0.6.4a0+1015"
|
||||
__version__ = "0.6.5a0+1015"
|
||||
|
||||
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-official-extensions"
|
||||
version = "0.6.4a0+1015"
|
||||
version = "0.6.5a0+1015"
|
||||
description = "Reviewed local command adapters and broker implementation for JWeBot"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"jwebot-sdk==0.6.4a0+1015",
|
||||
"jwebot-sdk==0.6.5a0+1015",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
|
||||
@@ -84,4 +84,4 @@ __all__ = [
|
||||
"render_feature_list",
|
||||
]
|
||||
|
||||
__version__ = "0.6.4a0+1015"
|
||||
__version__ = "0.6.5a0+1015"
|
||||
|
||||
+51
-14
@@ -11,6 +11,8 @@ MIN_PLAYERS = 2
|
||||
MAX_PLAYERS = 8
|
||||
AI_PERSON_KEY = "__local_ai_team__"
|
||||
AI_DISPLAY_NAME = "AI 战队"
|
||||
AI_SECOND_PERSON_KEY = "__local_ai_team_2__"
|
||||
AI_SECOND_DISPLAY_NAME = "AI 战队 B"
|
||||
HOST_NAME = "Taffy"
|
||||
STARTING_GOLD = 10
|
||||
STARTING_HP = 100
|
||||
@@ -325,6 +327,11 @@ def _preparation_prompt(state: dict[str, Any]) -> str:
|
||||
|
||||
def _format_description(state: dict[str, Any]) -> str:
|
||||
team_count = len(state["join_order"])
|
||||
if state.get("ai_test_variant") == "two_plus_two":
|
||||
return (
|
||||
"本届采用 2 人+2 AI 测试赛:报名种子 1v4、2v3 进行 BO5 "
|
||||
"半决赛,胜者进入 BO5 总决赛"
|
||||
)
|
||||
if state.get("ai_test_mode"):
|
||||
return (
|
||||
"本届采用 3 人+1 AI 测试赛:报名种子 1v4、2v3 进行 BO5 "
|
||||
@@ -844,7 +851,7 @@ def _auto_manage_ai(
|
||||
+ CARD_BY_ID[str(incoming["card_id"])]["name"]
|
||||
)
|
||||
text = (
|
||||
f"{AI_DISPLAY_NAME} 自动运营:{'、'.join(actions)};"
|
||||
f"{player['display_name']} 自动运营:{'、'.join(actions)};"
|
||||
f"金币 {player['gold']},五人首发战力 "
|
||||
f"{lineup_power(player)['power']:.1f}。"
|
||||
)
|
||||
@@ -1573,7 +1580,8 @@ class CsMajorAutochess:
|
||||
def rules() -> str:
|
||||
return (
|
||||
"CS自走棋:/自走棋开始 创建报名;/自走棋报名;主持人 /自走棋开赛。\n"
|
||||
"三名真人报名后,主持人可用 /自走棋加AI战队 开启 3 人+1 AI 测试模式。\n"
|
||||
"两名或三名真人报名后,主持人可用 /自走棋加AI战队,"
|
||||
"分别开启 2 人+2 AI 或 3 人+1 AI 测试模式。\n"
|
||||
"每人固定 5 名首发 + 1 名教练,开赛即发完整首发;每轮公开 6 人商店。"
|
||||
"/选1~/选6 购买;/阵容;/上阵 备战编号 [首发位];/出售 备战编号。\n"
|
||||
"教练只提供全队 +2%~+10% 加成;/升级教练;/刷新;/锁定。"
|
||||
@@ -1664,6 +1672,7 @@ class CsMajorAutochess:
|
||||
"round_plan": [],
|
||||
"group_results": [],
|
||||
"ai_test_mode": False,
|
||||
"ai_test_variant": "",
|
||||
"preparation_order": [],
|
||||
"preparation_cursor": 0,
|
||||
"qualifiers": [],
|
||||
@@ -1694,7 +1703,7 @@ class CsMajorAutochess:
|
||||
return _result(
|
||||
state,
|
||||
_host_text(
|
||||
"3 人+1 AI 测试模式已经锁定参赛名单,不能再加入真人。"
|
||||
"AI 测试模式已经锁定参赛名单,不能再加入真人。"
|
||||
),
|
||||
)
|
||||
if person_key in state["players"]:
|
||||
@@ -1724,27 +1733,47 @@ class CsMajorAutochess:
|
||||
state,
|
||||
f"只有主持人 {state['host_display_name']} 可以加入 AI 战队",
|
||||
)
|
||||
if AI_PERSON_KEY in state["players"]:
|
||||
return _result(state, "3 人+1 AI 测试战队已经加入")
|
||||
if any(
|
||||
player.get("is_ai")
|
||||
for player in state["players"].values()
|
||||
):
|
||||
return _result(state, "AI 测试战队已经加入")
|
||||
human_keys = [
|
||||
key
|
||||
for key in state["join_order"]
|
||||
if not state["players"][key].get("is_ai")
|
||||
]
|
||||
if len(human_keys) != 3:
|
||||
if len(human_keys) not in {2, 3}:
|
||||
return _result(
|
||||
state,
|
||||
"3 人+1 AI 测试模式要求先有且仅有 3 名真人报名",
|
||||
"AI 测试模式要求先有且仅有 2 名或 3 名真人报名",
|
||||
)
|
||||
state["players"][AI_PERSON_KEY] = _participant(
|
||||
AI_PERSON_KEY,
|
||||
AI_DISPLAY_NAME,
|
||||
if len(human_keys) == 2:
|
||||
ai_teams = (
|
||||
(AI_PERSON_KEY, "AI 战队 A"),
|
||||
(AI_SECOND_PERSON_KEY, AI_SECOND_DISPLAY_NAME),
|
||||
)
|
||||
variant = "two_plus_two"
|
||||
mode_label = "2 人+2 AI"
|
||||
join_text = (
|
||||
"AI 战队 A、AI 战队 B 已作为第 3、4 种子加入测试赛;"
|
||||
)
|
||||
else:
|
||||
ai_teams = ((AI_PERSON_KEY, AI_DISPLAY_NAME),)
|
||||
variant = "three_plus_one"
|
||||
mode_label = "3 人+1 AI"
|
||||
join_text = "AI 战队已作为第 4 种子加入测试赛;"
|
||||
for ai_key, ai_name in ai_teams:
|
||||
state["players"][ai_key] = _participant(
|
||||
ai_key,
|
||||
ai_name,
|
||||
is_ai=True,
|
||||
)
|
||||
state["join_order"].append(AI_PERSON_KEY)
|
||||
state["join_order"].append(ai_key)
|
||||
state["ai_test_mode"] = True
|
||||
state["ai_test_variant"] = variant
|
||||
text = _host_text(
|
||||
"AI 战队已作为第 4 种子加入测试赛;"
|
||||
f"{join_text}{mode_label} 编制已锁定;"
|
||||
"开赛后固定进行 1v4、2v3 半决赛,AI 将自动运营。"
|
||||
)
|
||||
story = _story(
|
||||
@@ -1752,8 +1781,16 @@ class CsMajorAutochess:
|
||||
"ai_team_joined",
|
||||
text,
|
||||
{
|
||||
"person_key": AI_PERSON_KEY,
|
||||
"seed": 4,
|
||||
"person_keys": [
|
||||
ai_key for ai_key, _ in ai_teams
|
||||
],
|
||||
"seeds": list(
|
||||
range(
|
||||
len(human_keys) + 1,
|
||||
len(human_keys) + len(ai_teams) + 1,
|
||||
)
|
||||
),
|
||||
"variant": variant,
|
||||
"test_mode": True,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-sdk"
|
||||
version = "0.6.4a0+1015"
|
||||
version = "0.6.5a0+1015"
|
||||
description = "Typed, capability-limited extension API for JWeBot plugins"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = []
|
||||
|
||||
@@ -38,4 +38,4 @@ __all__ = [
|
||||
"SingleGroupContext",
|
||||
]
|
||||
|
||||
__version__ = "0.6.4a0+1015"
|
||||
__version__ = "0.6.5a0+1015"
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
[tool.jwebot]
|
||||
release = "alpha 0.6.4.1015"
|
||||
release = "alpha 0.6.5.1015"
|
||||
production_deployments_enabled_by_default = false
|
||||
|
||||
[tool.jwebot.workspace]
|
||||
|
||||
@@ -374,11 +374,11 @@ class Release064AutochessTests(unittest.TestCase):
|
||||
|
||||
def test_three_humans_plus_ai_has_taffy_host_and_turn_handoffs(self):
|
||||
self.command("/自走棋开始")
|
||||
self.command("/自走棋报名", sender="Bob")
|
||||
self.assertIn(
|
||||
"先有且仅有 3 名真人",
|
||||
"2 名或 3 名真人",
|
||||
self.command("/自走棋加AI战队"),
|
||||
)
|
||||
self.command("/自走棋报名", sender="Bob")
|
||||
self.command("/自走棋报名", sender="Carol")
|
||||
self.assertIn(
|
||||
"只有主持人",
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
"""Behavioral coverage for the two-human plus two-AI test mode."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
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 Release065TwoHumanAiTests(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
|
||||
return self.engine.local.handle(
|
||||
{
|
||||
"question": question,
|
||||
"sender": sender,
|
||||
"group_fingerprint": "group-a",
|
||||
"event_id": event_id or f"two-ai-{self.counter}",
|
||||
},
|
||||
{},
|
||||
)
|
||||
|
||||
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 test_two_humans_add_two_ai_and_use_four_seed_bracket(self):
|
||||
self.command("/自走棋开始")
|
||||
self.command("/自走棋报名", sender="Bob")
|
||||
added = self.command("/自走棋加AI战队")
|
||||
self.assertIn("AI 战队 A、AI 战队 B", added)
|
||||
self.assertIn("第 3、4 种子", added)
|
||||
self.assertIn("2 人+2 AI", added)
|
||||
self.assertIn(
|
||||
"已经锁定参赛名单",
|
||||
self.command("/自走棋报名", sender="Carol"),
|
||||
)
|
||||
|
||||
launch = self.command("/自走棋开赛")
|
||||
state = self.state()
|
||||
self.assertIn("2 人+2 AI 测试赛", launch)
|
||||
self.assertEqual(state["ai_test_variant"], "two_plus_two")
|
||||
self.assertEqual(
|
||||
state["seed_order"][2:],
|
||||
[
|
||||
AUTOCHESS.AI_PERSON_KEY,
|
||||
AUTOCHESS.AI_SECOND_PERSON_KEY,
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
state["preparation_order"],
|
||||
state["seed_order"][:2],
|
||||
)
|
||||
self.assertTrue(
|
||||
state["players"][AUTOCHESS.AI_PERSON_KEY]["is_ai"]
|
||||
)
|
||||
self.assertTrue(
|
||||
state["players"][AUTOCHESS.AI_SECOND_PERSON_KEY]["is_ai"]
|
||||
)
|
||||
semifinal = state["round_plan"][-2]
|
||||
self.assertEqual(
|
||||
semifinal["pairs"],
|
||||
[
|
||||
[state["seed_order"][0], state["seed_order"][3]],
|
||||
[state["seed_order"][1], state["seed_order"][2]],
|
||||
],
|
||||
)
|
||||
|
||||
self.assertIn("到 bob 做准备了", self.command("/结束回合"))
|
||||
self.assertIn(
|
||||
"全员准备完毕",
|
||||
self.command("/结束回合", sender="Bob"),
|
||||
)
|
||||
settled = self.command("/自走棋结算")
|
||||
self.assertIn("AI 战队 A 自动运营", settled)
|
||||
self.assertIn("AI 战队 B 自动运营", settled)
|
||||
self.assertEqual(
|
||||
self.engine.ledger.connection.execute(
|
||||
"""
|
||||
SELECT COUNT(*) FROM autochess_story_logs
|
||||
WHERE story_type = 'ai_operation'
|
||||
"""
|
||||
).fetchone()[0],
|
||||
2,
|
||||
)
|
||||
|
||||
def test_add_two_ai_is_idempotent_and_cannot_be_repeated(self):
|
||||
self.command("/自走棋开始")
|
||||
self.command("/自走棋报名", sender="Bob")
|
||||
first = self.command(
|
||||
"/自走棋加AI战队",
|
||||
event_id="same-add-two-ai",
|
||||
)
|
||||
replay = self.command(
|
||||
"/自走棋加AI战队",
|
||||
event_id="same-add-two-ai",
|
||||
)
|
||||
self.assertEqual(first, replay)
|
||||
self.assertIn(
|
||||
"已经加入",
|
||||
self.command("/自走棋加AI战队"),
|
||||
)
|
||||
state = self.state()
|
||||
self.assertEqual(
|
||||
sum(
|
||||
bool(player.get("is_ai"))
|
||||
for player in state["players"].values()
|
||||
),
|
||||
2,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user