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