nonebot-plugin-werewolf 1.1.7__py3-none-any.whl → 1.1.8__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 +1 -1
  2. nonebot_plugin_werewolf/config.py +73 -15
  3. nonebot_plugin_werewolf/constant.py +54 -46
  4. nonebot_plugin_werewolf/exception.py +2 -4
  5. nonebot_plugin_werewolf/game.py +154 -136
  6. nonebot_plugin_werewolf/matchers/__init__.py +1 -0
  7. nonebot_plugin_werewolf/matchers/depends.py +4 -4
  8. nonebot_plugin_werewolf/matchers/edit_behavior.py +205 -0
  9. nonebot_plugin_werewolf/matchers/edit_preset.py +11 -11
  10. nonebot_plugin_werewolf/matchers/message_in_game.py +3 -1
  11. nonebot_plugin_werewolf/matchers/poke/chronocat_poke.py +8 -5
  12. nonebot_plugin_werewolf/matchers/poke/ob11_poke.py +3 -3
  13. nonebot_plugin_werewolf/matchers/start_game.py +213 -175
  14. nonebot_plugin_werewolf/matchers/superuser_ops.py +3 -3
  15. nonebot_plugin_werewolf/models.py +31 -19
  16. nonebot_plugin_werewolf/player_set.py +10 -8
  17. nonebot_plugin_werewolf/players/__init__.py +1 -1
  18. nonebot_plugin_werewolf/players/can_shoot.py +15 -15
  19. nonebot_plugin_werewolf/players/civilian.py +1 -1
  20. nonebot_plugin_werewolf/players/guard.py +16 -14
  21. nonebot_plugin_werewolf/players/hunter.py +1 -1
  22. nonebot_plugin_werewolf/players/idiot.py +3 -3
  23. nonebot_plugin_werewolf/players/{joker.py → jester.py} +4 -5
  24. nonebot_plugin_werewolf/players/player.py +90 -28
  25. nonebot_plugin_werewolf/players/prophet.py +11 -10
  26. nonebot_plugin_werewolf/players/werewolf.py +46 -11
  27. nonebot_plugin_werewolf/players/witch.py +29 -12
  28. nonebot_plugin_werewolf/players/wolfking.py +1 -1
  29. nonebot_plugin_werewolf/utils.py +105 -6
  30. {nonebot_plugin_werewolf-1.1.7.dist-info → nonebot_plugin_werewolf-1.1.8.dist-info}/METADATA +23 -20
  31. nonebot_plugin_werewolf-1.1.8.dist-info/RECORD +35 -0
  32. {nonebot_plugin_werewolf-1.1.7.dist-info → nonebot_plugin_werewolf-1.1.8.dist-info}/WHEEL +1 -1
  33. nonebot_plugin_werewolf-1.1.7.dist-info/RECORD +0 -34
  34. {nonebot_plugin_werewolf-1.1.7.dist-info → nonebot_plugin_werewolf-1.1.8.dist-info}/LICENSE +0 -0
  35. {nonebot_plugin_werewolf-1.1.7.dist-info → nonebot_plugin_werewolf-1.1.8.dist-info}/top_level.txt +0 -0
@@ -1,34 +1,43 @@
1
1
  import contextlib
2
2
  import functools
3
3
  import secrets
4
- from typing import ClassVar, NoReturn
4
+ from collections import defaultdict
5
+ from typing import NoReturn
5
6
 
6
7
  import anyio
7
- import anyio.abc
8
8
  import nonebot
9
9
  from nonebot.adapters import Bot
10
10
  from nonebot.utils import escape_tag
11
11
  from nonebot_plugin_alconna import At, Target, UniMessage
12
12
  from nonebot_plugin_alconna.uniseg.message import Receipt
13
13
  from nonebot_plugin_uninfo import Interface, SceneType
14
- from typing_extensions import Self, assert_never
15
14
 
16
- from .config import PresetData
17
- from .constant import STOP_COMMAND_PROMPT, game_status_conv, report_text, role_name_conv
15
+ from .config import GameBehavior, PresetData
16
+ from .constant import GAME_STATUS_CONV, REPORT_TEXT, ROLE_EMOJI, ROLE_NAME_CONV
18
17
  from .exception import GameFinished
19
18
  from .models import GameState, GameStatus, KillInfo, KillReason, Role, RoleGroup
20
19
  from .player_set import PlayerSet
21
20
  from .players import Player
22
- from .utils import InputStore, ObjectStream, link
21
+ from .utils import InputStore, ObjectStream, SendHandler, add_stop_button, link
23
22
 
24
23
  logger = nonebot.logger.opt(colors=True)
24
+ starting_games: dict[Target, dict[str, str]] = {}
25
+ running_games: set["Game"] = set()
26
+
27
+
28
+ def get_starting_games() -> dict[Target, dict[str, str]]:
29
+ return starting_games
30
+
31
+
32
+ def get_running_games() -> set["Game"]:
33
+ return running_games
25
34
 
26
35
 
27
36
  def init_players(bot: Bot, game: "Game", players: set[str]) -> PlayerSet:
28
37
  # group.colored_name not available yet
29
38
  logger.debug(f"初始化 <c>{game.group_id}</c> 的玩家职业")
30
39
 
31
- preset_data = PresetData.load()
40
+ preset_data = PresetData.get()
32
41
  if (preset := preset_data.role_preset.get(len(players))) is None:
33
42
  raise ValueError(
34
43
  f"玩家人数不符: "
@@ -40,11 +49,11 @@ def init_players(bot: Bot, game: "Game", players: set[str]) -> PlayerSet:
40
49
  roles: list[Role] = []
41
50
  roles.extend(preset_data.werewolf_priority[:w])
42
51
  roles.extend(preset_data.priesthood_proirity[:p])
43
- roles.extend([Role.Civilian] * c)
52
+ roles.extend([Role.CIVILIAN] * c)
44
53
 
45
- if c >= 2 and secrets.randbelow(100) <= preset_data.joker_probability * 100:
46
- roles.remove(Role.Civilian)
47
- roles.append(Role.Joker)
54
+ if c >= 2 and secrets.randbelow(100) <= preset_data.jester_probability * 100:
55
+ roles.remove(Role.CIVILIAN)
56
+ roles.append(Role.JESTER)
48
57
 
49
58
  def _select_role() -> Role:
50
59
  return roles.pop(secrets.randbelow(len(roles)))
@@ -57,28 +66,38 @@ def init_players(bot: Bot, game: "Game", players: set[str]) -> PlayerSet:
57
66
  return player_set
58
67
 
59
68
 
69
+ class _SendHandler(SendHandler[str | None]):
70
+ def solve_msg(
71
+ self,
72
+ msg: UniMessage,
73
+ stop_btn_label: str | None = None,
74
+ ) -> UniMessage:
75
+ if stop_btn_label is not None:
76
+ msg = add_stop_button(msg, stop_btn_label)
77
+ return msg
78
+
79
+
60
80
  class DeadChannel:
61
81
  players: PlayerSet
62
82
  finished: anyio.Event
63
83
  counter: dict[str, int]
64
84
  stream: ObjectStream[tuple[Player, UniMessage]]
65
- task_group: anyio.abc.TaskGroup
66
85
 
67
86
  def __init__(self, players: PlayerSet, finished: anyio.Event) -> None:
68
87
  self.players = players
69
88
  self.finished = finished
70
- self.counter = {p.user_id: 0 for p in self.players}
89
+ self.counter = {p.user_id: 0 for p in players}
71
90
  self.stream = ObjectStream[tuple[Player, UniMessage]](16)
72
91
 
73
92
  async def _decrease(self, user_id: str) -> None:
74
93
  await anyio.sleep(60)
75
94
  self.counter[user_id] -= 1
76
95
 
77
- async def _handle_finished(self) -> None:
96
+ async def _wait_finished(self) -> None:
78
97
  await self.finished.wait()
79
- self.task_group.cancel_scope.cancel()
98
+ self._task_group.cancel_scope.cancel()
80
99
 
81
- async def _handle_send(self) -> NoReturn:
100
+ async def _broadcast(self) -> NoReturn:
82
101
  while True:
83
102
  player, msg = await self.stream.recv()
84
103
  msg = f"玩家 {player.name}:\n" + msg
@@ -89,7 +108,7 @@ class DeadChannel:
89
108
  with contextlib.suppress(Exception):
90
109
  await player.send(f"消息转发失败: {err!r}")
91
110
 
92
- async def _handle_recv(self, player: Player) -> NoReturn:
111
+ async def _receive(self, player: Player) -> NoReturn:
93
112
  await player.killed.wait()
94
113
  user_id = player.user_id
95
114
 
@@ -105,30 +124,27 @@ class DeadChannel:
105
124
 
106
125
  while True:
107
126
  msg = await player.receive()
127
+ self.counter[user_id] += 1
128
+ self._task_group.start_soon(self._decrease, user_id)
108
129
 
109
130
  # 发言频率限制
110
- self.counter[user_id] += 1
111
- if self.counter[user_id] > 8:
131
+ if self.counter[user_id] > GameBehavior.get().dead_channel_rate_limit:
112
132
  await player.send("❌发言频率超过限制, 该消息被屏蔽")
113
133
  continue
114
134
 
115
135
  # 推送消息
116
136
  await self.stream.send((player, msg))
117
- self.task_group.start_soon(self._decrease, user_id)
118
137
 
119
138
  async def run(self) -> NoReturn:
120
139
  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)
