nonebot-plugin-werewolf 1.1.11__py3-none-any.whl → 1.1.13__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.
Files changed (28) hide show
  1. nonebot_plugin_werewolf/__init__.py +1 -1
  2. nonebot_plugin_werewolf/config.py +51 -37
  3. nonebot_plugin_werewolf/constant.py +0 -17
  4. nonebot_plugin_werewolf/dead_channel.py +79 -0
  5. nonebot_plugin_werewolf/game.py +46 -115
  6. nonebot_plugin_werewolf/matchers/_prepare_game.py +223 -0
  7. nonebot_plugin_werewolf/matchers/depends.py +2 -2
  8. nonebot_plugin_werewolf/matchers/edit_behavior.py +1 -0
  9. nonebot_plugin_werewolf/matchers/edit_preset.py +14 -12
  10. nonebot_plugin_werewolf/matchers/message_in_game.py +1 -1
  11. nonebot_plugin_werewolf/matchers/poke/chronocat_poke.py +3 -3
  12. nonebot_plugin_werewolf/matchers/poke/ob11_poke.py +3 -3
  13. nonebot_plugin_werewolf/matchers/start_game.py +20 -232
  14. nonebot_plugin_werewolf/matchers/superuser_ops.py +5 -8
  15. nonebot_plugin_werewolf/models.py +19 -0
  16. nonebot_plugin_werewolf/player.py +35 -31
  17. nonebot_plugin_werewolf/players/guard.py +2 -2
  18. nonebot_plugin_werewolf/players/prophet.py +2 -2
  19. nonebot_plugin_werewolf/players/shooter.py +2 -2
  20. nonebot_plugin_werewolf/players/werewolf.py +18 -19
  21. nonebot_plugin_werewolf/players/witch.py +4 -4
  22. nonebot_plugin_werewolf/utils.py +18 -56
  23. {nonebot_plugin_werewolf-1.1.11.dist-info → nonebot_plugin_werewolf-1.1.13.dist-info}/METADATA +71 -41
  24. nonebot_plugin_werewolf-1.1.13.dist-info/RECORD +37 -0
  25. {nonebot_plugin_werewolf-1.1.11.dist-info → nonebot_plugin_werewolf-1.1.13.dist-info}/WHEEL +1 -1
  26. nonebot_plugin_werewolf-1.1.11.dist-info/RECORD +0 -35
  27. {nonebot_plugin_werewolf-1.1.11.dist-info → nonebot_plugin_werewolf-1.1.13.dist-info}/licenses/LICENSE +0 -0
  28. {nonebot_plugin_werewolf-1.1.11.dist-info → nonebot_plugin_werewolf-1.1.13.dist-info}/top_level.txt +0 -0
@@ -10,7 +10,7 @@ from . import matchers as matchers
10
10
  from . import players as players
11
11
  from .config import Config
12
12
 
13
- __version__ = "1.1.11"
13
+ __version__ = "1.1.13"
14
14
  __plugin_meta__ = PluginMetadata(
15
15
  name="狼人杀",
16
16
  description="适用于 Nonebot2 的狼人杀插件",
@@ -1,10 +1,11 @@
1
1
  import json
2
+ import warnings
2
3
  from pathlib import Path
3
4
  from typing import Any, ClassVar, Final, Literal
4
5
  from typing_extensions import Self
5
6
 
6
7
  import nonebot
7
- from nonebot.compat import model_dump, type_validate_json
8
+ from nonebot.compat import model_dump, model_validator, type_validate_json
8
9
  from nonebot_plugin_localstore import get_plugin_data_file
9
10
  from pydantic import BaseModel, Field
10
11
 
@@ -12,37 +13,36 @@ from .constant import (
12
13
  DEFAULT_PRIESTHOOD_PRIORITY,
13
14
  DEFAULT_ROLE_PRESET,
14
15
  DEFAULT_WEREWOLF_PRIORITY,
15
- stop_command_prompt,
16
16
  )
17
17
  from .models import Role
18
18
 
19
19
 
20
20
  class ConfigFile(BaseModel):
21
- FILE: ClassVar[Path]
22
- _cache: ClassVar[Self | None] = None
21
+ _file_: ClassVar[Path]
22
+ _cache_: ClassVar[Self | None] = None
23
23
 
24
24
  def __init_subclass__(cls, **kwargs: Any) -> None: # noqa: ANN401
25
25
  super().__init_subclass__(**kwargs)
26
- if not cls.FILE.exists():
26
+ if not cls._file_.exists():
27
27
  cls().save()
28
28
 
29
29
  @classmethod
30
30
  def load(cls) -> Self:
31
- return type_validate_json(cls, cls.FILE.read_text())
31
+ return type_validate_json(cls, cls._file_.read_text())
32
32
 
33
33
  @classmethod
34
34
  def get(cls, *, use_cache: bool = True) -> Self:
35
- if cls._cache is None or not use_cache:
36
- cls._cache = cls.load()
37
- return cls._cache
35
+ if cls._cache_ is None or not use_cache:
36
+ cls._cache_ = cls.load()
37
+ return cls._cache_
38
38
 
39
39
  def save(self) -> None:
40
- self.FILE.write_text(json.dumps(model_dump(self)))
41
- type(self)._cache = self # noqa: SLF001
40
+ self._file_.write_text(json.dumps(model_dump(self)))
41
+ type(self)._cache_ = self
42
42
 
43
43
 
44
44
  class PresetData(ConfigFile):
45
- FILE: ClassVar[Path] = get_plugin_data_file("preset.json")
45
+ _file_: ClassVar[Path] = get_plugin_data_file("preset.json")
46
46
 
47
47
  role_preset: dict[int, tuple[int, int, int]] = DEFAULT_ROLE_PRESET.copy()
48
48
  werewolf_priority: list[Role] = DEFAULT_WEREWOLF_PRIORITY.copy()
@@ -50,36 +50,33 @@ class PresetData(ConfigFile):
50
50
  jester_probability: float = Field(default=0.0, ge=0.0, le=1.0)
51
51
 
52
52
 
53
+ class _Timeout(BaseModel):
54
+ prepare: int = Field(default=5 * 60, ge=5 * 60)
55
+ speak: int = Field(default=60, ge=60)
56
+ group_speak: int = Field(default=120, ge=120)
57
+ interact: int = Field(default=60, ge=60)
58
+ vote: int = Field(default=60, ge=60)
59
+ werewolf: int = Field(default=120, ge=120)
60
+
61
+ @property
62
+ def speak_timeout_prompt(self) -> str:
63
+ return f"限时{self.speak / 60:.1f}分钟, 发送 “{stop_command_prompt}” 结束发言"
64
+
65
+ @property
66
+ def group_speak_timeout_prompt(self) -> str:
67
+ return (
68
+ f"限时{self.group_speak / 60:.1f}分钟, "
69
+ f"全员发送 “{stop_command_prompt}” 结束发言"
70
+ )
71
+
72
+
53
73
  class GameBehavior(ConfigFile):
54
- FILE: ClassVar[Path] = get_plugin_data_file("behavior.json")
74
+ _file_: ClassVar[Path] = get_plugin_data_file("behavior.json")
55
75
 
56
76
  show_roles_list_on_start: bool = False
57
77
  speak_in_turn: bool = False
58
78
  dead_channel_rate_limit: int = 8 # per minute
59
79
  werewolf_multi_select: bool = False
60
-
61
- class _Timeout(BaseModel):
62
- prepare: int = Field(default=5 * 60, ge=5 * 60)
63
- speak: int = Field(default=60, ge=60)
64
- group_speak: int = Field(default=120, ge=120)
65
- interact: int = Field(default=60, ge=60)
66
- vote: int = Field(default=60, ge=60)
67
- werewolf: int = Field(default=120, ge=120)
68
-
69
- @property
70
- def speak_timeout_prompt(self) -> str:
71
- return (
72
- f"限时{self.speak / 60:.1f}分钟, "
73
- f"发送 “{stop_command_prompt()}” 结束发言"
74
- )
75
-
76
- @property
77
- def group_speak_timeout_prompt(self) -> str:
78
- return (
79
- f"限时{self.group_speak / 60:.1f}分钟, "
80
- f"全员发送 “{stop_command_prompt()}” 结束发言"
81
- )
82
-
83
80
  timeout: Final[_Timeout] = _Timeout()
84
81
 
85
82
 
@@ -94,7 +91,18 @@ class MatcherPriorityConfig(BaseModel):
94
91
  preset: int = 1
95
92
  behavior: int = 1
96
93
  in_game: int = 10
97
- stop: int = 10
94
+ stop: int = 9
95
+
96
+ @model_validator(mode="after")
97
+ @classmethod
98
+ def _validate(cls, model: Self) -> Self:
99
+ if model.in_game <= model.stop:
100
+ model.in_game = model.stop + 1
101
+ warnings.warn(
102
+ "in_game 的优先级必须低于 stop,已自动调整为 stop + 1",
103
+ stacklevel=2,
104
+ )
105
+ return model
98
106
 
99
107
 
100
108
  class PluginConfig(BaseModel):
@@ -103,6 +111,7 @@ class PluginConfig(BaseModel):
103
111
  stop_command: str | set[str] = "stop"
104
112
  require_at: bool | RequireAtConfig = True
105
113
  matcher_priority: MatcherPriorityConfig = MatcherPriorityConfig()
114
+ use_cmd_start: bool | None = None
106
115
 
107
116
  def get_stop_command(self) -> list[str]:
108
117
  return (
@@ -123,3 +132,8 @@ class Config(BaseModel):
123
132
 
124
133
  config = nonebot.get_plugin_config(Config).werewolf
125
134
  nonebot.logger.debug(f"加载插件配置: {config}")
135
+
136
+ stop_command_prompt = (
137
+ next(iter(sorted(nonebot.get_driver().config.command_start, key=len)), "")
138
+ + config.get_stop_command()[0]
139
+ )
@@ -1,25 +1,8 @@
1
- import functools
2
- from typing import TYPE_CHECKING
3
-
4
1
  from .models import GameStatus, KillReason, Role, RoleGroup
5
2
 
6
3
  STOP_COMMAND = "{{stop}}"
7
4
 
8
5
 
9
- def stop_command_prompt() -> str:
10
- import nonebot
11
-
12
- from .config import config # circular import
13
-
14
- cmd_starts = sorted(nonebot.get_driver().config.command_start, key=len)
15
- return next(iter(cmd_starts), "") + config.get_stop_command()[0]
16
-
17
-
18
- if not TYPE_CHECKING:
19
- stop_command_prompt = functools.cache(stop_command_prompt)
20
- del TYPE_CHECKING
21
-
22
-
23
6
  ROLE_NAME_CONV: dict[Role | RoleGroup, str] = {
24
7
  Role.WEREWOLF: "狼人",
25
8
  Role.WOLFKING: "狼王",
@@ -0,0 +1,79 @@
1
+ import contextlib
2
+ from typing import NoReturn
3
+
4
+ import anyio
5
+ import anyio.lowlevel
6
+ from nonebot_plugin_alconna import UniMessage
7
+
8
+ from .config import GameBehavior
9
+ from .player import Player
10
+ from .player_set import PlayerSet
11
+
12
+
13
+ class DeadChannel:
14
+ players: PlayerSet
15
+ finished: anyio.Event
16
+ counter: dict[str, int]
17
+
18
+ def __init__(self, players: PlayerSet, finished: anyio.Event) -> None:
19
+ self.players = players
20
+ self.finished = finished
21
+ self.counter = {p.user_id: 0 for p in players}
22
+
23
+ async def _decrease(self, user_id: str) -> None:
24
+ await anyio.sleep(60)
25
+ self.counter[user_id] -= 1
26
+
27
+ async def _wait_finished(self) -> None:
28
+ await self.finished.wait()
29
+ self._task_group.cancel_scope.cancel()
30
+
31
+ async def _broadcast(self) -> NoReturn:
32
+ stream = self.stream[1]
33
+ while True:
34
+ player, msg = await stream.receive()
35
+ msg = UniMessage.text(f"玩家 {player.name}:\n") + msg
36
+ target = self.players.killed().exclude(player)
37
+ try:
38
+ await target.broadcast(msg)
39
+ except Exception as err:
40
+ with contextlib.suppress(Exception):
41
+ await player.send(f"消息转发失败: {err!r}")
42
+
43
+ async def _receive(self, player: Player) -> NoReturn:
44
+ await player.killed.wait()
45
+ await anyio.lowlevel.checkpoint()
46
+ user_id = player.user_id
47
+ stream = self.stream[0]
48
+
49
+ await player.send(
50
+ "ℹ️你已加入死者频道,请勿在群组内继续发言\n"
51
+ "私聊发送消息将转发至其他已死亡玩家",
52
+ )
53
+ await (
54
+ self.players.killed()
55
+ .exclude(player)
56
+ .broadcast(f"ℹ️玩家 {player.name} 加入了死者频道")
57
+ )
58
+
59
+ while True:
60
+ msg = await player.receive()
61
+ self.counter[user_id] += 1
62
+ self._task_group.start_soon(self._decrease, user_id)
63
+
64
+ # 发言频率限制
65
+ if self.counter[user_id] > GameBehavior.get().dead_channel_rate_limit:
66
+ await player.send("❌发言频率超过限制, 该消息被屏蔽")
67
+ continue
68
+
69
+ # 推送消息
70
+ await stream.send((player, msg))
71
+
72
+ async def run(self) -> None:
73
+ self.stream = anyio.create_memory_object_stream[tuple[Player, UniMessage]](16)
74
+ send, recv = self.stream
75
+ async with send, recv, anyio.create_task_group() as self._task_group:
76
+ self._task_group.start_soon(self._wait_finished)
77
+ self._task_group.start_soon(self._broadcast)
78
+ for p in self.players:
79
+ self._task_group.start_soon(self._receive, p)
@@ -1,4 +1,3 @@
1
- import contextlib
2
1
  import functools
3
2
  import secrets
4
3
  from collections import Counter
@@ -11,35 +10,26 @@ from nonebot.adapters import Bot
11
10
  from nonebot.utils import escape_tag
12
11
  from nonebot_plugin_alconna import At, Target, UniMessage
13
12
  from nonebot_plugin_alconna.uniseg.receipt import Receipt
14
- from nonebot_plugin_uninfo import Interface, Scene, SceneType
13
+ from nonebot_plugin_uninfo import Scene, SceneType, get_interface
15
14
 
16
15
  from .config import GameBehavior, PresetData
17
- from .constant import GAME_STATUS_CONV, REPORT_TEXT, ROLE_EMOJI, ROLE_NAME_CONV
16
+ from .constant import GAME_STATUS_CONV, REPORT_TEXT
17
+ from .dead_channel import DeadChannel
18
18
  from .exception import GameFinished
19
19
  from .models import GameState, GameStatus, KillInfo, KillReason, Role, RoleGroup
20
20
  from .player import Player
21
21
  from .player_set import PlayerSet
22
- from .utils import InputStore, ObjectStream, SendHandler, add_stop_button, link
22
+ from .utils import InputStore, 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
+ running_games: dict[Target, "Game"] = {}
27
26
 
28
27
 
29
- def get_starting_games() -> dict[Target, dict[str, str]]:
30
- return starting_games
31
-
32
-
33
- def get_running_games() -> set["Game"]:
28
+ def get_running_games() -> dict[Target, "Game"]:
34
29
  return running_games
35
30
 
36
31
 
37
- async def init_players(
38
- bot: Bot,
39
- game: "Game",
40
- players: set[str],
41
- interface: Interface,
42
- ) -> PlayerSet:
32
+ async def init_players(bot: Bot, game: "Game", players: set[str]) -> PlayerSet:
43
33
  logger.debug(f"初始化 {game.colored_name} 的玩家职业")
44
34
 
45
35
  preset_data = PresetData.get()
@@ -51,21 +41,20 @@ async def init_players(
51
41
  )
52
42
 
53
43
  w, p, c = preset
54
- roles: list[Role] = []
55
- roles.extend(preset_data.werewolf_priority[:w])
56
- roles.extend(preset_data.priesthood_proirity[:p])
57
- roles.extend([Role.CIVILIAN] * c)
44
+ roles = [
45
+ *preset_data.werewolf_priority[:w],
46
+ *preset_data.priesthood_proirity[:p],
47
+ *([Role.CIVILIAN] * c),
48
+ ]
58
49
 
59
50
  if c >= 2 and secrets.randbelow(100) <= preset_data.jester_probability * 100:
60
51
  roles.remove(Role.CIVILIAN)
61
52
  roles.append(Role.JESTER)
62
53
 
63
- def _select_role() -> Role:
64
- return roles.pop(secrets.randbelow(len(roles)))
65
-
66
54
  player_set = PlayerSet()
67
55
  for user_id in players:
68
- player_set.add(await Player.new(_select_role(), bot, game, user_id, interface))
56
+ role = roles.pop(secrets.randbelow(len(roles)))
57
+ player_set.add(await Player.new(role, bot, game, user_id))
69
58
 
70
59
  logger.debug(f"职业分配完成: <e>{escape_tag(str(player_set))}</e>")
71
60
  return player_set
@@ -82,73 +71,6 @@ class _SendHandler(SendHandler[str | None]):
82
71
  return msg
83
72
 
84
73
 
85
- class DeadChannel:
86
- players: PlayerSet
87
- finished: anyio.Event
88
- counter: dict[str, int]
89
- stream: ObjectStream[tuple[Player, UniMessage]]
90
-
91
- def __init__(self, players: PlayerSet, finished: anyio.Event) -> None:
92
- self.players = players
93
- self.finished = finished
94
- self.counter = {p.user_id: 0 for p in players}
95
- self.stream = ObjectStream[tuple[Player, UniMessage]](16)
96
-
97
- async def _decrease(self, user_id: str) -> None:
98
- await anyio.sleep(60)
99
- self.counter[user_id] -= 1
100
-
101
- async def _wait_finished(self) -> None:
102
- await self.finished.wait()
103
- self._task_group.cancel_scope.cancel()
104
-
105
- async def _broadcast(self) -> NoReturn:
106
- while True:
107
- player, msg = await self.stream.recv()
108
- msg = f"玩家 {player.name}:\n" + msg
109
- target = self.players.killed().exclude(player)
110
- try:
111
- await target.broadcast(msg)
112
- except Exception as err:
113
- with contextlib.suppress(Exception):
114
- await player.send(f"消息转发失败: {err!r}")
115
-
116
- async def _receive(self, player: Player) -> NoReturn:
117
- await player.killed.wait()
118
- user_id = player.user_id
119
-
120
- await player.send(
121
- "ℹ️你已加入死者频道,请勿在群组内继续发言\n"
122
- "私聊发送消息将转发至其他已死亡玩家",
123
- )
124
- await (
125
- self.players.killed()
126
- .exclude(player)
127
- .broadcast(f"ℹ️玩家 {player.name} 加入了死者频道")
128
- )
129
-
130
- while True:
131
- msg = await player.receive()
132
- self.counter[user_id] += 1
133
- self._task_group.start_soon(self._decrease, user_id)
134
-
135
- # 发言频率限制
136
- if self.counter[user_id] > GameBehavior.get().dead_channel_rate_limit:
137
- await player.send("❌发言频率超过限制, 该消息被屏蔽")
138
- continue
139
-
140
- # 推送消息
141
- await self.stream.send((player, msg))
142
-
143
- async def run(self) -> None:
144
- async with anyio.create_task_group() as tg:
145
- self._task_group = tg
146
- tg.start_soon(self._wait_finished)
147
- tg.start_soon(self._broadcast)
148
- for p in self.players:
149
- tg.start_soon(self._receive, p)
150
-
151
-
152
74
  class Game:
153
75
  bot: Bot
154
76
  group: Target
@@ -174,15 +96,15 @@ class Game:
174
96
  bot: Bot,
175
97
  group: Target,
176
98
  players: set[str],
177
- interface: Interface,
178
99
  ) -> Self:
179
100
  self = cls(bot, group)
180
101
 
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)
102
+ if interface := get_interface(bot):
103
+ self._scene = await interface.get_scene(SceneType.GROUP, self.group_id)
104
+ if self._scene is None:
105
+ self._scene = await interface.get_scene(SceneType.GUILD, self.group_id)
184
106
 
185
- self.players = await init_players(bot, self, players, interface)
107
+ self.players = await init_players(bot, self, players)
186
108
  self._player_map |= {p.user_id: p for p in self.players}
187
109
  self._shuffled = self.players.shuffled
188
110
 
@@ -261,7 +183,7 @@ class Game:
261
183
  msg.text("\n\n📚职业列表:\n")
262
184
  counter = Counter(p.role for p in self.players)
263
185
  for role, cnt in sorted(counter.items(), key=lambda x: x[0].value):
264
- msg.text(f"- {ROLE_EMOJI[role]}{ROLE_NAME_CONV[role]}x{cnt}\n")
186
+ msg.text(f"- {role.emoji}{role.display}x{cnt}\n")
265
187
 
266
188
  async with anyio.create_task_group() as tg:
267
189
  tg.start_soon(self.send, msg)
@@ -304,7 +226,7 @@ class Game:
304
226
  self.state.shooter = shooter.selected = None
305
227
  await self.post_kill(shoot)
306
228
 
307
- async def run_night(self, players: PlayerSet) -> Player | None:
229
+ async def run_night(self, players: PlayerSet) -> None:
308
230
  async with anyio.create_task_group() as tg:
309
231
  for p in players:
310
232
  tg.start_soon(p.interact)
@@ -321,17 +243,18 @@ class Game:
321
243
  *players.select(RoleGroup.WEREWOLF),
322
244
  )
323
245
  else:
324
- killed = None
246
+ self.state.killed = None
325
247
 
326
248
  # 女巫操作目标
327
249
  for witch in self.state.poison:
328
- if witch.selected is None:
329
- continue
330
- if witch.selected not in self.state.protected: # 守卫未保护
250
+ if (
251
+ (selected := witch.selected) is not None # 理论上不会是 None (
252
+ and selected not in self.state.protected # 守卫保护
253
+ # 虽然应该没什么人会加多个女巫玩...但还是加上判断比较好
254
+ and selected not in self.state.antidote # 女巫使用解药
255
+ ):
331
256
  # 女巫毒杀玩家
332
- await witch.selected.kill(KillReason.POISON, witch)
333
-
334
- return killed
257
+ await selected.kill(KillReason.POISON, witch)
335
258
 
336
259
  async def run_discussion(self) -> None:
337
260
  timeout = self.behavior.timeout
@@ -434,8 +357,8 @@ class Game:
434
357
  await self.send("🌙天黑请闭眼...")
435
358
  players = self.players.alive()
436
359
 
437
- # 夜间交互,返回狼人目标
438
- killed = await self.run_night(players)
360
+ # 夜间交互
361
+ await self.run_night(players)
439
362
 
440
363
  # 公告
441
364
  self.state.day += 1
@@ -452,7 +375,11 @@ class Game:
452
375
  await self.send(msg)
453
376
 
454
377
  # 第一晚被狼人杀死的玩家发表遗言
455
- if self.state.day == 1 and killed is not None and not killed.alive:
378
+ if (
379
+ self.state.day == 1 # 仅第一晚
380
+ and (killed := self.state.killed) is not None # 狼人未空刀且未保护
381
+ and not killed.alive # kill 成功
382
+ ):
456
383
  await self.send(
457
384
  UniMessage.text("⚙️当前为第一天\n请被狼人杀死的 ")
458
385
  .at(killed.user_id)
@@ -474,7 +401,9 @@ class Game:
474
401
 
475
402
  # 开始投票
476
403
  await self.send(
477
- "🗳️讨论结束, 进入投票环节,限时1分钟\n请在私聊中进行投票交互"
404
+ "🗳️讨论结束, 进入投票环节, "
405
+ f"限时{self.behavior.timeout.vote / 60:.1f}分钟\n"
406
+ "请在私聊中进行投票交互"
478
407
  )
479
408
  self.state.state = GameState.State.VOTE
480
409
  await self.run_vote()
@@ -499,6 +428,7 @@ class Game:
499
428
  await self.mainloop()
500
429
  except anyio.get_cancelled_exc_class():
501
430
  logger.warning(f"{self.colored_name} 的狼人杀游戏进程被取消")
431
+ raise
502
432
  except GameFinished as result:
503
433
  await self.handle_game_finish(result.status)
504
434
  logger.info(f"{self.colored_name} 的狼人杀游戏进程正常退出")
@@ -509,26 +439,27 @@ class Game:
509
439
  if self._finished is not None:
510
440
  self._finished.set()
511
441
 
512
- async def start(self) -> None:
442
+ async def run(self) -> None:
513
443
  self._finished = anyio.Event()
514
444
  dead_channel = DeadChannel(self.players, self._finished)
515
- get_running_games().add(self)
445
+ get_running_games()[self.group] = self
516
446
 
517
447
  try:
518
448
  async with anyio.create_task_group() as self._task_group:
519
449
  self._task_group.start_soon(self.run_daemon)
520
450
  self._task_group.start_soon(dead_channel.run)
521
- except anyio.get_cancelled_exc_class():
522
- logger.warning(f"{self.colored_name} 的狼人杀游戏进程被取消")
523
451
  except Exception as err:
524
452
  msg = f"{self.colored_name} 的狼人杀守护进程出现错误: {err!r}"
525
453
  logger.opt(exception=err).error(msg)
526
454
  finally:
527
455
  self._finished = None
528
456
  self._task_group = None
529
- get_running_games().discard(self)
457
+ get_running_games().pop(self.group, None)
530
458
  InputStore.cleanup(self._player_map.keys(), self.group_id)
531
459
 
460
+ def start(self) -> None:
461
+ nonebot.get_driver().task_group.start_soon(self.run)
462
+
532
463
  def terminate(self) -> None:
533
464
  if self._task_group is not None:
534
465
  logger.warning(f"中止 {self.colored_name} 的狼人杀游戏进程")