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
@@ -1,19 +1,19 @@
1
1
  import functools
2
2
  import weakref
3
- from collections.abc import Callable
4
- from typing import TYPE_CHECKING, ClassVar, Final, TypeVar, final
3
+ from types import EllipsisType
4
+ from typing import TYPE_CHECKING, ClassVar, Final, Generic, TypeVar, final
5
+ from typing_extensions import Self, override
5
6
 
6
7
  import anyio
7
8
  import nonebot
8
9
  from nonebot.adapters import Bot
9
10
  from nonebot.utils import escape_tag
10
11
  from nonebot_plugin_alconna.uniseg import Receipt, Target, UniMessage
11
- from nonebot_plugin_uninfo import SceneType
12
+ from nonebot_plugin_uninfo import Interface, SceneType
12
13
 
13
- from ..config import GameBehavior
14
- from ..constant import ROLE_EMOJI, ROLE_NAME_CONV, STOP_COMMAND, stop_command_prompt
15
- from ..models import KillInfo, KillReason, Role, RoleGroup
16
- from ..utils import (
14
+ from .constant import ROLE_EMOJI, ROLE_NAME_CONV, STOP_COMMAND, stop_command_prompt
15
+ from .models import KillInfo, KillReason, Role, RoleGroup
16
+ from .utils import (
17
17
  InputStore,
18
18
  SendHandler,
19
19
  add_players_button,
@@ -23,15 +23,79 @@ from ..utils import (
23
23
  )
24
24
 
25
25
  if TYPE_CHECKING:
26
- from ..game import Game
27
- from ..player_set import PlayerSet
26
+ from .game import Game
27
+ from .player_set import PlayerSet
28
28
 
29
29
 
30
- _P = TypeVar("_P", bound=type["Player"])
31
-
32
30
  logger = nonebot.logger.opt(colors=True)
33
31
 
34
32
 
33
+ _P = TypeVar("_P", bound="Player")
34
+ _T = TypeVar("_T")
35
+
36
+
37
+ class ActionProvider(Generic[_P]):
38
+ p: _P
39
+
40
+ @final
41
+ def __init__(self, player: _P, /) -> None:
42
+ self.p = player
43
+
44
+ class proxy(Generic[_T]): # noqa: N801
45
+ def __init__(
46
+ self, _: type[_T] | None = None, /, *, readonly: bool = False
47
+ ) -> None:
48
+ self.readonly = readonly
49
+
50
+ def __set_name__(self, owner: type["ActionProvider"], name: str) -> None:
51
+ self.name = name
52
+
53
+ def __get__(self, obj: "ActionProvider", objtype: type) -> _T:
54
+ return getattr(obj.p, self.name)
55
+
56
+ def __set__(self, obj: "ActionProvider", value: _T) -> None:
57
+ if self.readonly:
58
+ raise AttributeError(f"readonly attribute {self.name}")
59
+ setattr(obj.p, self.name, value)
60
+
61
+ name = proxy[str](readonly=True)
62
+ user_id = proxy[str](readonly=True)
63
+ game = proxy["Game"](readonly=True)
64
+ selected = proxy["Player | None"]()
65
+
66
+
67
+ class InteractProvider(ActionProvider[_P], Generic[_P]):
68
+ async def before(self) -> None: ...
69
+ async def interact(self) -> None: ...
70
+ async def after(self) -> None: ...
71
+
72
+
73
+ class KillProvider(ActionProvider[_P], Generic[_P]):
74
+ alive = ActionProvider.proxy[bool]()
75
+ kill_info = ActionProvider.proxy[KillInfo | None]()
76
+
77
+ async def kill(self, reason: KillReason, *killers: "Player") -> KillInfo | None:
78
+ if self.alive:
79
+ self.alive = False
80
+ self.kill_info = KillInfo(reason=reason, killers=[p.name for p in killers])
81
+ return self.kill_info
82
+
83
+ async def post_kill(self) -> None: ...
84
+
85
+
86
+ class NotifyProvider(ActionProvider[_P], Generic[_P]):
87
+ role = ActionProvider.proxy[Role]()
88
+ role_group = ActionProvider.proxy[RoleGroup]()
89
+ role_name = ActionProvider.proxy[str]()
90
+
91
+ def message(self, message: UniMessage) -> UniMessage:
92
+ return message
93
+
94
+ async def notify(self) -> None:
95
+ msg = UniMessage.text(f"⚙️你的身份: {ROLE_EMOJI[self.role]}{self.role_name}\n")
96
+ await self.p.send(self.message(msg))
97
+
98
+
35
99
  class _SendHandler(SendHandler[str | None]):
36
100
  def solve_msg(
37
101
  self,
@@ -44,9 +108,13 @@ class _SendHandler(SendHandler[str | None]):
44
108
 
45
109
 
46
110
  class Player:
47
- __player_class: ClassVar[dict[Role, type["Player"]]] = {}
111
+ _player_class: ClassVar[dict[Role, type["Player"]]] = {}
112
+
48
113
  role: ClassVar[Role]
49
114
  role_group: ClassVar[RoleGroup]
115
+ interact_provider: ClassVar[type[InteractProvider[Self]] | None]
116
+ kill_provider: ClassVar[type[KillProvider[Self]]]
117
+ notify_provider: ClassVar[type[NotifyProvider[Self]]]
50
118
 
51
119
  bot: Final[Bot]
52
120
  alive: bool = True
@@ -55,65 +123,83 @@ class Player:
55
123
  selected: "Player | None" = None
56
124
 
57
125
  @final
58
- def __init__(self, bot: Bot, game: "Game", user_id: str) -> None:
59
- self.__user = Target(
60
- user_id,
61
- private=True,
62
- self_id=bot.self_id,
63
- adapter=bot.adapter.get_name(),
64
- )
126
+ def __init__(self, bot: Bot, game: "Game", user: Target) -> None:
127
+ self.__user = user
65
128
  self.__game_ref = weakref.ref(game)
66
129
  self.bot = bot
67
130
  self.killed = anyio.Event()
68
131
  self._member = None
69
- self._send_handler = _SendHandler()
70
- self._send_handler.update(self.__user, bot)
132
+ self._send_handler = _SendHandler(self.__user, bot)
71
133
 
72
- @classmethod
73
- def register_role(cls, role: Role, role_group: RoleGroup, /) -> Callable[[_P], _P]:
74
- def decorator(c: _P, /) -> _P:
75
- c.role = role
76
- c.role_group = role_group
77
- cls.__player_class[role] = c
78
- return c
79
-
80
- return decorator
134
+ @final
135
+ @override
136
+ def __init_subclass__(cls) -> None:
137
+ super().__init_subclass__()
138
+ if hasattr(cls, "role") and hasattr(cls, "role_group"):
139
+ cls._player_class[cls.role] = cls
140
+ if not hasattr(cls, "interact_provider"):
141
+ cls.interact_provider = None
142
+ if not hasattr(cls, "kill_provider"):
143
+ cls.kill_provider = KillProvider
144
+ if not hasattr(cls, "notify_provider"):
145
+ cls.notify_provider = NotifyProvider
81
146
 
82
147
  @final
83
148
  @classmethod
84
- def new(cls, role: Role, bot: Bot, game: "Game", user_id: str) -> "Player":
85
- if role not in cls.__player_class:
149
+ async def new(
150
+ cls,
151
+ role: Role,
152
+ bot: Bot,
153
+ game: "Game",
154
+ user_id: str,
155
+ interface: Interface,
156
+ ) -> "Player":
157
+ if role not in cls._player_class:
86
158
  raise ValueError(f"Unexpected role: {role!r}")
87
159
 
88
- return cls.__player_class[role](bot, game, user_id)
160
+ user = Target(
161
+ user_id,
162
+ private=True,
163
+ self_id=game.group.self_id,
164
+ scope=game.group.scope,
165
+ adapter=game.group.adapter,
166
+ extra=game.group.extra,
167
+ )
168
+ self = cls._player_class[role](bot, game, user)
169
+ await self._fetch_member(interface)
170
+ return self
89
171
 
90
172
  def __repr__(self) -> str:
91
173
  return f"<Player {self.role_name}: user={self.user_id!r} alive={self.alive}>"
92
174
 
175
+ @final
93
176
  @property
94
177
  def game(self) -> "Game":
95
178
  if game := self.__game_ref():
96
179
  return game
97
180
  raise ValueError("Game not exist")
98
181
 
182
+ @final
99
183
  @functools.cached_property
100
184
  def user_id(self) -> str:
101
185
  return self.__user.id
102
186
 
187
+ @final
103
188
  @functools.cached_property
104
189
  def role_name(self) -> str:
105
190
  return ROLE_NAME_CONV[self.role]
106
191
 
107
- async def _fetch_member(self) -> None:
108
- member = await self.game.interface.get_member(
192
+ @final
193
+ async def _fetch_member(self, interface: Interface) -> None:
194
+ member = await interface.get_member(
109
195
  SceneType.GROUP,
110
- self.game.group.id,
196
+ self.game.group_id,
111
197
  self.user_id,
112
198
  )
113
199
  if member is None:
114
- member = await self.game.interface.get_member(
200
+ member = await interface.get_member(
115
201
  SceneType.GUILD,
116
- self.game.group.id,
202
+ self.game.group_id,
117
203
  self.user_id,
118
204
  )
119
205
 
@@ -134,12 +220,10 @@ class Player:
134
220
  @final
135
221
  @property
136
222
  def colored_name(self) -> str:
137
- name = escape_tag(self.user_id)
223
+ name = f"<b><e>{escape_tag(self.user_id)}</e></b>"
138
224
 
139
- if self._member is None or (nick := self._member_nick) is None:
140
- name = f"<b><e>{name}</e></b>"
141
- else:
142
- name = f"<y>{nick}</y>(<b><e>{name}</e></b>)"
225
+ if (nick := self._member_nick) is not None:
226
+ name = f"<y>{nick}</y>({name})"
143
227
 
144
228
  if self._member is not None and self._member.user.avatar is not None:
145
229
  name = link(name, self._member.user.avatar)
@@ -183,47 +267,40 @@ class Player:
183
267
 
184
268
  @property
185
269
  def interact_timeout(self) -> float:
186
- return GameBehavior.get().timeout.interact
187
-
188
- async def _before_interact(self) -> None:
189
- return
190
-
191
- async def _interact(self) -> None:
192
- return
193
-
194
- async def _after_interact(self) -> None:
195
- return
270
+ return self.game.behavior.timeout.interact
196
271
 
272
+ @final
197
273
  async def interact(self) -> None:
198
- if not getattr(self._interact, "__override__", False):
274
+ if self.interact_provider is None:
199
275
  await self.send("ℹ️请等待其他玩家结束交互...")
200
276
  return
201
277
 
202
- await self._before_interact()
278
+ provider = self.interact_provider(self)
279
+
280
+ await provider.before()
203
281
 
204
282
  timeout = self.interact_timeout
205
283
  await self.send(f"✏️{self.role_name}交互开始,限时 {timeout / 60:.2f} 分钟")
206
284
 
207
285
  try:
208
286
  with anyio.fail_after(timeout):
209
- await self._interact()
287
+ await provider.interact()
210
288
  except TimeoutError:
211
289
  logger.debug(f"{self.role_name}交互超时 (<y>{timeout}</y>s)")
212
290
  await self.send(f"⚠️{self.role_name}交互超时")
213
291
 
214
- await self._after_interact()
292
+ await provider.after()
215
293
 
216
294
  async def notify_role(self) -> None:
217
- await self._fetch_member()
218
- await self.send(f"⚙️你的身份: {ROLE_EMOJI[self.role]}{self.role_name}")
295
+ await self.notify_provider(self).notify()
219
296
 
220
- async def kill(self, reason: KillReason, *killers: "Player") -> bool:
221
- if self.alive:
222
- self.alive = False
223
- self.kill_info = KillInfo(reason=reason, killers=[p.name for p in killers])
224
- return True
297
+ @final
298
+ async def kill(self, reason: KillReason, *killers: "Player") -> KillInfo | None:
299
+ return await self.kill_provider(self).kill(reason, *killers)
225
300
 
301
+ @final
226
302
  async def post_kill(self) -> None:
303
+ await self.kill_provider(self).post_kill()
227
304
  self.killed.set()
228
305
 
229
306
  async def vote(self, players: "PlayerSet") -> "Player | None":
@@ -238,8 +315,8 @@ class Player:
238
315
  )
239
316
 
240
317
  try:
241
- with anyio.fail_after(GameBehavior.get().timeout.vote):
242
- selected = await self._select_player(
318
+ with anyio.fail_after(self.game.behavior.timeout.vote):
319
+ selected = await self.select_player(
243
320
  players,
244
321
  on_stop="⚠️你选择了弃票",
245
322
  on_index_error="⚠️输入错误: 请发送编号选择玩家",
@@ -255,15 +332,16 @@ class Player:
255
332
  async def _check_selected(self, player: "Player") -> "Player | None":
256
333
  return player
257
334
 
258
- async def _select_player(
335
+ @final
336
+ async def select_player(
259
337
  self,
260
338
  players: "PlayerSet",
261
339
  *,
262
- on_stop: str | None = None,
340
+ on_stop: str | EllipsisType | None = ...,
263
341
  on_index_error: str | None = None,
264
342
  stop_btn_label: str | None = None,
265
343
  ) -> "Player | None":
266
- on_stop = on_stop or "ℹ️你选择了取消,回合结束"
344
+ on_stop = on_stop if on_stop is not None else "ℹ️你选择了取消,回合结束"
267
345
  on_index_error = (
268
346
  on_index_error or f"⚠️输入错误: 请发送玩家编号或 “{stop_command_prompt()}”"
269
347
  )
@@ -272,11 +350,10 @@ class Player:
272
350
  while selected is None:
273
351
  text = await self.receive_text()
274
352
  if text == STOP_COMMAND:
275
- if on_stop is not None:
353
+ if on_stop is not ...:
276
354
  await self.send(on_stop)
277
355
  return None
278
- index = check_index(text, len(players))
279
- if index is None:
356
+ if (index := check_index(text, players.size)) is None:
280
357
  await self.send(
281
358
  on_index_error,
282
359
  stop_btn_label=stop_btn_label,
@@ -1,38 +1,47 @@
1
1
  import functools
2
+ import random
3
+ from collections.abc import Iterable
4
+ from typing_extensions import Self
2
5
 
3
6
  import anyio
4
7
  from nonebot_plugin_alconna.uniseg import UniMessage
5
8
 
6
9
  from .models import Role, RoleGroup
7
- from .players import Player
10
+ from .player import Player
8
11
 
9
12
 
10
13
  class PlayerSet(set[Player]):
14
+ __slots__ = ("__dict__",) # for cached_property `sorted`
15
+
11
16
  @property
12
17
  def size(self) -> int:
13
18
  return len(self)
14
19
 
15
- def alive(self) -> "PlayerSet":
16
- return PlayerSet(p for p in self if p.alive)
20
+ @classmethod
21
+ def from_(cls, iterable: Iterable[Player], /) -> Self:
22
+ return cls(iterable)
23
+
24
+ def alive(self) -> Self:
25
+ return self.from_(p for p in self if p.alive)
17
26
 
18
- def dead(self) -> "PlayerSet":
19
- return PlayerSet(p for p in self if not p.alive)
27
+ def dead(self) -> Self:
28
+ return self.from_(p for p in self if not p.alive)
20
29
 
21
- def killed(self) -> "PlayerSet":
22
- return PlayerSet(p for p in self if p.killed.is_set())
30
+ def killed(self) -> Self:
31
+ return self.from_(p for p in self if p.killed.is_set())
23
32
 
24
- def include(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
25
- return PlayerSet(
33
+ def include(self, *types: Player | Role | RoleGroup) -> Self:
34
+ return self.from_(
26
35
  player
27
36
  for player in self
28
37
  if (player in types or player.role in types or player.role_group in types)
29
38
  )
30
39
 
31
- def select(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
40
+ def select(self, *types: Player | Role | RoleGroup) -> Self:
32
41
  return self.include(*types)
33
42
 
34
- def exclude(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
35
- return PlayerSet(
43
+ def exclude(self, *types: Player | Role | RoleGroup) -> Self:
44
+ return self.from_(
36
45
  player
37
46
  for player in self
38
47
  if (
@@ -42,13 +51,19 @@ class PlayerSet(set[Player]):
42
51
  )
43
52
  )
44
53
 
45
- def player_selected(self) -> "PlayerSet":
46
- return PlayerSet(p.selected for p in self.alive() if p.selected is not None)
54
+ def player_selected(self) -> Self:
55
+ return self.from_(p.selected for p in self.alive() if (p.selected is not None))
47
56
 
48
57
  @functools.cached_property
49
58
  def sorted(self) -> list[Player]:
50
59
  return sorted(self, key=lambda p: p.user_id)
51
60
 
61
+ @property
62
+ def shuffled(self) -> list[Player]:
63
+ players = self.sorted.copy()
64
+ random.shuffle(players)
65
+ return players
66
+
52
67
  async def vote(self) -> dict[Player, list[Player]]:
53
68
  players = self.alive()
54
69
  result: dict[Player, list[Player]] = {}
@@ -56,7 +71,7 @@ class PlayerSet(set[Player]):
56
71
  async def _vote(player: Player) -> None:
57
72
  vote = await player.vote(players)
58
73
  if vote is not None:
59
- result[vote] = [*result.get(vote, []), player]
74
+ result.setdefault(vote, []).append(player)
60
75
 
61
76
  async with anyio.create_task_group() as tg:
62
77
  for p in players:
@@ -68,16 +83,17 @@ class PlayerSet(set[Player]):
68
83
  if not self:
69
84
  return
70
85
 
86
+ send = functools.partial(
87
+ Player.send,
88
+ message=message,
89
+ stop_btn_label=None,
90
+ select_players=None,
91
+ skip_handler=True,
92
+ )
93
+
71
94
  async with anyio.create_task_group() as tg:
72
95
  for p in self:
73
- func = functools.partial(
74
- p.send,
75
- message=message,
76
- stop_btn_label=None,
77
- select_players=None,
78
- skip_handler=True,
79
- )
80
- tg.start_soon(func)
96
+ tg.start_soon(send, p)
81
97
 
82
98
  def show(self) -> str:
83
99
  return "\n".join(f"{i}. {p.name}" for i, p in enumerate(self.sorted, 1))
@@ -3,7 +3,6 @@ from .guard import Guard as Guard
3
3
  from .hunter import Hunter as Hunter
4
4
  from .idiot import Idiot as Idiot
5
5
  from .jester import Jester as Jester
6
- from .player import Player as Player
7
6
  from .prophet import Prophet as Prophet
8
7
  from .werewolf import Werewolf as Werewolf
9
8
  from .witch import Witch as Witch
@@ -1,7 +1,7 @@
1
1
  from ..models import Role, RoleGroup
2
- from .player import Player
2
+ from ..player import Player
3
3
 
4
4
 
5
- @Player.register_role(Role.CIVILIAN, RoleGroup.GOODGUY)
6
5
  class Civilian(Player):
7
- pass
6
+ role = Role.CIVILIAN
7
+ role_group = RoleGroup.GOODGUY
@@ -2,23 +2,14 @@ from typing_extensions import override
2
2
 
3
3
  from ..constant import stop_command_prompt
4
4
  from ..models import GameState, Role, RoleGroup
5
- from .player import Player
5
+ from ..player import InteractProvider, Player
6
6
 
7
7
 
8
- @Player.register_role(Role.GUARD, RoleGroup.GOODGUY)
9
- class Guard(Player):
10
- @override
11
- async def _check_selected(self, player: Player) -> Player | 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
-
8
+ class GuardInteractProvider(InteractProvider["Guard"]):
18
9
  @override
19
- async def _interact(self) -> None:
10
+ async def interact(self) -> None:
20
11
  players = self.game.players.alive()
21
- await self.send(
12
+ await self.p.send(
22
13
  "💫请选择需要保护的玩家:\n"
23
14
  f"{players.show()}\n\n"
24
15
  "🛡️发送编号选择玩家\n"
@@ -27,7 +18,21 @@ class Guard(Player):
27
18
  select_players=players,
28
19
  )
29
20
 
30
- self.selected = await self._select_player(players, stop_btn_label="结束回合")
21
+ self.selected = await self.p.select_player(players, stop_btn_label="结束回合")
31
22
  if self.selected:
32
23
  self.game.state.protected.add(self.selected)
33
- await self.send(f"✅本回合保护的玩家: {self.selected.name}")
24
+ await self.p.send(f"✅本回合保护的玩家: {self.selected.name}")
25
+
26
+
27
+ class Guard(Player):
28
+ role = Role.GUARD
29
+ role_group = RoleGroup.GOODGUY
30
+ interact_provider = GuardInteractProvider
31
+
32
+ @override
33
+ async def _check_selected(self, player: Player) -> Player | None:
34
+ if self.game.state.state == GameState.State.NIGHT and player is self.selected:
35
+ await self.send("⚠️守卫不能连续两晚保护同一目标,请重新选择")
36
+ return None
37
+
38
+ return player
@@ -1,8 +1,9 @@
1
1
  from ..models import Role, RoleGroup
2
- from .can_shoot import CanShoot
3
- from .player import Player
2
+ from ..player import Player
3
+ from .shooter import ShooterKillProvider
4
4
 
5
5
 
6
- @Player.register_role(Role.HUNTER, RoleGroup.GOODGUY)
7
- class Hunter(CanShoot, Player):
8
- pass
6
+ class Hunter(Player):
7
+ role = Role.HUNTER
8
+ role_group = RoleGroup.GOODGUY
9
+ kill_provider = ShooterKillProvider
@@ -1,30 +1,20 @@
1
- from __future__ import annotations
2
-
3
1
  from typing import TYPE_CHECKING
4
2
  from typing_extensions import override
5
3
 
6
4
  from nonebot_plugin_alconna.uniseg import UniMessage
7
5
 
8
- from ..models import KillReason, Role, RoleGroup
9
- from .player import Player
6
+ from ..models import KillInfo, KillReason, Role, RoleGroup
7
+ from ..player import KillProvider, NotifyProvider, Player
10
8
 
11
9
  if TYPE_CHECKING:
12
10
  from ..player_set import PlayerSet
13
11
 
14
12
 
15
- @Player.register_role(Role.IDIOT, RoleGroup.GOODGUY)
16
- class Idiot(Player):
17
- voted: bool = False
18
-
19
- @override
20
- async def notify_role(self) -> None:
21
- await super().notify_role()
22
- await self.send(
23
- "作为白痴,你可以在首次被投票放逐时免疫放逐,但在之后的投票中无法继续投票"
24
- )
13
+ class IdiotKillProvider(KillProvider["Idiot"]):
14
+ voted = KillProvider.proxy(bool)
25
15
 
26
16
  @override
27
- async def kill(self, reason: KillReason, *killers: Player) -> bool:
17
+ async def kill(self, reason: KillReason, *killers: Player) -> KillInfo | None:
28
18
  if reason == KillReason.VOTE and not self.voted:
29
19
  self.voted = True
30
20
  await self.game.send(
@@ -33,11 +23,29 @@ class Idiot(Player):
33
23
  .text(" 的身份是白痴\n")
34
24
  .text("免疫本次投票放逐,且接下来无法参与投票"),
35
25
  )
36
- return False
26
+ return None
27
+
37
28
  return await super().kill(reason, *killers)
38
29
 
30
+
31
+ class IdiotNotifyProvider(NotifyProvider["Idiot"]):
32
+ @override
33
+ def message(self, message: UniMessage) -> UniMessage:
34
+ return message.text(
35
+ "作为白痴,你可以在首次被投票放逐时免疫放逐,但在之后的投票中无法继续投票"
36
+ )
37
+
38
+
39
+ class Idiot(Player):
40
+ role = Role.IDIOT
41
+ role_group = RoleGroup.GOODGUY
42
+ kill_provider = IdiotKillProvider
43
+ notify_provider = IdiotNotifyProvider
44
+
45
+ voted: bool = False
46
+
39
47
  @override
40
- async def vote(self, players: PlayerSet) -> Player | None:
48
+ async def vote(self, players: "PlayerSet") -> Player | None:
41
49
  if self.voted:
42
50
  await self.send("ℹ️你已经发动过白痴身份的技能,无法参与本次投票")
43
51
  return None
@@ -1,24 +1,29 @@
1
- from typing import TYPE_CHECKING
2
1
  from typing_extensions import override
3
2
 
3
+ from nonebot_plugin_alconna import UniMessage
4
+
4
5
  from ..exception import GameFinished
5
- from ..models import GameStatus, KillReason, Role, RoleGroup
6
- from .player import Player
6
+ from ..models import GameStatus, KillInfo, KillReason, Role, RoleGroup
7
+ from ..player import KillProvider, NotifyProvider, Player
7
8
 
8
9
 
9
- @Player.register_role(Role.JESTER, RoleGroup.OTHERS)
10
- class Jester(Player):
11
- @override
12
- async def notify_role(self) -> None:
13
- await super().notify_role()
14
- await self.send("⚙️你的胜利条件: 被投票放逐")
10
+ class JesterKillProvider(KillProvider["Jester"]):
11
+ async def kill(self, reason: KillReason, *killers: Player) -> KillInfo | None:
12
+ kill_info = await super().kill(reason, *killers)
13
+ if kill_info is not None and reason == KillReason.VOTE:
14
+ self.game.killed_players.append((self.name, kill_info))
15
+ raise GameFinished(GameStatus.JESTER)
16
+ return kill_info
17
+
15
18
 
19
+ class JesterNotifyProvider(NotifyProvider["Jester"]):
16
20
  @override
17
- async def kill(self, reason: KillReason, *killers: Player) -> bool:
18
- await super().kill(reason, *killers)
19
- if reason == KillReason.VOTE:
20
- if TYPE_CHECKING:
21
- assert self.kill_info is not None
22
- self.game.killed_players.append((self.name, self.kill_info))
23
- raise GameFinished(GameStatus.JESTER)
24
- return True
21
+ def message(self, message: UniMessage) -> UniMessage:
22
+ return message.text("⚙️你的胜利条件: 被投票放逐")
23
+
24
+
25
+ class Jester(Player):
26
+ role = Role.JESTER
27
+ role_group = RoleGroup.OTHERS
28
+ kill_provider = JesterKillProvider
29
+ notify_provider = JesterNotifyProvider