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