nonebot-plugin-werewolf 1.0.6__py3-none-any.whl → 1.1.0__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.
@@ -8,7 +8,7 @@ require("nonebot_plugin_waiter")
8
8
  from . import matchers as matchers
9
9
  from .config import Config
10
10
 
11
- __version__ = "1.0.6"
11
+ __version__ = "1.1.0"
12
12
  __plugin_meta__ = PluginMetadata(
13
13
  name="狼人杀",
14
14
  description="适用于 Nonebot2 的狼人杀插件",
@@ -1,10 +1,81 @@
1
- from nonebot import get_plugin_config
2
- from pydantic import BaseModel
1
+ from typing import Literal, overload
2
+
3
+ from nonebot import get_plugin_config, logger
4
+ from nonebot.compat import PYDANTIC_V2
5
+ from pydantic import BaseModel, Field
6
+ from typing_extensions import Self
7
+
8
+ from .constant import (
9
+ Role,
10
+ default_priesthood_proirity,
11
+ default_role_preset,
12
+ default_werewolf_priority,
13
+ )
14
+
15
+ if PYDANTIC_V2:
16
+ from pydantic import model_validator as model_validator
17
+ else:
18
+ from pydantic import root_validator
19
+
20
+ @overload
21
+ def model_validator(*, mode: Literal["before"]): ... # noqa: ANN201
22
+
23
+ @overload
24
+ def model_validator(*, mode: Literal["after"]): ... # noqa: ANN201
25
+
26
+ def model_validator(*, mode: Literal["before", "after"]):
27
+ return root_validator(
28
+ pre=mode == "before", # pyright: ignore[reportArgumentType]
29
+ allow_reuse=True,
30
+ )
3
31
 
4
32
 
5
33
  class PluginConfig(BaseModel):
6
- enable_poke: bool = True
7
- override_preset: list[tuple[int, int, int, int]] | None = None
34
+ enable_poke: bool = Field(default=True)
35
+ role_preset: list[tuple[int, int, int, int]] | dict[int, tuple[int, int, int]] = (
36
+ Field(default_factory=default_role_preset.copy)
37
+ )
38
+ werewolf_priority: list[Role] = Field(
39
+ default_factory=default_werewolf_priority.copy
40
+ )
41
+ priesthood_proirity: list[Role] = Field(
42
+ default_factory=default_priesthood_proirity.copy
43
+ )
44
+ joker_probability: float = Field(default=0.0, ge=0.0, le=1.0)
45
+
46
+ @model_validator(mode="after")
47
+ def _validate(self) -> Self:
48
+ if isinstance(self.role_preset, list):
49
+ for preset in self.role_preset:
50
+ if preset[0] != sum(preset[1:]):
51
+ raise RuntimeError(
52
+ "配置项 `role_preset` 错误: "
53
+ f"预设总人数为 {preset[0]}, 实际总人数为 {sum(preset[1:])} "
54
+ f"({', '.join(map(str, preset[1:]))})"
55
+ )
56
+ self.role_preset = default_role_preset | {
57
+ i[0]: i[1:] for i in self.role_preset
58
+ }
59
+ logger.debug(f"覆写配置 role_preset: {self.role_preset}")
60
+
61
+ min_length = max(i[0] for i in self.role_preset.values())
62
+ if len(self.werewolf_priority) < min_length:
63
+ raise RuntimeError(
64
+ f"配置项 `werewolf_priority` 错误: 应至少为 {min_length} 项"
65
+ )
66
+
67
+ min_length = max(i[1] for i in self.role_preset.values())
68
+ if len(self.priesthood_proirity) < min_length:
69
+ raise RuntimeError(
70
+ f"配置项 `priesthood_proirity` 错误: 应至少为 {min_length} 项"
71
+ )
72
+
73
+ return self
74
+
75
+ def get_role_preset(self) -> dict[int, tuple[int, int, int]]:
76
+ if isinstance(self.role_preset, list):
77
+ self.role_preset = {i[0]: i[1:] for i in self.role_preset}
78
+ return self.role_preset
8
79
 
9
80
 
10
81
  class Config(BaseModel):
@@ -12,3 +83,4 @@ class Config(BaseModel):
12
83
 
13
84
 
14
85
  config = get_plugin_config(Config).werewolf
86
+ logger.debug(f"加载插件配置: {config}")
@@ -4,46 +4,47 @@ from dataclasses import dataclass
4
4
  from enum import Enum, auto
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- from strenum import StrEnum
8
-
9
- from .config import config
10
-
11
7
  if TYPE_CHECKING:
12
8
  from .player import Player
13
9
 
14
10
 
15
- class Role(StrEnum):
11
+ class Role(Enum):
16
12
  # 狼人
17
- Werewolf = auto()
18
- WolfKing = auto()
13
+ Werewolf = 1
14
+ WolfKing = 2
19
15
 
20
16
  # 神职
21
- Prophet = auto()
22
- Witch = auto()
23
- Hunter = auto()
24
- Guard = auto()
25
- Idiot = auto()
17
+ Prophet = 11
18
+ Witch = 12
19
+ Hunter = 13
20
+ Guard = 14
21
+ Idiot = 15
22
+
23
+ # 其他
24
+ Joker = 51
26
25
 
27
26
  # 平民
28
- Civilian = auto()
27
+ Civilian = 0
29
28
 
30
29
 
31
- class RoleGroup(StrEnum):
30
+ class RoleGroup(Enum):
32
31
  Werewolf = auto()
33
32
  GoodGuy = auto()
33
+ Others = auto()
34
34
 
35
35
 
36
36
  class KillReason(Enum):
37
- Kill = auto()
37
+ Werewolf = auto()
38
38
  Poison = auto()
39
39
  Shoot = auto()
40
40
  Vote = auto()
41
41
 
42
42
 
43
43
  class GameStatus(Enum):
44
- Good = auto()
45
- Bad = auto()
44
+ GoodGuy = auto()
45
+ Werewolf = auto()
46
46
  Unset = auto()
47
+ Joker = auto()
47
48
 
48
49
 
49
50
  @dataclass
@@ -55,7 +56,7 @@ class GameState:
55
56
  potion: tuple[Player | None, tuple[bool, bool]] = (None, (False, False))
56
57
 
57
58
 
58
- role_name_conv: dict[Role, str] = {
59
+ role_name_conv: dict[Role | RoleGroup, str] = {
59
60
  Role.Werewolf: "狼人",
60
61
  Role.WolfKing: "狼王",
61
62
  Role.Prophet: "预言家",
@@ -63,16 +64,14 @@ role_name_conv: dict[Role, str] = {
63
64
  Role.Hunter: "猎人",
64
65
  Role.Guard: "守卫",
65
66
  Role.Idiot: "白痴",
67
+ Role.Joker: "小丑",
66
68
  Role.Civilian: "平民",
67
- }
68
-
69
-
70
- role_group_name_conv: dict[RoleGroup, str] = {
71
69
  RoleGroup.Werewolf: "狼人",
72
70
  RoleGroup.GoodGuy: "好人",
71
+ RoleGroup.Others: "其他",
73
72
  }
74
73
 
75
- player_preset: dict[int, tuple[int, int, int]] = {
74
+ default_role_preset: dict[int, tuple[int, int, int]] = {
76
75
  # 总人数: (狼, 神, 民)
77
76
  6: (1, 2, 3),
78
77
  7: (2, 2, 3),
@@ -83,6 +82,16 @@ player_preset: dict[int, tuple[int, int, int]] = {
83
82
  12: (4, 5, 3),
84
83
  }
85
84
 
86
- if config.override_preset is not None:
87
- player_preset |= {i[0]: i[1:] for i in config.override_preset}
88
- player_preset |= {i[0]: i[1:] for i in config.override_preset}
85
+ default_werewolf_priority: list[Role] = [
86
+ Role.Werewolf,
87
+ Role.Werewolf,
88
+ Role.WolfKing,
89
+ Role.Werewolf,
90
+ ]
91
+ default_priesthood_proirity: list[Role] = [
92
+ Role.Witch,
93
+ Role.Prophet,
94
+ Role.Hunter,
95
+ Role.Guard,
96
+ Role.Idiot,
97
+ ]
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from .constant import GameStatus
7
+
8
+
9
+ class Error(Exception):
10
+ """插件错误类型基类"""
11
+
12
+
13
+ class GameFinishedError(Error):
14
+ """游戏结束时抛出,无视游戏进程进入结算"""
15
+
16
+ status: GameStatus
17
+
18
+ def __init__(self, status: GameStatus) -> None:
19
+ self.status = status
@@ -1,46 +1,59 @@
1
+ from __future__ import annotations
2
+
1
3
  import asyncio
2
4
  import asyncio.timeouts
3
5
  import contextlib
4
6
  import random
5
7
  import time
8
+ from typing import TYPE_CHECKING, NoReturn
6
9
 
7
- from nonebot.adapters import Bot
8
10
  from nonebot.log import logger
9
- from nonebot_plugin_alconna import Target, UniMessage
11
+ from nonebot_plugin_alconna import At, Target, UniMessage
10
12
 
11
- from .constant import GameState, GameStatus, KillReason, Role, RoleGroup, player_preset
13
+ from .config import config
14
+ from .constant import GameState, GameStatus, KillReason, Role, RoleGroup, role_name_conv
15
+ from .exception import GameFinishedError
12
16
  from .player import Player
13
17
  from .player_set import PlayerSet
14
18
  from .utils import InputStore
15
19
 
20
+ if TYPE_CHECKING:
21
+ from nonebot.adapters import Bot
22
+ from nonebot_plugin_alconna.uniseg.message import Receipt
23
+
16
24
  starting_games: dict[str, dict[str, str]] = {}
17
- running_games: dict[str, tuple["Game", asyncio.Task[None], asyncio.Task[None]]] = {}
25
+ running_games: dict[str, Game] = {}
18
26
 
19
27
 
20
- def init_players(bot: Bot, game: "Game", players: dict[str, str]) -> PlayerSet:
21
- preset = player_preset.get(len(players))
28
+ def init_players(bot: Bot, game: Game, players: dict[str, str]) -> PlayerSet:
29
+ logger.opt(colors=True).debug(f"初始化 <c>{game.group.id}</c> 的玩家职业")
30
+ role_preset = config.get_role_preset()
31
+ preset = role_preset.get(len(players))
22
32
  if preset is None:
23
33
  raise ValueError(
24
34
  f"玩家人数不符: "
25
- f"应为 {', '.join(map(str, player_preset))} 人, 传入{len(players)}人"
35
+ f"应为 {', '.join(map(str, role_preset))} 人, 传入{len(players)}人"
26
36
  )
27
37
 
28
38
  roles: list[Role] = []
29
- roles.extend(
30
- [Role.Werewolf, Role.Werewolf, Role.WolfKing, Role.Werewolf][: preset[0]]
31
- )
32
- roles.extend(
33
- [Role.Prophet, Role.Witch, Role.Hunter, Role.Guard, Role.Idiot][: preset[1]]
34
- )
39
+ roles.extend(config.werewolf_priority[: preset[0]])
40
+ roles.extend(config.priesthood_proirity[: preset[1]])
35
41
  roles.extend([Role.Civilian] * preset[2])
36
42
 
37
- r = random.Random(time.time())
43
+ r = random.Random(time.time()) # noqa: S311
44
+
45
+ if roles.count(Role.Civilian) >= 2 and r.random() <= config.joker_probability:
46
+ roles.remove(Role.Civilian)
47
+ roles.append(Role.Joker)
48
+
38
49
  shuffled: list[Role] = []
39
50
  for _ in range(len(players)):
40
51
  idx = r.randint(0, len(roles) - 1)
41
52
  shuffled.append(roles.pop(idx))
42
53
 
43
- async def selector(target_: Target, b: Bot):
54
+ logger.debug(f"职业分配: {shuffled}")
55
+
56
+ async def selector(target_: Target, b: Bot) -> bool:
44
57
  return target_.self_id == bot.self_id and b is bot
45
58
 
46
59
  return PlayerSet(
@@ -56,7 +69,7 @@ def init_players(bot: Bot, game: "Game", players: dict[str, str]) -> PlayerSet:
56
69
  ),
57
70
  players[user_id],
58
71
  )
59
- for user_id, role in zip(players, shuffled)
72
+ for user_id, role in zip(players, shuffled, strict=True)
60
73
  )
61
74
 
62
75
 
@@ -64,24 +77,28 @@ class Game:
64
77
  bot: Bot
65
78
  group: Target
66
79
  players: PlayerSet
80
+ _player_map: dict[str, Player]
67
81
  state: GameState
68
82
  killed_players: list[Player]
69
83
 
70
- def __init__(
71
- self,
72
- bot: Bot,
73
- group: Target,
74
- players: dict[str, str],
75
- ) -> None:
84
+ def __init__(self, bot: Bot, group: Target, players: dict[str, str]) -> None:
76
85
  self.bot = bot
77
86
  self.group = group
78
87
  self.players = init_players(bot, self, players)
88
+ self._player_map = {p.user_id: p for p in self.players}
79
89
  self.state = GameState(0)
80
90
  self.killed_players = []
81
91
 
82
- async def send(self, message: str | UniMessage):
92
+ async def send(self, message: str | UniMessage) -> Receipt:
83
93
  if isinstance(message, str):
84
94
  message = UniMessage.text(message)
95
+ text = f"<b><e>{self.group.id}</e></b> | <g>Send</g> | "
96
+ for seg in message:
97
+ if isinstance(seg, At):
98
+ text += f"<y>@{self._player_map[seg.target].name}</y>"
99
+ else:
100
+ text += str(seg)
101
+ logger.opt(colors=True).info(text.replace("\n", "\\n"))
85
102
  return await message.send(self.group, self.bot)
86
103
 
87
104
  def at_all(self) -> UniMessage:
@@ -95,14 +112,18 @@ class Game:
95
112
  w = players.select(RoleGroup.Werewolf)
96
113
  p = players.exclude(RoleGroup.Werewolf)
97
114
 
115
+ # 狼人数量大于其他职业数量
98
116
  if w.size >= p.size:
99
- return GameStatus.Bad
100
- if not p.select(Role.Civilian):
101
- return GameStatus.Bad
102
- if not p.exclude(Role.Civilian):
103
- return GameStatus.Bad
117
+ raise GameFinishedError(GameStatus.Werewolf)
118
+ # 屠边-村民/中立全灭
119
+ if not p.select(Role.Civilian, RoleGroup.Others).size:
120
+ raise GameFinishedError(GameStatus.Werewolf)
121
+ # 屠边-神职全灭
122
+ if not p.exclude(Role.Civilian).size:
123
+ raise GameFinishedError(GameStatus.Werewolf)
124
+ # 狼人全灭
104
125
  if not w.size:
105
- return GameStatus.Good
126
+ raise GameFinishedError(GameStatus.GoodGuy)
106
127
 
107
128
  return GameStatus.Unset
108
129
 
@@ -117,7 +138,7 @@ class Game:
117
138
  p.name for p in player.kill_info.killers
118
139
  )
119
140
  match player.kill_info.reason:
120
- case KillReason.Kill:
141
+ case KillReason.Werewolf:
121
142
  msg += " 刀了"
122
143
  case KillReason.Poison:
123
144
  msg += " 毒死"
@@ -130,7 +151,7 @@ class Game:
130
151
  return msg.strip()
131
152
 
132
153
  async def notify_player_role(self) -> None:
133
- preset = player_preset[len(self.players)]
154
+ preset = config.get_role_preset()[len(self.players)]
134
155
  await asyncio.gather(
135
156
  self.send(
136
157
  self.at_all()
@@ -149,7 +170,7 @@ class Game:
149
170
  if isinstance(players, Player):
150
171
  players = PlayerSet([players])
151
172
 
152
- async def wait(p: Player):
173
+ async def wait(p: Player) -> None:
153
174
  while True:
154
175
  msg = await InputStore.fetch(p.user_id, self.group.id)
155
176
  if msg.extract_plain_text() == "/stop":
@@ -169,9 +190,9 @@ class Game:
169
190
  type_.role_name # Player
170
191
  if isinstance(type_, Player)
171
192
  else (
172
- type_.name # Role
193
+ role_name_conv[type_] # Role
173
194
  if isinstance(type_, Role)
174
- else f"{type_.name}阵营" # RoleGroup
195
+ else f"{role_name_conv[type_]}阵营" # RoleGroup
175
196
  )
176
197
  )
177
198
 
@@ -179,6 +200,7 @@ class Game:
179
200
  try:
180
201
  await players.interact(timeout_secs)
181
202
  except TimeoutError:
203
+ logger.opt(colors=True).debug(f"{text}交互超时 (<y>{timeout_secs}</y>s)")
182
204
  await players.broadcast(f"{text}交互时间结束")
183
205
 
184
206
  async def select_killed(self) -> None:
@@ -198,7 +220,7 @@ class Game:
198
220
  await self.interact(Role.Witch, 60)
199
221
  # 否则等待 5-20s
200
222
  else:
201
- await asyncio.sleep(random.uniform(5, 20))
223
+ await asyncio.sleep(random.uniform(5, 20)) # noqa: S311
202
224
 
203
225
  async def handle_new_dead(self, players: Player | PlayerSet) -> None:
204
226
  if isinstance(players, Player):
@@ -250,6 +272,8 @@ class Game:
250
272
  # 收集到的总票数
251
273
  total_votes = sum(map(len, vote_result.values()))
252
274
 
275
+ logger.debug(f"投票结果: {vote_result}")
276
+
253
277
  # 投票结果公示
254
278
  msg = UniMessage.text("投票结果:\n")
255
279
  for p, v in sorted(vote_result.items(), key=lambda x: len(x[1]), reverse=True):
@@ -299,23 +323,23 @@ class Game:
299
323
  loop = asyncio.get_event_loop()
300
324
  queue: asyncio.Queue[tuple[Player, UniMessage]] = asyncio.Queue()
301
325
 
302
- async def send():
326
+ async def send() -> NoReturn:
303
327
  while True:
304
328
  player, msg = await queue.get()
305
329
  msg = f"玩家 {player.name}:\n" + msg
306
- await self.players.dead().exclude(player).broadcast(msg)
330
+ await self.players.killed().exclude(player).broadcast(msg)
331
+ queue.task_done()
332
+
333
+ async def recv(player: Player) -> NoReturn:
334
+ await player.killed.wait()
307
335
 
308
- async def recv(player: Player):
309
336
  counter = 0
310
337
 
311
- def decrease():
338
+ def decrease() -> None:
312
339
  nonlocal counter
313
340
  counter -= 1
314
341
 
315
342
  while True:
316
- if not player.killed:
317
- await asyncio.sleep(1)
318
- continue
319
343
  msg = await player.receive()
320
344
  counter += 1
321
345
  if counter <= 10:
@@ -326,13 +350,14 @@ class Game:
326
350
 
327
351
  await asyncio.gather(send(), *[recv(p) for p in self.players])
328
352
 
329
- async def run(self) -> None:
353
+ async def run(self) -> NoReturn:
330
354
  # 告知玩家角色信息
331
355
  await self.notify_player_role()
332
356
  # 天数记录 主要用于第一晚狼人击杀的遗言
333
357
  day_count = 0
334
358
 
335
- while self.check_game_status() == GameStatus.Unset:
359
+ # 游戏主循环
360
+ while True:
336
361
  # 重置游戏状态,进入下一夜
337
362
  self.state = GameState(day_count)
338
363
  players = self.players.alive()
@@ -341,10 +366,12 @@ class Game:
341
366
  # 狼人、预言家、守卫 同时交互,女巫在狼人后交互
342
367
  await asyncio.gather(
343
368
  self.select_killed(),
344
- players.select(Role.Witch).broadcast("请等待狼人决定目标..."),
345
- players.select(Role.Civilian).broadcast("请等待其他玩家结束交互..."),
346
369
  self.interact(Role.Prophet, 60),
347
370
  self.interact(Role.Guard, 60),
371
+ players.select(Role.Witch).broadcast("请等待狼人决定目标..."),
372
+ players.select(Role.Civilian, RoleGroup.Others).broadcast(
373
+ "请等待其他玩家结束交互..."
374
+ ),
348
375
  )
349
376
 
350
377
  # 狼人击杀目标
@@ -354,13 +381,13 @@ class Game:
354
381
  # 女巫的操作目标和内容
355
382
  potioned, (antidote, poison) = self.state.potion
356
383
 
357
- # 狼人未空刀
358
- if killed is not None:
359
- # 除非守卫保护或女巫使用解药,否则狼人正常击杀玩家
360
- if not ((killed is protected) or (antidote and potioned is killed)):
361
- await killed.kill(
362
- KillReason.Kill, *players.select(RoleGroup.Werewolf)
363
- )
384
+ # 狼人未空刀,除非守卫保护或女巫使用解药,否则狼人正常击杀玩家
385
+ if killed is not None and (
386
+ not ((killed is protected) or (antidote and potioned is killed))
387
+ ):
388
+ await killed.kill(
389
+ KillReason.Werewolf, *players.select(RoleGroup.Werewolf)
390
+ )
364
391
  # 如果女巫使用毒药且守卫未保护,杀死该玩家
365
392
  if poison and (potioned is not None) and (potioned is not protected):
366
393
  await potioned.kill(KillReason.Poison, *players.select(Role.Witch))
@@ -389,8 +416,7 @@ class Game:
389
416
  await self.post_kill(dead)
390
417
 
391
418
  # 判断游戏状态
392
- if self.check_game_status() != GameStatus.Unset:
393
- break
419
+ self.check_game_status()
394
420
 
395
421
  # 公示存活玩家
396
422
  await self.send(f"当前存活玩家: \n\n{self.players.alive().show()}")
@@ -403,32 +429,55 @@ class Game:
403
429
  await self.send("讨论结束, 进入投票环节,限时1分钟\n请在私聊中进行投票交互")
404
430
  await self.run_vote()
405
431
 
406
- # 游戏结束
407
- winner = "好人" if self.check_game_status() == GameStatus.Good else "狼人"
432
+ # 判断游戏状态
433
+ self.check_game_status()
434
+
435
+ async def handle_game_finish(self, status: GameStatus) -> None:
436
+ match status:
437
+ case GameStatus.GoodGuy:
438
+ winner = "好人"
439
+ case GameStatus.Werewolf:
440
+ winner = "狼人"
441
+ case GameStatus.Joker:
442
+ winner = "小丑"
443
+ case GameStatus.Unset:
444
+ raise RuntimeError(f"错误的游戏状态: {status!r}")
445
+
408
446
  msg = UniMessage.text(f"🎉游戏结束,{winner}获胜\n\n")
409
447
  for p in sorted(self.players, key=lambda p: (p.role.value, p.user_id)):
410
448
  msg.at(p.user_id).text(f": {p.role_name}\n")
411
449
  await self.send(msg)
412
- await self.send(f"玩家死亡报告:\n\n{self.show_killed_players()}")
450
+ await self.send(f"📌玩家死亡报告:\n\n{self.show_killed_players()}")
413
451
 
414
- def start(self):
415
- task = asyncio.create_task(self.run())
452
+ def start(self) -> None:
453
+ finished = asyncio.Event()
454
+ game_task = asyncio.create_task(self.run())
455
+ game_task.add_done_callback(lambda _: finished.set())
416
456
  dead_channel = asyncio.create_task(self.run_dead_channel())
417
457
 
418
- async def daemon():
419
- while not task.done(): # noqa: ASYNC110
420
- await asyncio.sleep(1)
458
+ async def daemon() -> None:
459
+ await finished.wait()
421
460
 
422
461
  try:
423
- task.result()
424
- except asyncio.CancelledError as err:
425
- logger.warning(f"狼人杀游戏进程被取消: {err}")
462
+ game_task.result()
463
+ except asyncio.CancelledError:
464
+ logger.warning(f"{self.group.id} 的狼人杀游戏进程被取消")
465
+ except GameFinishedError as result:
466
+ await self.handle_game_finish(result.status)
467
+ logger.info(f"{self.group.id} 的狼人杀游戏进程正常退出")
426
468
  except Exception as err:
427
- msg = f"狼人杀游戏进程出现错误: {err!r}"
469
+ msg = f"{self.group.id} 的狼人杀游戏进程出现未知错误: {err!r}"
428
470
  logger.opt(exception=err).error(msg)
429
471
  await self.send(msg)
430
472
  finally:
431
473
  dead_channel.cancel()
432
474
  running_games.pop(self.group.id, None)
433
475
 
434
- running_games[self.group.id] = (self, task, asyncio.create_task(daemon()))
476
+ def daemon_callback(task: asyncio.Task[None]) -> None:
477
+ if err := task.exception():
478
+ logger.opt(exception=err).error(
479
+ f"{self.group.id} 的狼人杀守护进程出现错误: {err!r}"
480
+ )
481
+
482
+ running_games[self.group.id] = self
483
+ asyncio.create_task(daemon()).add_done_callback(daemon_callback)
@@ -4,11 +4,12 @@ from typing import Annotated
4
4
 
5
5
  from nonebot import on_command, on_message
6
6
  from nonebot.adapters import Bot, Event
7
+ from nonebot.exception import FinishedException
7
8
  from nonebot.rule import to_me
8
9
  from nonebot_plugin_alconna import MsgTarget, UniMessage, UniMsg
9
10
  from nonebot_plugin_userinfo import EventUserInfo, UserInfo
10
11
 
11
- from .game import Game, running_games, starting_games
12
+ from .game import Game
12
13
  from .ob11_ext import ob11_ext_enabled
13
14
  from .utils import InputStore, is_group, prepare_game, rule_in_game, rule_not_in_game
14
15
 
@@ -35,9 +36,6 @@ async def handle_start(
35
36
  target: MsgTarget,
36
37
  admin_info: Annotated[UserInfo, EventUserInfo()],
37
38
  ) -> None:
38
- if target.id in running_games:
39
- await UniMessage.text("当前群聊内有正在进行的游戏,无法创建游戏").finish()
40
-
41
39
  admin_id = event.get_user_id()
42
40
  msg = (
43
41
  UniMessage.at(admin_id)
@@ -51,15 +49,15 @@ async def handle_start(
51
49
  msg.text("\n可使用戳一戳代替游戏交互中的 “/stop” 命令")
52
50
  await msg.text("\n\n游戏准备阶段限时5分钟,超时将自动结束").send()
53
51
 
54
- players = starting_games[target.id] = {admin_id: admin_info.user_name}
52
+ players = {admin_id: admin_info.user_name}
55
53
 
56
54
  try:
57
55
  async with asyncio.timeouts.timeout(5 * 60):
58
56
  await prepare_game(event, players)
57
+ except FinishedException:
58
+ raise
59
59
  except TimeoutError:
60
60
  await UniMessage.text("游戏准备超时,已自动结束").finish()
61
- finally:
62
- del starting_games[target.id]
63
61
 
64
- game = Game(bot=bot, group=target, players=players)
62
+ game = Game(bot, target, players)
65
63
  game.start()
@@ -62,7 +62,7 @@ with contextlib.suppress(ImportError):
62
62
  group_id=int(group_id),
63
63
  user_id=int(user_id),
64
64
  )
65
- players[user_id] = res.get("nickname") or user_id
65
+ players[user_id] = res.get("card") or res.get("nickname") or user_id
66
66
  await bot.send(event, MessageSegment.at(user_id) + "成功加入游戏")
67
67
 
68
68
  def ob11_ext_enabled() -> bool: