nonebot-plugin-werewolf 1.1.8__py3-none-any.whl → 1.1.10__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 (29) hide show
  1. nonebot_plugin_werewolf/__init__.py +1 -1
  2. nonebot_plugin_werewolf/config.py +1 -0
  3. nonebot_plugin_werewolf/constant.py +6 -1
  4. nonebot_plugin_werewolf/game.py +73 -64
  5. nonebot_plugin_werewolf/matchers/edit_behavior.py +19 -3
  6. nonebot_plugin_werewolf/matchers/poke/chronocat_poke.py +2 -2
  7. nonebot_plugin_werewolf/matchers/poke/ob11_poke.py +2 -2
  8. nonebot_plugin_werewolf/matchers/start_game.py +31 -30
  9. nonebot_plugin_werewolf/models.py +15 -3
  10. nonebot_plugin_werewolf/{players/player.py → player.py} +149 -72
  11. nonebot_plugin_werewolf/player_set.py +39 -23
  12. nonebot_plugin_werewolf/players/__init__.py +0 -1
  13. nonebot_plugin_werewolf/players/civilian.py +3 -3
  14. nonebot_plugin_werewolf/players/guard.py +20 -15
  15. nonebot_plugin_werewolf/players/hunter.py +6 -5
  16. nonebot_plugin_werewolf/players/idiot.py +25 -17
  17. nonebot_plugin_werewolf/players/jester.py +22 -17
  18. nonebot_plugin_werewolf/players/prophet.py +13 -8
  19. nonebot_plugin_werewolf/players/{can_shoot.py → shooter.py} +10 -10
  20. nonebot_plugin_werewolf/players/werewolf.py +69 -45
  21. nonebot_plugin_werewolf/players/witch.py +30 -23
  22. nonebot_plugin_werewolf/players/wolfking.py +14 -8
  23. nonebot_plugin_werewolf/utils.py +18 -7
  24. {nonebot_plugin_werewolf-1.1.8.dist-info → nonebot_plugin_werewolf-1.1.10.dist-info}/METADATA +14 -2
  25. nonebot_plugin_werewolf-1.1.10.dist-info/RECORD +35 -0
  26. {nonebot_plugin_werewolf-1.1.8.dist-info → nonebot_plugin_werewolf-1.1.10.dist-info}/WHEEL +1 -1
  27. nonebot_plugin_werewolf-1.1.8.dist-info/RECORD +0 -35
  28. {nonebot_plugin_werewolf-1.1.8.dist-info → nonebot_plugin_werewolf-1.1.10.dist-info/licenses}/LICENSE +0 -0
  29. {nonebot_plugin_werewolf-1.1.8.dist-info → nonebot_plugin_werewolf-1.1.10.dist-info}/top_level.txt +0 -0
@@ -10,7 +10,7 @@ from . import matchers as matchers
10
10
  from . import players as players
11
11
  from .config import Config
12
12
 
