nonebot-plugin-werewolf 1.1.0__py3-none-any.whl → 1.1.2__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.1.0"
11
+ __version__ = "1.1.2"
12
12
  __plugin_meta__ = PluginMetadata(
13
13
  name="狼人杀",
14
14
  description="适用于 Nonebot2 的狼人杀插件",
@@ -0,0 +1,110 @@
1
+ import asyncio
2
+ import enum
3
+ import sys
4
+ from types import TracebackType
5
+ from typing import final
6
+
7
+ if sys.version_info >= (3, 11):
8
+ from asyncio.timeouts import timeout as timeout
9
+
10
+ else:
11
+ # ruff: noqa: S101
12
+
13
+ class _State(enum.Enum):
14
+ CREATED = "created"
15
+ ENTERED = "active"
16
+ EXPIRING = "expiring"
17
+ EXPIRED = "expired"
18
+ EXITED = "finished"
19
+
20
+ @final
21
+ class Timeout:
22
+ def __init__(self, when: float | None) -> None:
23
+ self._state = _State.CREATED
24
+ self._timeout_handler: asyncio.Handle | None = None
25
+ self._task: asyncio.Task | None = None
26
+ if when is not None:
27
+ when = asyncio.get_running_loop().time() + when
28
+ self._when = when
29
+
30
+ def when(self) -> float | None:
31
+ return self._when
32
+
33
+ def reschedule(self, when: float | None) -> None:
34
+ if self._state is not _State.ENTERED:
35
+ if self._state is _State.CREATED:
36
+ raise RuntimeError("Timeout has not been entered")
37
+ raise RuntimeError(
38
+ f"Cannot change state of {self._state.value} Timeout",
39
+ )
40
+
41
+ self._when = when
42
+
43
+ if self._timeout_handler is not None:
44
+ self._timeout_handler.cancel()
45
+
46
+ if when is None:
47
+ self._timeout_handler = None
48
+ else:
49
+ loop = asyncio.get_running_loop()
50
+ if when <= loop.time():
51
+ self._timeout_handler = loop.call_soon(self._on_timeout)
52
+ else:
53
+ self._timeout_handler = loop.call_at(when, self._on_timeout)
54
+
55
+ def expired(self) -> bool:
56
+ return self._state in (_State.EXPIRING, _State.EXPIRED)
57
+
58
+ def __repr__(self) -> str:
59
+ info = [""]
60
+ if self._state is _State.ENTERED:
61
+ when = round(self._when, 3) if self._when is not None else None
62
+ info.append(f"when={when}")
63
+ info_str = " ".join(info)
64
+ return f"<Timeout [{self._state.value}]{info_str}>"
65
+
66
+ async def __aenter__(self) -> "Timeout":
67
+ if self._state is not _State.CREATED:
68
+ raise RuntimeError("Timeout has already been entered")
69
+ task = asyncio.current_task()
70
+ if task is None:
71
+ raise RuntimeError("Timeout should be used inside a task")
72
+ self._state = _State.ENTERED
73
+ self._task = task
74
+ self.reschedule(self._when)
75
+ return self
76
+
77
+ async def __aexit__(
78
+ self,
79
+ exc_type: type[BaseException] | None,
80
+ exc_val: BaseException | None,
81
+ exc_tb: TracebackType | None,
82
+ ) -> bool | None:
83
+ assert self._state in (_State.ENTERED, _State.EXPIRING)
84
+
85
+ if self._timeout_handler is not None:
86
+ self._timeout_handler.cancel()
87
+ self._timeout_handler = None
88
+
89
+ if self._state is _State.EXPIRING:
90
+ self._state = _State.EXPIRED
91
+
92
+ if exc_type is asyncio.CancelledError:
93
+ raise TimeoutError from exc_val
94
+ elif self._state is _State.ENTERED:
95
+ self._state = _State.EXITED
96
+
97
+ return None
98
+
99
+ def _on_timeout(self) -> None:
100
+ assert self._state is _State.ENTERED
101
+ assert self._task is not None
102
+ self._task.cancel()
103
+ self._state = _State.EXPIRING
104
+ self._timeout_handler = None
105
+
106
+ def timeout(delay: float | None) -> Timeout:
107
+ return Timeout(delay)
108
+
109
+
110
+ __all__ = ["timeout"]
@@ -48,7 +48,7 @@ class PluginConfig(BaseModel):
48
48
  if isinstance(self.role_preset, list):
49
49
  for preset in self.role_preset:
50
50
  if preset[0] != sum(preset[1:]):
51
- raise RuntimeError(
51
+ raise ValueError(
52
52
  "配置项 `role_preset` 错误: "
53
53
  f"预设总人数为 {preset[0]}, 实际总人数为 {sum(preset[1:])} "
54
54
  f"({', '.join(map(str, preset[1:]))})"
@@ -60,13 +60,13 @@ class PluginConfig(BaseModel):
60
60
 
61
61
  min_length = max(i[0] for i in self.role_preset.values())
62
62
  if len(self.werewolf_priority) < min_length:
63
- raise RuntimeError(
63
+ raise ValueError(
64
64
  f"配置项 `werewolf_priority` 错误: 应至少为 {min_length} 项"
65
65
  )
66
66
 
67
67
  min_length = max(i[1] for i in self.role_preset.values())
68
68
  if len(self.priesthood_proirity) < min_length:
69
- raise RuntimeError(
69
+ raise ValueError(
70
70
  f"配置项 `priesthood_proirity` 错误: 应至少为 {min_length} 项"
71
71
  )
72
72
 
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from dataclasses import dataclass
3
+ import dataclasses
4
4
  from enum import Enum, auto
5
5
  from typing import TYPE_CHECKING
6
6
 
@@ -43,17 +43,17 @@ class KillReason(Enum):
43
43
  class GameStatus(Enum):
44
44
  GoodGuy = auto()
45
45
  Werewolf = auto()
46
- Unset = auto()
47
46
  Joker = auto()
48
47
 
49
48
 
50
- @dataclass
49
+ @dataclasses.dataclass
51
50
  class GameState:
52
51
  day: int
53
52
  killed: Player | None = None
54
53
  shoot: tuple[Player, Player] | tuple[None, None] = (None, None)
55
- protected: Player | None = None
56
- potion: tuple[Player | None, tuple[bool, bool]] = (None, (False, False))
54
+ antidote: set[Player] = dataclasses.field(default_factory=set)
55
+ poison: set[Player] = dataclasses.field(default_factory=set)
56
+ protected: set[Player] = dataclasses.field(default_factory=set)
57
57
 
58
58
 
59
59
  role_name_conv: dict[Role | RoleGroup, str] = {
@@ -10,7 +10,7 @@ class Error(Exception):
10
10
  """插件错误类型基类"""
11
11
 
12
12
 
13
- class GameFinishedError(Error):
13
+ class GameFinished(Error): # noqa: N818
14
14
  """游戏结束时抛出,无视游戏进程进入结算"""
15
15
 
16
16
  status: GameStatus
@@ -1,18 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import asyncio.timeouts
5
4
  import contextlib
6
- import random
7
- import time
5
+ import secrets
8
6
  from typing import TYPE_CHECKING, NoReturn
9
7
 
10
8
  from nonebot.log import logger
11
9
  from nonebot_plugin_alconna import At, Target, UniMessage
12
10
 
11
+ from ._timeout import timeout
13
12
  from .config import config
14
13
  from .constant import GameState, GameStatus, KillReason, Role, RoleGroup, role_name_conv
15
- from .exception import GameFinishedError
14
+ from .exception import GameFinished
16
15
  from .player import Player
17
16
  from .player_set import PlayerSet
18
17
  from .utils import InputStore
@@ -35,20 +34,19 @@ def init_players(bot: Bot, game: Game, players: dict[str, str]) -> PlayerSet:
35
34
  f"应为 {', '.join(map(str, role_preset))} 人, 传入{len(players)}人"
36
35
  )
37
36
 
37
+ w, p, c = preset
38
38
  roles: list[Role] = []
39
- roles.extend(config.werewolf_priority[: preset[0]])
40
- roles.extend(config.priesthood_proirity[: preset[1]])
41
- roles.extend([Role.Civilian] * preset[2])
39
+ roles.extend(config.werewolf_priority[:w])
40
+ roles.extend(config.priesthood_proirity[:p])
41
+ roles.extend([Role.Civilian] * c)
42
42
 
43
- r = random.Random(time.time()) # noqa: S311
44
-
45
- if roles.count(Role.Civilian) >= 2 and r.random() <= config.joker_probability:
43
+ if c >= 2 and secrets.randbelow(100) <= config.joker_probability * 100:
46
44
  roles.remove(Role.Civilian)
47
45
  roles.append(Role.Joker)
48
46
 
49
47
  shuffled: list[Role] = []
50
- for _ in range(len(players)):
51
- idx = r.randint(0, len(roles) - 1)
48
+ while roles:
49
+ idx = secrets.randbelow(len(roles))
52
50
  shuffled.append(roles.pop(idx))
53
51
 
54
52
  logger.debug(f"职业分配: {shuffled}")
@@ -107,25 +105,23 @@ class Game:
107
105
  msg.at(p.user_id)
108
106
  return msg
109
107
 
110
- def check_game_status(self) -> GameStatus:
108
+ def check_game_status(self) -> None:
111
109
  players = self.players.alive()
112
110
  w = players.select(RoleGroup.Werewolf)
113
111
  p = players.exclude(RoleGroup.Werewolf)
114
112
 
115
113
  # 狼人数量大于其他职业数量
116
114
  if w.size >= p.size:
117
- raise GameFinishedError(GameStatus.Werewolf)
115
+ raise GameFinished(GameStatus.Werewolf)
118
116
  # 屠边-村民/中立全灭
119
117
  if not p.select(Role.Civilian, RoleGroup.Others).size:
120
- raise GameFinishedError(GameStatus.Werewolf)
118
+ raise GameFinished(GameStatus.Werewolf)
121
119
  # 屠边-神职全灭
122
120
  if not p.exclude(Role.Civilian).size:
123
- raise GameFinishedError(GameStatus.Werewolf)
121
+ raise GameFinished(GameStatus.Werewolf)
124
122
  # 狼人全灭
125
123
  if not w.size:
126
- raise GameFinishedError(GameStatus.GoodGuy)
127
-
128
- return GameStatus.Unset
124
+ raise GameFinished(GameStatus.GoodGuy)
129
125
 
130
126
  def show_killed_players(self) -> str:
131
127
  msg = ""
@@ -177,7 +173,7 @@ class Game:
177
173
  break
178
174
 
179
175
  with contextlib.suppress(TimeoutError):
180
- async with asyncio.timeouts.timeout(timeout_secs):
176
+ async with timeout(timeout_secs):
181
177
  await asyncio.gather(*[wait(p) for p in players])
182
178
 
183
179
  async def interact(
@@ -220,7 +216,7 @@ class Game:
220
216
  await self.interact(Role.Witch, 60)
221
217
  # 否则等待 5-20s
222
218
  else:
223
- await asyncio.sleep(random.uniform(5, 20)) # noqa: S311
219
+ await asyncio.sleep(5 + secrets.randbelow(15))
224
220
 
225
221
  async def handle_new_dead(self, players: Player | PlayerSet) -> None:
226
222
  if isinstance(players, Player):
@@ -369,28 +365,30 @@ class Game:
369
365
  self.interact(Role.Prophet, 60),
370
366
  self.interact(Role.Guard, 60),
371
367
  players.select(Role.Witch).broadcast("请等待狼人决定目标..."),
372
- players.select(Role.Civilian, RoleGroup.Others).broadcast(
373
- "请等待其他玩家结束交互..."
374
- ),
368
+ players.exclude(
369
+ RoleGroup.Werewolf, Role.Prophet, Role.Witch, Role.Guard
370
+ ).broadcast("请等待其他玩家结束交互..."),
375
371
  )
376
372
 
377
373
  # 狼人击杀目标
378
- killed = self.state.killed
379
- # 守卫保护目标
380
- protected = self.state.protected
381
- # 女巫的操作目标和内容
382
- potioned, (antidote, poison) = self.state.potion
383
-
384
- # 狼人未空刀,除非守卫保护或女巫使用解药,否则狼人正常击杀玩家
385
- if killed is not None and (
386
- not ((killed is protected) or (antidote and potioned is killed))
374
+ if (
375
+ (killed := self.state.killed) is not None # 狼人未空刀
376
+ and killed not in self.state.protected # 守卫保护
377
+ and killed not in self.state.antidote # 女巫使用解药
387
378
  ):
379
+ # 狼人正常击杀玩家
388
380
  await killed.kill(
389
- KillReason.Werewolf, *players.select(RoleGroup.Werewolf)
381
+ KillReason.Werewolf,
382
+ *players.select(RoleGroup.Werewolf),
390
383
  )
391
- # 如果女巫使用毒药且守卫未保护,杀死该玩家
392
- if poison and (potioned is not None) and (potioned is not protected):
393
- await potioned.kill(KillReason.Poison, *players.select(Role.Witch))
384
+
385
+ # 女巫操作目标
386
+ for witch in self.state.poison:
387
+ if witch.selected is None:
388
+ continue
389
+ if witch.selected not in self.state.protected: # 守卫未保护
390
+ # 女巫毒杀玩家
391
+ await witch.selected.kill(KillReason.Poison, witch)
394
392
 
395
393
  day_count += 1
396
394
  msg = UniMessage.text(f"『第{day_count}天』天亮了...\n")
@@ -440,8 +438,6 @@ class Game:
440
438
  winner = "狼人"
441
439
  case GameStatus.Joker:
442
440
  winner = "小丑"
443
- case GameStatus.Unset:
444
- raise RuntimeError(f"错误的游戏状态: {status!r}")
445
441
 
446
442
  msg = UniMessage.text(f"🎉游戏结束,{winner}获胜\n\n")
447
443
  for p in sorted(self.players, key=lambda p: (p.role.value, p.user_id)):
@@ -462,7 +458,7 @@ class Game:
462
458
  game_task.result()
463
459
  except asyncio.CancelledError:
464
460
  logger.warning(f"{self.group.id} 的狼人杀游戏进程被取消")
465
- except GameFinishedError as result:
461
+ except GameFinished as result:
466
462
  await self.handle_game_finish(result.status)
467
463
  logger.info(f"{self.group.id} 的狼人杀游戏进程正常退出")
468
464
  except Exception as err:
@@ -1,5 +1,3 @@
1
- import asyncio
2
- import asyncio.timeouts
3
1
  from typing import Annotated
4
2
 
5
3
  from nonebot import on_command, on_message
@@ -9,6 +7,7 @@ from nonebot.rule import to_me
9
7
  from nonebot_plugin_alconna import MsgTarget, UniMessage, UniMsg
10
8
  from nonebot_plugin_userinfo import EventUserInfo, UserInfo
11
9
 
10
+ from ._timeout import timeout
12
11
  from .game import Game
13
12
  from .ob11_ext import ob11_ext_enabled
14
13
  from .utils import InputStore, is_group, prepare_game, rule_in_game, rule_not_in_game
@@ -52,7 +51,7 @@ async def handle_start(
52
51
  players = {admin_id: admin_info.user_name}
53
52
 
54
53
  try:
55
- async with asyncio.timeouts.timeout(5 * 60):
54
+ async with timeout(5 * 60):
56
55
  await prepare_game(event, players)
57
56
  except FinishedException:
58
57
  raise
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import asyncio.timeouts
4
+ import weakref
5
5
  from dataclasses import dataclass
6
6
  from typing import TYPE_CHECKING, ClassVar, TypeVar, final
7
7
 
@@ -10,7 +10,7 @@ from nonebot_plugin_alconna.uniseg import Receipt, Target, UniMessage
10
10
  from typing_extensions import override
11
11
 
12
12
  from .constant import GameStatus, KillReason, Role, RoleGroup, role_name_conv
13
- from .exception import GameFinishedError
13
+ from .exception import GameFinished
14
14
  from .utils import InputStore, check_index
15
15
 
16
16
  if TYPE_CHECKING:
@@ -47,7 +47,7 @@ class Player:
47
47
  role_group: ClassVar[RoleGroup]
48
48
 
49
49
  bot: Bot
50
- game: Game
50
+ _game_ref: weakref.ReferenceType[Game]
51
51
  user: Target
52
52
  name: str
53
53
  alive: bool = True
@@ -58,7 +58,7 @@ class Player:
58
58
  @final
59
59
  def __init__(self, bot: Bot, game: Game, user: Target, name: str) -> None:
60
60
  self.bot = bot
61
- self.game = game
61
+ self._game_ref = weakref.ref(game)
62
62
  self.user = user
63
63
  self.name = name
64
64
  self.killed = asyncio.Event()
@@ -81,6 +81,12 @@ class Player:
81
81
  def __repr__(self) -> str:
82
82
  return f"<{self.role_name}: user={self.name!r} alive={self.alive}>"
83
83
 
84
+ @property
85
+ def game(self) -> Game:
86
+ if game := self._game_ref():
87
+ return game
88
+ raise ValueError("Game not exist")
89
+
84
90
  @property
85
91
  def user_id(self) -> str:
86
92
  return self.user.id
@@ -95,7 +101,7 @@ class Player:
95
101
  logger.opt(colors=True).info(
96
102
  f"<b><e>{self.game.group.id}</e></b> | "
97
103
  f"[<b><m>{self.role_name}</m></b>] "
98
- f"<y>{self.name}</y>(<e>{self.user_id}</e>) | "
104
+ f"<y>{self.name}</y>(<b><e>{self.user_id}</e></b>) | "
99
105
  f"{text}",
100
106
  )
101
107
 
@@ -307,24 +313,6 @@ class Witch(Player):
307
313
  antidote: int = 1
308
314
  poison: int = 1
309
315
 
310
- def set_state(
311
- self,
312
- *,
313
- antidote: Player | None = None,
314
- posion: Player | None = None,
315
- ) -> None:
316
- if antidote is not None:
317
- self.antidote = 0
318
- self.selected = antidote
319
- self.game.state.potion = (antidote, (True, False))
320
- elif posion is not None:
321
- self.poison = 0
322
- self.selected = posion
323
- self.game.state.potion = (posion, (False, True))
324
- else:
325
- self.selected = None
326
- self.game.state.potion = (None, (False, False))
327
-
328
316
  async def handle_killed(self) -> bool:
329
317
  msg = UniMessage()
330
318
  if (killed := self.game.state.killed) is not None:
@@ -343,7 +331,8 @@ class Witch(Player):
343
331
  text = await self.receive_text()
344
332
  if text == "1":
345
333
  self.antidote = 0
346
- self.set_state(antidote=killed)
334
+ self.selected = killed
335
+ self.game.state.antidote.add(killed)
347
336
  await self.send(f"你对 {killed.name} 使用了解药,回合结束")
348
337
  return True
349
338
  if text == "/stop":
@@ -357,7 +346,6 @@ class Witch(Player):
357
346
 
358
347
  if not self.poison:
359
348
  await self.send("你没有可以使用的药水,回合结束")
360
- self.set_state()
361
349
  return
362
350
 
363
351
  players = self.game.players.alive()
@@ -377,14 +365,13 @@ class Witch(Player):
377
365
  break
378
366
  if text == "/stop":
379
367
  await self.send("你选择不使用毒药,回合结束")
380
- self.set_state()
381
368
  return
382
369
  await self.send("输入错误: 请发送玩家编号或 “/stop”")
383
370
 
384
371
  self.poison = 0
385
- self.selected = player = players[selected]
386
- self.set_state(posion=player)
387
- await self.send(f"当前回合选择对玩家 {player.name} 使用毒药\n回合结束")
372
+ self.selected = players[selected]
373
+ self.game.state.poison.add(self)
374
+ await self.send(f"当前回合选择对玩家 {self.selected.name} 使用毒药\n回合结束")
388
375
 
389
376
 
390
377
  @register_role(Role.Hunter, RoleGroup.GoodGuy)
@@ -418,7 +405,8 @@ class Guard(Player):
418
405
  break
419
406
  await self.send("输入错误,请发送编号选择玩家")
420
407
 
421
- self.game.state.protected = self.selected = players[selected]
408
+ self.selected = players[selected]
409
+ self.game.state.protected.add(self.selected)
422
410
  await self.send(f"本回合保护的玩家: {self.selected.name}")
423
411
 
424
412
 
@@ -462,11 +450,11 @@ class Joker(Player):
462
450
 
463
451
  @override
464
452
  async def kill(self, reason: KillReason, *killers: Player) -> bool:
465
- result = await super().kill(reason, *killers)
453
+ await super().kill(reason, *killers)
466
454
  if reason == KillReason.Vote:
467
455
  self.game.killed_players.append(self)
468
- raise GameFinishedError(GameStatus.Joker)
469
- return result
456
+ raise GameFinished(GameStatus.Joker)
457
+ return True
470
458
 
471
459
 
472
460
  @register_role(Role.Civilian, RoleGroup.GoodGuy)
@@ -1,9 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import asyncio.timeouts
5
4
  from typing import TYPE_CHECKING
6
5
 
6
+ from ._timeout import timeout
7
7
  from .player import Player
8
8
 
9
9
  if TYPE_CHECKING:
@@ -54,13 +54,13 @@ class PlayerSet(set[Player]):
54
54
  return sorted(self, key=lambda p: p.user_id)
55
55
 
56
56
  async def interact(self, timeout_secs: float = 60) -> None:
57
- async with asyncio.timeouts.timeout(timeout_secs):
57
+ async with timeout(timeout_secs):
58
58
  await asyncio.gather(*[p.interact() for p in self.alive()])
59
59
 
60
60
  async def vote(self, timeout_secs: float = 60) -> dict[Player, list[Player]]:
61
61
  async def vote(player: Player) -> tuple[Player, Player] | None:
62
62
  try:
63
- async with asyncio.timeouts.timeout(timeout_secs):
63
+ async with timeout(timeout_secs):
64
64
  return await player.vote(self)
65
65
  except TimeoutError:
66
66
  await player.send("投票超时,将视为弃票")
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- import asyncio.timeouts
3
2
  import re
4
3
  from collections import defaultdict
5
4
  from typing import Annotated, Any, ClassVar
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nonebot-plugin-werewolf
3
- Version: 1.1.0
3
+ Version: 1.1.2
4
4
  Summary: 适用于 Nonebot2 的狼人杀插件
5
5
  Author-email: wyf7685 <wyf7685@163.com>
6
6
  License: MIT
@@ -14,7 +14,7 @@ Requires-Dist: nonebot-plugin-waiter >=0.7.1
14
14
 
15
15
  <div align="center">
16
16
  <a href="https://v2.nonebot.dev/store">
17
- <img src="https://github.com/wyf7685/wyf7685/blob/main/assets/NoneBotPlugin.svg" width="300" alt="logo">
17
+ <img src="https://raw.githubusercontent.com/wyf7685/wyf7685/main/assets/NoneBotPlugin.svg" width="300" alt="logo">
18
18
  </a>
19
19
  </div>
20
20
 
@@ -29,10 +29,15 @@ _✨ 简单的狼人杀插件 ✨_
29
29
  [![python](https://img.shields.io/badge/python-3.10+-blue?logo=python&logoColor=edb641)](https://www.python.org/)
30
30
 
31
31
  [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
32
+ [![ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
32
33
  [![isort](https://img.shields.io/badge/%20imports-isort-%231674b1)](https://pycqa.github.io/isort/)
33
34
  [![black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
34
35
  [![pyright](https://img.shields.io/badge/types-pyright-797952.svg?logo=python&logoColor=edb641)](https://github.com/Microsoft/pyright)
35
- [![ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
36
+
37
+ [![wakatime](https://wakatime.com/badge/user/b097681b-c224-44ec-8e04-e1cf71744655/project/70a7f68d-5625-4989-9476-be6877408332.svg)](https://wakatime.com/badge/user/b097681b-c224-44ec-8e04-e1cf71744655/project/70a7f68d-5625-4989-9476-be6877408332)
38
+ [![pre-commit](https://results.pre-commit.ci/badge/github/wyf7685/nonebot-plugin-werewolf/master.svg)](https://results.pre-commit.ci/latest/github/wyf7685/nonebot-plugin-werewolf/master)
39
+ [![pyright](https://github.com/wyf7685/nonebot-plugin-werewolf/actions/workflows/pyright.yml/badge.svg?branch=master&event=push)](https://github.com/wyf7685/nonebot-plugin-werewolf/actions/workflows/pyright.yml)
40
+ [![publish](https://github.com/wyf7685/nonebot-plugin-werewolf/actions/workflows/pypi-publish.yml/badge.svg)](https://github.com/wyf7685/nonebot-plugin-werewolf/actions/workflows/pypi-publish.yml)
36
41
 
37
42
  [![NoneBot Registry](https://img.shields.io/endpoint?url=https%3A%2F%2Fnbbdg.lgc2333.top%2Fplugin%2Fnonebot-plugin-werewolf)](https://registry.nonebot.dev/plugin/nonebot-plugin-werewolf:nonebot_plugin_werewolf)
38
43
  [![Supported Adapters](https://img.shields.io/endpoint?url=https%3A%2F%2Fnbbdg.lgc2333.top%2Fplugin-adapters%2Fnonebot-plugin-werewolf)](https://registry.nonebot.dev/plugin/nonebot-plugin-werewolf:nonebot_plugin_werewolf)
@@ -45,6 +50,10 @@ _✨ 简单的狼人杀插件 ✨_
45
50
 
46
51
  ## 💿 安装
47
52
 
53
+ > [!note]
54
+ >
55
+ > 请确保 NoneBot2 使用的 Python 解释器版本 >=3.10
56
+
48
57
  <details open>
49
58
  <summary>使用 nb-cli 安装</summary>
50
59
  在 nonebot2 项目的根目录下打开命令行, 输入以下指令即可安装
@@ -231,6 +240,15 @@ werewolf__priesthood_proirity=[11, 12, 13, 14, 15]
231
240
 
232
241
  <!-- CHANGELOG -->
233
242
 
243
+ - 2024.09.18 v1.1.2
244
+
245
+ - 修改 Python 需求为 `>=3.10`
246
+
247
+ - 2024.09.11 v1.1.1
248
+
249
+ - 修改 Python 需求为 `>=3.11`
250
+ - 优化交互结果处理 ~~_可以在一局游戏中加入多个女巫了_~~
251
+
234
252
  - 2024.09.09 v1.1.0
235
253
 
236
254
  - 新增职业 `小丑`
@@ -0,0 +1,16 @@
1
+ nonebot_plugin_werewolf/__init__.py,sha256=qCS-gVMiMZcF9yT3SIMe8np-wmKX2vGT5DxYJ2jRxgY,734
2
+ nonebot_plugin_werewolf/_timeout.py,sha256=MVkA5oMoxOTV8Luc0BH2QPP8wwz4Tr9CJjeYgiPu_O4,3649
3
+ nonebot_plugin_werewolf/config.py,sha256=a5dyF21_3T1o6AFrK6onIiwXUNyEwj5Yg-c7V9qoueI,2923
4
+ nonebot_plugin_werewolf/constant.py,sha256=gUApZs-N3DRSwV2_B05mPi6fCmynSzVZb28fgqdLX6E,1894
5
+ nonebot_plugin_werewolf/exception.py,sha256=YSwxeogIB0YJqH9MP1bgxojiu-I_xQE44XnSk5bC1AQ,400
6
+ nonebot_plugin_werewolf/game.py,sha256=FqYAs8CalCRUKoyPTpqvKK5iAelV9ZQ5EmefNo5wE9E,17459
7
+ nonebot_plugin_werewolf/matchers.py,sha256=Xnq1n4xobdr1T_8ilsQfXns_iEsNx_h_-aQ5jZwpWx0,2080
8
+ nonebot_plugin_werewolf/ob11_ext.py,sha256=P8uc3AdN5K5MzJaK80WDK85VKFg_CK5avDHu7ueMkho,2418
9
+ nonebot_plugin_werewolf/player.py,sha256=2jWlJOIWNK3JVVKNaqROfvKPRQfopDXv7KAXGtXNmh4,14973
10
+ nonebot_plugin_werewolf/player_set.py,sha256=7f4GHklp-wBGROddVqGUQAMeMzKlN2iec_w5UJQXvz0,2726
11
+ nonebot_plugin_werewolf/utils.py,sha256=LiNitH3UYNqXnCOOAzjKifVpJNhurIt4wYgWVBUW1Zo,6811
12
+ nonebot_plugin_werewolf-1.1.2.dist-info/LICENSE,sha256=B_WbEqjGr6GYVNfEJPY31T1Opik7OtgOkhRs4Ig3e2M,1064
13
+ nonebot_plugin_werewolf-1.1.2.dist-info/METADATA,sha256=0W7Nb6qqwJvW7xVdLHxM4643DuJ3YN9683owdDoUOhs,10363
14
+ nonebot_plugin_werewolf-1.1.2.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
15
+ nonebot_plugin_werewolf-1.1.2.dist-info/top_level.txt,sha256=wLTfg8sTKbH9lLT9LtU118C9cTspEBJareLsrYM52YA,24
16
+ nonebot_plugin_werewolf-1.1.2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (74.1.2)
2
+ Generator: setuptools (75.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,15 +0,0 @@
1
- nonebot_plugin_werewolf/__init__.py,sha256=A2138SSGhd2FdZ8V9Op50k5-pdfIVSfT0xBNG6qWacM,734
2
- nonebot_plugin_werewolf/config.py,sha256=8HnSt7sk8WIwnQlHm0tLdiSk6OTwbysDwoQf8cvFgoE,2929
3
- nonebot_plugin_werewolf/constant.py,sha256=yrhyw67KTaZ7DoGWZl3USWheaHSsUI94kl6ChDs0phI,1829
4
- nonebot_plugin_werewolf/exception.py,sha256=Ui8rB1sBg6_p8JHSvrjCpUaXJlgrM_9XsLJ09k8F1bg,391
5
- nonebot_plugin_werewolf/game.py,sha256=t1Gq-adeD5xprDH4uxfGF70wV_N-YZKBXn2CrMEH5Sc,17761
6
- nonebot_plugin_werewolf/matchers.py,sha256=5FioDfARlxTEibGL1JMKUMzmTzOwljjWKAJxBnyzaYs,2106
7
- nonebot_plugin_werewolf/ob11_ext.py,sha256=P8uc3AdN5K5MzJaK80WDK85VKFg_CK5avDHu7ueMkho,2418
8
- nonebot_plugin_werewolf/player.py,sha256=IdkVKt31DMw6W_0idW04c-ix6JVtg7VPl7ZaegxUN0Y,15360
9
- nonebot_plugin_werewolf/player_set.py,sha256=AI8v6KjH9ACtP4lvrd5IJfayan0qALmpTRLC3s_3DFw,2754
10
- nonebot_plugin_werewolf/utils.py,sha256=l7oNSK471SV5CaB1eEVyZm10XSBFf_TwNotvb30U6vE,6835
11
- nonebot_plugin_werewolf-1.1.0.dist-info/LICENSE,sha256=B_WbEqjGr6GYVNfEJPY31T1Opik7OtgOkhRs4Ig3e2M,1064
12
- nonebot_plugin_werewolf-1.1.0.dist-info/METADATA,sha256=6hjSQYd7yYi9UgZ4OLqDUDwHhL6imH21MMDEu9hS6AA,9234
13
- nonebot_plugin_werewolf-1.1.0.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
14
- nonebot_plugin_werewolf-1.1.0.dist-info/top_level.txt,sha256=wLTfg8sTKbH9lLT9LtU118C9cTspEBJareLsrYM52YA,24
15
- nonebot_plugin_werewolf-1.1.0.dist-info/RECORD,,