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