nonebot-plugin-werewolf 1.1.7__py3-none-any.whl → 1.1.8__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 +1 -1
- nonebot_plugin_werewolf/config.py +73 -15
- nonebot_plugin_werewolf/constant.py +54 -46
- nonebot_plugin_werewolf/exception.py +2 -4
- nonebot_plugin_werewolf/game.py +154 -136
- nonebot_plugin_werewolf/matchers/__init__.py +1 -0
- nonebot_plugin_werewolf/matchers/depends.py +4 -4
- nonebot_plugin_werewolf/matchers/edit_behavior.py +205 -0
- nonebot_plugin_werewolf/matchers/edit_preset.py +11 -11
- nonebot_plugin_werewolf/matchers/message_in_game.py +3 -1
- nonebot_plugin_werewolf/matchers/poke/chronocat_poke.py +8 -5
- nonebot_plugin_werewolf/matchers/poke/ob11_poke.py +3 -3
- nonebot_plugin_werewolf/matchers/start_game.py +213 -175
- nonebot_plugin_werewolf/matchers/superuser_ops.py +3 -3
- nonebot_plugin_werewolf/models.py +31 -19
- nonebot_plugin_werewolf/player_set.py +10 -8
- nonebot_plugin_werewolf/players/__init__.py +1 -1
- nonebot_plugin_werewolf/players/can_shoot.py +15 -15
- nonebot_plugin_werewolf/players/civilian.py +1 -1
- nonebot_plugin_werewolf/players/guard.py +16 -14
- nonebot_plugin_werewolf/players/hunter.py +1 -1
- nonebot_plugin_werewolf/players/idiot.py +3 -3
- nonebot_plugin_werewolf/players/{joker.py → jester.py} +4 -5
- nonebot_plugin_werewolf/players/player.py +90 -28
- nonebot_plugin_werewolf/players/prophet.py +11 -10
- nonebot_plugin_werewolf/players/werewolf.py +46 -11
- nonebot_plugin_werewolf/players/witch.py +29 -12
- nonebot_plugin_werewolf/players/wolfking.py +1 -1
- nonebot_plugin_werewolf/utils.py +105 -6
- {nonebot_plugin_werewolf-1.1.7.dist-info → nonebot_plugin_werewolf-1.1.8.dist-info}/METADATA +23 -20
- nonebot_plugin_werewolf-1.1.8.dist-info/RECORD +35 -0
- {nonebot_plugin_werewolf-1.1.7.dist-info → nonebot_plugin_werewolf-1.1.8.dist-info}/WHEEL +1 -1
- nonebot_plugin_werewolf-1.1.7.dist-info/RECORD +0 -34
- {nonebot_plugin_werewolf-1.1.7.dist-info → nonebot_plugin_werewolf-1.1.8.dist-info}/LICENSE +0 -0
- {nonebot_plugin_werewolf-1.1.7.dist-info → nonebot_plugin_werewolf-1.1.8.dist-info}/top_level.txt +0 -0
nonebot_plugin_werewolf/game.py
CHANGED
@@ -1,34 +1,43 @@
|
|
1
1
|
import contextlib
|
2
2
|
import functools
|
3
3
|
import secrets
|
4
|
-
from
|
4
|
+
from collections import defaultdict
|
5
|
+
from typing import NoReturn
|
5
6
|
|
6
7
|
import anyio
|
7
|
-
import anyio.abc
|
8
8
|
import nonebot
|
9
9
|
from nonebot.adapters import Bot
|
10
10
|
from nonebot.utils import escape_tag
|
11
11
|
from nonebot_plugin_alconna import At, Target, UniMessage
|
12
12
|
from nonebot_plugin_alconna.uniseg.message import Receipt
|
13
13
|
from nonebot_plugin_uninfo import Interface, SceneType
|
14
|
-
from typing_extensions import Self, assert_never
|
15
14
|
|
16
|
-
from .config import PresetData
|
17
|
-
from .constant import
|
15
|
+
from .config import GameBehavior, PresetData
|
16
|
+
from .constant import GAME_STATUS_CONV, REPORT_TEXT, ROLE_EMOJI, ROLE_NAME_CONV
|
18
17
|
from .exception import GameFinished
|
19
18
|
from .models import GameState, GameStatus, KillInfo, KillReason, Role, RoleGroup
|
20
19
|
from .player_set import PlayerSet
|
21
20
|
from .players import Player
|
22
|
-
from .utils import InputStore, ObjectStream, link
|
21
|
+
from .utils import InputStore, ObjectStream, SendHandler, add_stop_button, link
|
23
22
|
|
24
23
|
logger = nonebot.logger.opt(colors=True)
|
24
|
+
starting_games: dict[Target, dict[str, str]] = {}
|
25
|
+
running_games: set["Game"] = set()
|
26
|
+
|
27
|
+
|
28
|
+
def get_starting_games() -> dict[Target, dict[str, str]]:
|
29
|
+
return starting_games
|
30
|
+
|
31
|
+
|
32
|
+
def get_running_games() -> set["Game"]:
|
33
|
+
return running_games
|
25
34
|
|
26
35
|
|
27
36
|
def init_players(bot: Bot, game: "Game", players: set[str]) -> PlayerSet:
|
28
37
|
# group.colored_name not available yet
|
29
38
|
logger.debug(f"初始化 <c>{game.group_id}</c> 的玩家职业")
|
30
39
|
|
31
|
-
preset_data = PresetData.
|
40
|
+
preset_data = PresetData.get()
|
32
41
|
if (preset := preset_data.role_preset.get(len(players))) is None:
|
33
42
|
raise ValueError(
|
34
43
|
f"玩家人数不符: "
|
@@ -40,11 +49,11 @@ def init_players(bot: Bot, game: "Game", players: set[str]) -> PlayerSet:
|
|
40
49
|
roles: list[Role] = []
|
41
50
|
roles.extend(preset_data.werewolf_priority[:w])
|
42
51
|
roles.extend(preset_data.priesthood_proirity[:p])
|
43
|
-
roles.extend([Role.
|
52
|
+
roles.extend([Role.CIVILIAN] * c)
|
44
53
|
|
45
|
-
if c >= 2 and secrets.randbelow(100) <= preset_data.
|
46
|
-
roles.remove(Role.
|
47
|
-
roles.append(Role.
|
54
|
+
if c >= 2 and secrets.randbelow(100) <= preset_data.jester_probability * 100:
|
55
|
+
roles.remove(Role.CIVILIAN)
|
56
|
+
roles.append(Role.JESTER)
|
48
57
|
|
49
58
|
def _select_role() -> Role:
|
50
59
|
return roles.pop(secrets.randbelow(len(roles)))
|
@@ -57,28 +66,38 @@ def init_players(bot: Bot, game: "Game", players: set[str]) -> PlayerSet:
|
|
57
66
|
return player_set
|
58
67
|
|
59
68
|
|
69
|
+
class _SendHandler(SendHandler[str | None]):
|
70
|
+
def solve_msg(
|
71
|
+
self,
|
72
|
+
msg: UniMessage,
|
73
|
+
stop_btn_label: str | None = None,
|
74
|
+
) -> UniMessage:
|
75
|
+
if stop_btn_label is not None:
|
76
|
+
msg = add_stop_button(msg, stop_btn_label)
|
77
|
+
return msg
|
78
|
+
|
79
|
+
|
60
80
|
class DeadChannel:
|
61
81
|
players: PlayerSet
|
62
82
|
finished: anyio.Event
|
63
83
|
counter: dict[str, int]
|
64
84
|
stream: ObjectStream[tuple[Player, UniMessage]]
|
65
|
-
task_group: anyio.abc.TaskGroup
|
66
85
|
|
67
86
|
def __init__(self, players: PlayerSet, finished: anyio.Event) -> None:
|
68
87
|
self.players = players
|
69
88
|
self.finished = finished
|
70
|
-
self.counter = {p.user_id: 0 for p in
|
89
|
+
self.counter = {p.user_id: 0 for p in players}
|
71
90
|
self.stream = ObjectStream[tuple[Player, UniMessage]](16)
|
72
91
|
|
73
92
|
async def _decrease(self, user_id: str) -> None:
|
74
93
|
await anyio.sleep(60)
|
75
94
|
self.counter[user_id] -= 1
|
76
95
|
|
77
|
-
async def
|
96
|
+
async def _wait_finished(self) -> None:
|
78
97
|
await self.finished.wait()
|
79
|
-
self.
|
98
|
+
self._task_group.cancel_scope.cancel()
|
80
99
|
|
81
|
-
async def
|
100
|
+
async def _broadcast(self) -> NoReturn:
|
82
101
|
while True:
|
83
102
|
player, msg = await self.stream.recv()
|
84
103
|
msg = f"玩家 {player.name}:\n" + msg
|
@@ -89,7 +108,7 @@ class DeadChannel:
|
|
89
108
|
with contextlib.suppress(Exception):
|
90
109
|
await player.send(f"消息转发失败: {err!r}")
|
91
110
|
|
92
|
-
async def
|
111
|
+
async def _receive(self, player: Player) -> NoReturn:
|
93
112
|
await player.killed.wait()
|
94
113
|
user_id = player.user_id
|
95
114
|
|
@@ -105,30 +124,27 @@ class DeadChannel:
|
|
105
124
|
|
106
125
|
while True:
|
107
126
|
msg = await player.receive()
|
127
|
+
self.counter[user_id] += 1
|
128
|
+
self._task_group.start_soon(self._decrease, user_id)
|
108
129
|
|
109
130
|
# 发言频率限制
|
110
|
-
self.counter[user_id]
|
111
|
-
if self.counter[user_id] > 8:
|
131
|
+
if self.counter[user_id] > GameBehavior.get().dead_channel_rate_limit:
|
112
132
|
await player.send("❌发言频率超过限制, 该消息被屏蔽")
|
113
133
|
continue
|
114
134
|
|
115
135
|
# 推送消息
|
116
136
|
await self.stream.send((player, msg))
|
117
|
-
self.task_group.start_soon(self._decrease, user_id)
|
118
137
|
|
119
138
|
async def run(self) -> NoReturn:
|
120
139
|
async with anyio.create_task_group() as tg:
|
121
|
-
self.
|
122
|
-
tg.start_soon(self.
|
123
|
-
tg.start_soon(self.
|
140
|
+
self._task_group = tg
|
141
|
+
tg.start_soon(self._wait_finished)
|
142
|
+
tg.start_soon(self._broadcast)
|
124
143
|
for p in self.players:
|
125
|
-
tg.start_soon(self.
|
144
|
+
tg.start_soon(self._receive, p)
|
126
145
|
|
127
146
|
|
128
147
|
class Game:
|
129
|
-
starting_games: ClassVar[dict[Target, dict[str, str]]] = {}
|
130
|
-
running_games: ClassVar[set[Self]] = set()
|
131
|
-
|
132
148
|
bot: Bot
|
133
149
|
group: Target
|
134
150
|
players: PlayerSet
|
@@ -151,7 +167,10 @@ class Game:
|
|
151
167
|
self.killed_players = []
|
152
168
|
self._player_map = {p.user_id: p for p in self.players}
|
153
169
|
self._scene = None
|
170
|
+
self._finished = None
|
154
171
|
self._task_group = None
|
172
|
+
self._send_handler = _SendHandler()
|
173
|
+
self._send_handler.update(group)
|
155
174
|
|
156
175
|
async def _fetch_group_scene(self) -> None:
|
157
176
|
scene = await self.interface.get_scene(SceneType.GROUP, self.group_id)
|
@@ -171,88 +190,86 @@ class Game:
|
|
171
190
|
name = f"<y>{escape_tag(self._scene.name)}</y>({name})"
|
172
191
|
return link(name, self._scene and self._scene.avatar)
|
173
192
|
|
174
|
-
|
193
|
+
def log(self, text: str) -> None:
|
194
|
+
logger.info(f"{self.colored_name} | {text}")
|
195
|
+
|
196
|
+
async def send(
|
197
|
+
self,
|
198
|
+
message: str | UniMessage,
|
199
|
+
stop_btn_label: str | None = None,
|
200
|
+
) -> Receipt:
|
175
201
|
if isinstance(message, str):
|
176
202
|
message = UniMessage.text(message)
|
177
203
|
|
178
|
-
text =
|
204
|
+
text = ["<g>Send</g> | "]
|
179
205
|
for seg in message:
|
180
206
|
if isinstance(seg, At):
|
181
207
|
name = seg.target
|
182
208
|
if name in self._player_map:
|
183
209
|
name = self._player_map[name].colored_name
|
184
|
-
text
|
210
|
+
text.append(f"<y>@{name}</y>")
|
185
211
|
else:
|
186
|
-
text
|
212
|
+
text.append(escape_tag(str(seg)).replace("\n", "\\n"))
|
187
213
|
|
188
|
-
|
189
|
-
return await
|
214
|
+
self.log("".join(text))
|
215
|
+
return await self._send_handler.send(message, stop_btn_label)
|
190
216
|
|
191
217
|
def raise_for_status(self) -> None:
|
192
218
|
players = self.players.alive()
|
193
|
-
w = players.select(RoleGroup.
|
194
|
-
p = players.exclude(RoleGroup.
|
219
|
+
w = players.select(RoleGroup.WEREWOLF)
|
220
|
+
p = players.exclude(RoleGroup.WEREWOLF)
|
195
221
|
|
196
222
|
# 狼人数量大于其他职业数量
|
197
223
|
if w.size >= p.size:
|
198
|
-
raise GameFinished(GameStatus.
|
224
|
+
raise GameFinished(GameStatus.WEREWOLF)
|
199
225
|
# 屠边-村民/中立全灭
|
200
|
-
if not p.select(Role.
|
201
|
-
raise GameFinished(GameStatus.
|
226
|
+
if not p.select(Role.CIVILIAN, RoleGroup.OTHERS).size:
|
227
|
+
raise GameFinished(GameStatus.WEREWOLF)
|
202
228
|
# 屠边-神职全灭
|
203
|
-
if not p.exclude(Role.
|
204
|
-
raise GameFinished(GameStatus.
|
229
|
+
if not p.exclude(Role.CIVILIAN, RoleGroup.OTHERS).size:
|
230
|
+
raise GameFinished(GameStatus.WEREWOLF)
|
205
231
|
# 狼人全灭
|
206
232
|
if not w.size:
|
207
|
-
raise GameFinished(GameStatus.
|
233
|
+
raise GameFinished(GameStatus.GOODGUY)
|
208
234
|
|
209
235
|
async def notify_player_role(self) -> None:
|
210
236
|
msg = UniMessage()
|
211
237
|
for p in sorted(self.players, key=lambda p: p.user_id):
|
212
238
|
msg.at(p.user_id)
|
213
239
|
|
214
|
-
w, p, c = PresetData.
|
240
|
+
w, p, c = PresetData.get().role_preset[len(self.players)]
|
215
241
|
msg = (
|
216
242
|
msg.text("\n\n📝正在分配职业,请注意查看私聊消息\n")
|
217
243
|
.text(f"当前玩家数: {len(self.players)}\n")
|
218
244
|
.text(f"职业分配: 狼人x{w}, 神职x{p}, 平民x{c}")
|
219
245
|
)
|
220
246
|
|
247
|
+
if GameBehavior.get().show_roles_list_on_start:
|
248
|
+
role_cnt: dict[Role, int] = defaultdict(lambda: 0)
|
249
|
+
for role in sorted((p.role for p in self.players), key=lambda r: r.value):
|
250
|
+
role_cnt[role] += 1
|
251
|
+
|
252
|
+
msg.text("\n\n📚职业列表:\n")
|
253
|
+
for role, cnt in role_cnt.items():
|
254
|
+
msg.text(f"- {ROLE_EMOJI[role]}{ROLE_NAME_CONV[role]}x{cnt}\n")
|
255
|
+
|
221
256
|
async with anyio.create_task_group() as tg:
|
222
257
|
tg.start_soon(self.send, msg)
|
223
258
|
for p in self.players:
|
224
259
|
tg.start_soon(p.notify_role)
|
225
260
|
|
226
|
-
async def wait_stop(
|
261
|
+
async def wait_stop(
|
262
|
+
self,
|
263
|
+
*players: Player,
|
264
|
+
timeout_secs: float | None = None,
|
265
|
+
) -> None:
|
266
|
+
if timeout_secs is None:
|
267
|
+
timeout_secs = GameBehavior.get().timeout.speak
|
227
268
|
with anyio.move_on_after(timeout_secs):
|
228
269
|
async with anyio.create_task_group() as tg:
|
229
270
|
for p in players:
|
230
271
|
tg.start_soon(InputStore.fetch_until_stop, p.user_id, self.group_id)
|
231
272
|
|
232
|
-
async def interact(
|
233
|
-
self,
|
234
|
-
player_type: Player | Role | RoleGroup,
|
235
|
-
timeout_secs: float,
|
236
|
-
) -> None:
|
237
|
-
players = self.players.alive().select(player_type)
|
238
|
-
match player_type:
|
239
|
-
case Player():
|
240
|
-
text = player_type.role_name
|
241
|
-
case Role():
|
242
|
-
text = role_name_conv[player_type]
|
243
|
-
case RoleGroup():
|
244
|
-
text = f"{role_name_conv[player_type]}阵营"
|
245
|
-
case x:
|
246
|
-
assert_never(x)
|
247
|
-
|
248
|
-
await players.broadcast(f"✏️{text}交互开始,限时 {timeout_secs/60:.2f} 分钟")
|
249
|
-
try:
|
250
|
-
with anyio.fail_after(timeout_secs):
|
251
|
-
await players.interact()
|
252
|
-
except TimeoutError:
|
253
|
-
logger.debug(f"{text}交互超时 (<y>{timeout_secs}</y>s)")
|
254
|
-
await players.broadcast(f"⚠️{text}交互超时")
|
255
|
-
|
256
273
|
async def post_kill(self, players: Player | PlayerSet) -> None:
|
257
274
|
if isinstance(players, Player):
|
258
275
|
players = PlayerSet([players])
|
@@ -261,59 +278,26 @@ class Game:
|
|
261
278
|
|
262
279
|
for player in players.dead():
|
263
280
|
await player.post_kill()
|
264
|
-
if player.kill_info is
|
265
|
-
|
281
|
+
if player.kill_info is None:
|
282
|
+
continue
|
266
283
|
|
284
|
+
self.killed_players.append((player.name, player.kill_info))
|
267
285
|
shooter = self.state.shoot
|
268
286
|
if shooter is not None and (shoot := shooter.selected) is not None:
|
269
287
|
await self.send(
|
270
288
|
UniMessage.text("🔫玩家 ")
|
271
289
|
.at(shoot.user_id)
|
272
|
-
.text(f" 被{shooter.
|
273
|
-
.text(
|
290
|
+
.text(f" 被{shooter.name}射杀, 请发表遗言\n")
|
291
|
+
.text(GameBehavior.get().timeout.speak_timeout_prompt)
|
274
292
|
)
|
275
|
-
await self.wait_stop(shoot
|
293
|
+
await self.wait_stop(shoot)
|
276
294
|
self.state.shoot = shooter.selected = None
|
277
295
|
await self.post_kill(shoot)
|
278
296
|
|
279
|
-
async def select_killed(self) -> None:
|
280
|
-
players = self.players.alive()
|
281
|
-
self.state.killed = None
|
282
|
-
|
283
|
-
w = players.select(RoleGroup.Werewolf)
|
284
|
-
await self.interact(RoleGroup.Werewolf, 120)
|
285
|
-
if (s := w.player_selected()).size == 1:
|
286
|
-
self.state.killed = s.pop()
|
287
|
-
await w.broadcast(f"🔪今晚选择的目标为: {self.state.killed.name}")
|
288
|
-
else:
|
289
|
-
await w.broadcast("⚠️狼人阵营意见未统一,此晚空刀")
|
290
|
-
|
291
|
-
# 如果女巫存活,正常交互,限时1分钟
|
292
|
-
if players.include(Role.Witch):
|
293
|
-
await self.interact(Role.Witch, 60)
|
294
|
-
# 否则等待 5-20s
|
295
|
-
else:
|
296
|
-
await anyio.sleep(5 + secrets.randbelow(15))
|
297
|
-
|
298
297
|
async def run_night(self, players: PlayerSet) -> Player | None:
|
299
|
-
# 狼人、预言家、守卫 同时交互,女巫在狼人后交互
|
300
298
|
async with anyio.create_task_group() as tg:
|
301
|
-
|
302
|
-
|
303
|
-
players.select(Role.Witch).broadcast,
|
304
|
-
"ℹ️请等待狼人决定目标...",
|
305
|
-
)
|
306
|
-
tg.start_soon(self.interact, Role.Prophet, 60)
|
307
|
-
tg.start_soon(self.interact, Role.Guard, 60)
|
308
|
-
tg.start_soon(
|
309
|
-
players.exclude(
|
310
|
-
RoleGroup.Werewolf,
|
311
|
-
Role.Prophet,
|
312
|
-
Role.Witch,
|
313
|
-
Role.Guard,
|
314
|
-
).broadcast,
|
315
|
-
"ℹ️请等待其他玩家结束交互...",
|
316
|
-
)
|
299
|
+
for p in players:
|
300
|
+
tg.start_soon(p.interact)
|
317
301
|
|
318
302
|
# 狼人击杀目标
|
319
303
|
if (
|
@@ -323,9 +307,11 @@ class Game:
|
|
323
307
|
):
|
324
308
|
# 狼人正常击杀玩家
|
325
309
|
await killed.kill(
|
326
|
-
KillReason.
|
327
|
-
*players.select(RoleGroup.
|
310
|
+
KillReason.WEREWOLF,
|
311
|
+
*players.select(RoleGroup.WEREWOLF),
|
328
312
|
)
|
313
|
+
else:
|
314
|
+
killed = None
|
329
315
|
|
330
316
|
# 女巫操作目标
|
331
317
|
for witch in self.state.poison:
|
@@ -333,10 +319,32 @@ class Game:
|
|
333
319
|
continue
|
334
320
|
if witch.selected not in self.state.protected: # 守卫未保护
|
335
321
|
# 女巫毒杀玩家
|
336
|
-
await witch.selected.kill(KillReason.
|
322
|
+
await witch.selected.kill(KillReason.POISON, witch)
|
337
323
|
|
338
324
|
return killed
|
339
325
|
|
326
|
+
async def run_discussion(self) -> None:
|
327
|
+
behavior = GameBehavior.get()
|
328
|
+
timeout = behavior.timeout
|
329
|
+
|
330
|
+
if not behavior.speak_in_turn:
|
331
|
+
speak_timeout = timeout.group_speak
|
332
|
+
await self.send(
|
333
|
+
f"💬接下来开始自由讨论\n{timeout.group_speak_timeout_prompt}",
|
334
|
+
stop_btn_label="结束发言",
|
335
|
+
)
|
336
|
+
await self.wait_stop(*self.players.alive(), timeout_secs=speak_timeout)
|
337
|
+
else:
|
338
|
+
await self.send("💬接下来开始自由讨论")
|
339
|
+
speak_timeout = timeout.speak
|
340
|
+
for player in self.players.alive().sorted:
|
341
|
+
await self.send(
|
342
|
+
f"💬轮到你发言\n{timeout.speak_timeout_prompt}",
|
343
|
+
stop_btn_label="结束发言",
|
344
|
+
)
|
345
|
+
await self.wait_stop(player, timeout_secs=speak_timeout)
|
346
|
+
await self.send("💬所有玩家发言结束")
|
347
|
+
|
340
348
|
async def run_vote(self) -> None:
|
341
349
|
# 筛选当前存活玩家
|
342
350
|
players = self.players.alive()
|
@@ -352,12 +360,17 @@ class Game:
|
|
352
360
|
|
353
361
|
# 投票结果公示
|
354
362
|
msg = UniMessage.text("📊投票结果:\n")
|
355
|
-
for
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
363
|
+
for player, votes in sorted(
|
364
|
+
vote_result.items(),
|
365
|
+
key=lambda x: len(x[1]),
|
366
|
+
reverse=True,
|
367
|
+
):
|
368
|
+
if player is not None:
|
369
|
+
msg.at(player.user_id).text(f": {len(votes)} 票\n")
|
370
|
+
vote_reversed.setdefault(len(votes), []).append(player)
|
371
|
+
if (discarded_votes := (len(players) - total_votes)) > 0:
|
372
|
+
msg.text(f"弃票: {discarded_votes} 票\n")
|
373
|
+
msg.text("\n")
|
361
374
|
|
362
375
|
# 全员弃票 # 不是哥们?
|
363
376
|
if total_votes == 0:
|
@@ -382,7 +395,7 @@ class Game:
|
|
382
395
|
|
383
396
|
# 仅有一名玩家票数最高
|
384
397
|
voted = vs.pop()
|
385
|
-
if not await voted.kill(KillReason.
|
398
|
+
if not await voted.kill(KillReason.VOTE, *vote_result[voted]):
|
386
399
|
# 投票放逐失败 (例: 白痴)
|
387
400
|
return
|
388
401
|
|
@@ -391,9 +404,10 @@ class Game:
|
|
391
404
|
UniMessage.text("🔨玩家 ")
|
392
405
|
.at(voted.user_id)
|
393
406
|
.text(" 被投票放逐, 请发表遗言\n")
|
394
|
-
.text(
|
407
|
+
.text(GameBehavior.get().timeout.speak_timeout_prompt),
|
408
|
+
stop_btn_label="结束发言",
|
395
409
|
)
|
396
|
-
await self.wait_stop(voted
|
410
|
+
await self.wait_stop(voted)
|
397
411
|
await self.post_kill(voted)
|
398
412
|
|
399
413
|
async def mainloop(self) -> NoReturn:
|
@@ -404,12 +418,16 @@ class Game:
|
|
404
418
|
while True:
|
405
419
|
# 重置游戏状态,进入下一夜
|
406
420
|
self.state.reset()
|
421
|
+
self.state.state = GameState.State.NIGHT
|
407
422
|
await self.send("🌙天黑请闭眼...")
|
408
423
|
players = self.players.alive()
|
424
|
+
|
425
|
+
# 夜间交互,返回狼人目标
|
409
426
|
killed = await self.run_night(players)
|
410
427
|
|
411
428
|
# 公告
|
412
429
|
self.state.day += 1
|
430
|
+
self.state.state = GameState.State.DAY
|
413
431
|
msg = UniMessage.text(f"『第{self.state.day}天』☀️天亮了...\n")
|
414
432
|
# 没有玩家死亡,平安夜
|
415
433
|
if not (dead := players.dead()):
|
@@ -427,9 +445,10 @@ class Game:
|
|
427
445
|
UniMessage.text("⚙️当前为第一天\n请被狼人杀死的 ")
|
428
446
|
.at(killed.user_id)
|
429
447
|
.text(" 发表遗言\n")
|
430
|
-
.text(
|
448
|
+
.text(GameBehavior.get().timeout.speak_timeout_prompt),
|
449
|
+
stop_btn_label="结束发言",
|
431
450
|
)
|
432
|
-
await self.wait_stop(killed
|
451
|
+
await self.wait_stop(killed)
|
433
452
|
await self.post_kill(dead)
|
434
453
|
|
435
454
|
# 判断游戏状态
|
@@ -439,34 +458,31 @@ class Game:
|
|
439
458
|
await self.send(f"📝当前存活玩家: \n\n{self.players.alive().show()}")
|
440
459
|
|
441
460
|
# 开始自由讨论
|
442
|
-
await self.
|
443
|
-
"💬接下来开始自由讨论\n限时2分钟, "
|
444
|
-
f"全员发送 “{STOP_COMMAND_PROMPT}” 结束发言"
|
445
|
-
)
|
446
|
-
await self.wait_stop(*self.players.alive(), timeout_secs=120)
|
461
|
+
await self.run_discussion()
|
447
462
|
|
448
463
|
# 开始投票
|
449
464
|
await self.send(
|
450
465
|
"🗳️讨论结束, 进入投票环节,限时1分钟\n请在私聊中进行投票交互"
|
451
466
|
)
|
467
|
+
self.state.state = GameState.State.VOTE
|
452
468
|
await self.run_vote()
|
453
469
|
|
454
470
|
# 判断游戏状态
|
455
471
|
self.raise_for_status()
|
456
472
|
|
457
473
|
async def handle_game_finish(self, status: GameStatus) -> None:
|
458
|
-
msg = UniMessage.text(f"🎉游戏结束,{
|
474
|
+
msg = UniMessage.text(f"🎉游戏结束,{GAME_STATUS_CONV[status]}获胜\n\n")
|
459
475
|
for p in sorted(self.players, key=lambda p: (p.role.value, p.user_id)):
|
460
476
|
msg.at(p.user_id).text(f": {p.role_name}\n")
|
461
477
|
await self.send(msg)
|
462
478
|
|
463
479
|
report: list[str] = ["📌玩家死亡报告:"]
|
464
480
|
for name, info in self.killed_players:
|
465
|
-
emoji, action =
|
481
|
+
emoji, action = REPORT_TEXT[info.reason]
|
466
482
|
report.append(f"{emoji} {name} 被 {', '.join(info.killers)} {action}")
|
467
483
|
await self.send("\n\n".join(report))
|
468
484
|
|
469
|
-
async def
|
485
|
+
async def run_daemon(self) -> None:
|
470
486
|
try:
|
471
487
|
await self.mainloop()
|
472
488
|
except anyio.get_cancelled_exc_class():
|
@@ -479,18 +495,19 @@ class Game:
|
|
479
495
|
logger.exception(msg)
|
480
496
|
await self.send(f"❌狼人杀游戏进程出现未知错误: {err!r}")
|
481
497
|
finally:
|
482
|
-
|
498
|
+
if self._finished is not None:
|
499
|
+
self._finished.set()
|
483
500
|
|
484
501
|
async def start(self) -> None:
|
485
502
|
await self._fetch_group_scene()
|
486
|
-
|
487
|
-
dead_channel = DeadChannel(self.players,
|
488
|
-
|
503
|
+
self._finished = anyio.Event()
|
504
|
+
dead_channel = DeadChannel(self.players, self._finished)
|
505
|
+
get_running_games().add(self)
|
489
506
|
|
490
507
|
try:
|
491
508
|
async with anyio.create_task_group() as tg:
|
492
509
|
self._task_group = tg
|
493
|
-
tg.start_soon(self.
|
510
|
+
tg.start_soon(self.run_daemon)
|
494
511
|
tg.start_soon(dead_channel.run)
|
495
512
|
except anyio.get_cancelled_exc_class():
|
496
513
|
logger.warning(f"{self.colored_name} 的狼人杀游戏进程被取消")
|
@@ -498,8 +515,9 @@ class Game:
|
|
498
515
|
msg = f"{self.colored_name} 的狼人杀守护进程出现错误: {err!r}"
|
499
516
|
logger.opt(exception=err).error(msg)
|
500
517
|
finally:
|
518
|
+
self._finished = None
|
501
519
|
self._task_group = None
|
502
|
-
|
520
|
+
get_running_games().discard(self)
|
503
521
|
InputStore.cleanup(list(self._player_map), self.group_id)
|
504
522
|
|
505
523
|
def terminate(self) -> None:
|
@@ -3,27 +3,27 @@ import itertools
|
|
3
3
|
from nonebot.adapters import Bot, Event
|
4
4
|
from nonebot_plugin_alconna import MsgTarget, UniMessage
|
5
5
|
|
6
|
-
from ..game import Game
|
6
|
+
from ..game import Game, get_running_games
|
7
7
|
|
8
8
|
|
9
9
|
def user_in_game(self_id: str, user_id: str, group_id: str | None) -> bool:
|
10
10
|
if group_id is None:
|
11
11
|
return any(
|
12
12
|
self_id == p.bot.self_id and user_id == p.user_id
|
13
|
-
for p in itertools.chain(*[g.players for g in
|
13
|
+
for p in itertools.chain(*[g.players for g in get_running_games()])
|
14
14
|
)
|
15
15
|
|
16
16
|
def check(game: Game) -> bool:
|
17
17
|
return self_id == game.group.self_id and group_id == game.group.id
|
18
18
|
|
19
|
-
if game := next(filter(check,
|
19
|
+
if game := next(filter(check, get_running_games()), None):
|
20
20
|
return any(user_id == player.user_id for player in game.players)
|
21
21
|
|
22
22
|
return False
|
23
23
|
|
24
24
|
|
25
25
|
async def rule_in_game(bot: Bot, event: Event) -> bool:
|
26
|
-
if not
|
26
|
+
if not get_running_games():
|
27
27
|
return False
|
28
28
|
|
29
29
|
try:
|