nonebot-plugin-werewolf 1.1.5__py3-none-any.whl → 1.1.6__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 (31) hide show
  1. nonebot_plugin_werewolf/__init__.py +2 -1
  2. nonebot_plugin_werewolf/config.py +18 -55
  3. nonebot_plugin_werewolf/constant.py +14 -74
  4. nonebot_plugin_werewolf/exception.py +1 -1
  5. nonebot_plugin_werewolf/game.py +198 -216
  6. nonebot_plugin_werewolf/matchers/__init__.py +2 -0
  7. nonebot_plugin_werewolf/matchers/depends.py +17 -5
  8. nonebot_plugin_werewolf/matchers/edit_preset.py +263 -0
  9. nonebot_plugin_werewolf/matchers/message_in_game.py +8 -3
  10. nonebot_plugin_werewolf/matchers/start_game.py +140 -48
  11. nonebot_plugin_werewolf/matchers/superuser_ops.py +24 -0
  12. nonebot_plugin_werewolf/models.py +73 -0
  13. nonebot_plugin_werewolf/player_set.py +1 -1
  14. nonebot_plugin_werewolf/players/can_shoot.py +4 -3
  15. nonebot_plugin_werewolf/players/civilian.py +1 -1
  16. nonebot_plugin_werewolf/players/guard.py +2 -1
  17. nonebot_plugin_werewolf/players/hunter.py +1 -1
  18. nonebot_plugin_werewolf/players/idiot.py +1 -1
  19. nonebot_plugin_werewolf/players/joker.py +6 -2
  20. nonebot_plugin_werewolf/players/player.py +15 -24
  21. nonebot_plugin_werewolf/players/prophet.py +2 -1
  22. nonebot_plugin_werewolf/players/werewolf.py +16 -14
  23. nonebot_plugin_werewolf/players/witch.py +2 -1
  24. nonebot_plugin_werewolf/players/wolfking.py +1 -1
  25. nonebot_plugin_werewolf/utils.py +69 -5
  26. {nonebot_plugin_werewolf-1.1.5.dist-info → nonebot_plugin_werewolf-1.1.6.dist-info}/METADATA +67 -67
  27. nonebot_plugin_werewolf-1.1.6.dist-info/RECORD +34 -0
  28. {nonebot_plugin_werewolf-1.1.5.dist-info → nonebot_plugin_werewolf-1.1.6.dist-info}/WHEEL +1 -1
  29. nonebot_plugin_werewolf-1.1.5.dist-info/RECORD +0 -31
  30. {nonebot_plugin_werewolf-1.1.5.dist-info → nonebot_plugin_werewolf-1.1.6.dist-info}/LICENSE +0 -0
  31. {nonebot_plugin_werewolf-1.1.5.dist-info → nonebot_plugin_werewolf-1.1.6.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,9 @@
1
+ import contextlib
1
2
  import secrets
2
3
  from typing import ClassVar, NoReturn
3
4
 
4
5
  import anyio
6
+ import anyio.abc
5
7
  from nonebot.adapters import Bot
6
8
  from nonebot.log import logger
7
9
  from nonebot.utils import escape_tag
@@ -10,39 +12,32 @@ from nonebot_plugin_alconna.uniseg.message import Receipt
10
12
  from nonebot_plugin_uninfo import Interface, SceneType
11
13
  from typing_extensions import Self, assert_never
12
14
 
13
- from .config import config
14
- from .constant import (
15
- STOP_COMMAND,
16
- STOP_COMMAND_PROMPT,
17
- GameState,
18
- GameStatus,
19
- KillReason,
20
- Role,
21
- RoleGroup,
22
- role_name_conv,
23
- )
15
+ from .config import PresetData
16
+ from .constant import STOP_COMMAND_PROMPT, game_status_conv, report_text, role_name_conv
24
17
  from .exception import GameFinished
18
+ from .models import GameState, GameStatus, KillInfo, KillReason, Role, RoleGroup
25
19
  from .player_set import PlayerSet
26
20
  from .players import Player
27
- from .utils import InputStore, link
21
+ from .utils import InputStore, ObjectStream, link
28
22
 
29
23
 
30
24
  def init_players(bot: Bot, game: "Game", players: set[str]) -> PlayerSet:
31
25
  logger.opt(colors=True).debug(f"初始化 <c>{game.group.id}</c> 的玩家职业")
32
- role_preset = config.get_role_preset()
33
- if (preset := role_preset.get(len(players))) is None:
26
+ preset_data = PresetData.load()
27
+ if (preset := preset_data.role_preset.get(len(players))) is None:
34
28
  raise ValueError(
35
29
  f"玩家人数不符: "
36
- f"应为 {', '.join(map(str, role_preset))} 人, 传入{len(players)}人"
30
+ f"应为 {', '.join(map(str, preset_data.role_preset))} 人, "
31
+ f"传入{len(players)}人"
37
32
  )
38
33
 
39
34
  w, p, c = preset
40
35
  roles: list[Role] = []
41
- roles.extend(config.werewolf_priority[:w])
42
- roles.extend(config.priesthood_proirity[:p])
36
+ roles.extend(preset_data.werewolf_priority[:w])
37
+ roles.extend(preset_data.priesthood_proirity[:p])
43
38
  roles.extend([Role.Civilian] * c)
44
39
 
45
- if c >= 2 and secrets.randbelow(100) <= config.joker_probability * 100:
40
+ if c >= 2 and secrets.randbelow(100) <= preset_data.joker_probability * 100:
46
41
  roles.remove(Role.Civilian)
47
42
  roles.append(Role.Joker)
48
43
 
@@ -57,6 +52,74 @@ def init_players(bot: Bot, game: "Game", players: set[str]) -> PlayerSet:
57
52
  return player_set
58
53
 
59
54
 
55
+ class DeadChannel:
56
+ players: PlayerSet
57
+ finished: anyio.Event
58
+ counter: dict[str, int]
59
+ stream: ObjectStream[tuple[Player, UniMessage]]
60
+ task_group: anyio.abc.TaskGroup
61
+
62
+ def __init__(self, players: PlayerSet, finished: anyio.Event) -> None:
63
+ self.players = players
64
+ self.finished = finished
65
+ self.counter = {p.user_id: 0 for p in self.players}
66
+ self.stream = ObjectStream[tuple[Player, UniMessage]](16)
67
+
68
+ async def _decrease(self, user_id: str) -> None:
69
+ await anyio.sleep(60)
70
+ self.counter[user_id] -= 1
71
+
72
+ async def _handle_finished(self) -> None:
73
+ await self.finished.wait()
74
+ self.task_group.cancel_scope.cancel()
75
+
76
+ async def _handle_send(self) -> None:
77
+ while True:
78
+ player, msg = await self.stream.recv()
79
+ msg = f"玩家 {player.name}:\n" + msg
80
+ target = self.players.killed().exclude(player)
81
+ try:
82
+ await target.broadcast(msg)
83
+ except Exception as err:
84
+ with contextlib.suppress(Exception):
85
+ await player.send(f"消息转发失败: {err!r}")
86
+
87
+ async def _handle_recv(self, player: Player) -> NoReturn:
88
+ await player.killed.wait()
89
+ user_id = player.user_id
90
+
91
+ await player.send(
92
+ "ℹ️你已加入死者频道,请勿在群组内继续发言\n"
93
+ "私聊发送消息将转发至其他已死亡玩家",
94
+ )
95
+ await (
96
+ self.players.killed()
97
+ .exclude(player)
98
+ .broadcast(f"ℹ️玩家 {player.name} 加入了死者频道")
99
+ )
100
+
101
+ while True:
102
+ msg = await player.receive()
103
+
104
+ # 发言频率限制
105
+ self.counter[user_id] += 1
106
+ if self.counter[user_id] > 8:
107
+ await player.send("❌发言频率超过限制, 该消息被屏蔽")
108
+ continue
109
+
110
+ # 推送消息
111
+ await self.stream.send((player, msg))
112
+ self.task_group.start_soon(self._decrease, user_id)
113
+
114
+ async def run(self) -> NoReturn:
115
+ async with anyio.create_task_group() as tg:
116
+ self.task_group = tg
117
+ tg.start_soon(self._handle_finished)
118
+ tg.start_soon(self._handle_send)
119
+ for p in self.players:
120
+ tg.start_soon(self._handle_recv, p)
121
+
122
+
60
123
  class Game:
61
124
  starting_games: ClassVar[dict[Target, dict[str, str]]] = {}
62
125
  running_games: ClassVar[set[Self]] = set()
@@ -66,7 +129,7 @@ class Game:
66
129
  players: PlayerSet
67
130
  interface: Interface
68
131
  state: GameState
69
- killed_players: list[Player]
132
+ killed_players: list[tuple[str, KillInfo]]
70
133
 
71
134
  def __init__(
72
135
  self,
@@ -83,6 +146,7 @@ class Game:
83
146
  self.killed_players = []
84
147
  self._player_map = {p.user_id: p for p in self.players}
85
148
  self._scene = None
149
+ self._task_group = None
86
150
 
87
151
  async def _fetch_group_scene(self) -> None:
88
152
  scene = await self.interface.get_scene(SceneType.GROUP, self.group.id)
@@ -93,17 +157,10 @@ class Game:
93
157
 
94
158
  @property
95
159
  def colored_name(self) -> str:
96
- name = escape_tag(self.group.id)
97
-
98
- if self._scene is None or self._scene.name is None:
99
- name = f"<b><e>{name}</e></b>"
100
- else:
101
- name = f"<y>{escape_tag(self._scene.name)}</y>(<b><e>{name}</e></b>)"
102
-
103
- if self._scene is not None and self._scene.avatar is not None:
104
- name = link(name, self._scene.avatar)
105
-
106
- return name
160
+ name = f"<b><e>{escape_tag(self.group.id)}</e></b>"
161
+ if self._scene and self._scene.name is not None:
162
+ name = f"<y>{escape_tag(self._scene.name)}</y>({name})"
163
+ return link(name, self._scene and self._scene.avatar)
107
164
 
108
165
  async def send(self, message: str | UniMessage) -> Receipt:
109
166
  if isinstance(message, str):
@@ -112,20 +169,17 @@ class Game:
112
169
  text = f"{self.colored_name} | <g>Send</g> | "
113
170
  for seg in message:
114
171
  if isinstance(seg, At):
115
- text += f"<y>@{self._player_map[seg.target].colored_name}</y>"
172
+ name = seg.target
173
+ if name in self._player_map:
174
+ name = self._player_map[name].colored_name
175
+ text += f"<y>@{name}</y>"
116
176
  else:
117
177
  text += escape_tag(str(seg)).replace("\n", "\\n")
118
178
 
119
179
  logger.opt(colors=True).info(text)
120
180
  return await message.send(self.group, self.bot)
121
181
 
122
- def at_all(self) -> UniMessage:
123
- msg = UniMessage()
124
- for p in sorted(self.players, key=lambda p: (p.role_name, p.user_id)):
125
- msg.at(p.user_id)
126
- return msg
127
-
128
- def check_game_status(self) -> None:
182
+ def raise_for_status(self) -> None:
129
183
  players = self.players.alive()
130
184
  w = players.select(RoleGroup.Werewolf)
131
185
  p = players.exclude(RoleGroup.Werewolf)
@@ -143,38 +197,16 @@ class Game:
143
197
  if not w.size:
144
198
  raise GameFinished(GameStatus.GoodGuy)
145
199
 
146
- def show_killed_players(self) -> str:
147
- result: list[str] = []
148
-
149
- for player in self.killed_players:
150
- if player.kill_info is None:
151
- continue
152
-
153
- line = f"{player.name} 被 " + ", ".join(
154
- p.name for p in player.kill_info.killers
155
- )
156
- match player.kill_info.reason:
157
- case KillReason.Werewolf:
158
- line = f"🔪 {line} 刀了"
159
- case KillReason.Poison:
160
- line = f"🧪 {line} 毒死"
161
- case KillReason.Shoot:
162
- line = f"🔫 {line} 射杀"
163
- case KillReason.Vote:
164
- line = f"🗳️ {line} 票出"
165
- case x:
166
- assert_never(x)
167
- result.append(line)
168
-
169
- return "\n\n".join(result)
170
-
171
200
  async def notify_player_role(self) -> None:
172
- preset = config.get_role_preset()[len(self.players)]
201
+ msg = UniMessage()
202
+ for p in sorted(self.players, key=lambda p: p.user_id):
203
+ msg.at(p.user_id)
204
+
205
+ w, p, c = PresetData.load().role_preset[len(self.players)]
173
206
  msg = (
174
- self.at_all()
175
- .text("\n\n📝正在分配职业,请注意查看私聊消息\n")
207
+ msg.text("\n\n📝正在分配职业,请注意查看私聊消息\n")
176
208
  .text(f"当前玩家数: {len(self.players)}\n")
177
- .text(f"职业分配: 狼人x{preset[0]}, 神职x{preset[1]}, 平民x{preset[2]}")
209
+ .text(f"职业分配: 狼人x{w}, 神职x{p}, 平民x{c}")
178
210
  )
179
211
 
180
212
  async with anyio.create_task_group() as tg:
@@ -183,16 +215,10 @@ class Game:
183
215
  tg.start_soon(p.notify_role)
184
216
 
185
217
  async def wait_stop(self, *players: Player, timeout_secs: float) -> None:
186
- async def wait(p: Player) -> None:
187
- while True:
188
- msg = await InputStore.fetch(p.user_id, self.group.id)
189
- if msg.extract_plain_text().strip() == STOP_COMMAND:
190
- break
191
-
192
218
  with anyio.move_on_after(timeout_secs):
193
219
  async with anyio.create_task_group() as tg:
194
220
  for p in players:
195
- tg.start_soon(wait, p)
221
+ tg.start_soon(InputStore.fetch_until_stop, p.user_id, self.group.id)
196
222
 
197
223
  async def interact(
198
224
  self,
@@ -218,6 +244,29 @@ class Game:
218
244
  logger.opt(colors=True).debug(f"{text}交互超时 (<y>{timeout_secs}</y>s)")
219
245
  await players.broadcast(f"⚠️{text}交互超时")
220
246
 
247
+ async def post_kill(self, players: Player | PlayerSet) -> None:
248
+ if isinstance(players, Player):
249
+ players = PlayerSet([players])
250
+ if not players:
251
+ return
252
+
253
+ for player in players.dead():
254
+ await player.post_kill()
255
+ if player.kill_info is not None:
256
+ self.killed_players.append((player.name, player.kill_info))
257
+
258
+ shooter = self.state.shoot
259
+ if shooter is not None and (shoot := shooter.selected) is not None:
260
+ await self.send(
261
+ UniMessage.text("🔫玩家 ")
262
+ .at(shoot.user_id)
263
+ .text(f" 被{shooter.role_name}射杀, 请发表遗言\n")
264
+ .text(f"限时1分钟, 发送 “{STOP_COMMAND_PROMPT}” 结束发言")
265
+ )
266
+ await self.wait_stop(shoot, timeout_secs=60)
267
+ self.state.shoot = shooter.selected = None
268
+ await self.post_kill(shoot)
269
+
221
270
  async def select_killed(self) -> None:
222
271
  players = self.players.alive()
223
272
  self.state.killed = None
@@ -237,45 +286,47 @@ class Game:
237
286
  else:
238
287
  await anyio.sleep(5 + secrets.randbelow(15))
239
288
 
240
- async def handle_new_dead(self, players: Player | PlayerSet) -> None:
241
- if isinstance(players, Player):
242
- players = PlayerSet([players])
243
- if not players:
244
- return
245
-
289
+ async def run_night(self, players: PlayerSet) -> Player | None:
290
+ # 狼人、预言家、守卫 同时交互,女巫在狼人后交互
246
291
  async with anyio.create_task_group() as tg:
292
+ tg.start_soon(self.select_killed)
247
293
  tg.start_soon(
248
- players.broadcast,
249
- "ℹ️你已加入死者频道,请勿在群内继续发言\n"
250
- "私聊发送消息将转发至其他已死亡玩家",
294
+ players.select(Role.Witch).broadcast,
295
+ "ℹ️请等待狼人决定目标...",
251
296
  )
297
+ tg.start_soon(self.interact, Role.Prophet, 60)
298
+ tg.start_soon(self.interact, Role.Guard, 60)
252
299
  tg.start_soon(
253
- self.players.dead().exclude(*players).broadcast,
254
- f"ℹ️玩家 {', '.join(p.name for p in players)} 加入了死者频道",
300
+ players.exclude(
301
+ RoleGroup.Werewolf,
302
+ Role.Prophet,
303
+ Role.Witch,
304
+ Role.Guard,
305
+ ).broadcast,
306
+ "ℹ️请等待其他玩家结束交互...",
255
307
  )
256
308
 
257
- async def post_kill(self, players: Player | PlayerSet) -> None:
258
- if isinstance(players, Player):
259
- players = PlayerSet([players])
260
- if not players:
261
- return
309
+ # 狼人击杀目标
310
+ if (
311
+ (killed := self.state.killed) is not None # 狼人未空刀
312
+ and killed not in self.state.protected # 守卫保护
313
+ and killed not in self.state.antidote # 女巫使用解药
314
+ ):
315
+ # 狼人正常击杀玩家
316
+ await killed.kill(
317
+ KillReason.Werewolf,
318
+ *players.select(RoleGroup.Werewolf),
319
+ )
262
320
 
263
- for player in players.dead():
264
- await player.post_kill()
265
- await self.handle_new_dead(player)
266
- self.killed_players.append(player)
321
+ # 女巫操作目标
322
+ for witch in self.state.poison:
323
+ if witch.selected is None:
324
+ continue
325
+ if witch.selected not in self.state.protected: # 守卫未保护
326
+ # 女巫毒杀玩家
327
+ await witch.selected.kill(KillReason.Poison, witch)
267
328
 
268
- shooter = self.state.shoot
269
- if shooter is not None and (shoot := shooter.selected) is not None:
270
- await self.send(
271
- UniMessage.text("🔫玩家 ")
272
- .at(shoot.user_id)
273
- .text(f" 被{shooter.role_name}射杀, 请发表遗言\n")
274
- .text(f"限时1分钟, 发送 “{STOP_COMMAND_PROMPT}” 结束发言")
275
- )
276
- await self.wait_stop(shoot, timeout_secs=60)
277
- self.state.shoot = shooter.selected = None
278
- await self.post_kill(shoot)
329
+ return killed
279
330
 
280
331
  async def run_vote(self) -> None:
281
332
  # 筛选当前存活玩家
@@ -336,46 +387,7 @@ class Game:
336
387
  await self.wait_stop(voted, timeout_secs=60)
337
388
  await self.post_kill(voted)
338
389
 
339
- async def run_dead_channel(self, finished: anyio.Event) -> NoReturn:
340
- send, recv = anyio.create_memory_object_stream[tuple[Player, UniMessage]](10)
341
-
342
- async def handle_cancel() -> None:
343
- await finished.wait()
344
- tg.cancel_scope.cancel()
345
-
346
- async def handle_send() -> NoReturn:
347
- while True:
348
- player, msg = await recv.receive()
349
- msg = f"玩家 {player.name}:\n" + msg
350
- await self.players.killed().exclude(player).broadcast(msg)
351
-
352
- async def handle_recv(player: Player) -> NoReturn:
353
- await player.killed.wait()
354
-
355
- counter = 0
356
-
357
- async def decrease() -> None:
358
- nonlocal counter
359
- await anyio.sleep(60)
360
- counter -= 1
361
-
362
- while True:
363
- msg = await player.receive()
364
- counter += 1
365
- if counter <= 10:
366
- await send.send((player, msg))
367
- tg.start_soon(decrease)
368
- else:
369
- await player.send("❌发言频率超过限制, 该消息被屏蔽")
370
-
371
- async with anyio.create_task_group() as tg:
372
- tg.start_soon(handle_cancel)
373
- tg.start_soon(handle_send)
374
- for p in self.players:
375
- tg.start_soon(handle_recv, p)
376
-
377
- async def run(self) -> NoReturn:
378
- await self._fetch_group_scene()
390
+ async def mainloop(self) -> NoReturn:
379
391
  # 告知玩家角色信息
380
392
  await self.notify_player_role()
381
393
 
@@ -383,48 +395,11 @@ class Game:
383
395
  while True:
384
396
  # 重置游戏状态,进入下一夜
385
397
  self.state.reset()
386
- players = self.players.alive()
387
398
  await self.send("🌙天黑请闭眼...")
399
+ players = self.players.alive()
400
+ killed = await self.run_night(players)
388
401
 
389
- # 狼人、预言家、守卫 同时交互,女巫在狼人后交互
390
- async with anyio.create_task_group() as tg:
391
- tg.start_soon(self.select_killed)
392
- tg.start_soon(
393
- players.select(Role.Witch).broadcast,
394
- "ℹ️请等待狼人决定目标...",
395
- )
396
- tg.start_soon(self.interact, Role.Prophet, 60)
397
- tg.start_soon(self.interact, Role.Guard, 60)
398
- tg.start_soon(
399
- players.exclude(
400
- RoleGroup.Werewolf,
401
- Role.Prophet,
402
- Role.Witch,
403
- Role.Guard,
404
- ).broadcast,
405
- "ℹ️请等待其他玩家结束交互...",
406
- )
407
-
408
- # 狼人击杀目标
409
- if (
410
- (killed := self.state.killed) is not None # 狼人未空刀
411
- and killed not in self.state.protected # 守卫保护
412
- and killed not in self.state.antidote # 女巫使用解药
413
- ):
414
- # 狼人正常击杀玩家
415
- await killed.kill(
416
- KillReason.Werewolf,
417
- *players.select(RoleGroup.Werewolf),
418
- )
419
-
420
- # 女巫操作目标
421
- for witch in self.state.poison:
422
- if witch.selected is None:
423
- continue
424
- if witch.selected not in self.state.protected: # 守卫未保护
425
- # 女巫毒杀玩家
426
- await witch.selected.kill(KillReason.Poison, witch)
427
-
402
+ # 公告
428
403
  self.state.day += 1
429
404
  msg = UniMessage.text(f"『第{self.state.day}天』☀️天亮了...\n")
430
405
  # 没有玩家死亡,平安夜
@@ -449,7 +424,7 @@ class Game:
449
424
  await self.post_kill(dead)
450
425
 
451
426
  # 判断游戏状态
452
- self.check_game_status()
427
+ self.raise_for_status()
453
428
 
454
429
  # 公示存活玩家
455
430
  await self.send(f"📝当前存活玩家: \n\n{self.players.alive().show()}")
@@ -468,50 +443,57 @@ class Game:
468
443
  await self.run_vote()
469
444
 
470
445
  # 判断游戏状态
471
- self.check_game_status()
446
+ self.raise_for_status()
472
447
 
473
448
  async def handle_game_finish(self, status: GameStatus) -> None:
474
- match status:
475
- case GameStatus.GoodGuy:
476
- winner = "好人"
477
- case GameStatus.Werewolf:
478
- winner = "狼人"
479
- case GameStatus.Joker:
480
- winner = "小丑"
481
- case x:
482
- assert_never(x)
483
-
484
- msg = UniMessage.text(f"🎉游戏结束,{winner}获胜\n\n")
449
+ msg = UniMessage.text(f"🎉游戏结束,{game_status_conv[status]}获胜\n\n")
485
450
  for p in sorted(self.players, key=lambda p: (p.role.value, p.user_id)):
486
451
  msg.at(p.user_id).text(f": {p.role_name}\n")
487
452
  await self.send(msg)
488
- await self.send(f"📌玩家死亡报告:\n\n{self.show_killed_players()}")
489
453
 
490
- async def start(self) -> None:
491
- async def daemon() -> None:
492
- try:
493
- await self.run()
494
- except anyio.get_cancelled_exc_class():
495
- logger.warning(f"{self.group.id} 的狼人杀游戏进程被取消")
496
- except GameFinished as result:
497
- await self.handle_game_finish(result.status)
498
- logger.info(f"{self.group.id} 的狼人杀游戏进程正常退出")
499
- except Exception as err:
500
- msg = f"{self.group.id} 的狼人杀游戏进程出现未知错误: {err!r}"
501
- logger.exception(msg)
502
- await self.send(f"❌狼人杀游戏进程出现未知错误: {err!r}")
503
- finally:
504
- finished.set()
454
+ report: list[str] = ["📌玩家死亡报告:"]
455
+ for name, info in self.killed_players:
456
+ emoji, action = report_text[info.reason]
457
+ report.append(f"{emoji} {name} 被 {', '.join(info.killers)} {action}")
458
+ await self.send("\n\n".join(report))
459
+
460
+ async def daemon(self, finished: anyio.Event) -> None:
461
+ try:
462
+ await self.mainloop()
463
+ except anyio.get_cancelled_exc_class():
464
+ logger.warning(f"{self.group.id} 的狼人杀游戏进程被取消")
465
+ except GameFinished as result:
466
+ await self.handle_game_finish(result.status)
467
+ logger.info(f"{self.group.id} 的狼人杀游戏进程正常退出")
468
+ except Exception as err:
469
+ msg = f"{self.group.id} 的狼人杀游戏进程出现未知错误: {err!r}"
470
+ logger.exception(msg)
471
+ await self.send(f"❌狼人杀游戏进程出现未知错误: {err!r}")
472
+ finally:
473
+ finished.set()
505
474
 
475
+ async def start(self) -> None:
476
+ await self._fetch_group_scene()
506
477
  finished = anyio.Event()
478
+ dead_channel = DeadChannel(self.players, finished)
507
479
  self.running_games.add(self)
480
+
508
481
  try:
509
482
  async with anyio.create_task_group() as tg:
510
- tg.start_soon(daemon)
511
- tg.start_soon(self.run_dead_channel, finished)
483
+ self._task_group = tg
484
+ tg.start_soon(self.daemon, finished)
485
+ tg.start_soon(dead_channel.run)
486
+ except anyio.get_cancelled_exc_class():
487
+ logger.warning(f"{self.group.id} 的狼人杀游戏进程被取消")
512
488
  except Exception as err:
513
489
  msg = f"{self.group.id} 的狼人杀守护进程出现错误: {err!r}"
514
490
  logger.opt(exception=err).error(msg)
515
491
  finally:
492
+ self._task_group = None
516
493
  self.running_games.discard(self)
517
494
  InputStore.cleanup(list(self._player_map), self.group.id)
495
+
496
+ def terminate(self) -> None:
497
+ if self._task_group is not None:
498
+ logger.warning(f"中止 {self.group.id} 的狼人杀游戏进程")
499
+ self._task_group.cancel_scope.cancel()
@@ -1,2 +1,4 @@
1
+ from . import edit_preset as edit_preset
1
2
  from . import message_in_game as message_in_game
2
3
  from . import start_game as start_game
4
+ from . import superuser_ops as superuser_ops
@@ -1,7 +1,7 @@
1
1
  import itertools
2
2
 
3
3
  from nonebot.adapters import Bot, Event
4
- from nonebot_plugin_alconna import MsgTarget
4
+ from nonebot_plugin_alconna import MsgTarget, UniMessage
5
5
 
6
6
  from ..game import Game
7
7
 
@@ -22,16 +22,28 @@ def user_in_game(self_id: str, user_id: str, group_id: str | None) -> bool:
22
22
  return False
23
23
 
24
24
 
25
- async def rule_in_game(bot: Bot, event: Event, target: MsgTarget) -> bool:
25
+ async def rule_in_game(bot: Bot, event: Event) -> bool:
26
26
  if not Game.running_games:
27
27
  return False
28
+
29
+ try:
30
+ target = UniMessage.get_target(event, bot)
31
+ except NotImplementedError:
32
+ return False
33
+
28
34
  if target.private:
29
35
  return user_in_game(bot.self_id, target.id, None)
30
- return user_in_game(bot.self_id, event.get_user_id(), target.id)
36
+
37
+ try:
38
+ user_id = event.get_user_id()
39
+ except Exception:
40
+ return False
41
+
42
+ return user_in_game(bot.self_id, user_id, target.id)
31
43
 
32
44
 
33
- async def rule_not_in_game(bot: Bot, event: Event, target: MsgTarget) -> bool:
34
- return not await rule_in_game(bot, event, target)
45
+ async def rule_not_in_game(bot: Bot, event: Event) -> bool:
46
+ return not await rule_in_game(bot, event)
35
47
 
36
48
 
37
49
  async def is_group(target: MsgTarget) -> bool: