nonebot-plugin-werewolf 1.1.5__py3-none-any.whl → 1.1.6__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/config.py +18 -55
- nonebot_plugin_werewolf/constant.py +14 -74
- nonebot_plugin_werewolf/exception.py +1 -1
- nonebot_plugin_werewolf/game.py +198 -216
- nonebot_plugin_werewolf/matchers/__init__.py +2 -0
- nonebot_plugin_werewolf/matchers/depends.py +17 -5
- nonebot_plugin_werewolf/matchers/edit_preset.py +263 -0
- nonebot_plugin_werewolf/matchers/message_in_game.py +8 -3
- nonebot_plugin_werewolf/matchers/start_game.py +140 -48
- nonebot_plugin_werewolf/matchers/superuser_ops.py +24 -0
- nonebot_plugin_werewolf/models.py +73 -0
- nonebot_plugin_werewolf/player_set.py +1 -1
- nonebot_plugin_werewolf/players/can_shoot.py +4 -3
- nonebot_plugin_werewolf/players/civilian.py +1 -1
- nonebot_plugin_werewolf/players/guard.py +2 -1
- nonebot_plugin_werewolf/players/hunter.py +1 -1
- nonebot_plugin_werewolf/players/idiot.py +1 -1
- nonebot_plugin_werewolf/players/joker.py +6 -2
- nonebot_plugin_werewolf/players/player.py +15 -24
- nonebot_plugin_werewolf/players/prophet.py +2 -1
- nonebot_plugin_werewolf/players/werewolf.py +16 -14
- nonebot_plugin_werewolf/players/witch.py +2 -1
- nonebot_plugin_werewolf/players/wolfking.py +1 -1
- nonebot_plugin_werewolf/utils.py +69 -5
- {nonebot_plugin_werewolf-1.1.5.dist-info → nonebot_plugin_werewolf-1.1.6.dist-info}/METADATA +67 -67
- nonebot_plugin_werewolf-1.1.6.dist-info/RECORD +34 -0
- {nonebot_plugin_werewolf-1.1.5.dist-info → nonebot_plugin_werewolf-1.1.6.dist-info}/WHEEL +1 -1
- nonebot_plugin_werewolf-1.1.5.dist-info/RECORD +0 -31
- {nonebot_plugin_werewolf-1.1.5.dist-info → nonebot_plugin_werewolf-1.1.6.dist-info}/LICENSE +0 -0
- {nonebot_plugin_werewolf-1.1.5.dist-info → nonebot_plugin_werewolf-1.1.6.dist-info}/top_level.txt +0 -0
nonebot_plugin_werewolf/game.py
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
+
import contextlib
|
1
2
|
import secrets
|
2
3
|
from typing import ClassVar, NoReturn
|
3
4
|
|
4
5
|
import anyio
|
6
|
+
import anyio.abc
|
5
7
|
from nonebot.adapters import Bot
|
6
8
|
from nonebot.log import logger
|
7
9
|
from nonebot.utils import escape_tag
|
@@ -10,39 +12,32 @@ from nonebot_plugin_alconna.uniseg.message import Receipt
|
|
10
12
|
from nonebot_plugin_uninfo import Interface, SceneType
|
11
13
|
from typing_extensions import Self, assert_never
|
12
14
|
|
13
|
-
from .config 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
|
-
)
|
15
|
+
from .config import PresetData
|
16
|
+
from .constant import STOP_COMMAND_PROMPT, game_status_conv, report_text, role_name_conv
|
24
17
|
from .exception import GameFinished
|
18
|
+
from .models import GameState, GameStatus, KillInfo, KillReason, Role, RoleGroup
|
25
19
|
from .player_set import PlayerSet
|
26
20
|
from .players import Player
|
27
|
-
from .utils import InputStore, link
|
21
|
+
from .utils import InputStore, ObjectStream, link
|
28
22
|
|
29
23
|
|
30
24
|
def init_players(bot: Bot, game: "Game", players: set[str]) -> PlayerSet:
|
31
25
|
logger.opt(colors=True).debug(f"初始化 <c>{game.group.id}</c> 的玩家职业")
|
32
|
-
|
33
|
-
if (preset := role_preset.get(len(players))) is None:
|
26
|
+
preset_data = PresetData.load()
|
27
|
+
if (preset := preset_data.role_preset.get(len(players))) is None:
|
34
28
|
raise ValueError(
|
35
29
|
f"玩家人数不符: "
|
36
|
-
f"应为 {', '.join(map(str, role_preset))} 人,
|
30
|
+
f"应为 {', '.join(map(str, preset_data.role_preset))} 人, "
|
31
|
+
f"传入{len(players)}人"
|
37
32
|
)
|
38
33
|
|
39
34
|
w, p, c = preset
|
40
35
|
roles: list[Role] = []
|
41
|
-
roles.extend(
|
42
|
-
roles.extend(
|
36
|
+
roles.extend(preset_data.werewolf_priority[:w])
|
37
|
+
roles.extend(preset_data.priesthood_proirity[:p])
|
43
38
|
roles.extend([Role.Civilian] * c)
|
44
39
|
|
45
|
-
if c >= 2 and secrets.randbelow(100) <=
|
40
|
+
if c >= 2 and secrets.randbelow(100) <= preset_data.joker_probability * 100:
|
46
41
|
roles.remove(Role.Civilian)
|
47
42
|
roles.append(Role.Joker)
|
48
43
|
|
@@ -57,6 +52,74 @@ def init_players(bot: Bot, game: "Game", players: set[str]) -> PlayerSet:
|
|
57
52
|
return player_set
|
58
53
|
|
59
54
|
|
55
|
+
class DeadChannel:
|
56
|
+
players: PlayerSet
|
57
|
+
finished: anyio.Event
|
58
|
+
counter: dict[str, int]
|
59
|
+
stream: ObjectStream[tuple[Player, UniMessage]]
|
60
|
+
task_group: anyio.abc.TaskGroup
|
61
|
+
|
62
|
+
def __init__(self, players: PlayerSet, finished: anyio.Event) -> None:
|
63
|
+
self.players = players
|
64
|
+
self.finished = finished
|
65
|
+
self.counter = {p.user_id: 0 for p in self.players}
|
66
|
+
self.stream = ObjectStream[tuple[Player, UniMessage]](16)
|
67
|
+
|
68
|
+
async def _decrease(self, user_id: str) -> None:
|
69
|
+
await anyio.sleep(60)
|
70
|
+
self.counter[user_id] -= 1
|
71
|
+
|
72
|
+
async def _handle_finished(self) -> None:
|
73
|
+
await self.finished.wait()
|
74
|
+
self.task_group.cancel_scope.cancel()
|
75
|
+
|
76
|
+
async def _handle_send(self) -> None:
|
77
|
+
while True:
|
78
|
+
player, msg = await self.stream.recv()
|
79
|
+
msg = f"玩家 {player.name}:\n" + msg
|
80
|
+
target = self.players.killed().exclude(player)
|
81
|
+
try:
|
82
|
+
await target.broadcast(msg)
|
83
|
+
except Exception as err:
|
84
|
+
with contextlib.suppress(Exception):
|
85
|
+
await player.send(f"消息转发失败: {err!r}")
|
86
|
+
|
87
|
+
async def _handle_recv(self, player: Player) -> NoReturn:
|
88
|
+
await player.killed.wait()
|
89
|
+
user_id = player.user_id
|
90
|
+
|
91
|
+
await player.send(
|
92
|
+
"ℹ️你已加入死者频道,请勿在群组内继续发言\n"
|
93
|
+
"私聊发送消息将转发至其他已死亡玩家",
|
94
|
+
)
|
95
|
+
await (
|
96
|
+
self.players.killed()
|
97
|
+
.exclude(player)
|
98
|
+
.broadcast(f"ℹ️玩家 {player.name} 加入了死者频道")
|
99
|
+
)
|
100
|
+
|
101
|
+
while True:
|
102
|
+
msg = await player.receive()
|
103
|
+
|
104
|
+
# 发言频率限制
|
105
|
+
self.counter[user_id] += 1
|
106
|
+
if self.counter[user_id] > 8:
|
107
|
+
await player.send("❌发言频率超过限制, 该消息被屏蔽")
|
108
|
+
continue
|
109
|
+
|
110
|
+
# 推送消息
|
111
|
+
await self.stream.send((player, msg))
|
112
|
+
self.task_group.start_soon(self._decrease, user_id)
|
113
|
+
|
114
|
+
async def run(self) -> NoReturn:
|
115
|
+
async with anyio.create_task_group() as tg:
|
116
|
+
self.task_group = tg
|
117
|
+
tg.start_soon(self._handle_finished)
|
118
|
+
tg.start_soon(self._handle_send)
|
119
|
+
for p in self.players:
|
120
|
+
tg.start_soon(self._handle_recv, p)
|
121
|
+
|
122
|
+
|
60
123
|
class Game:
|
61
124
|
starting_games: ClassVar[dict[Target, dict[str, str]]] = {}
|
62
125
|
running_games: ClassVar[set[Self]] = set()
|
@@ -66,7 +129,7 @@ class Game:
|
|
66
129
|
players: PlayerSet
|
67
130
|
interface: Interface
|
68
131
|
state: GameState
|
69
|
-
killed_players: list[
|
132
|
+
killed_players: list[tuple[str, KillInfo]]
|
70
133
|
|
71
134
|
def __init__(
|
72
135
|
self,
|
@@ -83,6 +146,7 @@ class Game:
|
|
83
146
|
self.killed_players = []
|
84
147
|
self._player_map = {p.user_id: p for p in self.players}
|
85
148
|
self._scene = None
|
149
|
+
self._task_group = None
|
86
150
|
|
87
151
|
async def _fetch_group_scene(self) -> None:
|
88
152
|
scene = await self.interface.get_scene(SceneType.GROUP, self.group.id)
|
@@ -93,17 +157,10 @@ class Game:
|
|
93
157
|
|
94
158
|
@property
|
95
159
|
def colored_name(self) -> str:
|
96
|
-
name = escape_tag(self.group.id)
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
160
|
+
name = f"<b><e>{escape_tag(self.group.id)}</e></b>"
|
161
|
+
if self._scene and self._scene.name is not None:
|
162
|
+
name = f"<y>{escape_tag(self._scene.name)}</y>({name})"
|
163
|
+
return link(name, self._scene and self._scene.avatar)
|
107
164
|
|
108
165
|
async def send(self, message: str | UniMessage) -> Receipt:
|
109
166
|
if isinstance(message, str):
|
@@ -112,20 +169,17 @@ class Game:
|
|
112
169
|
text = f"{self.colored_name} | <g>Send</g> | "
|
113
170
|
for seg in message:
|
114
171
|
if isinstance(seg, At):
|
115
|
-
|
172
|
+
name = seg.target
|
173
|
+
if name in self._player_map:
|
174
|
+
name = self._player_map[name].colored_name
|
175
|
+
text += f"<y>@{name}</y>"
|
116
176
|
else:
|
117
177
|
text += escape_tag(str(seg)).replace("\n", "\\n")
|
118
178
|
|
119
179
|
logger.opt(colors=True).info(text)
|
120
180
|
return await message.send(self.group, self.bot)
|
121
181
|
|
122
|
-
def
|
123
|
-
msg = UniMessage()
|
124
|
-
for p in sorted(self.players, key=lambda p: (p.role_name, p.user_id)):
|
125
|
-
msg.at(p.user_id)
|
126
|
-
return msg
|
127
|
-
|
128
|
-
def check_game_status(self) -> None:
|
182
|
+
def raise_for_status(self) -> None:
|
129
183
|
players = self.players.alive()
|
130
184
|
w = players.select(RoleGroup.Werewolf)
|
131
185
|
p = players.exclude(RoleGroup.Werewolf)
|
@@ -143,38 +197,16 @@ class Game:
|
|
143
197
|
if not w.size:
|
144
198
|
raise GameFinished(GameStatus.GoodGuy)
|
145
199
|
|
146
|
-
def show_killed_players(self) -> str:
|
147
|
-
result: list[str] = []
|
148
|
-
|
149
|
-
for player in self.killed_players:
|
150
|
-
if player.kill_info is None:
|
151
|
-
continue
|
152
|
-
|
153
|
-
line = f"{player.name} 被 " + ", ".join(
|
154
|
-
p.name for p in player.kill_info.killers
|
155
|
-
)
|
156
|
-
match player.kill_info.reason:
|
157
|
-
case KillReason.Werewolf:
|
158
|
-
line = f"🔪 {line} 刀了"
|
159
|
-
case KillReason.Poison:
|
160
|
-
line = f"🧪 {line} 毒死"
|
161
|
-
case KillReason.Shoot:
|
162
|
-
line = f"🔫 {line} 射杀"
|
163
|
-
case KillReason.Vote:
|
164
|
-
line = f"🗳️ {line} 票出"
|
165
|
-
case x:
|
166
|
-
assert_never(x)
|
167
|
-
result.append(line)
|
168
|
-
|
169
|
-
return "\n\n".join(result)
|
170
|
-
|
171
200
|
async def notify_player_role(self) -> None:
|
172
|
-
|
201
|
+
msg = UniMessage()
|
202
|
+
for p in sorted(self.players, key=lambda p: p.user_id):
|
203
|
+
msg.at(p.user_id)
|
204
|
+
|
205
|
+
w, p, c = PresetData.load().role_preset[len(self.players)]
|
173
206
|
msg = (
|
174
|
-
|
175
|
-
.text("\n\n📝正在分配职业,请注意查看私聊消息\n")
|
207
|
+
msg.text("\n\n📝正在分配职业,请注意查看私聊消息\n")
|
176
208
|
.text(f"当前玩家数: {len(self.players)}\n")
|
177
|
-
.text(f"职业分配: 狼人x{
|
209
|
+
.text(f"职业分配: 狼人x{w}, 神职x{p}, 平民x{c}")
|
178
210
|
)
|
179
211
|
|
180
212
|
async with anyio.create_task_group() as tg:
|
@@ -183,16 +215,10 @@ class Game:
|
|
183
215
|
tg.start_soon(p.notify_role)
|
184
216
|
|
185
217
|
async def wait_stop(self, *players: Player, timeout_secs: float) -> None:
|
186
|
-
async def wait(p: Player) -> None:
|
187
|
-
while True:
|
188
|
-
msg = await InputStore.fetch(p.user_id, self.group.id)
|
189
|
-
if msg.extract_plain_text().strip() == STOP_COMMAND:
|
190
|
-
break
|
191
|
-
|
192
218
|
with anyio.move_on_after(timeout_secs):
|
193
219
|
async with anyio.create_task_group() as tg:
|
194
220
|
for p in players:
|
195
|
-
tg.start_soon(
|
221
|
+
tg.start_soon(InputStore.fetch_until_stop, p.user_id, self.group.id)
|
196
222
|
|
197
223
|
async def interact(
|
198
224
|
self,
|
@@ -218,6 +244,29 @@ class Game:
|
|
218
244
|
logger.opt(colors=True).debug(f"{text}交互超时 (<y>{timeout_secs}</y>s)")
|
219
245
|
await players.broadcast(f"⚠️{text}交互超时")
|
220
246
|
|
247
|
+
async def post_kill(self, players: Player | PlayerSet) -> None:
|
248
|
+
if isinstance(players, Player):
|
249
|
+
players = PlayerSet([players])
|
250
|
+
if not players:
|
251
|
+
return
|
252
|
+
|
253
|
+
for player in players.dead():
|
254
|
+
await player.post_kill()
|
255
|
+
if player.kill_info is not None:
|
256
|
+
self.killed_players.append((player.name, player.kill_info))
|
257
|
+
|
258
|
+
shooter = self.state.shoot
|
259
|
+
if shooter is not None and (shoot := shooter.selected) is not None:
|
260
|
+
await self.send(
|
261
|
+
UniMessage.text("🔫玩家 ")
|
262
|
+
.at(shoot.user_id)
|
263
|
+
.text(f" 被{shooter.role_name}射杀, 请发表遗言\n")
|
264
|
+
.text(f"限时1分钟, 发送 “{STOP_COMMAND_PROMPT}” 结束发言")
|
265
|
+
)
|
266
|
+
await self.wait_stop(shoot, timeout_secs=60)
|
267
|
+
self.state.shoot = shooter.selected = None
|
268
|
+
await self.post_kill(shoot)
|
269
|
+
|
221
270
|
async def select_killed(self) -> None:
|
222
271
|
players = self.players.alive()
|
223
272
|
self.state.killed = None
|
@@ -237,45 +286,47 @@ class Game:
|
|
237
286
|
else:
|
238
287
|
await anyio.sleep(5 + secrets.randbelow(15))
|
239
288
|
|
240
|
-
async def
|
241
|
-
|
242
|
-
players = PlayerSet([players])
|
243
|
-
if not players:
|
244
|
-
return
|
245
|
-
|
289
|
+
async def run_night(self, players: PlayerSet) -> Player | None:
|
290
|
+
# 狼人、预言家、守卫 同时交互,女巫在狼人后交互
|
246
291
|
async with anyio.create_task_group() as tg:
|
292
|
+
tg.start_soon(self.select_killed)
|
247
293
|
tg.start_soon(
|
248
|
-
players.broadcast,
|
249
|
-
"
|
250
|
-
"私聊发送消息将转发至其他已死亡玩家",
|
294
|
+
players.select(Role.Witch).broadcast,
|
295
|
+
"ℹ️请等待狼人决定目标...",
|
251
296
|
)
|
297
|
+
tg.start_soon(self.interact, Role.Prophet, 60)
|
298
|
+
tg.start_soon(self.interact, Role.Guard, 60)
|
252
299
|
tg.start_soon(
|
253
|
-
|
254
|
-
|
300
|
+
players.exclude(
|
301
|
+
RoleGroup.Werewolf,
|
302
|
+
Role.Prophet,
|
303
|
+
Role.Witch,
|
304
|
+
Role.Guard,
|
305
|
+
).broadcast,
|
306
|
+
"ℹ️请等待其他玩家结束交互...",
|
255
307
|
)
|
256
308
|
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
309
|
+
# 狼人击杀目标
|
310
|
+
if (
|
311
|
+
(killed := self.state.killed) is not None # 狼人未空刀
|
312
|
+
and killed not in self.state.protected # 守卫保护
|
313
|
+
and killed not in self.state.antidote # 女巫使用解药
|
314
|
+
):
|
315
|
+
# 狼人正常击杀玩家
|
316
|
+
await killed.kill(
|
317
|
+
KillReason.Werewolf,
|
318
|
+
*players.select(RoleGroup.Werewolf),
|
319
|
+
)
|
262
320
|
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
321
|
+
# 女巫操作目标
|
322
|
+
for witch in self.state.poison:
|
323
|
+
if witch.selected is None:
|
324
|
+
continue
|
325
|
+
if witch.selected not in self.state.protected: # 守卫未保护
|
326
|
+
# 女巫毒杀玩家
|
327
|
+
await witch.selected.kill(KillReason.Poison, witch)
|
267
328
|
|
268
|
-
|
269
|
-
if shooter is not None and (shoot := shooter.selected) is not None:
|
270
|
-
await self.send(
|
271
|
-
UniMessage.text("🔫玩家 ")
|
272
|
-
.at(shoot.user_id)
|
273
|
-
.text(f" 被{shooter.role_name}射杀, 请发表遗言\n")
|
274
|
-
.text(f"限时1分钟, 发送 “{STOP_COMMAND_PROMPT}” 结束发言")
|
275
|
-
)
|
276
|
-
await self.wait_stop(shoot, timeout_secs=60)
|
277
|
-
self.state.shoot = shooter.selected = None
|
278
|
-
await self.post_kill(shoot)
|
329
|
+
return killed
|
279
330
|
|
280
331
|
async def run_vote(self) -> None:
|
281
332
|
# 筛选当前存活玩家
|
@@ -336,46 +387,7 @@ class Game:
|
|
336
387
|
await self.wait_stop(voted, timeout_secs=60)
|
337
388
|
await self.post_kill(voted)
|
338
389
|
|
339
|
-
async def
|
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()
|
345
|
-
|
346
|
-
async def handle_send() -> NoReturn:
|
347
|
-
while True:
|
348
|
-
player, msg = await recv.receive()
|
349
|
-
msg = f"玩家 {player.name}:\n" + msg
|
350
|
-
await self.players.killed().exclude(player).broadcast(msg)
|
351
|
-
|
352
|
-
async def handle_recv(player: Player) -> NoReturn:
|
353
|
-
await player.killed.wait()
|
354
|
-
|
355
|
-
counter = 0
|
356
|
-
|
357
|
-
async def decrease() -> None:
|
358
|
-
nonlocal counter
|
359
|
-
await anyio.sleep(60)
|
360
|
-
counter -= 1
|
361
|
-
|
362
|
-
while True:
|
363
|
-
msg = await player.receive()
|
364
|
-
counter += 1
|
365
|
-
if counter <= 10:
|
366
|
-
await send.send((player, msg))
|
367
|
-
tg.start_soon(decrease)
|
368
|
-
else:
|
369
|
-
await player.send("❌发言频率超过限制, 该消息被屏蔽")
|
370
|
-
|
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)
|
376
|
-
|
377
|
-
async def run(self) -> NoReturn:
|
378
|
-
await self._fetch_group_scene()
|
390
|
+
async def mainloop(self) -> NoReturn:
|
379
391
|
# 告知玩家角色信息
|
380
392
|
await self.notify_player_role()
|
381
393
|
|
@@ -383,48 +395,11 @@ class Game:
|
|
383
395
|
while True:
|
384
396
|
# 重置游戏状态,进入下一夜
|
385
397
|
self.state.reset()
|
386
|
-
players = self.players.alive()
|
387
398
|
await self.send("🌙天黑请闭眼...")
|
399
|
+
players = self.players.alive()
|
400
|
+
killed = await self.run_night(players)
|
388
401
|
|
389
|
-
#
|
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
|
-
)
|
407
|
-
|
408
|
-
# 狼人击杀目标
|
409
|
-
if (
|
410
|
-
(killed := self.state.killed) is not None # 狼人未空刀
|
411
|
-
and killed not in self.state.protected # 守卫保护
|
412
|
-
and killed not in self.state.antidote # 女巫使用解药
|
413
|
-
):
|
414
|
-
# 狼人正常击杀玩家
|
415
|
-
await killed.kill(
|
416
|
-
KillReason.Werewolf,
|
417
|
-
*players.select(RoleGroup.Werewolf),
|
418
|
-
)
|
419
|
-
|
420
|
-
# 女巫操作目标
|
421
|
-
for witch in self.state.poison:
|
422
|
-
if witch.selected is None:
|
423
|
-
continue
|
424
|
-
if witch.selected not in self.state.protected: # 守卫未保护
|
425
|
-
# 女巫毒杀玩家
|
426
|
-
await witch.selected.kill(KillReason.Poison, witch)
|
427
|
-
|
402
|
+
# 公告
|
428
403
|
self.state.day += 1
|
429
404
|
msg = UniMessage.text(f"『第{self.state.day}天』☀️天亮了...\n")
|
430
405
|
# 没有玩家死亡,平安夜
|
@@ -449,7 +424,7 @@ class Game:
|
|
449
424
|
await self.post_kill(dead)
|
450
425
|
|
451
426
|
# 判断游戏状态
|
452
|
-
self.
|
427
|
+
self.raise_for_status()
|
453
428
|
|
454
429
|
# 公示存活玩家
|
455
430
|
await self.send(f"📝当前存活玩家: \n\n{self.players.alive().show()}")
|
@@ -468,50 +443,57 @@ class Game:
|
|
468
443
|
await self.run_vote()
|
469
444
|
|
470
445
|
# 判断游戏状态
|
471
|
-
self.
|
446
|
+
self.raise_for_status()
|
472
447
|
|
473
448
|
async def handle_game_finish(self, status: GameStatus) -> None:
|
474
|
-
|
475
|
-
case GameStatus.GoodGuy:
|
476
|
-
winner = "好人"
|
477
|
-
case GameStatus.Werewolf:
|
478
|
-
winner = "狼人"
|
479
|
-
case GameStatus.Joker:
|
480
|
-
winner = "小丑"
|
481
|
-
case x:
|
482
|
-
assert_never(x)
|
483
|
-
|
484
|
-
msg = UniMessage.text(f"🎉游戏结束,{winner}获胜\n\n")
|
449
|
+
msg = UniMessage.text(f"🎉游戏结束,{game_status_conv[status]}获胜\n\n")
|
485
450
|
for p in sorted(self.players, key=lambda p: (p.role.value, p.user_id)):
|
486
451
|
msg.at(p.user_id).text(f": {p.role_name}\n")
|
487
452
|
await self.send(msg)
|
488
|
-
await self.send(f"📌玩家死亡报告:\n\n{self.show_killed_players()}")
|
489
453
|
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
454
|
+
report: list[str] = ["📌玩家死亡报告:"]
|
455
|
+
for name, info in self.killed_players:
|
456
|
+
emoji, action = report_text[info.reason]
|
457
|
+
report.append(f"{emoji} {name} 被 {', '.join(info.killers)} {action}")
|
458
|
+
await self.send("\n\n".join(report))
|
459
|
+
|
460
|
+
async def daemon(self, finished: anyio.Event) -> None:
|
461
|
+
try:
|
462
|
+
await self.mainloop()
|
463
|
+
except anyio.get_cancelled_exc_class():
|
464
|
+
logger.warning(f"{self.group.id} 的狼人杀游戏进程被取消")
|
465
|
+
except GameFinished as result:
|
466
|
+
await self.handle_game_finish(result.status)
|
467
|
+
logger.info(f"{self.group.id} 的狼人杀游戏进程正常退出")
|
468
|
+
except Exception as err:
|
469
|
+
msg = f"{self.group.id} 的狼人杀游戏进程出现未知错误: {err!r}"
|
470
|
+
logger.exception(msg)
|
471
|
+
await self.send(f"❌狼人杀游戏进程出现未知错误: {err!r}")
|
472
|
+
finally:
|
473
|
+
finished.set()
|
505
474
|
|
475
|
+
async def start(self) -> None:
|
476
|
+
await self._fetch_group_scene()
|
506
477
|
finished = anyio.Event()
|
478
|
+
dead_channel = DeadChannel(self.players, finished)
|
507
479
|
self.running_games.add(self)
|
480
|
+
|
508
481
|
try:
|
509
482
|
async with anyio.create_task_group() as tg:
|
510
|
-
tg
|
511
|
-
tg.start_soon(self.
|
483
|
+
self._task_group = tg
|
484
|
+
tg.start_soon(self.daemon, finished)
|
485
|
+
tg.start_soon(dead_channel.run)
|
486
|
+
except anyio.get_cancelled_exc_class():
|
487
|
+
logger.warning(f"{self.group.id} 的狼人杀游戏进程被取消")
|
512
488
|
except Exception as err:
|
513
489
|
msg = f"{self.group.id} 的狼人杀守护进程出现错误: {err!r}"
|
514
490
|
logger.opt(exception=err).error(msg)
|
515
491
|
finally:
|
492
|
+
self._task_group = None
|
516
493
|
self.running_games.discard(self)
|
517
494
|
InputStore.cleanup(list(self._player_map), self.group.id)
|
495
|
+
|
496
|
+
def terminate(self) -> None:
|
497
|
+
if self._task_group is not None:
|
498
|
+
logger.warning(f"中止 {self.group.id} 的狼人杀游戏进程")
|
499
|
+
self._task_group.cancel_scope.cancel()
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import itertools
|
2
2
|
|
3
3
|
from nonebot.adapters import Bot, Event
|
4
|
-
from nonebot_plugin_alconna import MsgTarget
|
4
|
+
from nonebot_plugin_alconna import MsgTarget, UniMessage
|
5
5
|
|
6
6
|
from ..game import Game
|
7
7
|
|
@@ -22,16 +22,28 @@ def user_in_game(self_id: str, user_id: str, group_id: str | None) -> bool:
|
|
22
22
|
return False
|
23
23
|
|
24
24
|
|
25
|
-
async def rule_in_game(bot: Bot, event: Event
|
25
|
+
async def rule_in_game(bot: Bot, event: Event) -> bool:
|
26
26
|
if not Game.running_games:
|
27
27
|
return False
|
28
|
+
|
29
|
+
try:
|
30
|
+
target = UniMessage.get_target(event, bot)
|
31
|
+
except NotImplementedError:
|
32
|
+
return False
|
33
|
+
|
28
34
|
if target.private:
|
29
35
|
return user_in_game(bot.self_id, target.id, None)
|
30
|
-
|
36
|
+
|
37
|
+
try:
|
38
|
+
user_id = event.get_user_id()
|
39
|
+
except Exception:
|
40
|
+
return False
|
41
|
+
|
42
|
+
return user_in_game(bot.self_id, user_id, target.id)
|
31
43
|
|
32
44
|
|
33
|
-
async def rule_not_in_game(bot: Bot, event: Event
|
34
|
-
return not await rule_in_game(bot, event
|
45
|
+
async def rule_not_in_game(bot: Bot, event: Event) -> bool:
|
46
|
+
return not await rule_in_game(bot, event)
|
35
47
|
|
36
48
|
|
37
49
|
async def is_group(target: MsgTarget) -> bool:
|