nonebot-plugin-werewolf 1.1.7__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 (38) hide show
  1. nonebot_plugin_werewolf/__init__.py +1 -1
  2. nonebot_plugin_werewolf/config.py +74 -15
  3. nonebot_plugin_werewolf/constant.py +59 -46
  4. nonebot_plugin_werewolf/exception.py +2 -4
  5. nonebot_plugin_werewolf/game.py +200 -171
  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 +217 -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 +214 -175
  14. nonebot_plugin_werewolf/matchers/superuser_ops.py +3 -3
  15. nonebot_plugin_werewolf/models.py +46 -22
  16. nonebot_plugin_werewolf/player.py +366 -0
  17. nonebot_plugin_werewolf/player_set.py +40 -22
  18. nonebot_plugin_werewolf/players/__init__.py +1 -2
  19. nonebot_plugin_werewolf/players/civilian.py +3 -3
  20. nonebot_plugin_werewolf/players/guard.py +27 -20
  21. nonebot_plugin_werewolf/players/hunter.py +6 -5
  22. nonebot_plugin_werewolf/players/idiot.py +27 -19
  23. nonebot_plugin_werewolf/players/jester.py +29 -0
  24. nonebot_plugin_werewolf/players/prophet.py +20 -14
  25. nonebot_plugin_werewolf/players/shooter.py +54 -0
  26. nonebot_plugin_werewolf/players/werewolf.py +88 -29
  27. nonebot_plugin_werewolf/players/witch.py +48 -24
  28. nonebot_plugin_werewolf/players/wolfking.py +14 -8
  29. nonebot_plugin_werewolf/utils.py +107 -8
  30. {nonebot_plugin_werewolf-1.1.7.dist-info → nonebot_plugin_werewolf-1.1.9.dist-info}/METADATA +30 -20
  31. nonebot_plugin_werewolf-1.1.9.dist-info/RECORD +35 -0
  32. {nonebot_plugin_werewolf-1.1.7.dist-info → nonebot_plugin_werewolf-1.1.9.dist-info}/WHEEL +1 -1
  33. nonebot_plugin_werewolf/players/can_shoot.py +0 -54
  34. nonebot_plugin_werewolf/players/joker.py +0 -25
  35. nonebot_plugin_werewolf/players/player.py +0 -226
  36. nonebot_plugin_werewolf-1.1.7.dist-info/RECORD +0 -34
  37. {nonebot_plugin_werewolf-1.1.7.dist-info → nonebot_plugin_werewolf-1.1.9.dist-info/licenses}/LICENSE +0 -0
  38. {nonebot_plugin_werewolf-1.1.7.dist-info → nonebot_plugin_werewolf-1.1.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,366 @@
1
+ import functools
2
+ import weakref
3
+ from types import EllipsisType
4
+ from typing import TYPE_CHECKING, ClassVar, Final, Generic, TypeVar, final
5
+ from typing_extensions import Self, override
6
+
7
+ import anyio
8
+ import nonebot
9
+ from nonebot.adapters import Bot
10
+ from nonebot.utils import escape_tag
11
+ from nonebot_plugin_alconna.uniseg import Receipt, Target, UniMessage
12
+ from nonebot_plugin_uninfo import Interface, SceneType
13
+
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
+ InputStore,
18
+ SendHandler,
19
+ add_players_button,
20
+ add_stop_button,
21
+ check_index,
22
+ link,
23
+ )
24
+
25
+ if TYPE_CHECKING:
26
+ from .game import Game
27
+ from .player_set import PlayerSet
28
+
29
+
30
+ logger = nonebot.logger.opt(colors=True)
31
+
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
+
99
+ class _SendHandler(SendHandler[str | None]):
100
+ def solve_msg(
101
+ self,
102
+ msg: UniMessage,
103
+ stop_btn_label: str | None = None,
104
+ ) -> UniMessage:
105
+ if stop_btn_label is not None:
106
+ msg = add_stop_button(msg, stop_btn_label)
107
+ return msg
108
+
109
+
110
+ class Player:
111
+ _player_class: ClassVar[dict[Role, type["Player"]]] = {}
112
+
113
+ role: ClassVar[Role]
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]]]
118
+
119
+ bot: Final[Bot]
120
+ alive: bool = True
121
+ killed: Final[anyio.Event]
122
+ kill_info: KillInfo | None = None
123
+ selected: "Player | None" = None
124
+
125
+ @final
126
+ def __init__(self, bot: Bot, game: "Game", user: Target) -> None:
127
+ self.__user = user
128
+ self.__game_ref = weakref.ref(game)
129
+ self.bot = bot
130
+ self.killed = anyio.Event()
131
+ self._member = None
132
+ self._send_handler = _SendHandler()
133
+ self._send_handler.update(self.__user, bot)
134
+
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
147
+
148
+ @final
149
+ @classmethod
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:
159
+ raise ValueError(f"Unexpected role: {role!r}")
160
+
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
172
+
173
+ def __repr__(self) -> str:
174
+ return f"<Player {self.role_name}: user={self.user_id!r} alive={self.alive}>"
175
+
176
+ @final
177
+ @property
178
+ def game(self) -> "Game":
179
+ if game := self.__game_ref():
180
+ return game
181
+ raise ValueError("Game not exist")
182
+
183
+ @final
184
+ @functools.cached_property
185
+ def user_id(self) -> str:
186
+ return self.__user.id
187
+
188
+ @final
189
+ @functools.cached_property
190
+ def role_name(self) -> str:
191
+ return ROLE_NAME_CONV[self.role]
192
+
193
+ @final
194
+ async def _fetch_member(self, interface: Interface) -> None:
195
+ member = await interface.get_member(
196
+ SceneType.GROUP,
197
+ self.game.group_id,
198
+ self.user_id,
199
+ )
200
+ if member is None:
201
+ member = await interface.get_member(
202
+ SceneType.GUILD,
203
+ self.game.group_id,
204
+ self.user_id,
205
+ )
206
+
207
+ self._member = member
208
+
209
+ @final
210
+ @property
211
+ def _member_nick(self) -> str | None:
212
+ return self._member and (
213
+ self._member.nick or self._member.user.nick or self._member.user.name
214
+ )
215
+
216
+ @final
217
+ @property
218
+ def name(self) -> str:
219
+ return self._member_nick or self.user_id
220
+
221
+ @final
222
+ @property
223
+ def colored_name(self) -> str:
224
+ name = f"<b><e>{escape_tag(self.user_id)}</e></b>"
225
+
226
+ if (nick := self._member_nick) is not None:
227
+ name = f"<y>{nick}</y>({name})"
228
+
229
+ if self._member is not None and self._member.user.avatar is not None:
230
+ name = link(name, self._member.user.avatar)
231
+
232
+ return name
233
+
234
+ @final
235
+ def log(self, text: str) -> None:
236
+ text = text.replace("\n", "\\n")
237
+ self.game.log(f"[<b><m>{self.role_name}</m></b>] {self.colored_name} | {text}")
238
+
239
+ @final
240
+ async def send(
241
+ self,
242
+ message: str | UniMessage,
243
+ *,
244
+ stop_btn_label: str | None = None,
245
+ select_players: "PlayerSet | None" = None,
246
+ skip_handler: bool = False,
247
+ ) -> Receipt:
248
+ if isinstance(message, str):
249
+ message = UniMessage.text(message)
250
+
251
+ self.log(f"<g>Send</g> | {escape_tag(str(message))}")
252
+
253
+ if select_players:
254
+ message = add_players_button(message, select_players)
255
+ if skip_handler:
256
+ return await message.send(self.__user, self.bot)
257
+ return await self._send_handler.send(message, stop_btn_label)
258
+
259
+ @final
260
+ async def receive(self) -> UniMessage:
261
+ result = await InputStore.fetch(self.user_id)
262
+ self.log(f"<y>Recv</y> | {escape_tag(str(result))}")
263
+ return result
264
+
265
+ @final
266
+ async def receive_text(self) -> str:
267
+ return (await self.receive()).extract_plain_text()
268
+
269
+ @property
270
+ def interact_timeout(self) -> float:
271
+ return self.game.behavior.timeout.interact
272
+
273
+ @final
274
+ async def interact(self) -> None:
275
+ if self.interact_provider is None:
276
+ await self.send("ℹ️请等待其他玩家结束交互...")
277
+ return
278
+
279
+ provider = self.interact_provider(self)
280
+
281
+ await provider.before()
282
+
283
+ timeout = self.interact_timeout
284
+ await self.send(f"✏️{self.role_name}交互开始,限时 {timeout / 60:.2f} 分钟")
285
+
286
+ try:
287
+ with anyio.fail_after(timeout):
288
+ await provider.interact()
289
+ except TimeoutError:
290
+ logger.debug(f"{self.role_name}交互超时 (<y>{timeout}</y>s)")
291
+ await self.send(f"⚠️{self.role_name}交互超时")
292
+
293
+ await provider.after()
294
+
295
+ async def notify_role(self) -> None:
296
+ await self.notify_provider(self).notify()
297
+
298
+ @final
299
+ async def kill(self, reason: KillReason, *killers: "Player") -> KillInfo | None:
300
+ return await self.kill_provider(self).kill(reason, *killers)
301
+
302
+ @final
303
+ async def post_kill(self) -> None:
304
+ await self.kill_provider(self).post_kill()
305
+ self.killed.set()
306
+
307
+ async def vote(self, players: "PlayerSet") -> "Player | None":
308
+ await self.send(
309
+ f"💫请选择需要投票的玩家:\n"
310
+ f"{players.show()}\n\n"
311
+ "🗳️发送编号选择玩家\n"
312
+ f"❌发送 “{stop_command_prompt()}” 弃票\n\n"
313
+ "限时1分钟,超时将视为弃票",
314
+ stop_btn_label="弃票",
315
+ select_players=players,
316
+ )
317
+
318
+ try:
319
+ with anyio.fail_after(self.game.behavior.timeout.vote):
320
+ selected = await self.select_player(
321
+ players,
322
+ on_stop="⚠️你选择了弃票",
323
+ on_index_error="⚠️输入错误: 请发送编号选择玩家",
324
+ )
325
+ except TimeoutError:
326
+ selected = None
327
+ await self.send("⚠️投票超时,将视为弃票")
328
+
329
+ if selected is not None:
330
+ await self.send(f"🔨投票的玩家: {selected.name}")
331
+ return selected
332
+
333
+ async def _check_selected(self, player: "Player") -> "Player | None":
334
+ return player
335
+
336
+ @final
337
+ async def select_player(
338
+ self,
339
+ players: "PlayerSet",
340
+ *,
341
+ on_stop: str | EllipsisType | None = ...,
342
+ on_index_error: str | None = None,
343
+ stop_btn_label: str | None = None,
344
+ ) -> "Player | None":
345
+ on_stop = on_stop if on_stop is not None else "ℹ️你选择了取消,回合结束"
346
+ on_index_error = (
347
+ on_index_error or f"⚠️输入错误: 请发送玩家编号或 “{stop_command_prompt()}”"
348
+ )
349
+ selected = None
350
+
351
+ while selected is None:
352
+ text = await self.receive_text()
353
+ if text == STOP_COMMAND:
354
+ if on_stop is not ...:
355
+ await self.send(on_stop)
356
+ return None
357
+ if (index := check_index(text, players.size)) is None:
358
+ await self.send(
359
+ on_index_error,
360
+ stop_btn_label=stop_btn_label,
361
+ select_players=players,
362
+ )
363
+ continue
364
+ selected = await self._check_selected(players[index - 1])
365
+
366
+ return selected
@@ -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,17 +51,18 @@ 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
 
52
- async def interact(self) -> None:
53
- async with anyio.create_task_group() as tg:
54
- for p in self.alive():
55
- tg.start_soon(p.interact)
61
+ @property
62
+ def shuffled(self) -> list[Player]:
63
+ players = self.sorted.copy()
64
+ random.shuffle(players)
65
+ return players
56
66
 
57
67
  async def vote(self) -> dict[Player, list[Player]]:
58
68
  players = self.alive()
@@ -61,7 +71,7 @@ class PlayerSet(set[Player]):
61
71
  async def _vote(player: Player) -> None:
62
72
  vote = await player.vote(players)
63
73
  if vote is not None:
64
- result[vote] = [*result.get(vote, []), player]
74
+ result.setdefault(vote, []).append(player)
65
75
 
66
76
  async with anyio.create_task_group() as tg:
67
77
  for p in players:
@@ -73,12 +83,20 @@ class PlayerSet(set[Player]):
73
83
  if not self:
74
84
  return
75
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
+
76
94
  async with anyio.create_task_group() as tg:
77
95
  for p in self:
78
- tg.start_soon(p.send, message)
96
+ tg.start_soon(send, p)
79
97
 
80
98
  def show(self) -> str:
81
99
  return "\n".join(f"{i}. {p.name}" for i, p in enumerate(self.sorted, 1))
82
100
 
83
- def __getitem__(self, __index: int) -> Player:
84
- return self.sorted[__index]
101
+ def __getitem__(self, index: int, /) -> Player:
102
+ return self.sorted[index]
@@ -2,8 +2,7 @@ from .civilian import Civilian as Civilian
2
2
  from .guard import Guard as Guard
3
3
  from .hunter import Hunter as Hunter
4
4
  from .idiot import Idiot as Idiot
5
- from .joker import Joker as Joker
6
- from .player import Player as Player
5
+ from .jester import Jester as Jester
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
@@ -1,31 +1,38 @@
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
6
- from .player import Player
3
+ from ..constant import stop_command_prompt
4
+ from ..models import GameState, Role, RoleGroup
5
+ from ..player import InteractProvider, Player
7
6
 
8
7
 
9
- @Player.register_role(Role.Guard, RoleGroup.GoodGuy)
10
- class Guard(Player):
11
- @override
12
- 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
17
-
8
+ class GuardInteractProvider(InteractProvider["Guard"]):
18
9
  @override
19
10
  async def interact(self) -> None:
20
11
  players = self.game.players.alive()
21
- await self.send(
22
- UniMessage.text("💫请选择需要保护的玩家:\n")
23
- .text(players.show())
24
- .text("\n\n🛡️发送编号选择玩家")
25
- .text(f"\n❌发送 “{STOP_COMMAND_PROMPT}” 结束回合")
12
+ await self.p.send(
13
+ "💫请选择需要保护的玩家:\n"
14
+ f"{players.show()}\n\n"
15
+ "🛡️发送编号选择玩家\n"
16
+ f"❌发送 “{stop_command_prompt()}” 结束回合",
17
+ stop_btn_label="结束回合",
18
+ select_players=players,
26
19
  )
27
20
 
28
- self.selected = await self._select_player(players)
21
+ self.selected = await self.p.select_player(players, stop_btn_label="结束回合")
29
22
  if self.selected:
30
23
  self.game.state.protected.add(self.selected)
31
- 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,31 +1,21 @@
1
- from __future__ import annotations
2
-
3
1
  from typing import TYPE_CHECKING
2
+ from typing_extensions import override
4
3
 
5
4
  from nonebot_plugin_alconna.uniseg import UniMessage
6
- from typing_extensions import override
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
13
+ class IdiotKillProvider(KillProvider["Idiot"]):
14
+ voted = KillProvider.proxy(bool)
18
15
 
19
16
  @override
20
- async def notify_role(self) -> None:
21
- await super().notify_role()
22
- await self.send(
23
- "作为白痴,你可以在首次被投票放逐时免疫放逐,但在之后的投票中无法继续投票"
24
- )
25
-
26
- @override
27
- async def kill(self, reason: KillReason, *killers: Player) -> bool:
28
- if reason == KillReason.Vote and not self.voted:
17
+ async def kill(self, reason: KillReason, *killers: Player) -> KillInfo | None:
18
+ if reason == KillReason.VOTE and not self.voted:
29
19
  self.voted = True
30
20
  await self.game.send(
31
21
  UniMessage.text("⚙️玩家")
@@ -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
@@ -0,0 +1,29 @@
1
+ from typing_extensions import override
2
+
3
+ from nonebot_plugin_alconna import UniMessage
4
+
5
+ from ..exception import GameFinished
6
+ from ..models import GameStatus, KillInfo, KillReason, Role, RoleGroup
7
+ from ..player import KillProvider, NotifyProvider, Player
8
+
9
+
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
+
18
+
19
+ class JesterNotifyProvider(NotifyProvider["Jester"]):
20
+ @override
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