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