140
+ self._task_group = tg
141
+ tg.start_soon(self._wait_finished)
142
+ tg.start_soon(self._broadcast)
124
143
  for p in self.players:
125
- tg.start_soon(self._handle_recv, p)
144
+ tg.start_soon(self._receive, p)
126
145
 
127
146
 
128
147
  class Game:
129
- starting_games: ClassVar[dict[Target, dict[str, str]]] = {}
130
- running_games: ClassVar[set[Self]] = set()
131
-
132
148
  bot: Bot
133
149
  group: Target
134
150
  players: PlayerSet
@@ -151,7 +167,10 @@ class Game:
151
167
  self.killed_players = []
152
168
  self._player_map = {p.user_id: p for p in self.players}
153
169
  self._scene = None
170
+ self._finished = None
154
171
  self._task_group = None
172
+ self._send_handler = _SendHandler()
173
+ self._send_handler.update(group)
155
174
 
156
175
  async def _fetch_group_scene(self) -> None:
157
176
  scene = await self.interface.get_scene(SceneType.GROUP, self.group_id)
@@ -171,88 +190,86 @@ class Game:
171
190
  name = f"<y>{escape_tag(self._scene.name)}</y>({name})"
172
191
  return link(name, self._scene and self._scene.avatar)
173
192
 
174
- async def send(self, message: str | UniMessage) -> Receipt:
193
+ def log(self, text: str) -> None:
194
+ logger.info(f"{self.colored_name} | {text}")
195
+
196
+ async def send(
197
+ self,
198
+ message: str | UniMessage,
199
+ stop_btn_label: str | None = None,
200
+ ) -> Receipt:
175
201
  if isinstance(message, str):
176
202
  message = UniMessage.text(message)
177
203
 
178
- text = f"{self.colored_name} | <g>Send</g> | "
204
+ text = ["<g>Send</g> | "]
179
205
  for seg in message:
180
206
  if isinstance(seg, At):
181
207
  name = seg.target
182
208
  if name in self._player_map:
183
209
  name = self._player_map[name].colored_name
184
- text += f"<y>@{name}</y>"
210
+ text.append(f"<y>@{name}</y>")
185
211
  else:
186
- text += escape_tag(str(seg)).replace("\n", "\\n")
212
+ text.append(escape_tag(str(seg)).replace("\n", "\\n"))
187
213
 
188
- logger.info(text)
189
- return await message.send(self.group, self.bot)
214
+ self.log("".join(text))
215
+ return await self._send_handler.send(message, stop_btn_label)
190
216
 
191
217
  def raise_for_status(self) -> None:
192
218
  players = self.players.alive()
193
- w = players.select(RoleGroup.Werewolf)
194
- p = players.exclude(RoleGroup.Werewolf)
219
+ w = players.select(RoleGroup.WEREWOLF)
220
+ p = players.exclude(RoleGroup.WEREWOLF)
195
221
 
196
222
  # 狼人数量大于其他职业数量
197
223
  if w.size >= p.size:
198
- raise GameFinished(GameStatus.Werewolf)
224
+ raise GameFinished(GameStatus.WEREWOLF)
199
225
  # 屠边-村民/中立全灭
200
- if not p.select(Role.Civilian, RoleGroup.Others).size:
201
- raise GameFinished(GameStatus.Werewolf)
226
+ if not p.select(Role.CIVILIAN, RoleGroup.OTHERS).size:
227
+ raise GameFinished(GameStatus.WEREWOLF)
202
228
  # 屠边-神职全灭
