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