nonebot-plugin-werewolf 1.1.8__py3-none-any.whl → 1.1.9__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 +61 -50
  5. nonebot_plugin_werewolf/matchers/edit_behavior.py +14 -2
  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} +148 -70
  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 +6 -6
  24. {nonebot_plugin_werewolf-1.1.8.dist-info → nonebot_plugin_werewolf-1.1.9.dist-info}/METADATA +9 -2
  25. nonebot_plugin_werewolf-1.1.9.dist-info/RECORD +35 -0
  26. {nonebot_plugin_werewolf-1.1.8.dist-info → nonebot_plugin_werewolf-1.1.9.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.9.dist-info/licenses}/LICENSE +0 -0
  29. {nonebot_plugin_werewolf-1.1.8.dist-info → nonebot_plugin_werewolf-1.1.9.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,13 +123,8 @@ 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()
@@ -69,51 +132,75 @@ class Player:
69
132
  self._send_handler = _SendHandler()
70
133
  self._send_handler.update(self.__user, bot)
71
134
 
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
135
+ @final
136
+ @override
137
+ def __init_subclass__(cls) -> None:
138
+ super().__init_subclass__()
139
+ if hasattr(cls, "role") and hasattr(cls, "role_group"):
140
+ cls._player_class[cls.role] = cls
141
+ if not hasattr(cls, "interact_provider"):
142
+ cls.interact_provider = None
143
+ if not hasattr(cls, "kill_provider"):
144
+ cls.kill_provider = KillProvider
145
+ if not hasattr(cls, "notify_provider"):
146
+ cls.notify_provider = NotifyProvider
81
147
 
82
148
  @final
83
149
  @classmethod
84
- def new(cls, role: Role, bot: Bot, game: "Game", user_id: str) -> "Player":
85
- if role not in cls.__player_class:
150
+ async def new(
151
+ cls,
152
+ role: Role,
153
+ bot: Bot,
154
+ game: "Game",
155
+ user_id: str,
156
+ interface: Interface,
157
+ ) -> "Player":
158
+ if role not in cls._player_class:
86
159
  raise ValueError(f"Unexpected role: {role!r}")
87
160
 
88
- return cls.__player_class[role](bot, game, user_id)
161
+ user = Target(
162
+ user_id,
163
+ private=True,
164
+ self_id=game.group.self_id,
165
+ scope=game.group.scope,
166
+ adapter=game.group.adapter,
167
+ extra=game.group.extra,
168
+ )
169
+ self = cls._player_class[role](bot, game, user)
170
+ await self._fetch_member(interface)
171
+ return self
89
172
 
90
173
  def __repr__(self) -> str:
91
174
  return f"<Player {self.role_name}: user={self.user_id!r} alive={self.alive}>"
92
175
 
176
+ @final
93
177
  @property
94
178
  def game(self) -> "Game":
95
179
  if game := self.__game_ref():
96
180
  return game
97
181
  raise ValueError("Game not exist")
98
182
 
183
+ @final
99
184
  @functools.cached_property
100
185
  def user_id(self) -> str:
101
186
  return self.__user.id
102
187
 
188
+ @final
103
189
  @functools.cached_property
104
190
  def role_name(self) -> str:
105
191
  return ROLE_NAME_CONV[self.role]
106
192
 
107
- async def _fetch_member(self) -> None:
108
- member = await self.game.interface.get_member(
193
+ @final
194
+ async def _fetch_member(self, interface: Interface) -> None:
195
+ member = await interface.get_member(
109
196
  SceneType.GROUP,
110
- self.game.group.id,
197
+ self.game.group_id,
111
198
  self.user_id,
112
199
  )
113
200
  if member is None:
114
- member = await self.game.interface.get_member(
201
+ member = await interface.get_member(
115
202
  SceneType.GUILD,
116
- self.game.group.id,
203
+ self.game.group_id,
117
204
  self.user_id,
118
205
  )
119
206
 
@@ -134,12 +221,10 @@ class Player:
134
221
  @final
135
222
  @property
136
223
  def colored_name(self) -> str:
137
- name = escape_tag(self.user_id)
224
+ name = f"<b><e>{escape_tag(self.user_id)}</e></b>"
138
225
 
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>)"
226
+ if (nick := self._member_nick) is not None:
227
+ name = f"<y>{nick}</y>({name})"
143
228
 
144
229
  if self._member is not None and self._member.user.avatar is not None:
145
230
  name = link(name, self._member.user.avatar)
@@ -183,47 +268,40 @@ class Player:
183
268
 
184
269
  @property
185
270
  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
271
+ return self.game.behavior.timeout.interact
196
272
 
273
+ @final
197
274
  async def interact(self) -> None:
198
- if not getattr(self._interact, "__override__", False):
275
+ if self.interact_provider is None:
199
276
  await self.send("ℹ️请等待其他玩家结束交互...")
200
277
  return
201
278
 
202
- await self._before_interact()
279
+ provider = self.interact_provider(self)
280
+
281
+ await provider.before()
203
282
 
204
283
  timeout = self.interact_timeout
205
284
  await self.send(f"✏️{self.role_name}交互开始,限时 {timeout / 60:.2f} 分钟")
206
285
 
207
286
  try:
208
287
  with anyio.fail_after(timeout):
209
- await self._interact()
288
+ await provider.interact()
210
289
  except TimeoutError:
211
290
  logger.debug(f"{self.role_name}交互超时 (<y>{timeout}</y>s)")
212
291
  await self.send(f"⚠️{self.role_name}交互超时")
213
292
 
214
- await self._after_interact()
293
+ await provider.after()
215
294
 
216
295
  async def notify_role(self) -> None:
217
- await self._fetch_member()
218
- await self.send(f"⚙️你的身份: {ROLE_EMOJI[self.role]}{self.role_name}")
296
+ await self.notify_provider(self).notify()
219
297
 
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
298
+ @final
299
+ async def kill(self, reason: KillReason, *killers: "Player") -> KillInfo | None:
300
+ return await self.kill_provider(self).kill(reason, *killers)
225
301
 
302
+ @final
226
303
  async def post_kill(self) -> None:
304
+ await self.kill_provider(self).post_kill()
227
305
  self.killed.set()
228
306
 
229
307
  async def vote(self, players: "PlayerSet") -> "Player | None":
@@ -238,8 +316,8 @@ class Player:
238
316
  )
239
317
 
240
318
  try:
241
- with anyio.fail_after(GameBehavior.get().timeout.vote):
242
- selected = await self._select_player(
319
+ with anyio.fail_after(self.game.behavior.timeout.vote):
320
+ selected = await self.select_player(
243
321
  players,
244
322
  on_stop="⚠️你选择了弃票",
245
323
  on_index_error="⚠️输入错误: 请发送编号选择玩家",
@@ -255,15 +333,16 @@ class Player:
255
333
  async def _check_selected(self, player: "Player") -> "Player | None":
256
334
  return player
257
335
 
258
- async def _select_player(
336
+ @final
337
+ async def select_player(
259
338
  self,
260
339
  players: "PlayerSet",
261
340
  *,
262
- on_stop: str | None = None,
341
+ on_stop: str | EllipsisType | None = ...,
263
342
  on_index_error: str | None = None,
264
343
  stop_btn_label: str | None = None,
265
344
  ) -> "Player | None":
266
- on_stop = on_stop or "ℹ️你选择了取消,回合结束"
345
+ on_stop = on_stop if on_stop is not None else "ℹ️你选择了取消,回合结束"
267
346
  on_index_error = (
268
347
  on_index_error or f"⚠️输入错误: 请发送玩家编号或 “{stop_command_prompt()}”"
269
348
  )
@@ -272,11 +351,10 @@ class Player:
272
351
  while selected is None:
273
352
  text = await self.receive_text()
274
353
  if text == STOP_COMMAND:
275
- if on_stop is not None:
354
+ if on_stop is not ...:
276
355
  await self.send(on_stop)
277
356
  return None
278
- index = check_index(text, len(players))
279
- if index is None:
357
+ if (index := check_index(text, players.size)) is None:
280
358
  await self.send(
281
359
  on_index_error,
282
360
  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