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