nonebot-plugin-werewolf 1.1.8__py3-none-any.whl → 1.1.10__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 +1 -0
- nonebot_plugin_werewolf/constant.py +6 -1
- nonebot_plugin_werewolf/game.py +73 -64
- nonebot_plugin_werewolf/matchers/edit_behavior.py +19 -3
- nonebot_plugin_werewolf/matchers/poke/chronocat_poke.py +2 -2
- nonebot_plugin_werewolf/matchers/poke/ob11_poke.py +2 -2
- nonebot_plugin_werewolf/matchers/start_game.py +31 -30
- nonebot_plugin_werewolf/models.py +15 -3
- nonebot_plugin_werewolf/{players/player.py → player.py} +149 -72
- nonebot_plugin_werewolf/player_set.py +39 -23
- nonebot_plugin_werewolf/players/__init__.py +0 -1
- nonebot_plugin_werewolf/players/civilian.py +3 -3
- nonebot_plugin_werewolf/players/guard.py +20 -15
- nonebot_plugin_werewolf/players/hunter.py +6 -5
- nonebot_plugin_werewolf/players/idiot.py +25 -17
- nonebot_plugin_werewolf/players/jester.py +22 -17
- nonebot_plugin_werewolf/players/prophet.py +13 -8
- nonebot_plugin_werewolf/players/{can_shoot.py → shooter.py} +10 -10
- nonebot_plugin_werewolf/players/werewolf.py +69 -45
- nonebot_plugin_werewolf/players/witch.py +30 -23
- nonebot_plugin_werewolf/players/wolfking.py +14 -8
- nonebot_plugin_werewolf/utils.py +18 -7
- {nonebot_plugin_werewolf-1.1.8.dist-info → nonebot_plugin_werewolf-1.1.10.dist-info}/METADATA +14 -2
- nonebot_plugin_werewolf-1.1.10.dist-info/RECORD +35 -0
- {nonebot_plugin_werewolf-1.1.8.dist-info → nonebot_plugin_werewolf-1.1.10.dist-info}/WHEEL +1 -1
- nonebot_plugin_werewolf-1.1.8.dist-info/RECORD +0 -35
- {nonebot_plugin_werewolf-1.1.8.dist-info → nonebot_plugin_werewolf-1.1.10.dist-info/licenses}/LICENSE +0 -0
- {nonebot_plugin_werewolf-1.1.8.dist-info → nonebot_plugin_werewolf-1.1.10.dist-info}/top_level.txt +0 -0
@@ -56,6 +56,7 @@ class GameBehavior(ConfigFile):
|
|
56
56
|
show_roles_list_on_start: bool = False
|
57
57
|
speak_in_turn: bool = False
|
58
58
|
dead_channel_rate_limit: int = 8 # per minute
|
59
|
+
werewolf_multi_select: bool = False
|
59
60
|
|
60
61
|
class _Timeout(BaseModel):
|
61
62
|
prepare: int = Field(default=5 * 60, ge=5 * 60)
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import functools
|
2
|
+
from typing import TYPE_CHECKING
|
2
3
|
|
3
4
|
import nonebot
|
4
5
|
|
@@ -10,13 +11,17 @@ COMMAND_START = next(
|
|
10
11
|
)
|
11
12
|
|
12
13
|
|
13
|
-
@functools.cache
|
14
14
|
def stop_command_prompt() -> str:
|
15
15
|
from .config import config # circular import
|
16
16
|
|
17
17
|
return COMMAND_START + config.get_stop_command()[0]
|
18
18
|
|
19
19
|
|
20
|
+
if not TYPE_CHECKING:
|
21
|
+
stop_command_prompt = functools.cache(stop_command_prompt)
|
22
|
+
del TYPE_CHECKING
|
23
|
+
|
24
|
+
|
20
25
|
ROLE_NAME_CONV: dict[Role | RoleGroup, str] = {
|
21
26
|
Role.WEREWOLF: "狼人",
|
22
27
|
Role.WOLFKING: "狼王",
|
nonebot_plugin_werewolf/game.py
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
import contextlib
|
2
2
|
import functools
|
3
3
|
import secrets
|
4
|
-
from collections import
|
5
|
-
from typing import NoReturn
|
4
|
+
from collections import Counter
|
5
|
+
from typing import NoReturn, final
|
6
|
+
from typing_extensions import Self
|
6
7
|
|
7
8
|
import anyio
|
8
9
|
import nonebot
|
@@ -16,8 +17,8 @@ from .config import GameBehavior, PresetData
|
|
16
17
|
from .constant import GAME_STATUS_CONV, REPORT_TEXT, ROLE_EMOJI, ROLE_NAME_CONV
|
17
18
|
from .exception import GameFinished
|
18
19
|
from .models import GameState, GameStatus, KillInfo, KillReason, Role, RoleGroup
|
20
|
+
from .player import Player
|
19
21
|
from .player_set import PlayerSet
|
20
|
-
from .players import Player
|
21
22
|
from .utils import InputStore, ObjectStream, SendHandler, add_stop_button, link
|
22
23
|
|
23
24
|
logger = nonebot.logger.opt(colors=True)
|
@@ -33,9 +34,13 @@ def get_running_games() -> set["Game"]:
|
|
33
34
|
return running_games
|
34
35
|
|
35
36
|
|
36
|
-
def init_players(
|
37
|
-
|
38
|
-
|
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} 的玩家职业")
|
39
44
|
|
40
45
|
preset_data = PresetData.get()
|
41
46
|
if (preset := preset_data.role_preset.get(len(players))) is None:
|
@@ -58,11 +63,11 @@ def init_players(bot: Bot, game: "Game", players: set[str]) -> PlayerSet:
|
|
58
63
|
def _select_role() -> Role:
|
59
64
|
return roles.pop(secrets.randbelow(len(roles)))
|
60
65
|
|
61
|
-
player_set = PlayerSet(
|
62
|
-
|
63
|
-
|
64
|
-
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))
|
65
69
|
|
70
|
+
logger.debug(f"职业分配完成: <e>{escape_tag(str(player_set))}</e>")
|
66
71
|
return player_set
|
67
72
|
|
68
73
|
|
@@ -135,7 +140,7 @@ class DeadChannel:
|
|
135
140
|
# 推送消息
|
136
141
|
await self.stream.send((player, msg))
|
137
142
|
|
138
|
-
async def run(self) ->
|
143
|
+
async def run(self) -> None:
|
139
144
|
async with anyio.create_task_group() as tg:
|
140
145
|
self._task_group = tg
|
141
146
|
tg.start_soon(self._wait_finished)
|
@@ -148,36 +153,40 @@ class Game:
|
|
148
153
|
bot: Bot
|
149
154
|
group: Target
|
150
155
|
players: PlayerSet
|
151
|
-
interface: Interface
|
152
156
|
state: GameState
|
153
157
|
killed_players: list[tuple[str, KillInfo]]
|
154
158
|
|
155
|
-
def __init__(
|
156
|
-
self,
|
157
|
-
bot: Bot,
|
158
|
-
group: Target,
|
159
|
-
players: set[str],
|
160
|
-
interface: Interface,
|
161
|
-
) -> None:
|
159
|
+
def __init__(self, bot: Bot, group: Target) -> None:
|
162
160
|
self.bot = bot
|
163
161
|
self.group = group
|
164
|
-
self.players = init_players(bot, self, players)
|
165
|
-
self.interface = interface
|
166
162
|
self.state = GameState(0)
|
167
163
|
self.killed_players = []
|
168
|
-
self._player_map
|
164
|
+
self._player_map: dict[str, Player] = {}
|
165
|
+
self._shuffled: list[Player] = []
|
169
166
|
self._scene = None
|
170
|
-
self._finished = None
|
171
|
-
self.
|
172
|
-
self._send_handler = _SendHandler()
|
173
|
-
self._send_handler.update(group)
|
167
|
+
self._finished = self._task_group = None
|
168
|
+
self._send_handler = _SendHandler(group, bot)
|
174
169
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
170
|
+
@final
|
171
|
+
@classmethod
|
172
|
+
async def new(
|
173
|
+
cls,
|
174
|
+
bot: Bot,
|
175
|
+
group: Target,
|
176
|
+
players: set[str],
|
177
|
+
interface: Interface,
|
178
|
+
) -> Self:
|
179
|
+
self = cls(bot, group)
|
180
|
+
|
181
|
+
self._scene = await interface.get_scene(SceneType.GROUP, self.group_id)
|
182
|
+
if self._scene is None:
|
183
|
+
self._scene = await interface.get_scene(SceneType.GUILD, self.group_id)
|
184
|
+
|
185
|
+
self.players = await init_players(bot, self, players, interface)
|
186
|
+
self._player_map |= {p.user_id: p for p in self.players}
|
187
|
+
self._shuffled = self.players.shuffled
|
179
188
|
|
180
|
-
self
|
189
|
+
return self
|
181
190
|
|
182
191
|
@functools.cached_property
|
183
192
|
def group_id(self) -> str:
|
@@ -232,6 +241,10 @@ class Game:
|
|
232
241
|
if not w.size:
|
233
242
|
raise GameFinished(GameStatus.GOODGUY)
|
234
243
|
|
244
|
+
@property
|
245
|
+
def behavior(self) -> GameBehavior:
|
246
|
+
return GameBehavior.get()
|
247
|
+
|
235
248
|
async def notify_player_role(self) -> None:
|
236
249
|
msg = UniMessage()
|
237
250
|
for p in sorted(self.players, key=lambda p: p.user_id):
|
@@ -244,13 +257,10 @@ class Game:
|
|
244
257
|
.text(f"职业分配: 狼人x{w}, 神职x{p}, 平民x{c}")
|
245
258
|
)
|
246
259
|
|
247
|
-
if
|
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
|
-
|
260
|
+
if self.behavior.show_roles_list_on_start:
|
252
261
|
msg.text("\n\n📚职业列表:\n")
|
253
|
-
|
262
|
+
counter = Counter(p.role for p in self.players)
|
263
|
+
for role, cnt in sorted(counter.items(), key=lambda x: x[0].value):
|
254
264
|
msg.text(f"- {ROLE_EMOJI[role]}{ROLE_NAME_CONV[role]}x{cnt}\n")
|
255
265
|
|
256
266
|
async with anyio.create_task_group() as tg:
|
@@ -264,7 +274,7 @@ class Game:
|
|
264
274
|
timeout_secs: float | None = None,
|
265
275
|
) -> None:
|
266
276
|
if timeout_secs is None:
|
267
|
-
timeout_secs =
|
277
|
+
timeout_secs = self.behavior.timeout.speak
|
268
278
|
with anyio.move_on_after(timeout_secs):
|
269
279
|
async with anyio.create_task_group() as tg:
|
270
280
|
for p in players:
|
@@ -280,18 +290,18 @@ class Game:
|
|
280
290
|
await player.post_kill()
|
281
291
|
if player.kill_info is None:
|
282
292
|
continue
|
283
|
-
|
284
293
|
self.killed_players.append((player.name, player.kill_info))
|
285
|
-
|
294
|
+
|
295
|
+
shooter = self.state.shooter
|
286
296
|
if shooter is not None and (shoot := shooter.selected) is not None:
|
287
297
|
await self.send(
|
288
298
|
UniMessage.text("🔫玩家 ")
|
289
299
|
.at(shoot.user_id)
|
290
300
|
.text(f" 被{shooter.name}射杀, 请发表遗言\n")
|
291
|
-
.text(
|
301
|
+
.text(self.behavior.timeout.speak_timeout_prompt)
|
292
302
|
)
|
293
303
|
await self.wait_stop(shoot)
|
294
|
-
self.state.
|
304
|
+
self.state.shooter = shooter.selected = None
|
295
305
|
await self.post_kill(shoot)
|
296
306
|
|
297
307
|
async def run_night(self, players: PlayerSet) -> Player | None:
|
@@ -324,25 +334,27 @@ class Game:
|
|
324
334
|
return killed
|
325
335
|
|
326
336
|
async def run_discussion(self) -> None:
|
327
|
-
|
328
|
-
timeout = behavior.timeout
|
337
|
+
timeout = self.behavior.timeout
|
329
338
|
|
330
|
-
if not behavior.speak_in_turn:
|
331
|
-
speak_timeout = timeout.group_speak
|
339
|
+
if not self.behavior.speak_in_turn:
|
332
340
|
await self.send(
|
333
341
|
f"💬接下来开始自由讨论\n{timeout.group_speak_timeout_prompt}",
|
334
342
|
stop_btn_label="结束发言",
|
335
343
|
)
|
336
|
-
await self.wait_stop(
|
344
|
+
await self.wait_stop(
|
345
|
+
*self.players.alive(),
|
346
|
+
timeout_secs=timeout.group_speak,
|
347
|
+
)
|
337
348
|
else:
|
338
|
-
await self.send("
|
339
|
-
|
340
|
-
for player in self.players.alive().sorted:
|
349
|
+
await self.send("💬接下来开始轮流发言")
|
350
|
+
for player in filter(lambda p: p.alive, self._shuffled):
|
341
351
|
await self.send(
|
342
|
-
|
352
|
+
UniMessage.text("💬")
|
353
|
+
.at(player.user_id)
|
354
|
+
.text(f"\n轮到你发言\n{timeout.speak_timeout_prompt}"),
|
343
355
|
stop_btn_label="结束发言",
|
344
356
|
)
|
345
|
-
await self.wait_stop(player, timeout_secs=
|
357
|
+
await self.wait_stop(player, timeout_secs=timeout.speak)
|
346
358
|
await self.send("💬所有玩家发言结束")
|
347
359
|
|
348
360
|
async def run_vote(self) -> None:
|
@@ -395,7 +407,7 @@ class Game:
|
|
395
407
|
|
396
408
|
# 仅有一名玩家票数最高
|
397
409
|
voted = vs.pop()
|
398
|
-
if
|
410
|
+
if await voted.kill(KillReason.VOTE, *vote_result[voted]) is None:
|
399
411
|
# 投票放逐失败 (例: 白痴)
|
400
412
|
return
|
401
413
|
|
@@ -404,7 +416,7 @@ class Game:
|
|
404
416
|
UniMessage.text("🔨玩家 ")
|
405
417
|
.at(voted.user_id)
|
406
418
|
.text(" 被投票放逐, 请发表遗言\n")
|
407
|
-
.text(
|
419
|
+
.text(self.behavior.timeout.speak_timeout_prompt),
|
408
420
|
stop_btn_label="结束发言",
|
409
421
|
)
|
410
422
|
await self.wait_stop(voted)
|
@@ -445,7 +457,7 @@ class Game:
|
|
445
457
|
UniMessage.text("⚙️当前为第一天\n请被狼人杀死的 ")
|
446
458
|
.at(killed.user_id)
|
447
459
|
.text(" 发表遗言\n")
|
448
|
-
.text(
|
460
|
+
.text(self.behavior.timeout.speak_timeout_prompt),
|
449
461
|
stop_btn_label="结束发言",
|
450
462
|
)
|
451
463
|
await self.wait_stop(killed)
|
@@ -476,7 +488,7 @@ class Game:
|
|
476
488
|
msg.at(p.user_id).text(f": {p.role_name}\n")
|
477
489
|
await self.send(msg)
|
478
490
|
|
479
|
-
report
|
491
|
+
report = ["📌玩家死亡报告:"]
|
480
492
|
for name, info in self.killed_players:
|
481
493
|
emoji, action = REPORT_TEXT[info.reason]
|
482
494
|
report.append(f"{emoji} {name} 被 {', '.join(info.killers)} {action}")
|
@@ -491,24 +503,21 @@ class Game:
|
|
491
503
|
await self.handle_game_finish(result.status)
|
492
504
|
logger.info(f"{self.colored_name} 的狼人杀游戏进程正常退出")
|
493
505
|
except Exception as err:
|
494
|
-
|
495
|
-
logger.exception(msg)
|
506
|
+
logger.exception(f"{self.colored_name} 的狼人杀游戏进程出现未知错误")
|
496
507
|
await self.send(f"❌狼人杀游戏进程出现未知错误: {err!r}")
|
497
508
|
finally:
|
498
509
|
if self._finished is not None:
|
499
510
|
self._finished.set()
|
500
511
|
|
501
512
|
async def start(self) -> None:
|
502
|
-
await self._fetch_group_scene()
|
503
513
|
self._finished = anyio.Event()
|
504
514
|
dead_channel = DeadChannel(self.players, self._finished)
|
505
515
|
get_running_games().add(self)
|
506
516
|
|
507
517
|
try:
|
508
|
-
async with anyio.create_task_group() as
|
509
|
-
self._task_group
|
510
|
-
|
511
|
-
tg.start_soon(dead_channel.run)
|
518
|
+
async with anyio.create_task_group() as self._task_group:
|
519
|
+
self._task_group.start_soon(self.run_daemon)
|
520
|
+
self._task_group.start_soon(dead_channel.run)
|
512
521
|
except anyio.get_cancelled_exc_class():
|
513
522
|
logger.warning(f"{self.colored_name} 的狼人杀游戏进程被取消")
|
514
523
|
except Exception as err:
|
@@ -518,7 +527,7 @@ class Game:
|
|
518
527
|
self._finished = None
|
519
528
|
self._task_group = None
|
520
529
|
get_running_games().discard(self)
|
521
|
-
InputStore.cleanup(
|
530
|
+
InputStore.cleanup(self._player_map.keys(), self.group_id)
|
522
531
|
|
523
532
|
def terminate(self) -> None:
|
524
533
|
if self._task_group is not None:
|
@@ -44,7 +44,7 @@ alc = Alconna(
|
|
44
44
|
help_text="设置游戏开始时是否显示职业列表",
|
45
45
|
),
|
46
46
|
Subcommand(
|
47
|
-
"
|
47
|
+
"speak_in_turn",
|
48
48
|
Args["enabled#是否启用", bool],
|
49
49
|
alias={"发言顺序"},
|
50
50
|
help_text="设置是否按顺序发言",
|
@@ -55,6 +55,12 @@ alc = Alconna(
|
|
55
55
|
alias={"死亡聊天"},
|
56
56
|
help_text="设置死亡玩家发言频率限制",
|
57
57
|
),
|
58
|
+
Subcommand(
|
59
|
+
"werewolf_multi_select",
|
60
|
+
Args["enabled#是否启用", bool],
|
61
|
+
alias={"狼人多选"},
|
62
|
+
help_text="设置狼人多选时是否从已选玩家中随机选择目标, 为否时将视为空刀",
|
63
|
+
),
|
58
64
|
Subcommand(
|
59
65
|
"timeout",
|
60
66
|
Subcommand(
|
@@ -122,7 +128,7 @@ async def set_show_roles(behavior: Behavior, enabled: bool) -> None:
|
|
122
128
|
await finish(f"已{'启用' if enabled else '禁用'}游戏开始时显示职业列表")
|
123
129
|
|
124
130
|
|
125
|
-
@edit_behavior.assign("
|
131
|
+
@edit_behavior.assign("speak_in_turn")
|
126
132
|
async def set_speak_order(behavior: Behavior, enabled: bool) -> None:
|
127
133
|
behavior.speak_in_turn = enabled
|
128
134
|
await finish(f"已{'启用' if enabled else '禁用'}按顺序发言")
|
@@ -136,6 +142,15 @@ async def set_dead_chat(behavior: Behavior, limit: int) -> None:
|
|
136
142
|
await finish(f"已设置死亡玩家发言限制为 {limit} 次/分钟")
|
137
143
|
|
138
144
|
|
145
|
+
@edit_behavior.assign("werewolf_multi_select")
|
146
|
+
async def set_werewolf_multi_select(behavior: Behavior, enabled: bool) -> None:
|
147
|
+
behavior.werewolf_multi_select = enabled
|
148
|
+
await finish(
|
149
|
+
f"已{'启用' if enabled else '禁用'}狼人多选\n"
|
150
|
+
"注: 狼人意见未统一时随机选择已选玩家"
|
151
|
+
)
|
152
|
+
|
153
|
+
|
139
154
|
@edit_behavior.assign("timeout.prepare")
|
140
155
|
async def set_prepare_timeout(behavior: Behavior, time: int) -> None:
|
141
156
|
if time < 300:
|
@@ -193,13 +208,14 @@ async def handle_default(behavior: Behavior) -> None:
|
|
193
208
|
f"游戏开始发送职业列表: {'是' if behavior.show_roles_list_on_start else '否'}",
|
194
209
|
f"白天讨论按顺序发言: {'是' if behavior.speak_in_turn else '否'}",
|
195
210
|
f"死亡玩家发言转发限制: {behavior.dead_channel_rate_limit} 次/分钟",
|
211
|
+
f"狼人多选(意见未统一时随机选择已选玩家): {'是' if behavior.werewolf_multi_select else '否'}", # noqa: E501
|
196
212
|
"",
|
197
213
|
"超时时间设置:",
|
198
214
|
f"准备阶段: {timeout.prepare} 秒",
|
199
215
|
f"个人发言: {timeout.speak} 秒",
|
200
216
|
f"集体发言: {timeout.group_speak} 秒",
|
201
|
-
f"交互阶段: {timeout.interact} 秒",
|
202
217
|
f"投票阶段: {timeout.vote} 秒",
|
218
|
+
f"交互阶段: {timeout.interact} 秒",
|
203
219
|
f"狼人交互: {timeout.werewolf} 秒",
|
204
220
|
]
|
205
221
|
await finish("\n".join(lines))
|
@@ -76,7 +76,7 @@ with contextlib.suppress(ImportError):
|
|
76
76
|
user_id=user_id,
|
77
77
|
group_id=(event.guild and event.guild.id) or event.channel.id,
|
78
78
|
)
|
79
|
-
and
|
79
|
+
and target in get_starting_games()
|
80
80
|
)
|
81
81
|
|
82
82
|
@on_message(rule=_rule_poke_join).handle()
|
@@ -86,7 +86,7 @@ with contextlib.suppress(ImportError):
|
|
86
86
|
target: MsgTarget,
|
87
87
|
) -> None:
|
88
88
|
user_id = extract_poke_tome(event) or event.get_user_id()
|
89
|
-
players =
|
89
|
+
players = get_starting_games()[target]
|
90
90
|
|
91
91
|
if user_id not in players:
|
92
92
|
# XXX:
|
@@ -50,7 +50,7 @@ with contextlib.suppress(ImportError):
|
|
50
50
|
return (
|
51
51
|
(event.target_id == event.self_id)
|
52
52
|
and not user_in_game(bot.self_id, user_id, group_id)
|
53
|
-
and
|
53
|
+
and target in get_starting_games()
|
54
54
|
)
|
55
55
|
|
56
56
|
@on_notice(rule=_rule_poke_join).handle()
|
@@ -60,7 +60,7 @@ with contextlib.suppress(ImportError):
|
|
60
60
|
target: MsgTarget,
|
61
61
|
) -> None:
|
62
62
|
user_id = event.get_user_id()
|
63
|
-
players =
|
63
|
+
players = get_starting_games()[target]
|
64
64
|
|
65
65
|
if event.group_id is None or user_id in players:
|
66
66
|
return
|
@@ -118,14 +118,15 @@ class PrepareGame:
|
|
118
118
|
self.send_handler = self._SendHandler()
|
119
119
|
self.logger = nonebot.logger.opt(colors=True)
|
120
120
|
self.shoud_start_game = False
|
121
|
-
self.
|
121
|
+
get_starting_games()[self.group] = self.players
|
122
|
+
|
123
|
+
self._handlers: dict[str, Callable[[], Awaitable[bool | None]]] = {
|
122
124
|
"开始游戏": self._handle_start,
|
123
125
|
"结束游戏": self._handle_end,
|
124
126
|
"加入游戏": self._handle_join,
|
125
127
|
"退出游戏": self._handle_quit,
|
126
128
|
"当前玩家": self._handle_list,
|
127
129
|
}
|
128
|
-
get_starting_games()[self.group] = self.players
|
129
130
|
|
130
131
|
async def run(self) -> None:
|
131
132
|
try:
|
@@ -163,6 +164,33 @@ class PrepareGame:
|
|
163
164
|
if event is not None:
|
164
165
|
await self.stream.send((event, text, name))
|
165
166
|
|
167
|
+
async def _handle(self) -> None:
|
168
|
+
bot = current_bot.get()
|
169
|
+
superuser = SuperUser()
|
170
|
+
|
171
|
+
while not self.stream.closed:
|
172
|
+
event, text, name = await self.stream.recv()
|
173
|
+
user_id = event.get_user_id()
|
174
|
+
colored = f"<y>{escape_tag(name)}</y>(<c>{escape_tag(user_id)}</c>)"
|
175
|
+
self.current = self._Current(
|
176
|
+
id=user_id,
|
177
|
+
name=name,
|
178
|
+
colored=colored,
|
179
|
+
is_admin=user_id == self.admin_id,
|
180
|
+
is_super_user=await superuser(bot, event),
|
181
|
+
)
|
182
|
+
self.send_handler.update(event)
|
183
|
+
|
184
|
+
# 更新用户名
|
185
|
+
# 当用户通过 chronoca:poke 加入游戏时, 插件无法获取用户名, 原字典值为用户ID
|
186
|
+
if user_id in self.players and self.players.get(user_id) != name:
|
187
|
+
self.logger.debug(f"更新玩家显示名称: {self.current.colored}")
|
188
|
+
self.players[user_id] = name
|
189
|
+
|
190
|
+
handler = self._handlers.get(text)
|
191
|
+
if handler is not None and await handler():
|
192
|
+
return
|
193
|
+
|
166
194
|
async def _send(self, msg: str | UniMessage) -> None:
|
167
195
|
await self.send_handler.send(msg)
|
168
196
|
|
@@ -240,33 +268,6 @@ class PrepareGame:
|
|
240
268
|
)
|
241
269
|
await self._send("✨当前玩家:\n" + "\n".join(lines))
|
242
270
|
|
243
|
-
async def _handle(self) -> None:
|
244
|
-
bot = current_bot.get()
|
245
|
-
superuser = SuperUser()
|
246
|
-
|
247
|
-
while not self.stream.closed:
|
248
|
-
event, text, name = await self.stream.recv()
|
249
|
-
user_id = event.get_user_id()
|
250
|
-
colored = f"<y>{escape_tag(name)}</y>(<c>{escape_tag(user_id)}</c>)"
|
251
|
-
self.current = self._Current(
|
252
|
-
id=user_id,
|
253
|
-
name=name,
|
254
|
-
colored=colored,
|
255
|
-
is_admin=user_id == self.admin_id,
|
256
|
-
is_super_user=await superuser(bot, event),
|
257
|
-
)
|
258
|
-
self.send_handler.update(event)
|
259
|
-
|
260
|
-
# 更新用户名
|
261
|
-
# 当用户通过 chronoca:poke 加入游戏时, 插件无法获取用户名, 原字典值为用户ID
|
262
|
-
if user_id in self.players and self.players.get(user_id) != name:
|
263
|
-
self.logger.debug(f"更新玩家显示名称: {self.current.colored}")
|
264
|
-
self.players[user_id] = name
|
265
|
-
|
266
|
-
handler = self._msg_handler.get(text)
|
267
|
-
if handler is not None and await handler():
|
268
|
-
return
|
269
|
-
|
270
271
|
|
271
272
|
@start_game.handle()
|
272
273
|
async def handle_notice(target: MsgTarget) -> None:
|
@@ -328,5 +329,5 @@ async def handle_start(
|
|
328
329
|
await UniMessage.text("⚠️游戏准备超时,已自动结束").finish()
|
329
330
|
|
330
331
|
dump_players(target, players)
|
331
|
-
game = Game(bot, target, set(players), interface)
|
332
|
+
game = await Game.new(bot, target, set(players), interface)
|
332
333
|
await game.start()
|
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
|
|
5
5
|
import anyio
|
6
6
|
|
7
7
|
if TYPE_CHECKING:
|
8
|
-
from .
|
8
|
+
from .player import Player
|
9
9
|
|
10
10
|
|
11
11
|
class Role(int, Enum):
|
@@ -57,11 +57,13 @@ class GameState:
|
|
57
57
|
"""当前天数记录, 不会被 `reset()` 重置"""
|
58
58
|
state: State = State.NIGHT
|
59
59
|
"""当前游戏状态, 不会被 `reset()` 重置"""
|
60
|
+
_werewolf_interact_count: int = 0
|
61
|
+
"""内部属性, 记录当晚狼人交互状态"""
|
60
62
|
werewolf_finished: anyio.Event = dataclasses.field(default_factory=anyio.Event)
|
61
63
|
"""狼人交互是否结束"""
|
62
64
|
killed: "Player | None" = None
|
63
65
|
"""当晚狼人击杀目标, `None` 则为空刀"""
|
64
|
-
|
66
|
+
shooter: "Player | None" = None
|
65
67
|
"""当前执行射杀操作的玩家"""
|
66
68
|
antidote: set["Player"] = dataclasses.field(default_factory=set)
|
67
69
|
"""当晚女巫使用解药的目标"""
|
@@ -72,12 +74,22 @@ class GameState:
|
|
72
74
|
|
73
75
|
def reset(self) -> None:
|
74
76
|
self.werewolf_finished = anyio.Event()
|
77
|
+
self._werewolf_interact_count = 0
|
75
78
|
self.killed = None
|
76
|
-
self.
|
79
|
+
self.shooter = None
|
77
80
|
self.antidote = set()
|
78
81
|
self.poison = set()
|
79
82
|
self.protected = set()
|
80
83
|
|
84
|
+
def werewolf_start(self) -> None:
|
85
|
+
self._werewolf_interact_count += 1
|
86
|
+
|
87
|
+
def werewolf_end(self) -> bool:
|
88
|
+
self._werewolf_interact_count -= 1
|
89
|
+
if self._werewolf_interact_count == 0:
|
90
|
+
self.werewolf_finished.set()
|
91
|
+
return self.werewolf_finished.is_set()
|
92
|
+
|
81
93
|
|
82
94
|
@dataclasses.dataclass
|
83
95
|
class KillInfo:
|