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.
- nonebot_plugin_werewolf/__init__.py +1 -1
- nonebot_plugin_werewolf/config.py +74 -15
- nonebot_plugin_werewolf/constant.py +59 -46
- nonebot_plugin_werewolf/exception.py +2 -4
- nonebot_plugin_werewolf/game.py +200 -171
- nonebot_plugin_werewolf/matchers/__init__.py +1 -0
- nonebot_plugin_werewolf/matchers/depends.py +4 -4
- nonebot_plugin_werewolf/matchers/edit_behavior.py +217 -0
- nonebot_plugin_werewolf/matchers/edit_preset.py +11 -11
- nonebot_plugin_werewolf/matchers/message_in_game.py +3 -1
- nonebot_plugin_werewolf/matchers/poke/chronocat_poke.py +8 -5
- nonebot_plugin_werewolf/matchers/poke/ob11_poke.py +3 -3
- nonebot_plugin_werewolf/matchers/start_game.py +214 -175
- nonebot_plugin_werewolf/matchers/superuser_ops.py +3 -3
- nonebot_plugin_werewolf/models.py +46 -22
- nonebot_plugin_werewolf/player.py +366 -0
- nonebot_plugin_werewolf/player_set.py +40 -22
- nonebot_plugin_werewolf/players/__init__.py +1 -2
- nonebot_plugin_werewolf/players/civilian.py +3 -3
- nonebot_plugin_werewolf/players/guard.py +27 -20
- nonebot_plugin_werewolf/players/hunter.py +6 -5
- nonebot_plugin_werewolf/players/idiot.py +27 -19
- nonebot_plugin_werewolf/players/jester.py +29 -0
- nonebot_plugin_werewolf/players/prophet.py +20 -14
- nonebot_plugin_werewolf/players/shooter.py +54 -0
- nonebot_plugin_werewolf/players/werewolf.py +88 -29
- nonebot_plugin_werewolf/players/witch.py +48 -24
- nonebot_plugin_werewolf/players/wolfking.py +14 -8
- nonebot_plugin_werewolf/utils.py +107 -8
- {nonebot_plugin_werewolf-1.1.7.dist-info → nonebot_plugin_werewolf-1.1.9.dist-info}/METADATA +30 -20
- nonebot_plugin_werewolf-1.1.9.dist-info/RECORD +35 -0
- {nonebot_plugin_werewolf-1.1.7.dist-info → nonebot_plugin_werewolf-1.1.9.dist-info}/WHEEL +1 -1
- nonebot_plugin_werewolf/players/can_shoot.py +0 -54
- nonebot_plugin_werewolf/players/joker.py +0 -25
- nonebot_plugin_werewolf/players/player.py +0 -226
- nonebot_plugin_werewolf-1.1.7.dist-info/RECORD +0 -34
- {nonebot_plugin_werewolf-1.1.7.dist-info → nonebot_plugin_werewolf-1.1.9.dist-info/licenses}/LICENSE +0 -0
- {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 .
|
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
|
-
|
16
|
-
|
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) ->
|
19
|
-
return
|
27
|
+
def dead(self) -> Self:
|
28
|
+
return self.from_(p for p in self if not p.alive)
|
20
29
|
|
21
|
-
def killed(self) ->
|
22
|
-
return
|
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) ->
|
25
|
-
return
|
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) ->
|
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) ->
|
35
|
-
return
|
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) ->
|
46
|
-
return
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
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(
|
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,
|
84
|
-
return self.sorted[
|
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 .
|
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,31 +1,38 @@
|
|
1
|
-
from nonebot_plugin_alconna.uniseg import UniMessage
|
2
1
|
from typing_extensions import override
|
3
2
|
|
4
|
-
from ..constant import
|
5
|
-
from ..models import Role, RoleGroup
|
6
|
-
from
|
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
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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.
|
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
|
3
|
-
from .
|
2
|
+
from ..player import Player
|
3
|
+
from .shooter import ShooterKillProvider
|
4
4
|
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
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
|
-
|
16
|
-
|
17
|
-
voted: bool = False
|
13
|
+
class IdiotKillProvider(KillProvider["Idiot"]):
|
14
|
+
voted = KillProvider.proxy(bool)
|
18
15
|
|
19
16
|
@override
|
20
|
-
async def
|
21
|
-
|
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
|
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
|