nonebot-plugin-werewolf 1.1.2__py3-none-any.whl → 1.1.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. nonebot_plugin_werewolf/__init__.py +9 -4
  2. nonebot_plugin_werewolf/config.py +11 -15
  3. nonebot_plugin_werewolf/constant.py +40 -3
  4. nonebot_plugin_werewolf/game.py +219 -181
  5. nonebot_plugin_werewolf/matchers/__init__.py +2 -0
  6. nonebot_plugin_werewolf/matchers/depends.py +38 -0
  7. nonebot_plugin_werewolf/matchers/message_in_game.py +25 -0
  8. nonebot_plugin_werewolf/matchers/poke/__init__.py +8 -0
  9. nonebot_plugin_werewolf/matchers/poke/chronocat_poke.py +117 -0
  10. nonebot_plugin_werewolf/matchers/poke/ob11_poke.py +80 -0
  11. nonebot_plugin_werewolf/matchers/start_game.py +202 -0
  12. nonebot_plugin_werewolf/player_set.py +37 -36
  13. nonebot_plugin_werewolf/players/__init__.py +10 -0
  14. nonebot_plugin_werewolf/players/can_shoot.py +53 -0
  15. nonebot_plugin_werewolf/players/civilian.py +7 -0
  16. nonebot_plugin_werewolf/players/guard.py +30 -0
  17. nonebot_plugin_werewolf/players/hunter.py +8 -0
  18. nonebot_plugin_werewolf/players/idiot.py +44 -0
  19. nonebot_plugin_werewolf/players/joker.py +21 -0
  20. nonebot_plugin_werewolf/players/player.py +233 -0
  21. nonebot_plugin_werewolf/players/prophet.py +22 -0
  22. nonebot_plugin_werewolf/players/werewolf.py +89 -0
  23. nonebot_plugin_werewolf/players/witch.py +66 -0
  24. nonebot_plugin_werewolf/players/wolfking.py +14 -0
  25. nonebot_plugin_werewolf/utils.py +58 -173
  26. {nonebot_plugin_werewolf-1.1.2.dist-info → nonebot_plugin_werewolf-1.1.5.dist-info}/METADATA +24 -4
  27. nonebot_plugin_werewolf-1.1.5.dist-info/RECORD +31 -0
  28. {nonebot_plugin_werewolf-1.1.2.dist-info → nonebot_plugin_werewolf-1.1.5.dist-info}/WHEEL +1 -1
  29. nonebot_plugin_werewolf/_timeout.py +0 -110
  30. nonebot_plugin_werewolf/matchers.py +0 -62
  31. nonebot_plugin_werewolf/ob11_ext.py +0 -72
  32. nonebot_plugin_werewolf/player.py +0 -462
  33. nonebot_plugin_werewolf-1.1.2.dist-info/RECORD +0 -16
  34. {nonebot_plugin_werewolf-1.1.2.dist-info → nonebot_plugin_werewolf-1.1.5.dist-info}/LICENSE +0 -0
  35. {nonebot_plugin_werewolf-1.1.2.dist-info → nonebot_plugin_werewolf-1.1.5.dist-info}/top_level.txt +0 -0
@@ -1,16 +1,16 @@
1
- import asyncio
2
- import re
1
+ import functools
3
2
  from collections import defaultdict
4
- from typing import Annotated, Any, ClassVar
3
+ from typing import TYPE_CHECKING, Any, ClassVar, TypeVar
5
4
 
6
- import nonebot_plugin_waiter as waiter
7
- from nonebot.adapters import Event
8
- from nonebot.log import logger
9
- from nonebot.rule import to_me
10
- from nonebot_plugin_alconna import MsgTarget, UniMessage, UniMsg
11
- from nonebot_plugin_userinfo import EventUserInfo, UserInfo
5
+ import anyio
6
+ from nonebot_plugin_alconna import UniMessage
7
+ from nonebot_plugin_uninfo import Session
12
8
 
13
- from .config import config
9
+ if TYPE_CHECKING:
10
+ from .player_set import PlayerSet
11
+ from .players import Player
12
+
13
+ T = TypeVar("T")
14
14
 
15
15
 
16
16
  def check_index(text: str, arrlen: int) -> int | None:
@@ -21,181 +21,66 @@ def check_index(text: str, arrlen: int) -> int | None:
21
21
  return None
22
22
 
23
23
 
24
- class InputStore:
25
- locks: ClassVar[dict[str, asyncio.Lock]] = defaultdict(asyncio.Lock)
26
- futures: ClassVar[dict[str, asyncio.Future[UniMessage]]] = {}
24
+ def link(text: str, url: str | None) -> str:
25
+ return text if url is None else f"\u001b]8;;{url}\u0007{text}\u001b]8;;\u0007"
27
26
 
28
- @classmethod
29
- async def fetch(cls, user_id: str, group_id: str | None = None) -> UniMessage[Any]:
30
- key = f"{group_id}_{user_id}"
31
- async with cls.locks[key]:
32
- cls.futures[key] = asyncio.get_event_loop().create_future()
33
- try:
34
- return await cls.futures[key]
35
- finally:
36
- del cls.futures[key]
37
27
 
38
- @classmethod
39
- def put(cls, user_id: str, group_id: str | None, msg: UniMessage) -> None:
40
- key = f"{group_id}_{user_id}"
41
- if future := cls.futures.get(key):
42
- future.set_result(msg)
28
+ def extract_session_member_nick(session: Session) -> str | None:
29
+ return (
30
+ (session.member and session.member.nick)
31
+ or session.user.nick
32
+ or session.user.name
33
+ )
34
+
43
35
 
36
+ class _InputTask:
37
+ _event: anyio.Event
38
+ _msg: UniMessage
44
39
 
45
- def user_in_game(user_id: str, group_id: str | None) -> bool:
46
- from .game import running_games
40
+ def __init__(self) -> None:
41
+ self._event = anyio.Event()
47
42
 
48
- if group_id is not None and group_id not in running_games:
49
- return False
50
- games = running_games.values() if group_id is None else [running_games[group_id]]
51
- for game in games:
52
- return any(user_id == player.user_id for player in game.players)
53
- return False
43
+ def set(self, msg: UniMessage) -> None:
44
+ self._msg = msg
45
+ self._event.set()
54
46
 
47
+ async def wait(self) -> UniMessage:
48
+ await self._event.wait()
49
+ return self._msg
55
50
 
56
- async def rule_in_game(event: Event, target: MsgTarget) -> bool:
57
- from .game import running_games
58
51
 
59
- if not running_games:
60
- return False
61
- if target.private:
62
- return user_in_game(target.id, None)
63
- if target.id in running_games:
64
- return user_in_game(event.get_user_id(), target.id)
65
- return False
52
+ class InputStore:
53
+ locks: ClassVar[dict[str, anyio.Lock]] = defaultdict(anyio.Lock)
54
+ tasks: ClassVar[dict[str, _InputTask]] = {}
55
+
56
+ @classmethod
57
+ async def fetch(cls, user_id: str, group_id: str | None = None) -> UniMessage[Any]:
58
+ key = f"{group_id}_{user_id}"
59
+ async with cls.locks[key]:
60
+ cls.tasks[key] = task = _InputTask()
61
+ return await task.wait()
66
62
 
63
+ @classmethod
64
+ def put(cls, msg: UniMessage, user_id: str, group_id: str | None = None) -> None:
65
+ key = f"{group_id}_{user_id}"
66
+ if task := cls.tasks.pop(key, None):
67
+ task.set(msg)
67
68
 
68
- async def rule_not_in_game(event: Event, target: MsgTarget) -> bool:
69
- return not await rule_in_game(event, target)
69
+ @classmethod
70
+ def cleanup(cls, players: list[str], group_id: str) -> None:
71
+ for key in (f"{g}_{p}" for p in players for g in (group_id, None)):
72
+ if key in cls.locks:
73
+ del cls.locks[key]
74
+ if key in cls.tasks:
75
+ del cls.tasks[key]
70
76
 
71
77
 
72
- async def is_group(target: MsgTarget) -> bool:
73
- return not target.private
78
+ @functools.cache
79
+ def cached_player_set() -> type["PlayerSet"]:
80
+ from .player_set import PlayerSet
74
81
 
82
+ return PlayerSet
75
83
 
76
- async def _prepare_game_receive(
77
- queue: asyncio.Queue[tuple[str, str, str]],
78
- event: Event,
79
- group_id: str,
80
- ) -> None:
81
- async def rule(target_: MsgTarget) -> bool:
82
- return not target_.private and target_.id == group_id
83
84
 
84
- @waiter.waiter(
85
- waits=[event.get_type()],
86
- keep_session=False,
87
- rule=to_me() & rule & rule_not_in_game,
88
- )
89
- def wait(
90
- event: Event,
91
- info: Annotated[UserInfo | None, EventUserInfo()],
92
- msg: UniMsg,
93
- ) -> tuple[str, str, str]:
94
- return (
95
- event.get_user_id(),
96
- (
97
- (info.user_displayname or info.user_name)
98
- if info is not None
99
- else event.get_user_id()
100
- ),
101
- msg.extract_plain_text().strip(),
102
- )
103
-
104
- async for user, name, text in wait(default=(None, "", "")):
105
- if user is None:
106
- continue
107
- await queue.put((user, re.sub(r"[\u2066-\u2069]", "", name), text))
108
-
109
-
110
- async def _prepare_game_handle(
111
- queue: asyncio.Queue[tuple[str, str, str]],
112
- players: dict[str, str],
113
- admin_id: str,
114
- ) -> None:
115
- log = logger.opt(colors=True)
116
-
117
- while True:
118
- user, name, text = await queue.get()
119
- msg = UniMessage.at(user)
120
- colored = f"<y>{name}</y>(<c>{user}</c>)"
121
-
122
- match (text, user == admin_id):
123
- case ("开始游戏", True):
124
- player_num = len(players)
125
- role_preset = config.get_role_preset()
126
- if player_num < min(role_preset):
127
- await (
128
- msg.text(f"游戏至少需要 {min(role_preset)} 人, ")
129
- .text(f"当前已有 {player_num} 人")
130
- .send()
131
- )
132
- elif player_num > max(role_preset):
133
- await (
134
- msg.text(f"游戏最多需要 {max(role_preset)} 人, ")
135
- .text(f"当前已有 {player_num} 人")
136
- .send()
137
- )
138
- elif player_num not in role_preset:
139
- await (
140
- msg.text(f"不存在总人数为 {player_num} 的预设, ")
141
- .text("无法开始游戏")
142
- .send()
143
- )
144
- else:
145
- await msg.text("游戏即将开始...").send()
146
- log.info(f"游戏发起者 {colored} 开始游戏")
147
- return
148
-
149
- case ("开始游戏", False):
150
- await msg.text("只有游戏发起者可以开始游戏").send()
151
-
152
- case ("结束游戏", True):
153
- log.info(f"游戏发起者 {colored} 结束游戏")
154
- await msg.text("已结束当前游戏").finish()
155
-
156
- case ("结束游戏", False):
157
- await msg.text("只有游戏发起者可以结束游戏").send()
158
-
159
- case ("加入游戏", True):
160
- await msg.text("游戏发起者已经加入游戏了").send()
161
-
162
- case ("加入游戏", False):
163
- if user not in players:
164
- players[user] = name
165
- log.info(f"玩家 {colored} 加入游戏")
166
- await msg.text("成功加入游戏").send()
167
- else:
168
- await msg.text("你已经加入游戏了").send()
169
-
170
- case ("退出游戏", True):
171
- await msg.text("游戏发起者无法退出游戏").send()
172
-
173
- case ("退出游戏", False):
174
- if user in players:
175
- del players[user]
176
- log.info(f"玩家 {colored} 退出游戏")
177
- await msg.text("成功退出游戏").send()
178
- else:
179
- await msg.text("你还没有加入游戏").send()
180
-
181
- case ("当前玩家", _):
182
- msg.text("\n当前玩家:\n")
183
- for idx, name in enumerate(players.values(), 1):
184
- msg.text(f"\n{idx}. {name}")
185
- await msg.send()
186
-
187
-
188
- async def prepare_game(event: Event, players: dict[str, str]) -> None:
189
- from .game import starting_games
190
-
191
- group_id = UniMessage.get_target(event).id
192
- starting_games[group_id] = players
193
-
194
- queue: asyncio.Queue[tuple[str, str, str]] = asyncio.Queue()
195
- task_receive = asyncio.create_task(_prepare_game_receive(queue, event, group_id))
196
-
197
- try:
198
- await _prepare_game_handle(queue, players, event.get_user_id())
199
- finally:
200
- task_receive.cancel()
201
- del starting_games[group_id]
85
+ def as_player_set(*player: "Player") -> "PlayerSet":
86
+ return cached_player_set()(player)
@@ -1,16 +1,20 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nonebot-plugin-werewolf
3
- Version: 1.1.2
3
+ Version: 1.1.5
4
4
  Summary: 适用于 Nonebot2 的狼人杀插件
5
5
  Author-email: wyf7685 <wyf7685@163.com>
6
6
  License: MIT
7
+ Project-URL: homepage, https://github.com/wyf7685/nonebot-plugin-werewolf
8
+ Project-URL: repository, https://github.com/wyf7685/nonebot-plugin-werewolf
9
+ Project-URL: bug-tracker, https://github.com/wyf7685/nonebot-plugin-werewolf/issues
7
10
  Requires-Python: >=3.10
8
11
  Description-Content-Type: text/markdown
9
12
  License-File: LICENSE
10
13
  Requires-Dist: nonebot2 >=2.3.3
11
14
  Requires-Dist: nonebot-plugin-alconna >=0.52.1
12
- Requires-Dist: nonebot-plugin-userinfo >=0.2.6
15
+ Requires-Dist: nonebot-plugin-uninfo >=0.4.0
13
16
  Requires-Dist: nonebot-plugin-waiter >=0.7.1
17
+ Requires-Dist: anyio >=4.6.0
14
18
 
15
19
  <div align="center">
16
20
  <a href="https://v2.nonebot.dev/store">
@@ -39,6 +43,7 @@ _✨ 简单的狼人杀插件 ✨_
39
43
  [![pyright](https://github.com/wyf7685/nonebot-plugin-werewolf/actions/workflows/pyright.yml/badge.svg?branch=master&event=push)](https://github.com/wyf7685/nonebot-plugin-werewolf/actions/workflows/pyright.yml)
40
44
  [![publish](https://github.com/wyf7685/nonebot-plugin-werewolf/actions/workflows/pypi-publish.yml/badge.svg)](https://github.com/wyf7685/nonebot-plugin-werewolf/actions/workflows/pypi-publish.yml)
41
45
 
46
+ <!-- https://github.com/lgc2333/nonebot-registry-badge -->
42
47
  [![NoneBot Registry](https://img.shields.io/endpoint?url=https%3A%2F%2Fnbbdg.lgc2333.top%2Fplugin%2Fnonebot-plugin-werewolf)](https://registry.nonebot.dev/plugin/nonebot-plugin-werewolf:nonebot_plugin_werewolf)
43
48
  [![Supported Adapters](https://img.shields.io/endpoint?url=https%3A%2F%2Fnbbdg.lgc2333.top%2Fplugin-adapters%2Fnonebot-plugin-werewolf)](https://registry.nonebot.dev/plugin/nonebot-plugin-werewolf:nonebot_plugin_werewolf)
44
49
 
@@ -145,7 +150,7 @@ _✨ 简单的狼人杀插件 ✨_
145
150
 
146
151
  _其他交互参考游戏内提示_
147
152
 
148
- 对于 `OneBot V11` 适配器, 启用配置项 `werewolf__enable_poke` 后, 可以使用戳一戳代替 _准备阶段_ 的 `加入游戏` 操作 和 游戏内的 `/stop` 命令
153
+ 对于 `OneBot V11` 适配器和 `Satori` 适配器的 `chronocat`, 启用配置项 `werewolf__enable_poke` 后, 可以使用戳一戳代替 _准备阶段_ 的 `加入游戏` 操作 和 游戏内的 `stop` 命令
149
154
 
150
155
  ### 游戏内容
151
156
 
@@ -233,6 +238,10 @@ werewolf__priesthood_proirity=[11, 12, 13, 14, 15]
233
238
 
234
239
  </details>
235
240
 
241
+ ### 已知问题
242
+
243
+ - 截止 chronocat v0.2.19, 调用 [`guild.member.get`](https://github.com/chrononeko/chronocat/blob/8558ad9ff4319395d86abbfda22136939bf66780/packages/engine-chronocat-api/src/api/guild/member/get.ts) / [`user.get`](https://github.com/chrononeko/chronocat/blob/8558ad9ff4319395d86abbfda22136939bf66780/packages/engine-chronocat-api/src/api/user/get.ts) 均无法获取用户名,这将导致在交互过程中的玩家名显示为用户ID
244
+
236
245
  ## 📝 更新日志
237
246
 
238
247
  <details>
@@ -240,6 +249,17 @@ werewolf__priesthood_proirity=[11, 12, 13, 14, 15]
240
249
 
241
250
  <!-- CHANGELOG -->
242
251
 
252
+ - 2024.10.23 v1.1.5
253
+
254
+ - 添加对 chronocat:poke 的支持
255
+ - 游戏内 stop 命令使用 COMMAND_START
256
+ - 使用 `anyio` 重写并发逻辑
257
+
258
+ - 2024.10.06 v1.1.3
259
+
260
+ - 使用 `RF-Tar-Railt/nonebot-plugin-uninfo` 获取用户数据
261
+ - 优化交互文本
262
+
243
263
  - 2024.09.18 v1.1.2
244
264
 
245
265
  - 修改 Python 需求为 `>=3.10`
@@ -284,6 +304,6 @@ werewolf__priesthood_proirity=[11, 12, 13, 14, 15]
284
304
 
285
305
  - [`nonebot/nonebot2`](https://github.com/nonebot/nonebot2): 跨平台 Python 异步机器人框架
286
306
  - [`nonebot/plugin-alconna`](https://github.com/nonebot/plugin-alconna): 跨平台的消息处理接口
287
- - [`noneplugin/nonebot-plugin-userinfo`](https://github.com/noneplugin/nonebot-plugin-userinfo): 用户信息获取
307
+ - [`RF-Tar-Railt/nonebot-plugin-uninfo`](https://github.com/RF-Tar-Railt/nonebot-plugin-uninfo): 用户信息获取
288
308
  - [`RF-Tar-Railt/nonebot-plugin-waiter`](https://github.com/RF-Tar-Railt/nonebot-plugin-waiter): 灵活获取用户输入
289
309
  - `热心群友`: 协助测试插件
@@ -0,0 +1,31 @@
1
+ nonebot_plugin_werewolf/__init__.py,sha256=v65nKZbsbmaJPR0o2ZrXuumUvCuEOvDVtVmw65CyVn8,894
2
+ nonebot_plugin_werewolf/config.py,sha256=FKQDkb57ujcBYJZX-sLgxWmTVqSeR7T1ywphp7GOCcE,2803
3
+ nonebot_plugin_werewolf/constant.py,sha256=d4Awy96YBe_AJoAZyd_6Wojo0qi2BTVM_LW-nDbsxQE,2902
4
+ nonebot_plugin_werewolf/exception.py,sha256=YSwxeogIB0YJqH9MP1bgxojiu-I_xQE44XnSk5bC1AQ,400
5
+ nonebot_plugin_werewolf/game.py,sha256=yUfJtLWH7--TRPnofAGfrUkUvd_3VW1J9j8zhTrN9hc,19106
6
+ nonebot_plugin_werewolf/player_set.py,sha256=J5KFWSugpu1Zd_gToqa-Gweoikj8kppLpckVjl-4XH0,2540
7
+ nonebot_plugin_werewolf/utils.py,sha256=6uFwgf4LAI1SZ_qH6I2ilHt2vbs6NfTj5YhMvC4GacE,2279
8
+ nonebot_plugin_werewolf/matchers/__init__.py,sha256=_MwAZsXlpBLXyzHWqNLTQdMWw9z_O01L5Yo02dzGC9I,88
9
+ nonebot_plugin_werewolf/matchers/depends.py,sha256=6JFLxQDgRjStbMkpoWe1tYZHs0wVfsyn4KikKNpgx0Q,1160
10
+ nonebot_plugin_werewolf/matchers/message_in_game.py,sha256=YkphERN2efaB0VdU3oTCvvYptrqDaKFKUb9moUyqIpY,780
11
+ nonebot_plugin_werewolf/matchers/start_game.py,sha256=ZNO2ypV8kMlImR0fZVhg0ZI8mrOsW3I-eioT6Lhf2Cw,7427
12
+ nonebot_plugin_werewolf/matchers/poke/__init__.py,sha256=gYysvGjztN3iDQpX6v5nkPT195FXnk7fqP9kzByTES0,220
13
+ nonebot_plugin_werewolf/matchers/poke/chronocat_poke.py,sha256=SrFN8dQELFVDR5US9sOcE2gaOnH7AFiRVVK2RNa-Y5o,4023
14
+ nonebot_plugin_werewolf/matchers/poke/ob11_poke.py,sha256=LROxtbauw4xbB8UKglsx6b6hkKWs_17HmgK4NTXW1HY,2640
15
+ nonebot_plugin_werewolf/players/__init__.py,sha256=djAI5XxR2I-XvnH-lVqX_YCHB_AiT-6jdmwFE1ffN_0,379
16
+ nonebot_plugin_werewolf/players/can_shoot.py,sha256=e9QtRUNxNZ9n3MxX7o0bUmQLLPDazTOZky3F_2m6UhM,1813
17
+ nonebot_plugin_werewolf/players/civilian.py,sha256=TbrIxjG4g74C9Cd_F3OskuM-ksKUkOLiE6OgPqmo0pA,157
18
+ nonebot_plugin_werewolf/players/guard.py,sha256=T2L7f0Dcx6jjlOrGcHUQxy9RawszvP6AKaI8Lz9nItI,1112
19
+ nonebot_plugin_werewolf/players/hunter.py,sha256=NaJNjngKCHC8RI1YsYI5XtVyFq9JP5U1ywOTbjjUHq4,195
20
+ nonebot_plugin_werewolf/players/idiot.py,sha256=Tpjf7RPW1dJ3J4ajr5HWcIustHO-h6PNicum4SiofDE,1433
21
+ nonebot_plugin_werewolf/players/joker.py,sha256=J_t-IPKjyHWq8azxFdN_cmJhbgoDcokiMLf-pMXQInE,691
22
+ nonebot_plugin_werewolf/players/player.py,sha256=JUCev-3jnAqApNR-cFaHRHkQVIgBfn5HhHhLq8OGWgw,6954
23
+ nonebot_plugin_werewolf/players/prophet.py,sha256=VQa4RJUbLpG9ntB055ORl09oEAmZ1uOUi4P4q_P_9TY,897
24
+ nonebot_plugin_werewolf/players/werewolf.py,sha256=MZOsHWNnKvh_fxKMEIWqa_yJIYHVjA1fM5W3jkg_CtU,3433
25
+ nonebot_plugin_werewolf/players/witch.py,sha256=XDCVSXhUA9afh0iCdxV8Shfc1YGDkXYhu1h0v1xpbmg,2340
26
+ nonebot_plugin_werewolf/players/wolfking.py,sha256=oBC5JW0UULDkthurDBvHRNo3rqArm-zQ_1jSuyeHpQU,440
27
+ nonebot_plugin_werewolf-1.1.5.dist-info/LICENSE,sha256=B_WbEqjGr6GYVNfEJPY31T1Opik7OtgOkhRs4Ig3e2M,1064
28
+ nonebot_plugin_werewolf-1.1.5.dist-info/METADATA,sha256=j-vwRZiK1lobgZ10-X30Upf6Vuo1ZYMraWgQvStKjvI,11435
29
+ nonebot_plugin_werewolf-1.1.5.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
30
+ nonebot_plugin_werewolf-1.1.5.dist-info/top_level.txt,sha256=wLTfg8sTKbH9lLT9LtU118C9cTspEBJareLsrYM52YA,24
31
+ nonebot_plugin_werewolf-1.1.5.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.1.0)
2
+ Generator: setuptools (75.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,110 +0,0 @@
1
- import asyncio
2
- import enum
3
- import sys
4
- from types import TracebackType
5
- from typing import final
6
-
7
- if sys.version_info >= (3, 11):
8
- from asyncio.timeouts import timeout as timeout
9
-
10
- else:
11
- # ruff: noqa: S101
12
-
13
- class _State(enum.Enum):
14
- CREATED = "created"
15
- ENTERED = "active"
16
- EXPIRING = "expiring"
17
- EXPIRED = "expired"
18
- EXITED = "finished"
19
-
20
- @final
21
- class Timeout:
22
- def __init__(self, when: float | None) -> None:
23
- self._state = _State.CREATED
24
- self._timeout_handler: asyncio.Handle | None = None
25
- self._task: asyncio.Task | None = None
26
- if when is not None:
27
- when = asyncio.get_running_loop().time() + when
28
- self._when = when
29
-
30
- def when(self) -> float | None:
31
- return self._when
32
-
33
- def reschedule(self, when: float | None) -> None:
34
- if self._state is not _State.ENTERED:
35
- if self._state is _State.CREATED:
36
- raise RuntimeError("Timeout has not been entered")
37
- raise RuntimeError(
38
- f"Cannot change state of {self._state.value} Timeout",
39
- )
40
-
41
- self._when = when
42
-
43
- if self._timeout_handler is not None:
44
- self._timeout_handler.cancel()
45
-
46
- if when is None:
47
- self._timeout_handler = None
48
- else:
49
- loop = asyncio.get_running_loop()
50
- if when <= loop.time():
51
- self._timeout_handler = loop.call_soon(self._on_timeout)
52
- else:
53
- self._timeout_handler = loop.call_at(when, self._on_timeout)
54
-
55
- def expired(self) -> bool:
56
- return self._state in (_State.EXPIRING, _State.EXPIRED)
57
-
58
- def __repr__(self) -> str:
59
- info = [""]
60
- if self._state is _State.ENTERED:
61
- when = round(self._when, 3) if self._when is not None else None
62
- info.append(f"when={when}")
63
- info_str = " ".join(info)
64
- return f"<Timeout [{self._state.value}]{info_str}>"
65
-
66
- async def __aenter__(self) -> "Timeout":
67
- if self._state is not _State.CREATED:
68
- raise RuntimeError("Timeout has already been entered")
69
- task = asyncio.current_task()
70
- if task is None:
71
- raise RuntimeError("Timeout should be used inside a task")
72
- self._state = _State.ENTERED
73
- self._task = task
74
- self.reschedule(self._when)
75
- return self
76
-
77
- async def __aexit__(
78
- self,
79
- exc_type: type[BaseException] | None,
80
- exc_val: BaseException | None,
81
- exc_tb: TracebackType | None,
82
- ) -> bool | None:
83
- assert self._state in (_State.ENTERED, _State.EXPIRING)
84
-
85
- if self._timeout_handler is not None:
86
- self._timeout_handler.cancel()
87
- self._timeout_handler = None
88
-
89
- if self._state is _State.EXPIRING:
90
- self._state = _State.EXPIRED
91
-
92
- if exc_type is asyncio.CancelledError:
93
- raise TimeoutError from exc_val
94
- elif self._state is _State.ENTERED:
95
- self._state = _State.EXITED
96
-
97
- return None
98
-
99
- def _on_timeout(self) -> None:
100
- assert self._state is _State.ENTERED
101
- assert self._task is not None
102
- self._task.cancel()
103
- self._state = _State.EXPIRING
104
- self._timeout_handler = None
105
-
106
- def timeout(delay: float | None) -> Timeout:
107
- return Timeout(delay)
108
-
109
-
110
- __all__ = ["timeout"]
@@ -1,62 +0,0 @@
1
- from typing import Annotated
2
-
3
- from nonebot import on_command, on_message
4
- from nonebot.adapters import Bot, Event
5
- from nonebot.exception import FinishedException
6
- from nonebot.rule import to_me
7
- from nonebot_plugin_alconna import MsgTarget, UniMessage, UniMsg
8
- from nonebot_plugin_userinfo import EventUserInfo, UserInfo
9
-
10
- from ._timeout import timeout
11
- from .game import Game
12
- from .ob11_ext import ob11_ext_enabled
13
- from .utils import InputStore, is_group, prepare_game, rule_in_game, rule_not_in_game
14
-
15
- in_game_message = on_message(rule=rule_in_game)
16
- start_game = on_command(
17
- "werewolf",
18
- rule=to_me() & is_group & rule_not_in_game,
19
- aliases={"狼人杀"},
20
- )
21
-
22
-
23
- @in_game_message.handle()
24
- async def handle_input(event: Event, target: MsgTarget, msg: UniMsg) -> None:
25
- if target.private:
26
- InputStore.put(target.id, None, msg)
27
- else:
28
- InputStore.put(event.get_user_id(), target.id, msg)
29
-
30
-
31
- @start_game.handle()
32
- async def handle_start(
33
- bot: Bot,
34
- event: Event,
35
- target: MsgTarget,
36
- admin_info: Annotated[UserInfo, EventUserInfo()],
37
- ) -> None:
38
- admin_id = event.get_user_id()
39
- msg = (
40
- UniMessage.at(admin_id)
41
- .text("成功创建游戏\n")
42
- .text("玩家请 @我 发送 “加入游戏”、“退出游戏”\n")
43
- .text("玩家 @我 发送 “当前玩家” 可查看玩家列表\n")
44
- .text("游戏发起者 @我 发送 “结束游戏” 可结束当前游戏\n")
45
- .text("玩家均加入后,游戏发起者请 @我 发送 “开始游戏”\n")
46
- )
47
- if ob11_ext_enabled():
48
- msg.text("\n可使用戳一戳代替游戏交互中的 “/stop” 命令")
49
- await msg.text("\n\n游戏准备阶段限时5分钟,超时将自动结束").send()
50
-
51
- players = {admin_id: admin_info.user_name}
52
-
53
- try:
54
- async with timeout(5 * 60):
55
- await prepare_game(event, players)
56
- except FinishedException:
57
- raise
58
- except TimeoutError:
59
- await UniMessage.text("游戏准备超时,已自动结束").finish()
60
-
61
- game = Game(bot, target, players)
62
- game.start()
@@ -1,72 +0,0 @@
1
- import contextlib
2
-
3
- from nonebot import on_type
4
- from nonebot.internal.matcher import current_bot
5
- from nonebot_plugin_alconna import UniMessage
6
-
7
- from .config import config
8
- from .game import starting_games
9
- from .utils import InputStore, user_in_game
10
-
11
-
12
- def ob11_ext_enabled() -> bool:
13
- return False
14
-
15
-
16
- with contextlib.suppress(ImportError):
17
- from nonebot.adapters.onebot.v11 import Bot, MessageSegment
18
- from nonebot.adapters.onebot.v11.event import PokeNotifyEvent
19
-
20
- # 游戏内戳一戳等效 "/stop"
21
- async def _rule_poke_1(event: PokeNotifyEvent) -> bool:
22
- if not config.enable_poke:
23
- return False
24
-
25
- user_id = str(event.user_id)
26
- group_id = str(event.group_id) if event.group_id is not None else None
27
- return (
28
- config.enable_poke
29
- and (event.target_id == event.self_id)
30
- and user_in_game(user_id, group_id)
31
- )
32
-
33
- @on_type(PokeNotifyEvent, rule=_rule_poke_1).handle()
34
- async def handle_poke_1(event: PokeNotifyEvent) -> None:
35
- InputStore.put(
36
- user_id=str(event.user_id),
37
- group_id=str(event.group_id) if event.group_id is not None else None,
38
- msg=UniMessage.text("/stop"),
39
- )
40
-
41
- # 准备阶段戳一戳等效加入游戏
42
- async def _rule_poke_2(event: PokeNotifyEvent) -> bool:
43
- if not config.enable_poke or event.group_id is None:
44
- return False
45
-
46
- user_id = str(event.user_id)
47
- group_id = str(event.group_id)
48
- return (
49
- (event.target_id == event.self_id)
50
- and not user_in_game(user_id, group_id)
51
- and group_id in starting_games
52
- )
53
-
54
- @on_type(PokeNotifyEvent, rule=_rule_poke_2).handle()
55
- async def handle_poke_2(bot: Bot, event: PokeNotifyEvent) -> None:
56
- user_id = str(event.user_id)
57
- group_id = str(event.group_id)
58
- players = starting_games[group_id]
59
-
60
- if user_id not in players:
61
- res: dict[str, str] = await bot.get_group_member_info(
62
- group_id=int(group_id),
63
- user_id=int(user_id),
64
- )
65
- players[user_id] = res.get("card") or res.get("nickname") or user_id
66
- await bot.send(event, MessageSegment.at(user_id) + "成功加入游戏")
67
-
68
- def ob11_ext_enabled() -> bool:
69
- if not config.enable_poke:
70
- return False
71
-
72
- return isinstance(current_bot.get(), Bot)