nonebot-plugin-werewolf 1.1.3__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.
- nonebot_plugin_werewolf/__init__.py +2 -1
- nonebot_plugin_werewolf/constant.py +23 -1
- nonebot_plugin_werewolf/game.py +172 -113
- nonebot_plugin_werewolf/matchers/depends.py +38 -0
- nonebot_plugin_werewolf/matchers/message_in_game.py +14 -4
- nonebot_plugin_werewolf/matchers/poke/__init__.py +8 -0
- nonebot_plugin_werewolf/matchers/poke/chronocat_poke.py +117 -0
- nonebot_plugin_werewolf/matchers/{ob11_ext.py → poke/ob11_poke.py} +21 -19
- nonebot_plugin_werewolf/matchers/start_game.py +161 -15
- nonebot_plugin_werewolf/player_set.py +33 -34
- nonebot_plugin_werewolf/players/can_shoot.py +12 -18
- nonebot_plugin_werewolf/players/civilian.py +2 -2
- nonebot_plugin_werewolf/players/guard.py +15 -22
- nonebot_plugin_werewolf/players/hunter.py +2 -2
- nonebot_plugin_werewolf/players/idiot.py +3 -3
- nonebot_plugin_werewolf/players/joker.py +2 -2
- nonebot_plugin_werewolf/players/player.py +137 -65
- nonebot_plugin_werewolf/players/prophet.py +7 -15
- nonebot_plugin_werewolf/players/werewolf.py +51 -29
- nonebot_plugin_werewolf/players/witch.py +32 -38
- nonebot_plugin_werewolf/players/wolfking.py +2 -2
- nonebot_plugin_werewolf/utils.py +56 -190
- {nonebot_plugin_werewolf-1.1.3.dist-info → nonebot_plugin_werewolf-1.1.5.dist-info}/METADATA +14 -2
- nonebot_plugin_werewolf-1.1.5.dist-info/RECORD +31 -0
- {nonebot_plugin_werewolf-1.1.3.dist-info → nonebot_plugin_werewolf-1.1.5.dist-info}/WHEEL +1 -1
- nonebot_plugin_werewolf/_timeout.py +0 -110
- nonebot_plugin_werewolf-1.1.3.dist-info/RECORD +0 -29
- {nonebot_plugin_werewolf-1.1.3.dist-info → nonebot_plugin_werewolf-1.1.5.dist-info}/LICENSE +0 -0
- {nonebot_plugin_werewolf-1.1.3.dist-info → nonebot_plugin_werewolf-1.1.5.dist-info}/top_level.txt +0 -0
@@ -9,7 +9,7 @@ from . import matchers as matchers
|
|
9
9
|
from . import players as players
|
10
10
|
from .config import Config
|
11
11
|
|
12
|
-
__version__ = "1.1.
|
12
|
+
__version__ = "1.1.5"
|
13
13
|
__plugin_meta__ = PluginMetadata(
|
14
14
|
name="狼人杀",
|
15
15
|
description="适用于 Nonebot2 的狼人杀插件",
|
@@ -24,6 +24,7 @@ __plugin_meta__ = PluginMetadata(
|
|
24
24
|
),
|
25
25
|
extra={
|
26
26
|
"Author": "wyf7685",
|
27
|
+
"Version": __version__,
|
27
28
|
"Bug Tracker": "https://github.com/wyf7685/nonebot-plugin-werewolf/issues",
|
28
29
|
},
|
29
30
|
)
|
@@ -4,10 +4,19 @@ import dataclasses
|
|
4
4
|
from enum import Enum, auto
|
5
5
|
from typing import TYPE_CHECKING
|
6
6
|
|
7
|
+
import nonebot
|
8
|
+
|
7
9
|
if TYPE_CHECKING:
|
8
10
|
from .players import Player
|
9
11
|
|
10
12
|
|
13
|
+
COMMAND_START = next(
|
14
|
+
iter(sorted(nonebot.get_driver().config.command_start, key=len)), ""
|
15
|
+
)
|
16
|
+
STOP_COMMAND_PROMPT = f"{COMMAND_START}stop"
|
17
|
+
STOP_COMMAND = "{{stop}}"
|
18
|
+
|
19
|
+
|
11
20
|
class Role(Enum):
|
12
21
|
# 狼人
|
13
22
|
Werewolf = 1
|
@@ -49,11 +58,24 @@ class GameStatus(Enum):
|
|
49
58
|
@dataclasses.dataclass
|
50
59
|
class GameState:
|
51
60
|
day: int
|
61
|
+
"""当前天数记录, 不会被 `reset()` 重置"""
|
52
62
|
killed: Player | None = None
|
53
|
-
|
63
|
+
"""当晚狼人击杀目标, `None` 则为空刀"""
|
64
|
+
shoot: Player | None = None
|
65
|
+
"""当前执行射杀操作的玩家"""
|
54
66
|
antidote: set[Player] = dataclasses.field(default_factory=set)
|
67
|
+
"""当晚女巫使用解药的目标"""
|
55
68
|
poison: set[Player] = dataclasses.field(default_factory=set)
|
69
|
+
"""当晚使用了毒药的女巫"""
|
56
70
|
protected: set[Player] = dataclasses.field(default_factory=set)
|
71
|
+
"""当晚守卫保护的目标"""
|
72
|
+
|
73
|
+
def reset(self) -> None:
|
74
|
+
self.killed = None
|
75
|
+
self.shoot = None
|
76
|
+
self.antidote = set()
|
77
|
+
self.poison = set()
|
78
|
+
self.protected = set()
|
57
79
|
|
58
80
|
|
59
81
|
role_name_conv: dict[Role | RoleGroup, str] = {
|
nonebot_plugin_werewolf/game.py
CHANGED
@@ -1,27 +1,33 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import asyncio
|
4
|
-
import contextlib
|
5
1
|
import secrets
|
6
|
-
from typing import
|
2
|
+
from typing import ClassVar, NoReturn
|
7
3
|
|
4
|
+
import anyio
|
5
|
+
from nonebot.adapters import Bot
|
8
6
|
from nonebot.log import logger
|
7
|
+
from nonebot.utils import escape_tag
|
9
8
|
from nonebot_plugin_alconna import At, Target, UniMessage
|
9
|
+
from nonebot_plugin_alconna.uniseg.message import Receipt
|
10
|
+
from nonebot_plugin_uninfo import Interface, SceneType
|
11
|
+
from typing_extensions import Self, assert_never
|
10
12
|
|
11
|
-
from ._timeout import timeout
|
12
13
|
from .config import config
|
13
|
-
from .constant import
|
14
|
+
from .constant import (
|
15
|
+
STOP_COMMAND,
|
16
|
+
STOP_COMMAND_PROMPT,
|
17
|
+
GameState,
|
18
|
+
GameStatus,
|
19
|
+
KillReason,
|
20
|
+
Role,
|
21
|
+
RoleGroup,
|
22
|
+
role_name_conv,
|
23
|
+
)
|
14
24
|
from .exception import GameFinished
|
15
25
|
from .player_set import PlayerSet
|
16
26
|
from .players import Player
|
17
|
-
from .utils import InputStore
|
18
|
-
|
19
|
-
if TYPE_CHECKING:
|
20
|
-
from nonebot.adapters import Bot
|
21
|
-
from nonebot_plugin_alconna.uniseg.message import Receipt
|
27
|
+
from .utils import InputStore, link
|
22
28
|
|
23
29
|
|
24
|
-
def init_players(bot: Bot, game: Game, players:
|
30
|
+
def init_players(bot: Bot, game: "Game", players: set[str]) -> PlayerSet:
|
25
31
|
logger.opt(colors=True).debug(f"初始化 <c>{game.group.id}</c> 的玩家职业")
|
26
32
|
role_preset = config.get_role_preset()
|
27
33
|
if (preset := role_preset.get(len(players))) is None:
|
@@ -44,8 +50,7 @@ def init_players(bot: Bot, game: Game, players: dict[str, str]) -> PlayerSet:
|
|
44
50
|
return roles.pop(secrets.randbelow(len(roles)))
|
45
51
|
|
46
52
|
player_set = PlayerSet(
|
47
|
-
Player.new(_select_role(), bot, game, user_id
|
48
|
-
for user_id in players
|
53
|
+
Player.new(_select_role(), bot, game, user_id) for user_id in players
|
49
54
|
)
|
50
55
|
logger.debug(f"职业分配完成: {player_set}")
|
51
56
|
|
@@ -54,33 +59,64 @@ def init_players(bot: Bot, game: Game, players: dict[str, str]) -> PlayerSet:
|
|
54
59
|
|
55
60
|
class Game:
|
56
61
|
starting_games: ClassVar[dict[Target, dict[str, str]]] = {}
|
57
|
-
running_games: ClassVar[set[
|
62
|
+
running_games: ClassVar[set[Self]] = set()
|
58
63
|
|
59
64
|
bot: Bot
|
60
65
|
group: Target
|
61
66
|
players: PlayerSet
|
62
|
-
|
67
|
+
interface: Interface
|
63
68
|
state: GameState
|
64
69
|
killed_players: list[Player]
|
65
70
|
|
66
|
-
def __init__(
|
71
|
+
def __init__(
|
72
|
+
self,
|
73
|
+
bot: Bot,
|
74
|
+
group: Target,
|
75
|
+
players: set[str],
|
76
|
+
interface: Interface,
|
77
|
+
) -> None:
|
67
78
|
self.bot = bot
|
68
79
|
self.group = group
|
69
80
|
self.players = init_players(bot, self, players)
|
70
|
-
self.
|
81
|
+
self.interface = interface
|
71
82
|
self.state = GameState(0)
|
72
83
|
self.killed_players = []
|
84
|
+
self._player_map = {p.user_id: p for p in self.players}
|
85
|
+
self._scene = None
|
86
|
+
|
87
|
+
async def _fetch_group_scene(self) -> None:
|
88
|
+
scene = await self.interface.get_scene(SceneType.GROUP, self.group.id)
|
89
|
+
if scene is None:
|
90
|
+
scene = await self.interface.get_scene(SceneType.GUILD, self.group.id)
|
91
|
+
|
92
|
+
self._scene = scene
|
93
|
+
|
94
|
+
@property
|
95
|
+
def colored_name(self) -> str:
|
96
|
+
name = escape_tag(self.group.id)
|
97
|
+
|
98
|
+
if self._scene is None or self._scene.name is None:
|
99
|
+
name = f"<b><e>{name}</e></b>"
|
100
|
+
else:
|
101
|
+
name = f"<y>{escape_tag(self._scene.name)}</y>(<b><e>{name}</e></b>)"
|
102
|
+
|
103
|
+
if self._scene is not None and self._scene.avatar is not None:
|
104
|
+
name = link(name, self._scene.avatar)
|
105
|
+
|
106
|
+
return name
|
73
107
|
|
74
108
|
async def send(self, message: str | UniMessage) -> Receipt:
|
75
109
|
if isinstance(message, str):
|
76
110
|
message = UniMessage.text(message)
|
77
|
-
|
111
|
+
|
112
|
+
text = f"{self.colored_name} | <g>Send</g> | "
|
78
113
|
for seg in message:
|
79
114
|
if isinstance(seg, At):
|
80
|
-
text += f"<y>@{self._player_map[seg.target].
|
115
|
+
text += f"<y>@{self._player_map[seg.target].colored_name}</y>"
|
81
116
|
else:
|
82
|
-
text += str(seg)
|
83
|
-
|
117
|
+
text += escape_tag(str(seg)).replace("\n", "\\n")
|
118
|
+
|
119
|
+
logger.opt(colors=True).info(text)
|
84
120
|
return await message.send(self.group, self.bot)
|
85
121
|
|
86
122
|
def at_all(self) -> UniMessage:
|
@@ -126,32 +162,37 @@ class Game:
|
|
126
162
|
line = f"🔫 {line} 射杀"
|
127
163
|
case KillReason.Vote:
|
128
164
|
line = f"🗳️ {line} 票出"
|
165
|
+
case x:
|
166
|
+
assert_never(x)
|
129
167
|
result.append(line)
|
130
168
|
|
131
169
|
return "\n\n".join(result)
|
132
170
|
|
133
171
|
async def notify_player_role(self) -> None:
|
134
172
|
preset = config.get_role_preset()[len(self.players)]
|
135
|
-
|
136
|
-
self.
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
.text(f"职业分配: 狼人x{preset[0]}, 神职x{preset[1]}, 平民x{preset[2]}")
|
141
|
-
),
|
142
|
-
*[p.notify_role() for p in self.players],
|
173
|
+
msg = (
|
174
|
+
self.at_all()
|
175
|
+
.text("\n\n📝正在分配职业,请注意查看私聊消息\n")
|
176
|
+
.text(f"当前玩家数: {len(self.players)}\n")
|
177
|
+
.text(f"职业分配: 狼人x{preset[0]}, 神职x{preset[1]}, 平民x{preset[2]}")
|
143
178
|
)
|
144
179
|
|
180
|
+
async with anyio.create_task_group() as tg:
|
181
|
+
tg.start_soon(self.send, msg)
|
182
|
+
for p in self.players:
|
183
|
+
tg.start_soon(p.notify_role)
|
184
|
+
|
145
185
|
async def wait_stop(self, *players: Player, timeout_secs: float) -> None:
|
146
186
|
async def wait(p: Player) -> None:
|
147
187
|
while True:
|
148
188
|
msg = await InputStore.fetch(p.user_id, self.group.id)
|
149
|
-
if msg.extract_plain_text().strip() ==
|
189
|
+
if msg.extract_plain_text().strip() == STOP_COMMAND:
|
150
190
|
break
|
151
191
|
|
152
|
-
with
|
153
|
-
async with
|
154
|
-
|
192
|
+
with anyio.move_on_after(timeout_secs):
|
193
|
+
async with anyio.create_task_group() as tg:
|
194
|
+
for p in players:
|
195
|
+
tg.start_soon(wait, p)
|
155
196
|
|
156
197
|
async def interact(
|
157
198
|
self,
|
@@ -159,19 +200,23 @@ class Game:
|
|
159
200
|
timeout_secs: float,
|
160
201
|
) -> None:
|
161
202
|
players = self.players.alive().select(player_type)
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
203
|
+
match player_type:
|
204
|
+
case Player():
|
205
|
+
text = player_type.role_name
|
206
|
+
case Role():
|
207
|
+
text = role_name_conv[player_type]
|
208
|
+
case RoleGroup():
|
209
|
+
text = f"{role_name_conv[player_type]}阵营"
|
210
|
+
case x:
|
211
|
+
assert_never(x)
|
168
212
|
|
169
213
|
await players.broadcast(f"✏️{text}交互开始,限时 {timeout_secs/60:.2f} 分钟")
|
170
214
|
try:
|
171
|
-
|
215
|
+
with anyio.fail_after(timeout_secs):
|
216
|
+
await players.interact()
|
172
217
|
except TimeoutError:
|
173
|
-
logger.opt(colors=True).debug(f"
|
174
|
-
await players.broadcast(f"
|
218
|
+
logger.opt(colors=True).debug(f"{text}交互超时 (<y>{timeout_secs}</y>s)")
|
219
|
+
await players.broadcast(f"⚠️{text}交互超时")
|
175
220
|
|
176
221
|
async def select_killed(self) -> None:
|
177
222
|
players = self.players.alive()
|
@@ -190,7 +235,7 @@ class Game:
|
|
190
235
|
await self.interact(Role.Witch, 60)
|
191
236
|
# 否则等待 5-20s
|
192
237
|
else:
|
193
|
-
await
|
238
|
+
await anyio.sleep(5 + secrets.randbelow(15))
|
194
239
|
|
195
240
|
async def handle_new_dead(self, players: Player | PlayerSet) -> None:
|
196
241
|
if isinstance(players, Player):
|
@@ -198,15 +243,16 @@ class Game:
|
|
198
243
|
if not players:
|
199
244
|
return
|
200
245
|
|
201
|
-
|
202
|
-
|
246
|
+
async with anyio.create_task_group() as tg:
|
247
|
+
tg.start_soon(
|
248
|
+
players.broadcast,
|
203
249
|
"ℹ️你已加入死者频道,请勿在群内继续发言\n"
|
204
|
-
"私聊发送消息将转发至其他已死亡玩家"
|
205
|
-
)
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
250
|
+
"私聊发送消息将转发至其他已死亡玩家",
|
251
|
+
)
|
252
|
+
tg.start_soon(
|
253
|
+
self.players.dead().exclude(*players).broadcast,
|
254
|
+
f"ℹ️玩家 {', '.join(p.name for p in players)} 加入了死者频道",
|
255
|
+
)
|
210
256
|
|
211
257
|
async def post_kill(self, players: Player | PlayerSet) -> None:
|
212
258
|
if isinstance(players, Player):
|
@@ -219,16 +265,16 @@ class Game:
|
|
219
265
|
await self.handle_new_dead(player)
|
220
266
|
self.killed_players.append(player)
|
221
267
|
|
222
|
-
|
223
|
-
if shooter is not None and shoot is not None:
|
268
|
+
shooter = self.state.shoot
|
269
|
+
if shooter is not None and (shoot := shooter.selected) is not None:
|
224
270
|
await self.send(
|
225
|
-
UniMessage.text("
|
271
|
+
UniMessage.text("🔫玩家 ")
|
226
272
|
.at(shoot.user_id)
|
227
273
|
.text(f" 被{shooter.role_name}射杀, 请发表遗言\n")
|
228
|
-
.text("限时1分钟, 发送
|
274
|
+
.text(f"限时1分钟, 发送 “{STOP_COMMAND_PROMPT}” 结束发言")
|
229
275
|
)
|
230
276
|
await self.wait_stop(shoot, timeout_secs=60)
|
231
|
-
self.state.shoot =
|
277
|
+
self.state.shoot = shooter.selected = None
|
232
278
|
await self.post_kill(shoot)
|
233
279
|
|
234
280
|
async def run_vote(self) -> None:
|
@@ -236,7 +282,7 @@ class Game:
|
|
236
282
|
players = self.players.alive()
|
237
283
|
|
238
284
|
# 被票玩家: [投票玩家]
|
239
|
-
vote_result: dict[Player, list[Player]] = await players.vote(
|
285
|
+
vote_result: dict[Player, list[Player]] = await players.vote()
|
240
286
|
# 票数: [被票玩家]
|
241
287
|
vote_reversed: dict[int, list[Player]] = {}
|
242
288
|
# 收集到的总票数
|
@@ -250,17 +296,17 @@ class Game:
|
|
250
296
|
if p is not None:
|
251
297
|
msg.at(p.user_id).text(f": {len(v)} 票\n")
|
252
298
|
vote_reversed[len(v)] = [*vote_reversed.get(len(v), []), p]
|
253
|
-
if v := (len(players) - total_votes):
|
299
|
+
if (v := (len(players) - total_votes)) > 0:
|
254
300
|
msg.text(f"弃票: {v} 票\n\n")
|
255
301
|
|
256
302
|
# 全员弃票 # 不是哥们?
|
257
303
|
if total_votes == 0:
|
258
|
-
await self.send(msg.text("
|
304
|
+
await self.send(msg.text("🔨没有人被投票放逐"))
|
259
305
|
return
|
260
306
|
|
261
307
|
# 弃票大于最高票
|
262
308
|
if (len(players) - total_votes) >= max(vote_reversed.keys()):
|
263
|
-
await self.send(msg.text("🔨弃票数大于最高票数,
|
309
|
+
await self.send(msg.text("🔨弃票数大于最高票数, 没有人被投票放逐"))
|
264
310
|
return
|
265
311
|
|
266
312
|
# 平票
|
@@ -268,11 +314,11 @@ class Game:
|
|
268
314
|
await self.send(
|
269
315
|
msg.text("🔨玩家 ")
|
270
316
|
.text(", ".join(p.name for p in vs))
|
271
|
-
.text(" 平票,
|
317
|
+
.text(" 平票, 没有人被投票放逐")
|
272
318
|
)
|
273
319
|
return
|
274
320
|
|
275
|
-
await self.send(msg)
|
321
|
+
await self.send(msg.rstrip("\n"))
|
276
322
|
|
277
323
|
# 仅有一名玩家票数最高
|
278
324
|
voted = vs.pop()
|
@@ -285,65 +331,79 @@ class Game:
|
|
285
331
|
UniMessage.text("🔨玩家 ")
|
286
332
|
.at(voted.user_id)
|
287
333
|
.text(" 被投票放逐, 请发表遗言\n")
|
288
|
-
.text("限时1分钟, 发送
|
334
|
+
.text(f"限时1分钟, 发送 “{STOP_COMMAND_PROMPT}” 结束发言")
|
289
335
|
)
|
290
336
|
await self.wait_stop(voted, timeout_secs=60)
|
291
337
|
await self.post_kill(voted)
|
292
338
|
|
293
|
-
async def run_dead_channel(self) -> NoReturn:
|
294
|
-
|
295
|
-
|
339
|
+
async def run_dead_channel(self, finished: anyio.Event) -> NoReturn:
|
340
|
+
send, recv = anyio.create_memory_object_stream[tuple[Player, UniMessage]](10)
|
341
|
+
|
342
|
+
async def handle_cancel() -> None:
|
343
|
+
await finished.wait()
|
344
|
+
tg.cancel_scope.cancel()
|
296
345
|
|
297
|
-
async def
|
346
|
+
async def handle_send() -> NoReturn:
|
298
347
|
while True:
|
299
|
-
player, msg = await
|
348
|
+
player, msg = await recv.receive()
|
300
349
|
msg = f"玩家 {player.name}:\n" + msg
|
301
350
|
await self.players.killed().exclude(player).broadcast(msg)
|
302
|
-
queue.task_done()
|
303
351
|
|
304
|
-
async def
|
352
|
+
async def handle_recv(player: Player) -> NoReturn:
|
305
353
|
await player.killed.wait()
|
306
354
|
|
307
355
|
counter = 0
|
308
356
|
|
309
|
-
def decrease() -> None:
|
357
|
+
async def decrease() -> None:
|
310
358
|
nonlocal counter
|
359
|
+
await anyio.sleep(60)
|
311
360
|
counter -= 1
|
312
361
|
|
313
362
|
while True:
|
314
363
|
msg = await player.receive()
|
315
364
|
counter += 1
|
316
365
|
if counter <= 10:
|
317
|
-
await
|
318
|
-
|
366
|
+
await send.send((player, msg))
|
367
|
+
tg.start_soon(decrease)
|
319
368
|
else:
|
320
369
|
await player.send("❌发言频率超过限制, 该消息被屏蔽")
|
321
370
|
|
322
|
-
|
371
|
+
async with anyio.create_task_group() as tg:
|
372
|
+
tg.start_soon(handle_cancel)
|
373
|
+
tg.start_soon(handle_send)
|
374
|
+
for p in self.players:
|
375
|
+
tg.start_soon(handle_recv, p)
|
323
376
|
|
324
377
|
async def run(self) -> NoReturn:
|
378
|
+
await self._fetch_group_scene()
|
325
379
|
# 告知玩家角色信息
|
326
380
|
await self.notify_player_role()
|
327
|
-
# 天数记录 主要用于第一晚狼人击杀的遗言
|
328
|
-
day_count = 0
|
329
381
|
|
330
382
|
# 游戏主循环
|
331
383
|
while True:
|
332
384
|
# 重置游戏状态,进入下一夜
|
333
|
-
self.state
|
385
|
+
self.state.reset()
|
334
386
|
players = self.players.alive()
|
335
387
|
await self.send("🌙天黑请闭眼...")
|
336
388
|
|
337
389
|
# 狼人、预言家、守卫 同时交互,女巫在狼人后交互
|
338
|
-
|
339
|
-
self.select_killed
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
390
|
+
async with anyio.create_task_group() as tg:
|
391
|
+
tg.start_soon(self.select_killed)
|
392
|
+
tg.start_soon(
|
393
|
+
players.select(Role.Witch).broadcast,
|
394
|
+
"ℹ️请等待狼人决定目标...",
|
395
|
+
)
|
396
|
+
tg.start_soon(self.interact, Role.Prophet, 60)
|
397
|
+
tg.start_soon(self.interact, Role.Guard, 60)
|
398
|
+
tg.start_soon(
|
399
|
+
players.exclude(
|
400
|
+
RoleGroup.Werewolf,
|
401
|
+
Role.Prophet,
|
402
|
+
Role.Witch,
|
403
|
+
Role.Guard,
|
404
|
+
).broadcast,
|
405
|
+
"ℹ️请等待其他玩家结束交互...",
|
406
|
+
)
|
347
407
|
|
348
408
|
# 狼人击杀目标
|
349
409
|
if (
|
@@ -365,8 +425,8 @@ class Game:
|
|
365
425
|
# 女巫毒杀玩家
|
366
426
|
await witch.selected.kill(KillReason.Poison, witch)
|
367
427
|
|
368
|
-
|
369
|
-
msg = UniMessage.text(f"『第{
|
428
|
+
self.state.day += 1
|
429
|
+
msg = UniMessage.text(f"『第{self.state.day}天』☀️天亮了...\n")
|
370
430
|
# 没有玩家死亡,平安夜
|
371
431
|
if not (dead := players.dead()):
|
372
432
|
await self.send(msg.text("昨晚是平安夜"))
|
@@ -378,12 +438,12 @@ class Game:
|
|
378
438
|
await self.send(msg)
|
379
439
|
|
380
440
|
# 第一晚被狼人杀死的玩家发表遗言
|
381
|
-
if
|
441
|
+
if self.state.day == 1 and killed is not None and not killed.alive:
|
382
442
|
await self.send(
|
383
443
|
UniMessage.text("⚙️当前为第一天\n请被狼人杀死的 ")
|
384
444
|
.at(killed.user_id)
|
385
445
|
.text(" 发表遗言\n")
|
386
|
-
.text("限时1分钟, 发送
|
446
|
+
.text(f"限时1分钟, 发送 “{STOP_COMMAND_PROMPT}” 结束发言")
|
387
447
|
)
|
388
448
|
await self.wait_stop(killed, timeout_secs=60)
|
389
449
|
await self.post_kill(dead)
|
@@ -396,7 +456,8 @@ class Game:
|
|
396
456
|
|
397
457
|
# 开始自由讨论
|
398
458
|
await self.send(
|
399
|
-
"💬接下来开始自由讨论\n限时2分钟,
|
459
|
+
"💬接下来开始自由讨论\n限时2分钟, "
|
460
|
+
f"全员发送 “{STOP_COMMAND_PROMPT}” 结束发言"
|
400
461
|
)
|
401
462
|
await self.wait_stop(*self.players.alive(), timeout_secs=120)
|
402
463
|
|
@@ -417,6 +478,8 @@ class Game:
|
|
417
478
|
winner = "狼人"
|
418
479
|
case GameStatus.Joker:
|
419
480
|
winner = "小丑"
|
481
|
+
case x:
|
482
|
+
assert_never(x)
|
420
483
|
|
421
484
|
msg = UniMessage.text(f"🎉游戏结束,{winner}获胜\n\n")
|
422
485
|
for p in sorted(self.players, key=lambda p: (p.role.value, p.user_id)):
|
@@ -424,35 +487,31 @@ class Game:
|
|
424
487
|
await self.send(msg)
|
425
488
|
await self.send(f"📌玩家死亡报告:\n\n{self.show_killed_players()}")
|
426
489
|
|
427
|
-
def start(self) -> None:
|
428
|
-
finished = asyncio.Event()
|
429
|
-
game_task = asyncio.create_task(self.run())
|
430
|
-
game_task.add_done_callback(lambda _: finished.set())
|
431
|
-
dead_channel = asyncio.create_task(self.run_dead_channel())
|
432
|
-
|
490
|
+
async def start(self) -> None:
|
433
491
|
async def daemon() -> None:
|
434
|
-
await finished.wait()
|
435
|
-
|
436
492
|
try:
|
437
|
-
|
438
|
-
except
|
493
|
+
await self.run()
|
494
|
+
except anyio.get_cancelled_exc_class():
|
439
495
|
logger.warning(f"{self.group.id} 的狼人杀游戏进程被取消")
|
440
496
|
except GameFinished as result:
|
441
497
|
await self.handle_game_finish(result.status)
|
442
498
|
logger.info(f"{self.group.id} 的狼人杀游戏进程正常退出")
|
443
499
|
except Exception as err:
|
444
500
|
msg = f"{self.group.id} 的狼人杀游戏进程出现未知错误: {err!r}"
|
445
|
-
logger.
|
501
|
+
logger.exception(msg)
|
446
502
|
await self.send(f"❌狼人杀游戏进程出现未知错误: {err!r}")
|
447
503
|
finally:
|
448
|
-
|
449
|
-
self.running_games.discard(self)
|
450
|
-
|
451
|
-
def daemon_callback(task: asyncio.Task[None]) -> None:
|
452
|
-
if err := task.exception():
|
453
|
-
msg = f"{self.group.id} 的狼人杀守护进程出现错误: {err!r}"
|
454
|
-
logger.opt(exception=err).error(msg)
|
504
|
+
finished.set()
|
455
505
|
|
506
|
+
finished = anyio.Event()
|
456
507
|
self.running_games.add(self)
|
457
|
-
|
458
|
-
|
508
|
+
try:
|
509
|
+
async with anyio.create_task_group() as tg:
|
510
|
+
tg.start_soon(daemon)
|
511
|
+
tg.start_soon(self.run_dead_channel, finished)
|
512
|
+
except Exception as err:
|
513
|
+
msg = f"{self.group.id} 的狼人杀守护进程出现错误: {err!r}"
|
514
|
+
logger.opt(exception=err).error(msg)
|
515
|
+
finally:
|
516
|
+
self.running_games.discard(self)
|
517
|
+
InputStore.cleanup(list(self._player_map), self.group.id)
|
@@ -0,0 +1,38 @@
|
|
1
|
+
import itertools
|
2
|
+
|
3
|
+
from nonebot.adapters import Bot, Event
|
4
|
+
from nonebot_plugin_alconna import MsgTarget
|
5
|
+
|
6
|
+
from ..game import Game
|
7
|
+
|
8
|
+
|
9
|
+
def user_in_game(self_id: str, user_id: str, group_id: str | None) -> bool:
|
10
|
+
if group_id is None:
|
11
|
+
return any(
|
12
|
+
self_id == p.bot.self_id and user_id == p.user_id
|
13
|
+
for p in itertools.chain(*[g.players for g in Game.running_games])
|
14
|
+
)
|
15
|
+
|
16
|
+
def check(game: Game) -> bool:
|
17
|
+
return self_id == game.group.self_id and group_id == game.group.id
|
18
|
+
|
19
|
+
if game := next(filter(check, Game.running_games), None):
|
20
|
+
return any(user_id == player.user_id for player in game.players)
|
21
|
+
|
22
|
+
return False
|
23
|
+
|
24
|
+
|
25
|
+
async def rule_in_game(bot: Bot, event: Event, target: MsgTarget) -> bool:
|
26
|
+
if not Game.running_games:
|
27
|
+
return False
|
28
|
+
if target.private:
|
29
|
+
return user_in_game(bot.self_id, target.id, None)
|
30
|
+
return user_in_game(bot.self_id, event.get_user_id(), target.id)
|
31
|
+
|
32
|
+
|
33
|
+
async def rule_not_in_game(bot: Bot, event: Event, target: MsgTarget) -> bool:
|
34
|
+
return not await rule_in_game(bot, event, target)
|
35
|
+
|
36
|
+
|
37
|
+
async def is_group(target: MsgTarget) -> bool:
|
38
|
+
return not target.private
|
@@ -1,10 +1,12 @@
|
|
1
|
-
from nonebot import on_message
|
1
|
+
from nonebot import on_command, on_message
|
2
2
|
from nonebot.adapters import Event
|
3
|
-
from nonebot_plugin_alconna import MsgTarget, UniMsg
|
3
|
+
from nonebot_plugin_alconna import MsgTarget, UniMessage, UniMsg
|
4
4
|
|
5
|
-
from ..
|
5
|
+
from ..constant import STOP_COMMAND
|
6
|
+
from ..utils import InputStore
|
7
|
+
from .depends import rule_in_game
|
6
8
|
|
7
|
-
message_in_game = on_message(rule=rule_in_game)
|
9
|
+
message_in_game = on_message(rule=rule_in_game, priority=10)
|
8
10
|
|
9
11
|
|
10
12
|
@message_in_game.handle()
|
@@ -13,3 +15,11 @@ async def handle_input(event: Event, target: MsgTarget, msg: UniMsg) -> None:
|
|
13
15
|
InputStore.put(msg, target.id)
|
14
16
|
else:
|
15
17
|
InputStore.put(msg, event.get_user_id(), target.id)
|
18
|
+
|
19
|
+
|
20
|
+
stopcmd = on_command("stop", rule=rule_in_game, block=True)
|
21
|
+
|
22
|
+
|
23
|
+
@stopcmd.handle()
|
24
|
+
async def handle_stopcmd(event: Event, target: MsgTarget) -> None:
|
25
|
+
await handle_input(event=event, target=target, msg=UniMessage.text(STOP_COMMAND))
|