13
- __version__ = "1.1.8"
13
+ __version__ = "1.1.10"
14
14
  __plugin_meta__ = PluginMetadata(
15
15
  name="狼人杀",
16
16
  description="适用于 Nonebot2 的狼人杀插件",
@@ -56,6 +56,7 @@ class GameBehavior(ConfigFile):
56
56
  show_roles_list_on_start: bool = False
57
57
  speak_in_turn: bool = False
58
58
  dead_channel_rate_limit: int = 8 # per minute
59
+ werewolf_multi_select: bool = False
59
60
 
60
61
  class _Timeout(BaseModel):
61
62
  prepare: int = Field(default=5 * 60, ge=5 * 60)
@@ -1,4 +1,5 @@
1
1
  import functools
2
+ from typing import TYPE_CHECKING
2
3
 
3
4
  import nonebot
4
5
 
@@ -10,13 +11,17 @@ COMMAND_START = next(
10
11
  )
11
12
 
12
13
 
13
- @functools.cache
14
14
  def stop_command_prompt() -> str:
15
15
  from .config import config # circular import
16
16
 
17
17
  return COMMAND_START + config.get_stop_command()[0]
18
18
 
19
19
 
20
+ if not TYPE_CHECKING:
21
+ stop_command_prompt = functools.cache(stop_command_prompt)
22
+ del TYPE_CHECKING
23
+
24
+
20
25
  ROLE_NAME_CONV: dict[Role | RoleGroup, str] = {
21
26
  Role.WEREWOLF: "狼人",
22
27
  Role.WOLFKING: "狼王",
@@ -1,8 +1,9 @@
1
1
  import contextlib
2
2
  import functools
3
3
  import secrets
4
- from collections import defaultdict
5
- from typing import NoReturn
4
+ from collections import Counter
5
+ from typing import NoReturn, final
6
+ from typing_extensions import Self
6
7
 
7
8
  import anyio
8
9
  import nonebot
@@ -16,8 +17,8 @@ from .config import GameBehavior, PresetData
16
17
  from .constant import GAME_STATUS_CONV, REPORT_TEXT, ROLE_EMOJI, ROLE_NAME_CONV
17
18
  from .exception import GameFinished
18
19
  from .models import GameState, GameStatus, KillInfo, KillReason, Role, RoleGroup
20
+ from .player import Player
19
21
  from .player_set import PlayerSet
20
- from .players import Player
21
22
  from .utils import InputStore, ObjectStream, SendHandler, add_stop_button, link
22
23
 
23
24
  logger = nonebot.logger.opt(colors=True)
@@ -33,9 +34,13 @@ def get_running_games() -> set["Game"]:
33
34
  return running_games
34
35
 
35
36
 
36
- def init_players(bot: Bot, game: "Game", players: set[str]) -> PlayerSet:
37
- # group.colored_name not available yet
38
- logger.debug(f"初始化 <c>{game.group_id}</c> 的玩家职业")
37
+ async def init_players(
38
+ bot: Bot,
39
+ game: "Game",
40
+ players: set[str],
41
+ interface: Interface,
42
+ ) -> PlayerSet:
43
+ logger.debug(f"初始化 {game.colored_name} 的玩家职业")
39
44
 
40
45
  preset_data = PresetData.get()
41
46
  if (preset := preset_data.role_preset.get(len(players))) is None:
@@ -58,11 +63,11 @@ def init_players(bot: Bot, game: "Game", players: set[str]) -> PlayerSet:
58
63
  def _select_role() -> Role:
59
64
  return roles.pop(secrets.randbelow(len(roles)))
60
65
 
61
- player_set = PlayerSet(
62
- Player.new(_select_role(), bot, game, user_id) for user_id in players
63
- )
64
- logger.debug(f"职业分配完成: <e>{escape_tag(str(player_set))}</e>")
66
+ player_set = PlayerSet()
67
+ for user_id in players:
68
+ player_set.add(await Player.new(_select_role(), bot, game, user_id, interface))
65
69
 
70
+ logger.debug(f"职业分配完成: <e>{escape_tag(str(player_set))}</e>")
66
71
  return player_set
67
72
 
68
73
 
@@ -135,7 +140,7 @@ class DeadChannel:
135
140
  # 推送消息
136
141
  await self.stream.send((player, msg))
137
142
 
138
- async def run(self) -> NoReturn:
143
+ async def run(self) -> None:
139
144
  async with anyio.create_task_group() as tg:
140
145
  self._task_group = tg
141
146
  tg.start_soon(self._wait_finished)
@@ -148,36 +153,40 @@ class Game:
148
153
  bot: Bot
149
154
  group: Target
150
155
  players: PlayerSet
151
- interface: Interface
152
156
  state: GameState
153
157
  killed_players: list[tuple[str, KillInfo]]
154
158
 
155
- def __init__(
156
- self,
157
- bot: Bot,
158
- group: Target,
159
- players: set[str],
160
- interface: Interface,
161
- ) -> None:
159
+ def __init__(self, bot: Bot, group: Target) -> None:
162
160
  self.bot = bot
163
161
  self.group = group
164
- self.players = init_players(bot, self, players)
165
- self.interface = interface
166
162
  self.state = GameState(0)
167
163
  self.killed_players = []
168
- self._player_map = {p.user_id: p for p in self.players}
164
+ self._player_map: dict[str, Player] = {}
165
+ self._shuffled: list[Player] = []
169
166
  self._scene = None
170
- self._finished = None
171
- self._task_group = None
172
- self._send_handler = _SendHandler()
173
- self._send_handler.update(group)
167
+ self._finished = self._task_group = None
168
+ self._send_handler = _SendHandler(group, bot)
174
169
 
175
- async def _fetch_group_scene(self) -> None:
176
- scene = await self.interface.get_scene(SceneType.GROUP, self.group_id)
177
- if scene is None:
178
- scene = await self.interface.get_scene(SceneType.GUILD, self.group_id)
170
+ @final
171
+ @classmethod
172
+ async def new(
173
+ cls,
174
+ bot: Bot,
175
+ group: Target,
176
+ players: set[str],
177
+ interface: Interface,
178
+ ) -> Self:
179
+ self = cls(bot, group)
180
+
181
+ self._scene = await interface.get_scene(SceneType.GROUP, self.group_id)
182
+ if self._scene is None:
183
+ self._scene = await interface.get_scene(SceneType.GUILD, self.group_id)
184
+
185
+ self.players = await init_players(bot, self, players, interface)
186
+ self._player_map |= {p.user_id: p for p in self.players}
187
+ self._shuffled = self.players.shuffled
179
188
 
180
- self._scene = scene
189
+ return self
181
190
 
182
191
  @functools.cached_property
183
192
  def group_id(self) -> str:
@@ -232,6 +241,10 @@ class Game:
232
241
  if not w.size:
233
242
  raise GameFinished(GameStatus.GOODGUY)
234
243
 
244
+ @property
245
+ def behavior(self) -> GameBehavior:
246
+ return GameBehavior.get()
247
+
235
248
  async def notify_player_role(self) -> None:
236
249
  msg = UniMessage()
237
250
  for p in sorted(self.players, key=lambda p: p.user_id):
@@ -244,13 +257,10 @@ class Game:
244
257
  .text(f"职业分配: 狼人x{w}, 神职x{p}, 平民x{c}")
245
258
  )
246
259
 
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
-
260
+ if self.behavior.show_roles_list_on_start:
252
261
  msg.text("\n\n📚职业列表:\n")
253
- for role, cnt in role_cnt.items():
262
+ counter = Counter(p.role for p in self.players)
263
+ for role, cnt in sorted(counter.items(), key=lambda x: x[0].value):
254
264
  msg.text(f"- {ROLE_EMOJI[role]}{ROLE_NAME_CONV[role]}x{cnt}\n")
255
265
 
256
266
  async with anyio.create_task_group() as tg:
@@ -264,7 +274,7 @@ class Game:
264
274
  timeout_secs: float | None = None,
265
275
  ) -> None:
266
276
  if timeout_secs is None:
267
- timeout_secs = GameBehavior.get().timeout.speak
277
+ timeout_secs = self.behavior.timeout.speak
268
278
  with anyio.move_on_after(timeout_secs):
269
279
  async with anyio.create_task_group() as tg:
270
280
  for p in players:
@@ -280,18 +290,18 @@ class Game:
280
290
  await player.post_kill()
281
291
  if player.kill_info is None:
282
292
  continue
283
-
284
293
  self.killed_players.append((player.name, player.kill_info))
285
- shooter = self.state.shoot
294
+
295
+ shooter = self.state.shooter
286
296
  if shooter is not None and (shoot := shooter.selected) is not None:
287
297
  await self.send(
288
298
  UniMessage.text("🔫玩家 ")
289
299
  .at(shoot.user_id)
290
300
  .text(f" 被{shooter.name}射杀, 请发表遗言\n")
291
- .text(GameBehavior.get().timeout.speak_timeout_prompt)
301
+ .text(self.behavior.timeout.speak_timeout_prompt)
292
302
  )
293
303
  await self.wait_stop(shoot)
294
- self.state.shoot = shooter.selected = None
304
+ self.state.shooter = shooter.selected = None
295
305
  await self.post_kill(shoot)
296
306
 
297
307
  async def run_night(self, players: PlayerSet) -> Player | None:
@@ -324,25 +334,27 @@ class Game:
324
334
  return killed
325
335
 
326
336
  async def run_discussion(self) -> None:
327
- behavior = GameBehavior.get()
328
- timeout = behavior.timeout
337
+ timeout = self.behavior.timeout
329
338
 
330
- if not behavior.speak_in_turn:
331
- speak_timeout = timeout.group_speak
339
+ if not self.behavior.speak_in_turn:
332
340
  await self.send(
333
341
  f"💬接下来开始自由讨论\n{timeout.group_speak_timeout_prompt}",
334
342
  stop_btn_label="结束发言",
335
343
  )
336
- await self.wait_stop(*self.players.alive(), timeout_secs=speak_timeout)
344
+ await self.wait_stop(
345
+ *self.players.alive(),
346
+ timeout_secs=timeout.group_speak,
347
+ )
337
348
  else:
338
- await self.send("💬接下来开始自由讨论")
339
- speak_timeout = timeout.speak
340
- for player in self.players.alive().sorted:
349
+ await self.send("💬接下来开始轮流发言")
350
+ for player in filter(lambda p: p.alive, self._shuffled):
341
351
  await self.send(
342
- f"💬轮到你发言\n{timeout.speak_timeout_prompt}",
352
+ UniMessage.text("💬")
353
+ .at(player.user_id)
354
+ .text(f"\n轮到你发言\n{timeout.speak_timeout_prompt}"),
343
355
  stop_btn_label="结束发言",
344
356
  )
345
- await self.wait_stop(player, timeout_secs=speak_timeout)
357
+ await self.wait_stop(player, timeout_secs=timeout.speak)
346
358
  await self.send("💬所有玩家发言结束")
347
359
 
348
360
  async def run_vote(self) -> None:
@@ -395,7 +407,7 @@ class Game:
395
407
 
396
408
  # 仅有一名玩家票数最高
397
409
  voted = vs.pop()
398
- if not await voted.kill(KillReason.VOTE, *vote_result[voted]):
410
+ if await voted.kill(KillReason.VOTE, *vote_result[voted]) is None:
399
411
  # 投票放逐失败 (例: 白痴)
400
412
  return
401
413
 
@@ -404,7 +416,7 @@ class Game:
404
416
  UniMessage.text("🔨玩家 ")
405
417
  .at(voted.user_id)
406
418
  .text(" 被投票放逐, 请发表遗言\n")
407
- .text(GameBehavior.get().timeout.speak_timeout_prompt),
419
+ .text(self.behavior.timeout.speak_timeout_prompt),
408
420
  stop_btn_label="结束发言",
409
421
  )
410
422
  await self.wait_stop(voted)
@@ -445,7 +457,7 @@ class Game:
445
457
  UniMessage.text("⚙️当前为第一天\n请被狼人杀死的 ")
446
458
  .at(killed.user_id)
447
459
  .text(" 发表遗言\n")
448
- .text(GameBehavior.get().timeout.speak_timeout_prompt),
460
+ .text(self.behavior.timeout.speak_timeout_prompt),
449
461
  stop_btn_label="结束发言",
450
462
  )
451
463
  await self.wait_stop(killed)
@@ -476,7 +488,7 @@ class Game:
476
488
  msg.at(p.user_id).text(f": {p.role_name}\n")
477
489
  await self.send(msg)
478
490
 
479
- report: list[str] = ["📌玩家死亡报告:"]
491
+ report = ["📌玩家死亡报告:"]
480
492
  for name, info in self.killed_players:
481
493
  emoji, action = REPORT_TEXT[info.reason]
482
494
  report.append(f"{emoji} {name} 被 {', '.join(info.killers)} {action}")
@@ -491,24 +503,21 @@ class Game:
491
503
  await self.handle_game_finish(result.status)
492
504
  logger.info(f"{self.colored_name} 的狼人杀游戏进程正常退出")
493
505
  except Exception as err:
494
- msg = f"{self.colored_name} 的狼人杀游戏进程出现未知错误: {err!r}"
495
- logger.exception(msg)
506
+ logger.exception(f"{self.colored_name} 的狼人杀游戏进程出现未知错误")
496
507
  await self.send(f"❌狼人杀游戏进程出现未知错误: {err!r}")
497
508
  finally:
498
509
  if self._finished is not None:
499
510
  self._finished.set()
500
511
 
501
512
  async def start(self) -> None:
502
- await self._fetch_group_scene()
503
513
  self._finished = anyio.Event()
504
514
  dead_channel = DeadChannel(self.players, self._finished)
505
515
  get_running_games().add(self)
506
516
 
507
517
  try:
508
- async with anyio.create_task_group() as tg:
509
- self._task_group = tg
510
- tg.start_soon(self.run_daemon)
511
- tg.start_soon(dead_channel.run)
518
+ async with anyio.create_task_group() as self._task_group:
519
+ self._task_group.start_soon(self.run_daemon)
520
+ self._task_group.start_soon(dead_channel.run)
512
521
  except anyio.get_cancelled_exc_class():
513
522
  logger.warning(f"{self.colored_name} 的狼人杀游戏进程被取消")
514
523
  except Exception as err:
@@ -518,7 +527,7 @@ class Game:
518
527
  self._finished = None
519
528
  self._task_group = None
520
529
  get_running_games().discard(self)
521
- InputStore.cleanup(list(self._player_map), self.group_id)
530
+ InputStore.cleanup(self._player_map.keys(), self.group_id)
522
531
 
523
532
  def terminate(self) -> None:
524
533
  if self._task_group is not None:
@@ -44,7 +44,7 @@ alc = Alconna(
44
44
  help_text="设置游戏开始时是否显示职业列表",
45
45
  ),
46
46
  Subcommand(
47
- "speak_order",
47
+ "speak_in_turn",
48
48
  Args["enabled#是否启用", bool],
49
49
  alias={"发言顺序"},
50
50
  help_text="设置是否按顺序发言",
@@ -55,6 +55,12 @@ alc = Alconna(
55
55
  alias={"死亡聊天"},
56
56
  help_text="设置死亡玩家发言频率限制",
57
57
  ),
58
+ Subcommand(
59
+ "werewolf_multi_select",
60
+ Args["enabled#是否启用", bool],
61
+ alias={"狼人多选"},
62
+ help_text="设置狼人多选时是否从已选玩家中随机选择目标, 为否时将视为空刀",
63
+ ),
58
64
  Subcommand(
59
65
  "timeout",
60
66
  Subcommand(
@@ -122,7 +128,7 @@ async def set_show_roles(behavior: Behavior, enabled: bool) -> None:
122
128
  await finish(f"已{'启用' if enabled else '禁用'}游戏开始时显示职业列表")
123
129
 
124
130
 
125
- @edit_behavior.assign("speak_order")
131
+ @edit_behavior.assign("speak_in_turn")
126
132
  async def set_speak_order(behavior: Behavior, enabled: bool) -> None:
127
133
  behavior.speak_in_turn = enabled
128
134
  await finish(f"已{'启用' if enabled else '禁用'}按顺序发言")
@@ -136,6 +142,15 @@ async def set_dead_chat(behavior: Behavior, limit: int) -> None:
136
142
  await finish(f"已设置死亡玩家发言限制为 {limit} 次/分钟")
137
143
 
138
144
 
145
+ @edit_behavior.assign("werewolf_multi_select")
146
+ async def set_werewolf_multi_select(behavior: Behavior, enabled: bool) -> None:
147
+ behavior.werewolf_multi_select = enabled
148
+ await finish(
149
+ f"已{'启用' if enabled else '禁用'}狼人多选\n"
150
+ "注: 狼人意见未统一时随机选择已选玩家"
151
+ )
152
+
153
+
139
154
  @edit_behavior.assign("timeout.prepare")
140
155
  async def set_prepare_timeout(behavior: Behavior, time: int) -> None:
141
156
  if time < 300:
@@ -193,13 +208,14 @@ async def handle_default(behavior: Behavior) -> None:
193
208
  f"游戏开始发送职业列表: {'是' if behavior.show_roles_list_on_start else '否'}",
194
209
  f"白天讨论按顺序发言: {'是' if behavior.speak_in_turn else '否'}",
195
210
  f"死亡玩家发言转发限制: {behavior.dead_channel_rate_limit} 次/分钟",
211
+ f"狼人多选(意见未统一时随机选择已选玩家): {'是' if behavior.werewolf_multi_select else '否'}", # noqa: E501
196
212
  "",
197
213
  "超时时间设置:",
198
214
  f"准备阶段: {timeout.prepare} 秒",
199
215
  f"个人发言: {timeout.speak} 秒",
200
216
  f"集体发言: {timeout.group_speak} 秒",
201
- f"交互阶段: {timeout.interact} 秒",
202
217
  f"投票阶段: {timeout.vote} 秒",
218
+ f"交互阶段: {timeout.interact} 秒",
203
219
  f"狼人交互: {timeout.werewolf} 秒",
204
220
  ]
205
221
  await finish("\n".join(lines))
@@ -76,7 +76,7 @@ with contextlib.suppress(ImportError):
76
76
  user_id=user_id,
77
77
  group_id=(event.guild and event.guild.id) or event.channel.id,
78
78
  )
79
- and any(target.verify(group) for group in get_starting_games())
79
+ and target in get_starting_games()
80
80
  )
81
81
 
82
82
  @on_message(rule=_rule_poke_join).handle()
@@ -86,7 +86,7 @@ with contextlib.suppress(ImportError):
86
86
  target: MsgTarget,
87
87
  ) -> None:
88
88
  user_id = extract_poke_tome(event) or event.get_user_id()
89
- players = next(p for g, p in get_starting_games().items() if target.verify(g))
89
+ players = get_starting_games()[target]
90
90
 
91
91
  if user_id not in players:
92
92
  # XXX:
@@ -50,7 +50,7 @@ with contextlib.suppress(ImportError):
50
50
  return (
51
51
  (event.target_id == event.self_id)
52
52
  and not user_in_game(bot.self_id, user_id, group_id)
53
- and any(target.verify(group) for group in get_starting_games())
53
+ and target in get_starting_games()
54
54
  )
55
55
 
56
56
  @on_notice(rule=_rule_poke_join).handle()
@@ -60,7 +60,7 @@ with contextlib.suppress(ImportError):
60
60
  target: MsgTarget,
61
61
  ) -> None:
62
62
  user_id = event.get_user_id()
63
- players = next(p for g, p in get_starting_games().items() if target.verify(g))
63
+ players = get_starting_games()[target]
64
64
 
65
65
  if event.group_id is None or user_id in players:
66
66
  return
@@ -118,14 +118,15 @@ class PrepareGame:
118
118
  self.send_handler = self._SendHandler()
119
119
  self.logger = nonebot.logger.opt(colors=True)
120
120
  self.shoud_start_game = False
121
- self._msg_handler: dict[str, Callable[[], Awaitable[bool | None]]] = {
121
+ get_starting_games()[self.group] = self.players
122
+
123
+ self._handlers: dict[str, Callable[[], Awaitable[bool | None]]] = {
122
124
  "开始游戏": self._handle_start,
123
125
  "结束游戏": self._handle_end,
124
126
  "加入游戏": self._handle_join,
125
127
  "退出游戏": self._handle_quit,
126
128
  "当前玩家": self._handle_list,
127
129
  }
128
- get_starting_games()[self.group] = self.players
129
130
 
130
131
  async def run(self) -> None:
131
132
  try:
@@ -163,6 +164,33 @@ class PrepareGame:
163
164
  if event is not None:
164
165
  await self.stream.send((event, text, name))
165
166
 
167
+ async def _handle(self) -> None:
168
+ bot = current_bot.get()
169
+ superuser = SuperUser()
170
+
171
+ while not self.stream.closed:
172
+ event, text, name = await self.stream.recv()
173
+ user_id = event.get_user_id()
174
+ colored = f"<y>{escape_tag(name)}</y>(<c>{escape_tag(user_id)}</c>)"
175
+ self.current = self._Current(
176
+ id=user_id,
177
+ name=name,
178
+ colored=colored,
179
+ is_admin=user_id == self.admin_id,
180
+ is_super_user=await superuser(bot, event),
181
+ )
182
+ self.send_handler.update(event)
183
+
184
+ # 更新用户名
185
+ # 当用户通过 chronoca:poke 加入游戏时, 插件无法获取用户名, 原字典值为用户ID
186
+ if user_id in self.players and self.players.get(user_id) != name:
187
+ self.logger.debug(f"更新玩家显示名称: {self.current.colored}")
188
+ self.players[user_id] = name
189
+
190
+ handler = self._handlers.get(text)
191
+ if handler is not None and await handler():
192
+ return
193
+
166
194
  async def _send(self, msg: str | UniMessage) -> None:
167
195
  await self.send_handler.send(msg)
168
196
 
@@ -240,33 +268,6 @@ class PrepareGame:
240
268
  )
241
269
  await self._send("✨当前玩家:\n" + "\n".join(lines))
242
270
 
243
- async def _handle(self) -> None:
244
- bot = current_bot.get()
245
- superuser = SuperUser()
246
-
247
- while not self.stream.closed:
248
- event, text, name = await self.stream.recv()
249
- user_id = event.get_user_id()
250
- colored = f"<y>{escape_tag(name)}</y>(<c>{escape_tag(user_id)}</c>)"
251
- self.current = self._Current(
252
- id=user_id,
253
- name=name,
254
- colored=colored,
255
- is_admin=user_id == self.admin_id,
256
- is_super_user=await superuser(bot, event),
257
- )
258
- self.send_handler.update(event)
259
-
260
- # 更新用户名
261
- # 当用户通过 chronoca:poke 加入游戏时, 插件无法获取用户名, 原字典值为用户ID
262
- if user_id in self.players and self.players.get(user_id) != name:
263
- self.logger.debug(f"更新玩家显示名称: {self.current.colored}")
264
- self.players[user_id] = name
265
-
266
- handler = self._msg_handler.get(text)
267
- if handler is not None and await handler():
268
- return
269
-
270
271
 
271
272
  @start_game.handle()
272
273
  async def handle_notice(target: MsgTarget) -> None:
@@ -328,5 +329,5 @@ async def handle_start(
328
329
  await UniMessage.text("⚠️游戏准备超时,已自动结束").finish()
329
330
 
330
331
  dump_players(target, players)
331
- game = Game(bot, target, set(players), interface)
332
+ game = await Game.new(bot, target, set(players), interface)
332
333
  await game.start()
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
5
5
  import anyio
6
6
 
7
7
  if TYPE_CHECKING:
8
- from .players import Player
8
+ from .player import Player
9
9
 
10
10
 
11
11
  class Role(int, Enum):
@@ -57,11 +57,13 @@ class GameState:
57
57
  """当前天数记录, 不会被 `reset()` 重置"""
58
58
  state: State = State.NIGHT
59
59
  """当前游戏状态, 不会被 `reset()` 重置"""
60
+ _werewolf_interact_count: int = 0
61
+ """内部属性, 记录当晚狼人交互状态"""
60
62
  werewolf_finished: anyio.Event = dataclasses.field(default_factory=anyio.Event)
61
63
  """狼人交互是否结束"""
62
64
  killed: "Player | None" = None
63
65
  """当晚狼人击杀目标, `None` 则为空刀"""
64
- shoot: "Player | None" = None
66
+ shooter: "Player | None" = None
65
67
  """当前执行射杀操作的玩家"""
66
68
  antidote: set["Player"] = dataclasses.field(default_factory=set)
67
69
  """当晚女巫使用解药的目标"""
@@ -72,12 +74,22 @@ class GameState:
72
74
 
73
75
  def reset(self) -> None:
74
76
  self.werewolf_finished = anyio.Event()
77
+ self._werewolf_interact_count = 0
75
78
  self.killed = None
76
- self.shoot = None
79
+ self.shooter = None
77
80
  self.antidote = set()
78
81
  self.poison = set()
79
82
  self.protected = set()
80
83
 
84
+ def werewolf_start(self) -> None:
85
+ self._werewolf_interact_count += 1
86
+
87
+ def werewolf_end(self) -> bool:
88
+ self._werewolf_interact_count -= 1
89
+ if self._werewolf_interact_count == 0:
90
+ self.werewolf_finished.set()
91
+ return self.werewolf_finished.is_set()
92
+
81
93
 
82
94
  @dataclasses.dataclass
83
95
  class KillInfo: