nonebot-plugin-werewolf 1.1.2__py3-none-any.whl → 1.1.5__py3-none-any.whl

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