203
- if not p.exclude(Role.Civilian).size:
204
- raise GameFinished(GameStatus.Werewolf)
229
+ if not p.exclude(Role.CIVILIAN, RoleGroup.OTHERS).size:
230
+ raise GameFinished(GameStatus.WEREWOLF)
205
231
  # 狼人全灭
206
232
  if not w.size:
207
- raise GameFinished(GameStatus.GoodGuy)
233
+ raise GameFinished(GameStatus.GOODGUY)
208
234
 
209
235
  async def notify_player_role(self) -> None:
210
236
  msg = UniMessage()
211
237
  for p in sorted(self.players, key=lambda p: p.user_id):
212
238
  msg.at(p.user_id)
213
239
 
214
- w, p, c = PresetData.load().role_preset[len(self.players)]
240
+ w, p, c = PresetData.get().role_preset[len(self.players)]
215
241
  msg = (
216
242
  msg.text("\n\n📝正在分配职业,请注意查看私聊消息\n")
217
243
  .text(f"当前玩家数: {len(self.players)}\n")
218
244
  .text(f"职业分配: 狼人x{w}, 神职x{p}, 平民x{c}")
219
245
  )
220
246
 
247
+ if GameBehavior.get().show_roles_list_on_start:
248
+ role_cnt: dict[Role, int] = defaultdict(lambda: 0)
249
+ for role in sorted((p.role for p in self.players), key=lambda r: r.value):
250
+ role_cnt[role] += 1
251
+
252
+ msg.text("\n\n📚职业列表:\n")
253
+ for role, cnt in role_cnt.items():
254
+ msg.text(f"- {ROLE_EMOJI[role]}{ROLE_NAME_CONV[role]}x{cnt}\n")
255
+
221
256
  async with anyio.create_task_group() as tg:
222
257
  tg.start_soon(self.send, msg)
223
258
  for p in self.players:
224
259
  tg.start_soon(p.notify_role)
225
260
 
226
- async def wait_stop(self, *players: Player, timeout_secs: float) -> None:
261
+ async def wait_stop(
262
+ self,
263
+ *players: Player,
264
+ timeout_secs: float | None = None,
265
+ ) -> None:
266
+ if timeout_secs is None:
267
+ timeout_secs = GameBehavior.get().timeout.speak
227
268
  with anyio.move_on_after(timeout_secs):
228
269
  async with anyio.create_task_group() as tg:
229
270
  for p in players:
230
271
  tg.start_soon(InputStore.fetch_until_stop, p.user_id, self.group_id)
231
272
 
232
- async def interact(
233
- self,
234
- player_type: Player | Role | RoleGroup,
235
- timeout_secs: float,
236
- ) -> None:
237
- players = self.players.alive().select(player_type)
238
- match player_type:
239
- case Player():
240
- text = player_type.role_name
241
- case Role():
242
- text = role_name_conv[player_type]
243
- case RoleGroup():
244
- text = f"{role_name_conv[player_type]}阵营"
245
- case x:
246
- assert_never(x)
247
-
248
- await players.broadcast(f"✏️{text}交互开始,限时 {timeout_secs/60:.2f} 分钟")
249
- try:
250
- with anyio.fail_after(timeout_secs):
251
- await players.interact()
252
- except TimeoutError:
253
- logger.debug(f"{text}交互超时 (<y>{timeout_secs}</y>s)")
254
- await players.broadcast(f"⚠️{text}交互超时")
255
-
256
273
  async def post_kill(self, players: Player | PlayerSet) -> None:
257
274
  if isinstance(players, Player):
258
275
  players = PlayerSet([players])
@@ -261,59 +278,26 @@ class Game:
261
278
 
262
279
  for player in players.dead():
263
280
  await player.post_kill()
264
- if player.kill_info is not None:
265
- self.killed_players.append((player.name, player.kill_info))
281
+ if player.kill_info is None:
282
+ continue
266
283
 
284
+ self.killed_players.append((player.name, player.kill_info))
267
285
  shooter = self.state.shoot
268
286
  if shooter is not None and (shoot := shooter.selected) is not None:
269
287
  await self.send(
270
288
  UniMessage.text("🔫玩家 ")
271
289
  .at(shoot.user_id)
272
- .text(f" 被{shooter.role_name}射杀, 请发表遗言\n")
273
- .text(f"限时1分钟, 发送 “{STOP_COMMAND_PROMPT}” 结束发言")
290
+ .text(f" 被{shooter.name}射杀, 请发表遗言\n")
291
+ .text(GameBehavior.get().timeout.speak_timeout_prompt)
274
292
  )
275
- await self.wait_stop(shoot, timeout_secs=60)
293
+ await self.wait_stop(shoot)
276
294
  self.state.shoot = shooter.selected = None
277
295
  await self.post_kill(shoot)
278
296
 
279
- async def select_killed(self) -> None:
280
- players = self.players.alive()
281
- self.state.killed = None
282
-
283
- w = players.select(RoleGroup.Werewolf)
284
- await self.interact(RoleGroup.Werewolf, 120)
285
- if (s := w.player_selected()).size == 1:
286
- self.state.killed = s.pop()
287
- await w.broadcast(f"🔪今晚选择的目标为: {self.state.killed.name}")
288
- else:
289
- await w.broadcast("⚠️狼人阵营意见未统一,此晚空刀")
290
-
291
- # 如果女巫存活,正常交互,限时1分钟
292
- if players.include(Role.Witch):
293
- await self.interact(Role.Witch, 60)
294
- # 否则等待 5-20s
295
- else:
296
- await anyio.sleep(5 + secrets.randbelow(15))
297
-
298
297
  async def run_night(self, players: PlayerSet) -> Player | None:
299
- # 狼人、预言家、守卫 同时交互,女巫在狼人后交互
300
298
  async with anyio.create_task_group() as tg:
301
- tg.start_soon(self.select_killed)
302
- tg.start_soon(
303
- players.select(Role.Witch).broadcast,
304
- "ℹ️请等待狼人决定目标...",
305
- )
306
- tg.start_soon(self.interact, Role.Prophet, 60)
307
- tg.start_soon(self.interact, Role.Guard, 60)
308
- tg.start_soon(
309
- players.exclude(
310
- RoleGroup.Werewolf,
311
- Role.Prophet,
312
- Role.Witch,
313
- Role.Guard,
314
- ).broadcast,
315
- "ℹ️请等待其他玩家结束交互...",
316
- )
299
+ for p in players:
300
+ tg.start_soon(p.interact)
317
301
 
318
302
  # 狼人击杀目标
319
303
  if (
@@ -323,9 +307,11 @@ class Game:
323
307
  ):
324
308
  # 狼人正常击杀玩家
325
309
  await killed.kill(
326
- KillReason.Werewolf,
327
- *players.select(RoleGroup.Werewolf),
310
+ KillReason.WEREWOLF,
311
+ *players.select(RoleGroup.WEREWOLF),
328
312
  )
313
+ else:
314
+ killed = None
329
315
 
330
316
  # 女巫操作目标
331
317
  for witch in self.state.poison:
@@ -333,10 +319,32 @@ class Game:
333
319
  continue
334
320
  if witch.selected not in self.state.protected: # 守卫未保护
335
321
  # 女巫毒杀玩家
336
- await witch.selected.kill(KillReason.Poison, witch)
322
+ await witch.selected.kill(KillReason.POISON, witch)
337
323
 
338
324
  return killed
339
325
 
326
+ async def run_discussion(self) -> None:
327
+ behavior = GameBehavior.get()
328
+ timeout = behavior.timeout
329
+
330
+ if not behavior.speak_in_turn:
331
+ speak_timeout = timeout.group_speak
332
+ await self.send(
333
+ f"💬接下来开始自由讨论\n{timeout.group_speak_timeout_prompt}",
334
+ stop_btn_label="结束发言",
335
+ )
336
+ await self.wait_stop(*self.players.alive(), timeout_secs=speak_timeout)
337
+ else:
338
+ await self.send("💬接下来开始自由讨论")
339
+ speak_timeout = timeout.speak
340
+ for player in self.players.alive().sorted:
341
+ await self.send(
342
+ f"💬轮到你发言\n{timeout.speak_timeout_prompt}",
343
+ stop_btn_label="结束发言",
344
+ )
345
+ await self.wait_stop(player, timeout_secs=speak_timeout)
346
+ await self.send("💬所有玩家发言结束")
347
+
340
348
  async def run_vote(self) -> None:
341
349
  # 筛选当前存活玩家
342
350
  players = self.players.alive()
@@ -352,12 +360,17 @@ class Game:
352
360
 
353
361
  # 投票结果公示
354
362
  msg = UniMessage.text("📊投票结果:\n")
