nonebot-plugin-werewolf 1.1.3__py3-none-any.whl → 1.1.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. nonebot_plugin_werewolf/__init__.py +3 -1
  2. nonebot_plugin_werewolf/config.py +18 -55
  3. nonebot_plugin_werewolf/constant.py +20 -58
  4. nonebot_plugin_werewolf/exception.py +1 -1
  5. nonebot_plugin_werewolf/game.py +286 -245
  6. nonebot_plugin_werewolf/matchers/__init__.py +2 -0
  7. nonebot_plugin_werewolf/matchers/depends.py +50 -0
  8. nonebot_plugin_werewolf/matchers/edit_preset.py +263 -0
  9. nonebot_plugin_werewolf/matchers/message_in_game.py +18 -3
  10. nonebot_plugin_werewolf/matchers/poke/__init__.py +8 -0
  11. nonebot_plugin_werewolf/matchers/poke/chronocat_poke.py +117 -0
  12. nonebot_plugin_werewolf/matchers/{ob11_ext.py → poke/ob11_poke.py} +21 -19
  13. nonebot_plugin_werewolf/matchers/start_game.py +266 -28
  14. nonebot_plugin_werewolf/matchers/superuser_ops.py +24 -0
  15. nonebot_plugin_werewolf/models.py +73 -0
  16. nonebot_plugin_werewolf/player_set.py +33 -34
  17. nonebot_plugin_werewolf/players/can_shoot.py +15 -20
  18. nonebot_plugin_werewolf/players/civilian.py +3 -3
  19. nonebot_plugin_werewolf/players/guard.py +16 -22
  20. nonebot_plugin_werewolf/players/hunter.py +3 -3
  21. nonebot_plugin_werewolf/players/idiot.py +4 -4
  22. nonebot_plugin_werewolf/players/joker.py +8 -4
  23. nonebot_plugin_werewolf/players/player.py +133 -70
  24. nonebot_plugin_werewolf/players/prophet.py +8 -15
  25. nonebot_plugin_werewolf/players/werewolf.py +54 -30
  26. nonebot_plugin_werewolf/players/witch.py +33 -38
  27. nonebot_plugin_werewolf/players/wolfking.py +3 -3
  28. nonebot_plugin_werewolf/utils.py +109 -179
  29. {nonebot_plugin_werewolf-1.1.3.dist-info → nonebot_plugin_werewolf-1.1.6.dist-info}/METADATA +78 -66
  30. nonebot_plugin_werewolf-1.1.6.dist-info/RECORD +34 -0
  31. {nonebot_plugin_werewolf-1.1.3.dist-info → nonebot_plugin_werewolf-1.1.6.dist-info}/WHEEL +1 -1
  32. nonebot_plugin_werewolf/_timeout.py +0 -110
  33. nonebot_plugin_werewolf-1.1.3.dist-info/RECORD +0 -29
  34. {nonebot_plugin_werewolf-1.1.3.dist-info → nonebot_plugin_werewolf-1.1.6.dist-info}/LICENSE +0 -0
  35. {nonebot_plugin_werewolf-1.1.3.dist-info → nonebot_plugin_werewolf-1.1.6.dist-info}/top_level.txt +0 -0
@@ -5,14 +5,14 @@ from typing import TYPE_CHECKING
5
5
  from nonebot_plugin_alconna.uniseg import UniMessage
6
6
  from typing_extensions import override
7
7
 
8
- from ..constant import KillReason, Role, RoleGroup
9
- from .player import Player, register_role
8
+ from ..models import KillReason, Role, RoleGroup
9
+ from .player import Player
10
10
 
11
11
  if TYPE_CHECKING:
12
12
  from ..player_set import PlayerSet
13
13
 
14
14
 
15
- @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
 
@@ -37,7 +37,7 @@ class Idiot(Player):
37
37
  return await super().kill(reason, *killers)
38
38
 
39
39
  @override
40
- async def vote(self, players: PlayerSet) -> tuple[Player, Player] | None:
40
+ async def vote(self, players: PlayerSet) -> Player | None:
41
41
  if self.voted:
42
42
  await self.send("ℹ️你已经发动过白痴身份的技能,无法参与本次投票")
43
43
  return None
@@ -1,11 +1,13 @@
1
+ from typing import TYPE_CHECKING
2
+
1
3
  from typing_extensions import override
2
4
 
3
- from ..constant import GameStatus, KillReason, Role, RoleGroup
4
5
  from ..exception import GameFinished
5
- from .player import Player, register_role
6
+ from ..models import GameStatus, KillReason, Role, RoleGroup
7
+ from .player import Player
6
8
 
7
9
 
8
- @register_role(Role.Joker, RoleGroup.Others)
10
+ @Player.register_role(Role.Joker, RoleGroup.Others)
9
11
  class Joker(Player):
10
12
  @override
11
13
  async def notify_role(self) -> None:
@@ -16,6 +18,8 @@ class Joker(Player):
16
18
  async def kill(self, reason: KillReason, *killers: Player) -> bool:
17
19
  await super().kill(reason, *killers)
18
20
  if reason == KillReason.Vote:
19
- self.game.killed_players.append(self)
21
+ if TYPE_CHECKING:
22
+ assert self.kill_info is not None
23
+ self.game.killed_players.append((self.name, self.kill_info))
20
24
  raise GameFinished(GameStatus.Joker)
21
25
  return True
@@ -1,95 +1,137 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
1
  import functools
5
2
  import weakref
6
- from dataclasses import dataclass
3
+ from collections.abc import Callable
7
4
  from typing import TYPE_CHECKING, ClassVar, Final, TypeVar, final
8
5
 
6
+ import anyio
7
+ from nonebot.adapters import Bot
9
8
  from nonebot.log import logger
9
+ from nonebot.utils import escape_tag
10
10
  from nonebot_plugin_alconna.uniseg import Receipt, Target, UniMessage
11
+ from nonebot_plugin_uninfo import SceneType
11
12
 
12
- from ..constant import KillReason, Role, RoleGroup, role_emoji, role_name_conv
13
- from ..utils import InputStore, check_index
13
+ from ..constant import STOP_COMMAND, STOP_COMMAND_PROMPT, role_emoji, role_name_conv
14
+ from ..models import KillInfo, KillReason, Role, RoleGroup
15
+ from ..utils import InputStore, check_index, link
14
16
 
15
17
  if TYPE_CHECKING:
16
- from collections.abc import Callable
17
-
18
- from nonebot.adapters import Bot
19
-
20
18
  from ..game import Game
21
19
  from ..player_set import PlayerSet
22
20
 
23
21
 
24
- P = TypeVar("P", bound=type["Player"])
25
- PLAYER_CLASS: dict[Role, type[Player]] = {}
26
-
27
-
28
- @dataclass
29
- class KillInfo:
30
- reason: KillReason
31
- killers: PlayerSet
22
+ _P = TypeVar("_P", bound=type["Player"])
32
23
 
33
24
 
34
25
  class Player:
26
+ __player_class: ClassVar[dict[Role, type["Player"]]] = {}
35
27
  role: ClassVar[Role]
36
28
  role_group: ClassVar[RoleGroup]
37
29
 
38
30
  bot: Final[Bot]
39
- _game_ref: Final[weakref.ReferenceType[Game]]
40
- user: Final[Target]
41
- name: Final[str]
42
31
  alive: bool = True
43
- killed: Final[asyncio.Event]
32
+ killed: Final[anyio.Event]
44
33
  kill_info: KillInfo | None = None
45
- selected: Player | None = None
34
+ selected: "Player | None" = None
46
35
 
47
36
  @final
48
- def __init__(self, bot: Bot, game: Game, user_id: str, name: str) -> None:
49
- self.bot = bot
50
- self._game_ref = weakref.ref(game)
51
- self.user = Target(
37
+ def __init__(self, bot: Bot, game: "Game", user_id: str) -> None:
38
+ self.__user = Target(
52
39
  user_id,
53
40
  private=True,
54
41
  self_id=bot.self_id,
55
42
  adapter=bot.adapter.get_name(),
56
43
  )
57
- self.name = name
58
- self.killed = asyncio.Event()
44
+ self.__game_ref = weakref.ref(game)
45
+ self.bot = bot
46
+ self.killed = anyio.Event()
47
+ self._member = None
48
+
49
+ @classmethod
50
+ def register_role(cls, role: Role, role_group: RoleGroup, /) -> Callable[[_P], _P]:
51
+ def decorator(c: _P, /) -> _P:
52
+ c.role = role
53
+ c.role_group = role_group
54
+ cls.__player_class[role] = c
55
+ return c
56
+
57
+ return decorator
59
58
 
60
59
  @final
61
60
  @classmethod
62
- def new(cls, role: Role, bot: Bot, game: Game, user_id: str, name: str) -> Player:
63
- if role not in PLAYER_CLASS:
61
+ def new(cls, role: Role, bot: Bot, game: "Game", user_id: str) -> "Player":
62
+ if role not in cls.__player_class:
64
63
  raise ValueError(f"Unexpected role: {role!r}")
65
64
 
66
- return PLAYER_CLASS[role](bot, game, user_id, name)
65
+ return cls.__player_class[role](bot, game, user_id)
67
66
 
68
67
  def __repr__(self) -> str:
69
- return f"<Player {self.role_name}: user={self.name!r} alive={self.alive}>"
68
+ return (
69
+ f"<Player {self.role_name}: user={self.user_id!r} " f"alive={self.alive}>"
70
+ )
70
71
 
71
72
  @property
72
- def game(self) -> Game:
73
- if game := self._game_ref():
73
+ def game(self) -> "Game":
74
+ if game := self.__game_ref():
74
75
  return game
75
76
  raise ValueError("Game not exist")
76
77
 
77
78
  @functools.cached_property
78
79
  def user_id(self) -> str:
79
- return self.user.id
80
+ return self.__user.id
80
81
 
81
82
  @functools.cached_property
82
83
  def role_name(self) -> str:
83
84
  return role_name_conv[self.role]
84
85
 
86
+ async def _fetch_member(self) -> None:
87
+ member = await self.game.interface.get_member(
88
+ SceneType.GROUP,
89
+ self.game.group.id,
90
+ self.user_id,
91
+ )
92
+ if member is None:
93
+ member = await self.game.interface.get_member(
94
+ SceneType.GUILD,
95
+ self.game.group.id,
96
+ self.user_id,
97
+ )
98
+
99
+ self._member = member
100
+
101
+ @final
102
+ @property
103
+ def _member_nick(self) -> str | None:
104
+ return self._member and (
105
+ self._member.nick or self._member.user.nick or self._member.user.name
106
+ )
107
+
108
+ @final
109
+ @property
110
+ def name(self) -> str:
111
+ return self._member_nick or self.user_id
112
+
113
+ @final
114
+ @property
115
+ def colored_name(self) -> str:
116
+ name = escape_tag(self.user_id)
117
+
118
+ if self._member is None or (nick := self._member_nick) is None:
119
+ name = f"<b><e>{name}</e></b>"
120
+ else:
121
+ name = f"<y>{nick}</y>(<b><e>{name}</e></b>)"
122
+
123
+ if self._member is not None and self._member.user.avatar is not None:
124
+ name = link(name, self._member.user.avatar)
125
+
126
+ return name
127
+
85
128
  @final
86
129
  def _log(self, text: str) -> None:
87
130
  text = text.replace("\n", "\\n")
88
131
  logger.opt(colors=True).info(
89
- f"<b><e>{self.game.group.id}</e></b> | "
132
+ f"{self.game.colored_name} | "
90
133
  f"[<b><m>{self.role_name}</m></b>] "
91
- f"<y>{self.name}</y>(<b><e>{self.user_id}</e></b>) | "
92
- f"{text}",
134
+ f"{self.colored_name} | {text}",
93
135
  )
94
136
 
95
137
  @final
@@ -97,16 +139,16 @@ class Player:
97
139
  if isinstance(message, str):
98
140
  message = UniMessage.text(message)
99
141
 
100
- self._log(f"<g>Send</g> | {message}")
101
- return await message.send(target=self.user, bot=self.bot)
142
+ self._log(f"<g>Send</g> | {escape_tag(str(message))}")
143
+ return await message.send(target=self.__user, bot=self.bot)
102
144
 
103
145
  @final
104
146
  async def receive(self, prompt: str | UniMessage | None = None) -> UniMessage:
105
147
  if prompt:
106
148
  await self.send(prompt)
107
149
 
108
- result = await InputStore.fetch(self.user.id)
109
- self._log(f"<y>Recv</y> | {result}")
150
+ result = await InputStore.fetch(self.user_id)
151
+ self._log(f"<y>Recv</y> | {escape_tag(str(result))}")
110
152
  return result
111
153
 
112
154
  @final
@@ -117,45 +159,66 @@ class Player:
117
159
  return
118
160
 
119
161
  async def notify_role(self) -> None:
162
+ await self._fetch_member()
120
163
  await self.send(f"⚙️你的身份: {role_emoji[self.role]}{self.role_name}")
121
164
 
122
- async def kill(self, reason: KillReason, *killers: Player) -> bool:
123
- from ..player_set import PlayerSet
124
-
125
- self.alive = False
126
- self.kill_info = KillInfo(reason=reason, killers=PlayerSet(killers))
165
+ async def kill(self, reason: KillReason, *killers: "Player") -> bool:
166
+ if self.alive:
167
+ self.alive = False
168
+ self.kill_info = KillInfo(reason=reason, killers=[p.name for p in killers])
127
169
  return True
128
170
 
129
171
  async def post_kill(self) -> None:
130
172
  self.killed.set()
131
173
 
132
- async def vote(self, players: PlayerSet) -> tuple[Player, Player] | None:
174
+ async def vote(self, players: "PlayerSet") -> "Player | None":
133
175
  await self.send(
134
176
  f"💫请选择需要投票的玩家:\n{players.show()}"
135
- "\n\n🗳️发送编号选择玩家\n❌发送 “/stop” 弃票"
177
+ f"\n\n🗳️发送编号选择玩家\n❌发送 “{STOP_COMMAND_PROMPT}” 弃票"
178
+ f"\n\n限时1分钟,超时将视为弃票"
179
+ )
180
+
181
+ try:
182
+ with anyio.fail_after(60):
183
+ selected = await self._select_player(
184
+ players,
185
+ on_stop="⚠️你选择了弃票",
186
+ on_index_error="⚠️输入错误: 请发送编号选择玩家",
187
+ )
188
+ except TimeoutError:
189
+ selected = None
190
+ await self.send("⚠️投票超时,将视为弃票")
191
+
192
+ if selected is not None:
193
+ await self.send(f"🔨投票的玩家: {selected.name}")
194
+ return selected
195
+
196
+ async def _check_selected(self, player: "Player") -> "Player | None":
197
+ return player
198
+
199
+ async def _select_player(
200
+ self,
201
+ players: "PlayerSet",
202
+ *,
203
+ on_stop: str | None = None,
204
+ on_index_error: str | None = None,
205
+ ) -> "Player | None":
206
+ on_stop = on_stop or "ℹ️你选择了取消,回合结束"
207
+ on_index_error = (
208
+ on_index_error or f"⚠️输入错误: 请发送玩家编号或 “{STOP_COMMAND_PROMPT}”"
136
209
  )
210
+ selected = None
137
211
 
138
- while True:
212
+ while selected is None:
139
213
  text = await self.receive_text()
140
- if text == "/stop":
141
- await self.send("⚠️你选择了弃票")
214
+ if text == STOP_COMMAND:
215
+ if on_stop is not None:
216
+ await self.send(on_stop)
142
217
  return None
143
218
  index = check_index(text, len(players))
144
- if index is not None:
145
- selected = index - 1
146
- break
147
- await self.send("⚠️输入错误: 请发送编号选择玩家")
148
-
149
- player = players[selected]
150
- await self.send(f"🔨投票的玩家: {player.name}")
151
- return self, player
152
-
153
-
154
- def register_role(role: Role, role_group: RoleGroup, /) -> Callable[[P], P]:
155
- def decorator(cls: P, /) -> P:
156
- cls.role = role
157
- cls.role_group = role_group
158
- PLAYER_CLASS[role] = cls
159
- return cls
219
+ if index is None:
220
+ await self.send(on_index_error)
221
+ continue
222
+ selected = await self._check_selected(players[index - 1])
160
223
 
161
- return decorator
224
+ return selected
@@ -1,12 +1,12 @@
1
1
  from nonebot_plugin_alconna.uniseg import UniMessage
2
2
  from typing_extensions import override
3
3
 
4
- from ..constant import Role, RoleGroup
5
- from ..utils import check_index
6
- from .player import Player, register_role
4
+ from ..constant import STOP_COMMAND_PROMPT
5
+ from ..models import Role, RoleGroup
6
+ from .player import Player
7
7
 
8
8
 
9
- @register_role(Role.Prophet, RoleGroup.GoodGuy)
9
+ @Player.register_role(Role.Prophet, RoleGroup.GoodGuy)
10
10
  class Prophet(Player):
11
11
  @override
12
12
  async def interact(self) -> None:
@@ -15,16 +15,9 @@ class Prophet(Player):
15
15
  UniMessage.text("💫请选择需要查验身份的玩家:\n")
16
16
  .text(players.show())
17
17
  .text("\n\n🔮发送编号选择玩家")
18
+ .text(f"\n❌发送 “{STOP_COMMAND_PROMPT}” 结束回合(不查验身份)")
18
19
  )
19
20
 
20
- while True:
21
- text = await self.receive_text()
22
- index = check_index(text, len(players))
23
- if index is not None:
24
- selected = index - 1
25
- break
26
- await self.send("⚠️输入错误: 请发送编号选择玩家")
27
-
28
- player = players[selected]
29
- result = "狼人" if player.role_group == RoleGroup.Werewolf else "好人"
30
- await self.send(f"✏️玩家 {player.name} 的阵营是『{result}』")
21
+ if selected := await self._select_player(players):
22
+ result = "狼人" if selected.role_group == RoleGroup.Werewolf else "好人"
23
+ await self.send(f"✏️玩家 {selected.name} 的阵营是『{result}』")
@@ -1,14 +1,19 @@
1
- import asyncio
1
+ from typing import TYPE_CHECKING
2
2
 
3
+ import anyio
3
4
  from nonebot_plugin_alconna.uniseg import UniMessage
4
5
  from typing_extensions import override
5
6
 
6
- from ..constant import Role, RoleGroup
7
- from ..utils import check_index
8
- from .player import Player, register_role
7
+ from ..constant import STOP_COMMAND, STOP_COMMAND_PROMPT
8
+ from ..models import Role, RoleGroup
9
+ from ..utils import ObjectStream, check_index
10
+ from .player import Player
9
11
 
12
+ if TYPE_CHECKING:
13
+ from ..player_set import PlayerSet
10
14
 
11
- @register_role(Role.Werewolf, RoleGroup.Werewolf)
15
+
16
+ @Player.register_role(Role.Werewolf, RoleGroup.Werewolf)
12
17
  class Werewolf(Player):
13
18
  @override
14
19
  async def notify_role(self) -> None:
@@ -20,15 +25,50 @@ class Werewolf(Player):
20
25
  + "\n".join(f" {p.role_name}: {p.name}" for p in partners)
21
26
  )
22
27
 
28
+ async def _handle_interact(
29
+ self,
30
+ players: "PlayerSet",
31
+ stream: ObjectStream[str | UniMessage],
32
+ ) -> None:
33
+ self.selected = None
34
+
35
+ while True:
36
+ input_msg = await self.receive()
37
+ text = input_msg.extract_plain_text()
38
+ index = check_index(text, len(players))
39
+ if index is not None:
40
+ self.selected = players[index - 1]
41
+ msg = f"当前选择玩家: {self.selected.name}"
42
+ await self.send(f"🎯{msg}\n发送 “{STOP_COMMAND_PROMPT}” 结束回合")
43
+ await stream.send(f"📝队友 {self.name} {msg}")
44
+ if text == STOP_COMMAND:
45
+ if self.selected is not None:
46
+ await self.send("✅你已结束当前回合")
47
+ await stream.send(f"📝队友 {self.name} 结束当前回合")
48
+ stream.close()
49
+ return
50
+ await self.send("⚠️当前未选择玩家,无法结束回合")
51
+ 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:
60
+ try:
61
+ message = await stream.recv()
62
+ except anyio.EndOfStream:
63
+ return
64
+
65
+ await partners.broadcast(message)
66
+
23
67
  @override
24
68
  async def interact(self) -> None:
25
69
  players = self.game.players.alive()
26
70
  partners = players.select(RoleGroup.Werewolf).exclude(self)
27
71
 
28
- # 避免阻塞
29
- def broadcast(msg: str | UniMessage) -> asyncio.Task[None]:
30
- return asyncio.create_task(partners.broadcast(msg))
31
-
32
72
  msg = UniMessage()
33
73
  if partners:
34
74
  msg = (
@@ -40,28 +80,12 @@ class Werewolf(Player):
40
80
  msg.text("💫请选择今晚的目标:\n")
41
81
  .text(players.show())
42
82
  .text("\n\n🔪发送编号选择玩家")
43
- .text("\n❌发送 “/stop” 结束回合")
83
+ .text(f"\n❌发送 “{STOP_COMMAND_PROMPT}” 结束回合")
44
84
  .text("\n\n⚠️意见未统一将空刀")
45
85
  )
46
86
 
47
- selected = None
48
- finished = False
49
- while selected is None or not finished:
50
- input_msg = await self.receive()
51
- text = input_msg.extract_plain_text()
52
- index = check_index(text, len(players))
53
- if index is not None:
54
- selected = index - 1
55
- msg = f"当前选择玩家: {players[selected].name}"
56
- await self.send(f"🎯{msg}\n发送 “/stop” 结束回合")
57
- broadcast(f"📝队友 {self.name} {msg}")
58
- if text == "/stop":
59
- if selected is not None:
60
- finished = True
61
- await self.send("✅你已结束当前回合")
62
- broadcast(f"📝队友 {self.name} 结束当前回合")
63
- else:
64
- await self.send("⚠️当前未选择玩家,无法结束回合")
65
- broadcast(UniMessage.text(f"💬队友 {self.name}:\n") + input_msg)
87
+ stream = ObjectStream[str | UniMessage](8)
66
88
 
67
- self.selected = players[selected]
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)
@@ -1,41 +1,43 @@
1
1
  from nonebot_plugin_alconna.uniseg import UniMessage
2
2
  from typing_extensions import override
3
3
 
4
- from ..constant import Role, RoleGroup
5
- from ..utils import check_index
6
- from .player import Player, register_role
4
+ from ..constant import STOP_COMMAND_PROMPT
5
+ from ..models import Role, RoleGroup
6
+ from ..utils import as_player_set
7
+ from .player import Player
7
8
 
8
9
 
9
- @register_role(Role.Witch, RoleGroup.GoodGuy)
10
+ @Player.register_role(Role.Witch, RoleGroup.GoodGuy)
10
11
  class Witch(Player):
11
- antidote: int = 1
12
- poison: int = 1
12
+ antidote: bool = True
13
+ poison: bool = True
13
14
 
14
15
  async def handle_killed(self) -> bool:
15
- msg = UniMessage()
16
- if (killed := self.game.state.killed) is not None:
17
- msg.text(f"🔪今晚 {killed.name} 被刀了\n\n")
18
- else:
16
+ if (killed := self.game.state.killed) is None:
19
17
  await self.send("ℹ️今晚没有人被刀")
20
18
  return False
21
19
 
20
+ msg = UniMessage.text(f"🔪今晚 {killed.name} 被刀了\n\n")
21
+
22
22
  if not self.antidote:
23
23
  await self.send(msg.text("⚙️你已经用过解药了"))
24
24
  return False
25
25
 
26
- await self.send(msg.text("✏️使用解药请发送 “1”\n❌不使用解药请发送 “/stop”"))
26
+ msg.text(f"✏️使用解药请发送 “1”\n❌不使用解药请发送 “{STOP_COMMAND_PROMPT}”")
27
+ await self.send(msg)
28
+
29
+ if not await self._select_player(
30
+ as_player_set(killed),
31
+ on_stop=f"ℹ️你选择不对 {killed.name} 使用解药",
32
+ on_index_error=f"⚠️输入错误: 请输入 “1” 或 “{STOP_COMMAND_PROMPT}”",
33
+ ):
34
+ return False
27
35
 
28
- while True:
29
- text = await self.receive_text()
30
- if text == "1":
31
- self.antidote = 0
32
- self.selected = killed
33
- self.game.state.antidote.add(killed)
34
- await self.send(f"✅你对 {killed.name} 使用了解药,回合结束")
35
- return True
36
- if text == "/stop":
37
- return False
38
- await self.send("⚠️输入错误: 请输入 “1” 或 “/stop”")
36
+ self.antidote = False
37
+ self.selected = killed
38
+ self.game.state.antidote.add(killed)
39
+ await self.send(f"✅你对 {killed.name} 使用了解药,回合结束")
40
+ return True
39
41
 
40
42
  @override
41
43
  async def interact(self) -> None:
@@ -52,21 +54,14 @@ class Witch(Player):
52
54
  .text("玩家列表:\n")
53
55
  .text(players.show())
54
56
  .text("\n\n🧪发送玩家编号使用毒药")
55
- .text("\n❌发送 “/stop” 结束回合(不使用药水)")
57
+ .text(f"\n❌发送 “{STOP_COMMAND_PROMPT}” 结束回合(不使用药水)")
56
58
  )
57
59
 
58
- while True:
59
- text = await self.receive_text()
60
- index = check_index(text, len(players))
61
- if index is not None:
62
- selected = index - 1
63
- break
64
- if text == "/stop":
65
- await self.send("ℹ️你选择不使用毒药,回合结束")
66
- return
67
- await self.send("⚠️输入错误: 请发送玩家编号或 “/stop”")
68
-
69
- self.poison = 0
70
- self.selected = players[selected]
71
- self.game.state.poison.add(self)
72
- await self.send(f"✅当前回合选择对玩家 {self.selected.name} 使用毒药\n回合结束")
60
+ if selected := await self._select_player(
61
+ players,
62
+ on_stop="ℹ️你选择不使用毒药,回合结束",
63
+ ):
64
+ self.poison = False
65
+ self.selected = selected
66
+ self.game.state.poison.add(self)
67
+ await self.send(f"✅当前回合选择对玩家 {selected.name} 使用毒药\n回合结束")
@@ -1,12 +1,12 @@
1
1
  from typing_extensions import override
2
2
 
3
- from ..constant import Role, RoleGroup
3
+ from ..models import Role, RoleGroup
4
4
  from .can_shoot import CanShoot
5
- from .player import register_role
5
+ from .player import Player
6
6
  from .werewolf import Werewolf
7
7
 
8
8
 
9
- @register_role(Role.WolfKing, RoleGroup.Werewolf)
9
+ @Player.register_role(Role.WolfKing, RoleGroup.Werewolf)
10
10
  class WolfKing(CanShoot, Werewolf):
11
11
  @override
12
12
  async def notify_role(self) -> None: