diff --git a/CHANGELOG.md b/CHANGELOG.md index 74f091e..b4b53f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index a4a7081..e75a31f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/VERSION b/VERSION index 2c012ef..911eb06 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -alpha 0.6.4.1015 +alpha 0.6.5.1015 diff --git a/docs/cs-major-autochess-design.md b/docs/cs-major-autochess-design.md index 122e498..8746500 100644 --- a/docs/cs-major-autochess-design.md +++ b/docs/cs-major-autochess-design.md @@ -33,8 +33,10 @@ - 开局命令由任意玩家发起。 - 其他玩家在报名窗口内加入。 - 建议一局支持 `2~8` 名真人玩家。 -- 主持人可在报名阶段发送 `/自走棋加AI战队`,最多加入一支 AI 战队。 -- 三名真人加一支 AI 时,按四队赛制直接生成 `1v4`、`2v3` 半决赛。 +- 主持人可在报名阶段发送 `/自走棋加AI战队`: + - 两名真人时加入 AI 战队 A、B; + - 三名真人时加入一支 AI 战队。 +- 两种测试模式都补满四队,直接生成 `1v4`、`2v3` 半决赛。 - AI 占一个参赛席位,但不对应群成员,也不调用在线模型。 - AI 每回合以确定性规则买牌、升级教练、调整首发和选择克制战术。 diff --git a/packages/jwebot-config/pyproject.toml b/packages/jwebot-config/pyproject.toml index f78539a..3ce0018 100644 --- a/packages/jwebot-config/pyproject.toml +++ b/packages/jwebot-config/pyproject.toml @@ -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 = [ diff --git a/packages/jwebot-config/src/jwebot_config/__init__.py b/packages/jwebot-config/src/jwebot_config/__init__.py index 394bdbb..b77b468 100644 --- a/packages/jwebot-config/src/jwebot_config/__init__.py +++ b/packages/jwebot-config/src/jwebot_config/__init__.py @@ -36,4 +36,4 @@ __all__ = [ "write_default_profile", ] -__version__ = "0.6.4a0+1015" +__version__ = "0.6.5a0+1015" diff --git a/packages/jwebot-extension-host/pyproject.toml b/packages/jwebot-extension-host/pyproject.toml index 7c310b1..456be8d 100644 --- a/packages/jwebot-extension-host/pyproject.toml +++ b/packages/jwebot-extension-host/pyproject.toml @@ -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] diff --git a/packages/jwebot-extension-host/src/jwebot_extension_host/__init__.py b/packages/jwebot-extension-host/src/jwebot_extension_host/__init__.py index a8d0c52..80fd60c 100644 --- a/packages/jwebot-extension-host/src/jwebot_extension_host/__init__.py +++ b/packages/jwebot-extension-host/src/jwebot_extension_host/__init__.py @@ -16,4 +16,4 @@ __all__ = [ "process_request", ] -__version__ = "0.6.4a0+1015" +__version__ = "0.6.5a0+1015" diff --git a/packages/jwebot-kernel/pyproject.toml b/packages/jwebot-kernel/pyproject.toml index eeb0187..2d33488 100644 --- a/packages/jwebot-kernel/pyproject.toml +++ b/packages/jwebot-kernel/pyproject.toml @@ -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'", ] diff --git a/packages/jwebot-kernel/src/jwebot_kernel/__init__.py b/packages/jwebot-kernel/src/jwebot_kernel/__init__.py index 4b91e1a..02d118f 100644 --- a/packages/jwebot-kernel/src/jwebot_kernel/__init__.py +++ b/packages/jwebot-kernel/src/jwebot_kernel/__init__.py @@ -107,4 +107,4 @@ __all__ = [ "verify_installed_release_signature", ] -__version__ = "0.6.4a0+1015" +__version__ = "0.6.5a0+1015" diff --git a/packages/jwebot-official-extensions/pyproject.toml b/packages/jwebot-official-extensions/pyproject.toml index b33ff86..3698ddf 100644 --- a/packages/jwebot-official-extensions/pyproject.toml +++ b/packages/jwebot-official-extensions/pyproject.toml @@ -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] diff --git a/packages/jwebot-official-extensions/src/jwebot_official_extensions/__init__.py b/packages/jwebot-official-extensions/src/jwebot_official_extensions/__init__.py index b9eb31d..8fcf9b5 100644 --- a/packages/jwebot-official-extensions/src/jwebot_official_extensions/__init__.py +++ b/packages/jwebot-official-extensions/src/jwebot_official_extensions/__init__.py @@ -84,4 +84,4 @@ __all__ = [ "render_feature_list", ] -__version__ = "0.6.4a0+1015" +__version__ = "0.6.5a0+1015" diff --git a/packages/jwebot-official-extensions/src/jwebot_official_extensions/cs_autochess.py b/packages/jwebot-official-extensions/src/jwebot_official_extensions/cs_autochess.py index 1cf71e7..9184570 100644 --- a/packages/jwebot-official-extensions/src/jwebot_official_extensions/cs_autochess.py +++ b/packages/jwebot-official-extensions/src/jwebot_official_extensions/cs_autochess.py @@ -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, - is_ai=True, - ) - state["join_order"].append(AI_PERSON_KEY) + 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_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, }, ) diff --git a/packages/jwebot-sdk/pyproject.toml b/packages/jwebot-sdk/pyproject.toml index 0dc15b2..1cd135d 100644 --- a/packages/jwebot-sdk/pyproject.toml +++ b/packages/jwebot-sdk/pyproject.toml @@ -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 = [] diff --git a/packages/jwebot-sdk/src/jwebot_sdk/__init__.py b/packages/jwebot-sdk/src/jwebot_sdk/__init__.py index 1880bd8..a45fd8b 100644 --- a/packages/jwebot-sdk/src/jwebot_sdk/__init__.py +++ b/packages/jwebot-sdk/src/jwebot_sdk/__init__.py @@ -38,4 +38,4 @@ __all__ = [ "SingleGroupContext", ] -__version__ = "0.6.4a0+1015" +__version__ = "0.6.5a0+1015" diff --git a/pyproject.toml b/pyproject.toml index 1ed1c23..c461a7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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] diff --git a/tests/test_release_064.py b/tests/test_release_064.py index 9687c93..e906afb 100644 --- a/tests/test_release_064.py +++ b/tests/test_release_064.py @@ -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( "只有主持人", diff --git a/tests/test_release_065.py b/tests/test_release_065.py new file mode 100644 index 0000000..49fc7a4 --- /dev/null +++ b/tests/test_release_065.py @@ -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()