355
- for p, v in sorted(vote_result.items(), key=lambda x: len(x[1]), reverse=True):
356
- if p is not None:
357
- msg.at(p.user_id).text(f": {len(v)} 票\n")
358
- vote_reversed[len(v)] = [*vote_reversed.get(len(v), []), p]
359
- if (v := (len(players) - total_votes)) > 0:
360
- msg.text(f"弃票: {v} 票\n\n")
363
+ for player, votes in sorted(
364
+ vote_result.items(),
365
+ key=lambda x: len(x[1]),
366
+ reverse=True,
367
+ ):
368
+ if player is not None:
369
+ msg.at(player.user_id).text(f": {len(votes)} 票\n")
370
+ vote_reversed.setdefault(len(votes), []).append(player)
371
+ if (discarded_votes := (len(players) - total_votes)) > 0:
372
+ msg.text(f"弃票: {discarded_votes} 票\n")
373
+ msg.text("\n")
361
374
 
362
375
  # 全员弃票 # 不是哥们?
363
376
  if total_votes == 0:
@@ -382,7 +395,7 @@ class Game:
382
395
 
383
396
  # 仅有一名玩家票数最高
384
397
  voted = vs.pop()
385
- if not await voted.kill(KillReason.Vote, *vote_result[voted]):
398
+ if not await voted.kill(KillReason.VOTE, *vote_result[voted]):
386
399
  # 投票放逐失败 (例: 白痴)
387
400
  return
388
401
 
@@ -391,9 +404,10 @@ class Game:
391
404
  UniMessage.text("🔨玩家 ")
392
405
  .at(voted.user_id)
393
406
  .text(" 被投票放逐, 请发表遗言\n")
394
- .text(f"限时1分钟, 发送 “{STOP_COMMAND_PROMPT}” 结束发言")
407
+ .text(GameBehavior.get().timeout.speak_timeout_prompt),
408
+ stop_btn_label="结束发言",
395
409
  )
396
- await self.wait_stop(voted, timeout_secs=60)
410
+ await self.wait_stop(voted)
397
411
  await self.post_kill(voted)
398
412
 
399
413
  async def mainloop(self) -> NoReturn:
@@ -404,12 +418,16 @@ class Game:
404
418
  while True:
405
419
  # 重置游戏状态,进入下一夜
406
420
  self.state.reset()
421
+ self.state.state = GameState.State.NIGHT
407
422
  await self.send("🌙天黑请闭眼...")
408
423
  players = self.players.alive()
424
+
425
+ # 夜间交互,返回狼人目标
409
426
  killed = await self.run_night(players)
410
427
 
411
428
  # 公告
412
429
  self.state.day += 1
430
+ self.state.state = GameState.State.DAY
413
431
  msg = UniMessage.text(f"『第{self.state.day}天』☀️天亮了...\n")
414
432
  # 没有玩家死亡,平安夜
415
433
  if not (dead := players.dead()):
@@ -427,9 +445,10 @@ class Game:
427
445
  UniMessage.text("⚙️当前为第一天\n请被狼人杀死的 ")
428
446
  .at(killed.user_id)
429
447
  .text(" 发表遗言\n")
430
- .text(f"限时1分钟, 发送 “{STOP_COMMAND_PROMPT}” 结束发言")
448
+ .text(GameBehavior.get().timeout.speak_timeout_prompt),
449
+ stop_btn_label="结束发言",
431
450
  )
432
- await self.wait_stop(killed, timeout_secs=60)
451
+ await self.wait_stop(killed)
433
452
  await self.post_kill(dead)
434
453
 
435
454
  # 判断游戏状态
@@ -439,34 +458,31 @@ class Game:
439
458
  await self.send(f"📝当前存活玩家: \n\n{self.players.alive().show()}")
440
459
 
441
460
  # 开始自由讨论
442
- await self.send(
443
- "💬接下来开始自由讨论\n限时2分钟, "
444
- f"全员发送 “{STOP_COMMAND_PROMPT}” 结束发言"
445
- )
446
- await self.wait_stop(*self.players.alive(), timeout_secs=120)
461
+ await self.run_discussion()
447
462
 
448
463
  # 开始投票
449
464
  await self.send(
450
465
  "🗳️讨论结束, 进入投票环节,限时1分钟\n请在私聊中进行投票交互"
451
466
  )
467
+ self.state.state = GameState.State.VOTE
452
468
  await self.run_vote()
453
469
 
454
470
  # 判断游戏状态
455
471
  self.raise_for_status()
456
472
 
457
473
  async def handle_game_finish(self, status: GameStatus) -> None:
458
- msg = UniMessage.text(f"🎉游戏结束,{game_status_conv[status]}获胜\n\n")
474
+ msg = UniMessage.text(f"🎉游戏结束,{GAME_STATUS_CONV[status]}获胜\n\n")
459
475
  for p in sorted(self.players, key=lambda p: (p.role.value, p.user_id)):
460
476
  msg.at(p.user_id).text(f": {p.role_name}\n")
461
477
  await self.send(msg)
462
478
 
463
479
  report: list[str] = ["📌玩家死亡报告:"]
464
480
  for name, info in self.killed_players:
465
- emoji, action = report_text[info.reason]
481
+ emoji, action = REPORT_TEXT[info.reason]
466
482
  report.append(f"{emoji} {name} 被 {', '.join(info.killers)} {action}")
467
483
  await self.send("\n\n".join(report))
468
484
 
469
- async def daemon(self, finished: anyio.Event) -> None:
485
+ async def run_daemon(self) -> None:
470
486
  try:
471
487
  await self.mainloop()
472
488
  except anyio.get_cancelled_exc_class():
@@ -479,18 +495,19 @@ class Game:
479
495
  logger.exception(msg)
480
496
  await self.send(f"❌狼人杀游戏进程出现未知错误: {err!r}")
481
497
  finally:
482
- finished.set()
498
+ if self._finished is not None:
499
+ self._finished.set()
483
500
 
484
501
  async def start(self) -> None:
485
502
  await self._fetch_group_scene()
486
- finished = anyio.Event()
487
- dead_channel = DeadChannel(self.players, finished)
488
- self.running_games.add(self)
503
+ self._finished = anyio.Event()
504
+ dead_channel = DeadChannel(self.players, self._finished)
505
+ get_running_games().add(self)
489
506
 
490
507
  try:
491
508
  async with anyio.create_task_group() as tg:
492
509
  self._task_group = tg
493
- tg.start_soon(self.daemon, finished)
510
+ tg.start_soon(self.run_daemon)
494
511
  tg.start_soon(dead_channel.run)
495
512
  except anyio.get_cancelled_exc_class():
496
513
  logger.warning(f"{self.colored_name} 的狼人杀游戏进程被取消")
@@ -498,8 +515,9 @@ class Game:
498
515
  msg = f"{self.colored_name} 的狼人杀守护进程出现错误: {err!r}"
499
516
  logger.opt(exception=err).error(msg)
500
517
  finally:
518
+ self._finished = None
501
519
  self._task_group = None
502
- self.running_games.discard(self)
520
+ get_running_games().discard(self)
503
521
  InputStore.cleanup(list(self._player_map), self.group_id)
504
522
 
505
523
  def terminate(self) -> None:
@@ -1,3 +1,4 @@
1
+ from . import edit_behavior as edit_behavior
1
2
  from . import edit_preset as edit_preset
2
3
  from . import message_in_game as message_in_game
3
4
  from . import start_game as start_game
@@ -3,27 +3,27 @@ import itertools
3
3
  from nonebot.adapters import Bot, Event
4
4
  from nonebot_plugin_alconna import MsgTarget, UniMessage
5
5
 
6
- from ..game import Game
6
+ from ..game import Game, get_running_games
7
7
 
8
8
 
9
9
  def user_in_game(self_id: str, user_id: str, group_id: str | None) -> bool:
10
10
  if group_id is None:
11
11
  return any(
12
12
  self_id == p.bot.self_id and user_id == p.user_id
13
- for p in itertools.chain(*[g.players for g in Game.running_games])
13
+ for p in itertools.chain(*[g.players for g in get_running_games()])
14
14
  )
15
15
 
16
16
  def check(game: Game) -> bool:
17
17
  return self_id == game.group.self_id and group_id == game.group.id
18
18
 
19
- if game := next(filter(check, Game.running_games), None):
19
+ if game := next(filter(check, get_running_games()), None):
20
20
  return any(user_id == player.user_id for player in game.players)
21
21
 
22
22
  return False
23
23
 
24
24
 
25
25
  async def rule_in_game(bot: Bot, event: Event) -> bool:
26
- if not Game.running_games:
26
+ if not get_running_games():
27
27
  return False
28
28
 
29
29
  try: