nonebot-plugin-werewolf 1.1.6__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 +76 -18
  3. nonebot_plugin_werewolf/constant.py +54 -46
  4. nonebot_plugin_werewolf/exception.py +2 -4
  5. nonebot_plugin_werewolf/game.py +193 -166
  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 +93 -29
  25. nonebot_plugin_werewolf/players/prophet.py +11 -10
  26. nonebot_plugin_werewolf/players/werewolf.py +63 -31
  27. nonebot_plugin_werewolf/players/witch.py +29 -12
  28. nonebot_plugin_werewolf/players/wolfking.py +1 -1
  29. nonebot_plugin_werewolf/utils.py +106 -7
  30. {nonebot_plugin_werewolf-1.1.6.dist-info → nonebot_plugin_werewolf-1.1.8.dist-info}/METADATA +27 -20
  31. nonebot_plugin_werewolf-1.1.8.dist-info/RECORD +35 -0
  32. {nonebot_plugin_werewolf-1.1.6.dist-info → nonebot_plugin_werewolf-1.1.8.dist-info}/WHEEL +1 -1
  33. nonebot_plugin_werewolf-1.1.6.dist-info/RECORD +0 -34
  34. {nonebot_plugin_werewolf-1.1.6.dist-info → nonebot_plugin_werewolf-1.1.8.dist-info}/LICENSE +0 -0
  35. {nonebot_plugin_werewolf-1.1.6.dist-info → nonebot_plugin_werewolf-1.1.8.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,8 @@
1
- from nonebot_plugin_alconna.uniseg import UniMessage
2
1
  from typing_extensions import override
3
2
 
4
- from ..constant import STOP_COMMAND_PROMPT
3
+ from nonebot_plugin_alconna.uniseg import UniMessage
4
+
5
+ from ..constant import stop_command_prompt
5
6
  from ..models import KillReason
6
7
  from .player import Player
7
8
 
@@ -9,7 +10,7 @@ from .player import Player
9
10
  class CanShoot(Player):
10
11
  @override
11
12
  async def post_kill(self) -> None:
12
- if self.kill_info and self.kill_info.reason == KillReason.Poison:
13
+ if self.kill_info and self.kill_info.reason == KillReason.POISON:
13
14
  await self.send("⚠️你昨晚被女巫毒杀,无法使用技能")
14
15
  return await super().post_kill()
15
16
 
@@ -21,33 +22,32 @@ class CanShoot(Player):
21
22
 
22
23
  self.game.state.shoot = None
23
24
  shoot = await self.shoot()
24
-
25
+ msg = UniMessage.text("玩家 ").at(self.user_id).text(" ")
25
26
  if shoot is not None:
26
27
  self.game.state.shoot = self
27
- await self.send(
28
- UniMessage.text(f"🔫{self.role_name} ")
29
- .at(self.user_id)
30
- .text(" 射杀了玩家 ")
31
- .at(shoot.user_id)
32
- )
33
- await shoot.kill(KillReason.Shoot, self)
28
+ await self.game.send("🔫" + msg.text("射杀了玩家 ").at(shoot.user_id))
29
+ await shoot.kill(KillReason.SHOOT, self)
34
30
  self.selected = shoot
35
31
  else:
36
- await self.send(f"ℹ️{self.role_name}选择了取消技能")
32
+ await self.game.send("ℹ️" + msg.text("选择了取消技能"))
33
+
37
34
  return await super().post_kill()
38
35
 
39
36
  async def shoot(self) -> Player | None:
40
37
  players = self.game.players.alive().exclude(self)
41
38
  await self.send(
42
39
  "💫请选择需要射杀的玩家:\n"
43
- + players.show()
44
- + "\n\n🔫发送编号选择玩家"
45
- + f"\n❌发送 “{STOP_COMMAND_PROMPT}” 取消技能"
40
+ f"{players.show()}\n\n"
41
+ "🔫发送编号选择玩家\n"
42
+ f"❌发送 “{stop_command_prompt()}” 取消技能",
43
+ stop_btn_label="取消技能",
44
+ select_players=players,
46
45
  )
47
46
 
48
47
  if selected := await self._select_player(
49
48
  players,
50
49
  on_stop="ℹ️已取消技能,回合结束",
50
+ stop_btn_label="取消技能",
51
51
  ):
52
52
  await self.send(f"🎯选择射杀的玩家: {selected.name}")
53
53
 
@@ -2,6 +2,6 @@ from ..models import Role, RoleGroup
2
2
  from .player import Player
3
3
 
4
4
 
5
- @Player.register_role(Role.Civilian, RoleGroup.GoodGuy)
5
+ @Player.register_role(Role.CIVILIAN, RoleGroup.GOODGUY)
6
6
  class Civilian(Player):
7
7
  pass
@@ -1,31 +1,33 @@
1
- from nonebot_plugin_alconna.uniseg import UniMessage
2
1
  from typing_extensions import override
3
2
 
4
- from ..constant import STOP_COMMAND_PROMPT
5
- from ..models import Role, RoleGroup
3
+ from ..constant import stop_command_prompt
4
+ from ..models import GameState, Role, RoleGroup
6
5
  from .player import Player
7
6
 
8
7
 
9
- @Player.register_role(Role.Guard, RoleGroup.GoodGuy)
8
+ @Player.register_role(Role.GUARD, RoleGroup.GOODGUY)
10
9
  class Guard(Player):
11
10
  @override
12
11
  async def _check_selected(self, player: Player) -> Player | None:
13
- if player is not self.selected:
14
- return player
15
- await self.send("⚠️守卫不能连续两晚保护同一目标,请重新选择")
16
- return None
12
+ if self.game.state.state == GameState.State.NIGHT and player is self.selected:
13
+ await self.send("⚠️守卫不能连续两晚保护同一目标,请重新选择")
14
+ return None
15
+
16
+ return player
17
17
 
18
18
  @override
19
- async def interact(self) -> None:
19
+ async def _interact(self) -> None:
20
20
  players = self.game.players.alive()
21
21
  await self.send(
22
- UniMessage.text("💫请选择需要保护的玩家:\n")
23
- .text(players.show())
24
- .text("\n\n🛡️发送编号选择玩家")
25
- .text(f"\n❌发送 “{STOP_COMMAND_PROMPT}” 结束回合")
22
+ "💫请选择需要保护的玩家:\n"
23
+ f"{players.show()}\n\n"
24
+ "🛡️发送编号选择玩家\n"
25
+ f"❌发送 “{stop_command_prompt()}” 结束回合",
26
+ stop_btn_label="结束回合",
27
+ select_players=players,
26
28
  )
27
29
 
28
- self.selected = await self._select_player(players)
30
+ self.selected = await self._select_player(players, stop_btn_label="结束回合")
29
31
  if self.selected:
30
32
  self.game.state.protected.add(self.selected)
31
33
  await self.send(f"✅本回合保护的玩家: {self.selected.name}")
@@ -3,6 +3,6 @@ from .can_shoot import CanShoot
3
3
  from .player import Player
4
4
 
5
5
 
6
- @Player.register_role(Role.Hunter, RoleGroup.GoodGuy)
6
+ @Player.register_role(Role.HUNTER, RoleGroup.GOODGUY)
7
7
  class Hunter(CanShoot, Player):
8
8
  pass
@@ -1,9 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
+ from typing_extensions import override
4
5
 
5
6
  from nonebot_plugin_alconna.uniseg import UniMessage
6
- from typing_extensions import override
7
7
 
8
8
  from ..models import KillReason, Role, RoleGroup
9
9
  from .player import Player
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
12
12
  from ..player_set import PlayerSet
13
13
 
14
14
 
15
- @Player.register_role(Role.Idiot, RoleGroup.GoodGuy)
15
+ @Player.register_role(Role.IDIOT, RoleGroup.GOODGUY)
16
16
  class Idiot(Player):
17
17
  voted: bool = False
18
18
 
@@ -25,7 +25,7 @@ class Idiot(Player):
25
25
 
26
26
  @override
27
27
  async def kill(self, reason: KillReason, *killers: Player) -> bool:
28
- if reason == KillReason.Vote and not self.voted:
28
+ if reason == KillReason.VOTE and not self.voted:
29
29
  self.voted = True
30
30
  await self.game.send(
31
31
  UniMessage.text("⚙️玩家")
@@ -1,5 +1,4 @@
1
1
  from typing import TYPE_CHECKING
2
-
3
2
  from typing_extensions import override
4
3
 
5
4
  from ..exception import GameFinished
@@ -7,8 +6,8 @@ from ..models import GameStatus, KillReason, Role, RoleGroup
7
6
  from .player import Player
8
7
 
9
8
 
10
- @Player.register_role(Role.Joker, RoleGroup.Others)
11
- class Joker(Player):
9
+ @Player.register_role(Role.JESTER, RoleGroup.OTHERS)
10
+ class Jester(Player):
12
11
  @override
13
12
  async def notify_role(self) -> None:
14
13
  await super().notify_role()
@@ -17,9 +16,9 @@ class Joker(Player):
17
16
  @override
18
17
  async def kill(self, reason: KillReason, *killers: Player) -> bool:
19
18
  await super().kill(reason, *killers)
20
- if reason == KillReason.Vote:
19
+ if reason == KillReason.VOTE:
21
20
  if TYPE_CHECKING:
22
21
  assert self.kill_info is not None
23
22
  self.game.killed_players.append((self.name, self.kill_info))
24
- raise GameFinished(GameStatus.Joker)
23
+ raise GameFinished(GameStatus.JESTER)
25
24
  return True
@@ -4,15 +4,23 @@ from collections.abc import Callable
4
4
  from typing import TYPE_CHECKING, ClassVar, Final, TypeVar, final
5
5
 
6
6
  import anyio
7
+ import nonebot
7
8
  from nonebot.adapters import Bot
8
- from nonebot.log import logger
9
9
  from nonebot.utils import escape_tag
10
10
  from nonebot_plugin_alconna.uniseg import Receipt, Target, UniMessage
11
11
  from nonebot_plugin_uninfo import SceneType
12
12
 
13
- from ..constant import STOP_COMMAND, STOP_COMMAND_PROMPT, role_emoji, role_name_conv
13
+ from ..config import GameBehavior
14
+ from ..constant import ROLE_EMOJI, ROLE_NAME_CONV, STOP_COMMAND, stop_command_prompt
14
15
  from ..models import KillInfo, KillReason, Role, RoleGroup
15
- from ..utils import InputStore, check_index, link
16
+ from ..utils import (
17
+ InputStore,
18
+ SendHandler,
19
+ add_players_button,
20
+ add_stop_button,
21
+ check_index,
22
+ link,
23
+ )
16
24
 
17
25
  if TYPE_CHECKING:
18
26
  from ..game import Game
@@ -21,6 +29,19 @@ if TYPE_CHECKING:
21
29
 
22
30
  _P = TypeVar("_P", bound=type["Player"])
23
31
 
32
+ logger = nonebot.logger.opt(colors=True)
33
+
34
+
35
+ class _SendHandler(SendHandler[str | None]):
36
+ def solve_msg(
37
+ self,
38
+ msg: UniMessage,
39
+ stop_btn_label: str | None = None,
40
+ ) -> UniMessage:
41
+ if stop_btn_label is not None:
42
+ msg = add_stop_button(msg, stop_btn_label)
43
+ return msg
44
+
24
45
 
25
46
  class Player:
26
47
  __player_class: ClassVar[dict[Role, type["Player"]]] = {}
@@ -45,6 +66,8 @@ class Player:
45
66
  self.bot = bot
46
67
  self.killed = anyio.Event()
47
68
  self._member = None
69
+ self._send_handler = _SendHandler()
70
+ self._send_handler.update(self.__user, bot)
48
71
 
49
72
  @classmethod
50
73
  def register_role(cls, role: Role, role_group: RoleGroup, /) -> Callable[[_P], _P]:
@@ -65,9 +88,7 @@ class Player:
65
88
  return cls.__player_class[role](bot, game, user_id)
66
89
 
67
90
  def __repr__(self) -> str:
68
- return (
69
- f"<Player {self.role_name}: user={self.user_id!r} " f"alive={self.alive}>"
70
- )
91
+ return f"<Player {self.role_name}: user={self.user_id!r} alive={self.alive}>"
71
92
 
72
93
  @property
73
94
  def game(self) -> "Game":
@@ -81,7 +102,7 @@ class Player:
81
102
 
82
103
  @functools.cached_property
83
104
  def role_name(self) -> str:
84
- return role_name_conv[self.role]
105
+ return ROLE_NAME_CONV[self.role]
85
106
 
86
107
  async def _fetch_member(self) -> None:
87
108
  member = await self.game.interface.get_member(
@@ -126,41 +147,75 @@ class Player:
126
147
  return name
127
148
 
128
149
  @final
129
- def _log(self, text: str) -> None:
150
+ def log(self, text: str) -> None:
130
151
  text = text.replace("\n", "\\n")
131
- logger.opt(colors=True).info(
132
- f"{self.game.colored_name} | "
133
- f"[<b><m>{self.role_name}</m></b>] "
134
- f"{self.colored_name} | {text}",
135
- )
152
+ self.game.log(f"[<b><m>{self.role_name}</m></b>] {self.colored_name} | {text}")
136
153
 
137
154
  @final
138
- async def send(self, message: str | UniMessage) -> Receipt:
155
+ async def send(
156
+ self,
157
+ message: str | UniMessage,
158
+ *,
159
+ stop_btn_label: str | None = None,
160
+ select_players: "PlayerSet | None" = None,
161
+ skip_handler: bool = False,
162
+ ) -> Receipt:
139
163
  if isinstance(message, str):
140
164
  message = UniMessage.text(message)
141
165
 
142
- self._log(f"<g>Send</g> | {escape_tag(str(message))}")
143
- return await message.send(target=self.__user, bot=self.bot)
166
+ self.log(f"<g>Send</g> | {escape_tag(str(message))}")
144
167
 
145
- @final
146
- async def receive(self, prompt: str | UniMessage | None = None) -> UniMessage:
147
- if prompt:
148
- await self.send(prompt)
168
+ if select_players:
169
+ message = add_players_button(message, select_players)
170
+ if skip_handler:
171
+ return await message.send(self.__user, self.bot)
172
+ return await self._send_handler.send(message, stop_btn_label)
149
173
 
174
+ @final
175
+ async def receive(self) -> UniMessage:
150
176
  result = await InputStore.fetch(self.user_id)
151
- self._log(f"<y>Recv</y> | {escape_tag(str(result))}")
177
+ self.log(f"<y>Recv</y> | {escape_tag(str(result))}")
152
178
  return result
153
179
 
154
180
  @final
155
181
  async def receive_text(self) -> str:
156
182
  return (await self.receive()).extract_plain_text()
157
183
 
158
- async def interact(self) -> None:
184
+ @property
185
+ def interact_timeout(self) -> float:
186
+ return GameBehavior.get().timeout.interact
187
+
188
+ async def _before_interact(self) -> None:
159
189
  return
160
190
 
191
+ async def _interact(self) -> None:
192
+ return
193
+
194
+ async def _after_interact(self) -> None:
195
+ return
196
+
197
+ async def interact(self) -> None:
198
+ if not getattr(self._interact, "__override__", False):
199
+ await self.send("ℹ️请等待其他玩家结束交互...")
200
+ return
201
+
202
+ await self._before_interact()
203
+
204
+ timeout = self.interact_timeout
205
+ await self.send(f"✏️{self.role_name}交互开始,限时 {timeout / 60:.2f} 分钟")
206
+
207
+ try:
208
+ with anyio.fail_after(timeout):
209
+ await self._interact()
210
+ except TimeoutError:
211
+ logger.debug(f"{self.role_name}交互超时 (<y>{timeout}</y>s)")
212
+ await self.send(f"⚠️{self.role_name}交互超时")
213
+
214
+ await self._after_interact()
215
+
161
216
  async def notify_role(self) -> None:
162
217
  await self._fetch_member()
163
- await self.send(f"⚙️你的身份: {role_emoji[self.role]}{self.role_name}")
218
+ await self.send(f"⚙️你的身份: {ROLE_EMOJI[self.role]}{self.role_name}")
164
219
 
165
220
  async def kill(self, reason: KillReason, *killers: "Player") -> bool:
166
221
  if self.alive:
@@ -173,13 +228,17 @@ class Player:
173
228
 
174
229
  async def vote(self, players: "PlayerSet") -> "Player | None":
175
230
  await self.send(
176
- f"💫请选择需要投票的玩家:\n{players.show()}"
177
- f"\n\n🗳️发送编号选择玩家\n❌发送 “{STOP_COMMAND_PROMPT}” 弃票"
178
- f"\n\n限时1分钟,超时将视为弃票"
231
+ f"💫请选择需要投票的玩家:\n"
232
+ f"{players.show()}\n\n"
233
+ "🗳️发送编号选择玩家\n"
234
+ f"❌发送 “{stop_command_prompt()}” 弃票\n\n"
235
+ "限时1分钟,超时将视为弃票",
236
+ stop_btn_label="弃票",
237
+ select_players=players,
179
238
  )
180
239
 
181
240
  try:
182
- with anyio.fail_after(60):
241
+ with anyio.fail_after(GameBehavior.get().timeout.vote):
183
242
  selected = await self._select_player(
184
243
  players,
185
244
  on_stop="⚠️你选择了弃票",
@@ -202,10 +261,11 @@ class Player:
202
261
  *,
203
262
  on_stop: str | None = None,
204
263
  on_index_error: str | None = None,
264
+ stop_btn_label: str | None = None,
205
265
  ) -> "Player | None":
206
266
  on_stop = on_stop or "ℹ️你选择了取消,回合结束"
207
267
  on_index_error = (
208
- on_index_error or f"⚠️输入错误: 请发送玩家编号或 “{STOP_COMMAND_PROMPT}”"
268
+ on_index_error or f"⚠️输入错误: 请发送玩家编号或 “{stop_command_prompt()}”"
209
269
  )
210
270
  selected = None
211
271
 
@@ -217,7 +277,11 @@ class Player:
217
277
  return None
218
278
  index = check_index(text, len(players))
219
279
  if index is None:
220
- await self.send(on_index_error)
280
+ await self.send(
281
+ on_index_error,
282
+ stop_btn_label=stop_btn_label,
283
+ select_players=players,
284
+ )
221
285
  continue
222
286
  selected = await self._check_selected(players[index - 1])
223
287
 
@@ -1,23 +1,24 @@
1
- from nonebot_plugin_alconna.uniseg import UniMessage
2
1
  from typing_extensions import override
3
2
 
4
- from ..constant import STOP_COMMAND_PROMPT
3
+ from ..constant import stop_command_prompt
5
4
  from ..models import Role, RoleGroup
6
5
  from .player import Player
7
6
 
8
7
 
9
- @Player.register_role(Role.Prophet, RoleGroup.GoodGuy)
8
+ @Player.register_role(Role.PROPHET, RoleGroup.GOODGUY)
10
9
  class Prophet(Player):
11
10
  @override
12
- async def interact(self) -> None:
11
+ async def _interact(self) -> None:
13
12
  players = self.game.players.alive().exclude(self)
14
13
  await self.send(
15
- UniMessage.text("💫请选择需要查验身份的玩家:\n")
16
- .text(players.show())
17
- .text("\n\n🔮发送编号选择玩家")
18
- .text(f"\n❌发送 “{STOP_COMMAND_PROMPT}” 结束回合(不查验身份)")
14
+ "💫请选择需要查验身份的玩家:\n"
15
+ f"{players.show()}\n\n"
16
+ "🔮发送编号选择玩家\n"
17
+ f"❌发送 “{stop_command_prompt()}” 结束回合(不查验身份)",
18
+ stop_btn_label="结束回合",
19
+ select_players=players,
19
20
  )
20
21
 
21
- if selected := await self._select_player(players):
22
- result = "狼人" if selected.role_group == RoleGroup.Werewolf else "好人"
22
+ if selected := await self._select_player(players, stop_btn_label="结束回合"):
23
+ result = "狼人" if selected.role_group == RoleGroup.WEREWOLF else "好人"
23
24
  await self.send(f"✏️玩家 {selected.name} 的阵营是『{result}』")
@@ -1,10 +1,13 @@
1
+ import secrets
1
2
  from typing import TYPE_CHECKING
3
+ from typing_extensions import override
2
4
 
3
5
  import anyio
6
+ import nonebot
4
7
  from nonebot_plugin_alconna.uniseg import UniMessage
5
- from typing_extensions import override
6
8
 
7
- from ..constant import STOP_COMMAND, STOP_COMMAND_PROMPT
9
+ from ..config import GameBehavior
10
+ from ..constant import STOP_COMMAND, stop_command_prompt
8
11
  from ..models import Role, RoleGroup
9
12
  from ..utils import ObjectStream, check_index
10
13
  from .player import Player
@@ -12,24 +15,29 @@ from .player import Player
12
15
  if TYPE_CHECKING:
13
16
  from ..player_set import PlayerSet
14
17
 
18
+ logger = nonebot.logger.opt(colors=True)
19
+
15
20
 
16
- @Player.register_role(Role.Werewolf, RoleGroup.Werewolf)
21
+ @Player.register_role(Role.WEREWOLF, RoleGroup.WEREWOLF)
17
22
  class Werewolf(Player):
23
+ stream: ObjectStream[str | UniMessage]
24
+
25
+ @property
26
+ @override
27
+ def interact_timeout(self) -> float:
28
+ return GameBehavior.get().timeout.werewolf
29
+
18
30
  @override
19
31
  async def notify_role(self) -> None:
20
32
  await super().notify_role()
21
- partners = self.game.players.alive().select(RoleGroup.Werewolf).exclude(self)
33
+ partners = self.game.players.alive().select(RoleGroup.WEREWOLF).exclude(self)
22
34
  if partners:
23
35
  await self.send(
24
36
  "🐺你的队友:\n"
25
37
  + "\n".join(f" {p.role_name}: {p.name}" for p in partners)
26
38
  )
27
39
 
28
- async def _handle_interact(
29
- self,
30
- players: "PlayerSet",
31
- stream: ObjectStream[str | UniMessage],
32
- ) -> None:
40
+ async def _handle_interact(self, players: "PlayerSet") -> None:
33
41
  self.selected = None
34
42
 
35
43
  while True:
@@ -39,35 +47,40 @@ class Werewolf(Player):
39
47
  if index is not None:
40
48
  self.selected = players[index - 1]
41
49
  msg = f"当前选择玩家: {self.selected.name}"
42
- await self.send(f"🎯{msg}\n发送 “{STOP_COMMAND_PROMPT}” 结束回合")
43
- await stream.send(f"📝队友 {self.name} {msg}")
50
+ await self.send(
51
+ f"🎯{msg}\n发送 {stop_command_prompt()}” 结束回合",
52
+ stop_btn_label="结束回合",
53
+ select_players=players,
54
+ )
55
+ await self.stream.send(f"📝队友 {self.name} {msg}")
44
56
  if text == STOP_COMMAND:
45
57
  if self.selected is not None:
46
58
  await self.send("✅你已结束当前回合")
47
- await stream.send(f"📝队友 {self.name} 结束当前回合")
48
- stream.close()
59
+ await self.stream.send(f"📝队友 {self.name} 结束当前回合")
60
+ self.stream.close()
49
61
  return
50
- await self.send("⚠️当前未选择玩家,无法结束回合")
62
+ await self.send(
63
+ "⚠️当前未选择玩家,无法结束回合",
64
+ select_players=players,
65
+ )
51
66
  else:
52
- await stream.send(UniMessage.text(f"💬队友 {self.name}:\n") + input_msg)
53
-
54
- async def _handle_broadcast(
55
- self,
56
- partners: "PlayerSet",
57
- stream: ObjectStream[str | UniMessage],
58
- ) -> None:
59
- while not stream.closed:
67
+ await self.stream.send(
68
+ UniMessage.text(f"💬队友 {self.name}:\n") + input_msg
69
+ )
70
+
71
+ async def _handle_broadcast(self, partners: "PlayerSet") -> None:
72
+ while not self.stream.closed:
60
73
  try:
61
- message = await stream.recv()
74
+ message = await self.stream.recv()
62
75
  except anyio.EndOfStream:
63
76
  return
64
77
 
65
78
  await partners.broadcast(message)
66
79
 
67
80
  @override
68
- async def interact(self) -> None:
81
+ async def _interact(self) -> None:
69
82
  players = self.game.players.alive()
70
- partners = players.select(RoleGroup.Werewolf).exclude(self)
83
+ partners = players.select(RoleGroup.WEREWOLF).exclude(self)
71
84
 
72
85
  msg = UniMessage()
73
86
  if partners:
@@ -80,12 +93,31 @@ class Werewolf(Player):
80
93
  msg.text("💫请选择今晚的目标:\n")
81
94
  .text(players.show())
82
95
  .text("\n\n🔪发送编号选择玩家")
83
- .text(f"\n❌发送 “{STOP_COMMAND_PROMPT}” 结束回合")
84
- .text("\n\n⚠️意见未统一将空刀")
96
+ .text(f"\n❌发送 “{stop_command_prompt()}” 结束回合")
97
+ .text("\n\n⚠️意见未统一将空刀"),
98
+ select_players=players,
85
99
  )
86
100
 
87
- stream = ObjectStream[str | UniMessage](8)
101
+ self.stream = ObjectStream[str | UniMessage](8)
102
+
103
+ try:
104
+ async with anyio.create_task_group() as tg:
105
+ tg.start_soon(self._handle_interact, players)
106
+ tg.start_soon(self._handle_broadcast, partners)
107
+ finally:
108
+ del self.stream
109
+
110
+ @override
111
+ async def _after_interact(self) -> None:
112
+ state = self.game.state
113
+ if not state.werewolf_finished.is_set():
114
+ state.werewolf_finished.set()
115
+ w = self.game.players.alive().select(RoleGroup.WEREWOLF)
116
+ if (s := w.player_selected()).size == 1:
117
+ state.killed = s.pop()
118
+ await w.broadcast(f"🔪今晚选择的目标为: {state.killed.name}")
119
+ else:
120
+ await w.broadcast("⚠️狼人阵营意见未统一,此晚空刀")
88
121
 
89
- async with anyio.create_task_group() as tg:
90
- tg.start_soon(self._handle_interact, players, stream)
91
- tg.start_soon(self._handle_broadcast, partners, stream)
122
+ if not self.game.players.alive().select(Role.WITCH):
123
+ await anyio.sleep(5 + secrets.randbelow(15))
@@ -1,13 +1,17 @@
1
- from nonebot_plugin_alconna.uniseg import UniMessage
2
1
  from typing_extensions import override
3
2
 
4
- from ..constant import STOP_COMMAND_PROMPT
3
+ import nonebot
4
+ from nonebot_plugin_alconna.uniseg import UniMessage
5
+
6
+ from ..constant import stop_command_prompt
5
7
  from ..models import Role, RoleGroup
6
8
  from ..utils import as_player_set
7
9
  from .player import Player
8
10
 
11
+ logger = nonebot.logger.opt(colors=True)
12
+
9
13
 
10
- @Player.register_role(Role.Witch, RoleGroup.GoodGuy)
14
+ @Player.register_role(Role.WITCH, RoleGroup.GOODGUY)
11
15
  class Witch(Player):
12
16
  antidote: bool = True
13
17
  poison: bool = True
@@ -23,13 +27,18 @@ class Witch(Player):
23
27
  await self.send(msg.text("⚙️你已经用过解药了"))
24
28
  return False
25
29
 
26
- msg.text(f"✏️使用解药请发送 “1”\n❌不使用解药请发送 “{STOP_COMMAND_PROMPT}”")
27
- await self.send(msg)
30
+ msg.text(f"✏️使用解药请发送 “1”\n❌不使用解药请发送 “{stop_command_prompt()}”")
31
+ await self.send(
32
+ msg,
33
+ stop_btn_label="不使用解药",
34
+ select_players=as_player_set(killed),
35
+ )
28
36
 
29
37
  if not await self._select_player(
30
38
  as_player_set(killed),
31
39
  on_stop=f"ℹ️你选择不对 {killed.name} 使用解药",
32
- on_index_error=f"⚠️输入错误: 请输入 “1” 或 “{STOP_COMMAND_PROMPT}”",
40
+ on_index_error=f"⚠️输入错误: 请输入 “1” 或 “{stop_command_prompt()}”",
41
+ stop_btn_label="不使用解药",
33
42
  ):
34
43
  return False
35
44
 
@@ -40,7 +49,12 @@ class Witch(Player):
40
49
  return True
41
50
 
42
51
  @override
43
- async def interact(self) -> None:
52
+ async def _before_interact(self) -> None:
53
+ await self.send("ℹ️请等待狼人决定目标...")
54
+ await self.game.state.werewolf_finished.wait()
55
+
56
+ @override
57
+ async def _interact(self) -> None:
44
58
  if await self.handle_killed():
45
59
  return
46
60
 
@@ -50,16 +64,19 @@ class Witch(Player):
50
64
 
51
65
  players = self.game.players.alive()
52
66
  await self.send(
53
- UniMessage.text("💫你有一瓶毒药\n")
54
- .text("玩家列表:\n")
55
- .text(players.show())
56
- .text("\n\n🧪发送玩家编号使用毒药")
57
- .text(f"\n❌发送 “{STOP_COMMAND_PROMPT}” 结束回合(不使用药水)")
67
+ "💫你有一瓶毒药\n"
68
+ "玩家列表:\n"
69
+ f"{players.show()}\n\n"
70
+ "🧪发送玩家编号使用毒药\n"
71
+ f"❌发送 “{stop_command_prompt()}” 结束回合(不使用药水)",
72
+ stop_btn_label="结束回合",
73
+ select_players=players,
58
74
  )
59
75
 
60
76
  if selected := await self._select_player(
61
77
  players,
62
78
  on_stop="ℹ️你选择不使用毒药,回合结束",
79
+ stop_btn_label="结束回合",
63
80
  ):
64
81
  self.poison = False
65
82
  self.selected = selected