nonebot-plugin-werewolf 1.0.7__py3-none-any.whl → 1.1.1__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.7"
11
+ __version__ = "1.1.1"
12
12
  __plugin_meta__ = PluginMetadata(
13
13
  name="狼人杀",
14
14
  description="适用于 Nonebot2 的狼人杀插件",
@@ -1,14 +1,80 @@
1
- from nonebot import get_plugin_config
2
- from pydantic import BaseModel
1
+ from typing import Literal, Self, overload
3
2
 
4
- from .constant import Role
3
+ from nonebot import get_plugin_config, logger
4
+ from nonebot.compat import PYDANTIC_V2
5
+ from pydantic import BaseModel, Field
6
+
7
+ from .constant import (
8
+ Role,
9
+ default_priesthood_proirity,
10
+ default_role_preset,
11
+ default_werewolf_priority,
12
+ )
13
+
14
+ if PYDANTIC_V2:
15
+ from pydantic import model_validator as model_validator
16
+ else:
17
+ from pydantic import root_validator
18
+
19
+ @overload
20
+ def model_validator(*, mode: Literal["before"]): ... # noqa: ANN201
21
+
22
+ @overload
23
+ def model_validator(*, mode: Literal["after"]): ... # noqa: ANN201
24
+
25
+ def model_validator(*, mode: Literal["before", "after"]):
26
+ return root_validator(
27
+ pre=mode == "before", # pyright: ignore[reportArgumentType]
28
+ allow_reuse=True,
29
+ )
5
30
 
6
31
 
7
32
  class PluginConfig(BaseModel):
8
- enable_poke: bool = True
9
- role_preset: list[tuple[int, int, int, int]] | None = None
10
- werewolf_priority: list[Role] | None = None
11
- priesthood_proirity: list[Role] | None = None
33
+ enable_poke: bool = Field(default=True)
34
+ role_preset: list[tuple[int, int, int, int]] | dict[int, tuple[int, int, int]] = (
35
+ Field(default_factory=default_role_preset.copy)
36
+ )
37
+ werewolf_priority: list[Role] = Field(
38
+ default_factory=default_werewolf_priority.copy
39
+ )
40
+ priesthood_proirity: list[Role] = Field(
41
+ default_factory=default_priesthood_proirity.copy
42
+ )
43
+ joker_probability: float = Field(default=0.0, ge=0.0, le=1.0)
44
+
45
+ @model_validator(mode="after")
46
+ def _validate(self) -> Self:
47
+ if isinstance(self.role_preset, list):
48
+ for preset in self.role_preset:
49
+ if preset[0] != sum(preset[1:]):
50
+ raise RuntimeError(
51
+ "配置项 `role_preset` 错误: "
52
+ f"预设总人数为 {preset[0]}, 实际总人数为 {sum(preset[1:])} "
53
+ f"({', '.join(map(str, preset[1:]))})"
54
+ )
55
+ self.role_preset = default_role_preset | {
56
+ i[0]: i[1:] for i in self.role_preset
57
+ }
58
+ logger.debug(f"覆写配置 role_preset: {self.role_preset}")
59
+
60
+ min_length = max(i[0] for i in self.role_preset.values())
61
+ if len(self.werewolf_priority) < min_length:
62
+ raise RuntimeError(
63
+ f"配置项 `werewolf_priority` 错误: 应至少为 {min_length} 项"
64
+ )
65
+
66
+ min_length = max(i[1] for i in self.role_preset.values())
67
+ if len(self.priesthood_proirity) < min_length:
68
+ raise RuntimeError(
69
+ f"配置项 `priesthood_proirity` 错误: 应至少为 {min_length} 项"
70
+ )
71
+
72
+ return self
73
+
74
+ def get_role_preset(self) -> dict[int, tuple[int, int, int]]:
75
+ if isinstance(self.role_preset, list):
76
+ self.role_preset = {i[0]: i[1:] for i in self.role_preset}
77
+ return self.role_preset
12
78
 
13
79
 
14
80
  class Config(BaseModel):
@@ -16,3 +82,4 @@ class Config(BaseModel):
16
82
 
17
83
 
18
84
  config = get_plugin_config(Config).werewolf
85
+ logger.debug(f"加载插件配置: {config}")
@@ -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
 
@@ -20,6 +20,9 @@ class Role(Enum):
20
20
  Guard = 14
21
21
  Idiot = 15
22
22
 
23
+ # 其他
24
+ Joker = 51
25
+
23
26
  # 平民
24
27
  Civilian = 0
25
28
 
@@ -27,28 +30,30 @@ class Role(Enum):
27
30
  class RoleGroup(Enum):
28
31
  Werewolf = auto()
29
32
  GoodGuy = auto()
33
+ Others = auto()
30
34
 
31
35
 
32
36
  class KillReason(Enum):
33
- Kill = auto()
37
+ Werewolf = auto()
34
38
  Poison = auto()
35
39
  Shoot = auto()
36
40
  Vote = auto()
37
41
 
38
42
 
39
43
  class GameStatus(Enum):
40
- Good = auto()
41
- Bad = auto()
42
- Unset = auto()
44
+ GoodGuy = auto()
45
+ Werewolf = auto()
46
+ Joker = auto()
43
47
 
44
48
 
45
- @dataclass
49
+ @dataclasses.dataclass
46
50
  class GameState:
47
51
  day: int
48
52
  killed: Player | None = None
49
53
  shoot: tuple[Player, Player] | tuple[None, None] = (None, None)
50
- protected: Player | None = None
51
- potion: tuple[Player | None, tuple[bool, bool]] = (None, (False, False))
54
+ antidote: set[Player] = dataclasses.field(default_factory=set)
55
+ poison: set[tuple[Player, Player]] = dataclasses.field(default_factory=set)
56
+ protected: set[Player] = dataclasses.field(default_factory=set)
52
57
 
53
58
 
54
59
  role_name_conv: dict[Role | RoleGroup, str] = {
@@ -59,12 +64,14 @@ role_name_conv: dict[Role | RoleGroup, str] = {
59
64
  Role.Hunter: "猎人",
60
65
  Role.Guard: "守卫",
61
66
  Role.Idiot: "白痴",
67
+ Role.Joker: "小丑",
62
68
  Role.Civilian: "平民",
63
69
  RoleGroup.Werewolf: "狼人",
64
70
  RoleGroup.GoodGuy: "好人",
71
+ RoleGroup.Others: "其他",
65
72
  }
66
73
 
67
- role_preset: dict[int, tuple[int, int, int]] = {
74
+ default_role_preset: dict[int, tuple[int, int, int]] = {
68
75
  # 总人数: (狼, 神, 民)
69
76
  6: (1, 2, 3),
70
77
  7: (2, 2, 3),
@@ -75,50 +82,16 @@ role_preset: dict[int, tuple[int, int, int]] = {
75
82
  12: (4, 5, 3),
76
83
  }
77
84
 
78
- werewolf_priority: list[Role] = [
85
+ default_werewolf_priority: list[Role] = [
79
86
  Role.Werewolf,
80
87
  Role.Werewolf,
81
88
  Role.WolfKing,
82
89
  Role.Werewolf,
83
90
  ]
84
- priesthood_proirity: list[Role] = [
91
+ default_priesthood_proirity: list[Role] = [
85
92
  Role.Witch,
86
93
  Role.Prophet,
87
94
  Role.Hunter,
88
95
  Role.Guard,
89
96
  Role.Idiot,
90
97
  ]
91
-
92
-
93
- def __apply_config():
94
- from .config import config
95
-
96
- global role_preset, werewolf_priority, priesthood_proirity
97
-
98
- if config.role_preset is not None:
99
- for preset in config.role_preset:
100
- if preset[0] != preset[1:]:
101
- raise RuntimeError(
102
- "配置项 `role_preset` 错误: "
103
- f"预设总人数为 {preset[0]}, 实际总人数为 {sum(preset[1:])}"
104
- )
105
- role_preset |= {i[0]: i[1:] for i in config.role_preset}
106
-
107
- if (priority := config.werewolf_priority) is not None:
108
- min_length = max(i[0] for i in role_preset.values())
109
- if len(priority) < min_length:
110
- raise RuntimeError(
111
- f"配置项 `werewolf_priority` 错误: 应至少为 {min_length} 项"
112
- )
113
- werewolf_priority = priority
114
-
115
- if (priority := config.priesthood_proirity) is not None:
116
- min_length = max(i[1] for i in role_preset.values())
117
- if len(priority) < min_length:
118
- raise RuntimeError(
119
- f"配置项 `priesthood_proirity` 错误: 应至少为 {min_length} 项"
120
- )
121
- priesthood_proirity = priority
122
-
123
-
124
- __apply_config()
@@ -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
@@ -3,32 +3,30 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import asyncio.timeouts
5
5
  import contextlib
6
- import random
7
- import time
6
+ import secrets
7
+ from typing import TYPE_CHECKING, NoReturn
8
8
 
9
- from nonebot.adapters import Bot
10
9
  from nonebot.log import logger
11
- from nonebot_plugin_alconna import Target, UniMessage
12
-
13
- from .constant import (
14
- GameState,
15
- GameStatus,
16
- KillReason,
17
- Role,
18
- RoleGroup,
19
- priesthood_proirity,
20
- role_preset,
21
- werewolf_priority,
22
- )
10
+ from nonebot_plugin_alconna import At, Target, UniMessage
11
+
12
+ from .config import config
13
+ from .constant import GameState, GameStatus, KillReason, Role, RoleGroup, role_name_conv
14
+ from .exception import GameFinishedError
23
15
  from .player import Player
24
16
  from .player_set import PlayerSet
25
17
  from .utils import InputStore
26
18
 
19
+ if TYPE_CHECKING:
20
+ from nonebot.adapters import Bot
21
+ from nonebot_plugin_alconna.uniseg.message import Receipt
22
+
27
23
  starting_games: dict[str, dict[str, str]] = {}
28
24
  running_games: dict[str, Game] = {}
29
25
 
30
26
 
31
27
  def init_players(bot: Bot, game: Game, players: dict[str, str]) -> PlayerSet:
28
+ logger.opt(colors=True).debug(f"初始化 <c>{game.group.id}</c> 的玩家职业")
29
+ role_preset = config.get_role_preset()
32
30
  preset = role_preset.get(len(players))
33
31
  if preset is None:
34
32
  raise ValueError(
@@ -36,18 +34,24 @@ def init_players(bot: Bot, game: Game, players: dict[str, str]) -> PlayerSet:
36
34
  f"应为 {', '.join(map(str, role_preset))} 人, 传入{len(players)}人"
37
35
  )
38
36
 
37
+ w, p, c = preset
39
38
  roles: list[Role] = []
40
- roles.extend(werewolf_priority[: preset[0]])
41
- roles.extend(priesthood_proirity[: preset[1]])
42
- 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
+
43
+ if c >= 2 and secrets.randbelow(100) <= config.joker_probability * 100:
44
+ roles.remove(Role.Civilian)
45
+ roles.append(Role.Joker)
43
46
 
44
- r = random.Random(time.time())
45
47
  shuffled: list[Role] = []
46
- for _ in range(len(players)):
47
- idx = r.randint(0, len(roles) - 1)
48
+ while roles:
49
+ idx = secrets.randbelow(len(roles))
48
50
  shuffled.append(roles.pop(idx))
49
51
 
50
- async def selector(target_: Target, b: Bot):
52
+ logger.debug(f"职业分配: {shuffled}")
53
+
54
+ async def selector(target_: Target, b: Bot) -> bool:
51
55
  return target_.self_id == bot.self_id and b is bot
52
56
 
53
57
  return PlayerSet(
@@ -63,7 +67,7 @@ def init_players(bot: Bot, game: Game, players: dict[str, str]) -> PlayerSet:
63
67
  ),
64
68
  players[user_id],
65
69
  )
66
- for user_id, role in zip(players, shuffled)
70
+ for user_id, role in zip(players, shuffled, strict=True)
67
71
  )
68
72
 
69
73
 
@@ -71,24 +75,28 @@ class Game:
71
75
  bot: Bot
72
76
  group: Target
73
77
  players: PlayerSet
78
+ _player_map: dict[str, Player]
74
79
  state: GameState
75
80
  killed_players: list[Player]
76
81
 
77
- def __init__(
78
- self,
79
- bot: Bot,
80
- group: Target,
81
- players: dict[str, str],
82
- ) -> None:
82
+ def __init__(self, bot: Bot, group: Target, players: dict[str, str]) -> None:
83
83
  self.bot = bot
84
84
  self.group = group
85
85
  self.players = init_players(bot, self, players)
86
+ self._player_map = {p.user_id: p for p in self.players}
86
87
  self.state = GameState(0)
87
88
  self.killed_players = []
88
89
 
89
- async def send(self, message: str | UniMessage):
90
+ async def send(self, message: str | UniMessage) -> Receipt:
90
91
  if isinstance(message, str):
91
92
  message = UniMessage.text(message)
93
+ text = f"<b><e>{self.group.id}</e></b> | <g>Send</g> | "
94
+ for seg in message:
95
+ if isinstance(seg, At):
96
+ text += f"<y>@{self._player_map[seg.target].name}</y>"
97
+ else:
98
+ text += str(seg)
99
+ logger.opt(colors=True).info(text.replace("\n", "\\n"))
92
100
  return await message.send(self.group, self.bot)
93
101
 
94
102
  def at_all(self) -> UniMessage:
@@ -97,21 +105,23 @@ class Game:
97
105
  msg.at(p.user_id)
98
106
  return msg
99
107
 
100
- def check_game_status(self) -> GameStatus:
108
+ def check_game_status(self) -> None:
101
109
  players = self.players.alive()
102
110
  w = players.select(RoleGroup.Werewolf)
103
111
  p = players.exclude(RoleGroup.Werewolf)
104
112
 
113
+ # 狼人数量大于其他职业数量
105
114
  if w.size >= p.size:
106
- return GameStatus.Bad
107
- if not p.select(Role.Civilian):
108
- return GameStatus.Bad
109
- if not p.exclude(Role.Civilian):
110
- return GameStatus.Bad
115
+ raise GameFinishedError(GameStatus.Werewolf)
116
+ # 屠边-村民/中立全灭
117
+ if not p.select(Role.Civilian, RoleGroup.Others).size:
118
+ raise GameFinishedError(GameStatus.Werewolf)
119
+ # 屠边-神职全灭
120
+ if not p.exclude(Role.Civilian).size:
121
+ raise GameFinishedError(GameStatus.Werewolf)
122
+ # 狼人全灭
111
123
  if not w.size:
112
- return GameStatus.Good
113
-
114
- return GameStatus.Unset
124
+ raise GameFinishedError(GameStatus.GoodGuy)
115
125
 
116
126
  def show_killed_players(self) -> str:
117
127
  msg = ""
@@ -124,7 +134,7 @@ class Game:
124
134
  p.name for p in player.kill_info.killers
125
135
  )
126
136
  match player.kill_info.reason:
127
- case KillReason.Kill:
137
+ case KillReason.Werewolf:
128
138
  msg += " 刀了"
129
139
  case KillReason.Poison:
130
140
  msg += " 毒死"
@@ -137,7 +147,7 @@ class Game:
137
147
  return msg.strip()
138
148
 
139
149
  async def notify_player_role(self) -> None:
140
- preset = role_preset[len(self.players)]
150
+ preset = config.get_role_preset()[len(self.players)]
141
151
  await asyncio.gather(
142
152
  self.send(
143
153
  self.at_all()
@@ -156,7 +166,7 @@ class Game:
156
166
  if isinstance(players, Player):
157
167
  players = PlayerSet([players])
158
168
 
159
- async def wait(p: Player):
169
+ async def wait(p: Player) -> None:
160
170
  while True:
161
171
  msg = await InputStore.fetch(p.user_id, self.group.id)
162
172
  if msg.extract_plain_text() == "/stop":
@@ -176,9 +186,9 @@ class Game:
176
186
  type_.role_name # Player
177
187
  if isinstance(type_, Player)
178
188
  else (
179
- type_.name # Role
189
+ role_name_conv[type_] # Role
180
190
  if isinstance(type_, Role)
181
- else f"{type_.name}阵营" # RoleGroup
191
+ else f"{role_name_conv[type_]}阵营" # RoleGroup
182
192
  )
183
193
  )
184
194
 
@@ -186,6 +196,7 @@ class Game:
186
196
  try:
187
197
  await players.interact(timeout_secs)
188
198
  except TimeoutError:
199
+ logger.opt(colors=True).debug(f"{text}交互超时 (<y>{timeout_secs}</y>s)")
189
200
  await players.broadcast(f"{text}交互时间结束")
190
201
 
191
202
  async def select_killed(self) -> None:
@@ -205,7 +216,7 @@ class Game:
205
216
  await self.interact(Role.Witch, 60)
206
217
  # 否则等待 5-20s
207
218
  else:
208
- await asyncio.sleep(random.uniform(5, 20))
219
+ await asyncio.sleep(5 + secrets.randbelow(15))
209
220
 
210
221
  async def handle_new_dead(self, players: Player | PlayerSet) -> None:
211
222
  if isinstance(players, Player):
@@ -257,6 +268,8 @@ class Game:
257
268
  # 收集到的总票数
258
269
  total_votes = sum(map(len, vote_result.values()))
259
270
 
271
+ logger.debug(f"投票结果: {vote_result}")
272
+
260
273
  # 投票结果公示
261
274
  msg = UniMessage.text("投票结果:\n")
262
275
  for p, v in sorted(vote_result.items(), key=lambda x: len(x[1]), reverse=True):
@@ -306,23 +319,23 @@ class Game:
306
319
  loop = asyncio.get_event_loop()
307
320
  queue: asyncio.Queue[tuple[Player, UniMessage]] = asyncio.Queue()
308
321
 
309
- async def send():
322
+ async def send() -> NoReturn:
310
323
  while True:
311
324
  player, msg = await queue.get()
312
325
  msg = f"玩家 {player.name}:\n" + msg
313
- await self.players.dead().exclude(player).broadcast(msg)
326
+ await self.players.killed().exclude(player).broadcast(msg)
327
+ queue.task_done()
328
+
329
+ async def recv(player: Player) -> NoReturn:
330
+ await player.killed.wait()
314
331
 
315
- async def recv(player: Player):
316
332
  counter = 0
317
333
 
318
- def decrease():
334
+ def decrease() -> None:
319
335
  nonlocal counter
320
336
  counter -= 1
321
337
 
322
338
  while True:
323
- if not player.killed:
324
- await asyncio.sleep(1)
325
- continue
326
339
  msg = await player.receive()
327
340
  counter += 1
328
341
  if counter <= 10:
@@ -333,13 +346,14 @@ class Game:
333
346
 
334
347
  await asyncio.gather(send(), *[recv(p) for p in self.players])
335
348
 
336
- async def run(self) -> None:
349
+ async def run(self) -> NoReturn:
337
350
  # 告知玩家角色信息
338
351
  await self.notify_player_role()
339
352
  # 天数记录 主要用于第一晚狼人击杀的遗言
340
353
  day_count = 0
341
354
 
342
- while self.check_game_status() == GameStatus.Unset:
355
+ # 游戏主循环
356
+ while True:
343
357
  # 重置游戏状态,进入下一夜
344
358
  self.state = GameState(day_count)
345
359
  players = self.players.alive()
@@ -348,29 +362,31 @@ class Game:
348
362
  # 狼人、预言家、守卫 同时交互,女巫在狼人后交互
349
363
  await asyncio.gather(
350
364
  self.select_killed(),
351
- players.select(Role.Witch).broadcast("请等待狼人决定目标..."),
352
- players.select(Role.Civilian).broadcast("请等待其他玩家结束交互..."),
353
365
  self.interact(Role.Prophet, 60),
354
366
  self.interact(Role.Guard, 60),
367
+ players.select(Role.Witch).broadcast("请等待狼人决定目标..."),
368
+ players.exclude(
369
+ RoleGroup.Werewolf, Role.Prophet, Role.Witch, Role.Guard
370
+ ).broadcast("请等待其他玩家结束交互..."),
355
371
  )
356
372
 
357
373
  # 狼人击杀目标
358
- killed = self.state.killed
359
- # 守卫保护目标
360
- protected = self.state.protected
361
- # 女巫的操作目标和内容
362
- potioned, (antidote, poison) = self.state.potion
363
-
364
- # 狼人未空刀
365
- if killed is not None:
366
- # 除非守卫保护或女巫使用解药,否则狼人正常击杀玩家
367
- if not ((killed is protected) or (antidote and potioned is killed)):
368
- await killed.kill(
369
- KillReason.Kill, *players.select(RoleGroup.Werewolf)
370
- )
371
- # 如果女巫使用毒药且守卫未保护,杀死该玩家
372
- if poison and (potioned is not None) and (potioned is not protected):
373
- await potioned.kill(KillReason.Poison, *players.select(Role.Witch))
374
+ if (
375
+ (killed := self.state.killed) # 狼人未空刀
376
+ and killed not in self.state.protected # 守卫保护
377
+ and killed not in self.state.antidote # 女巫使用解药
378
+ ):
379
+ # 狼人正常击杀玩家
380
+ await killed.kill(
381
+ KillReason.Werewolf,
382
+ *players.select(RoleGroup.Werewolf),
383
+ )
384
+
385
+ # 女巫操作目标
386
+ for witch, potioned in self.state.poison:
387
+ if potioned not in self.state.protected: # 守卫未保护
388
+ # 女巫毒杀玩家
389
+ await potioned.kill(KillReason.Poison, witch)
374
390
 
375
391
  day_count += 1
376
392
  msg = UniMessage.text(f"『第{day_count}天』天亮了...\n")
@@ -396,8 +412,7 @@ class Game:
396
412
  await self.post_kill(dead)
397
413
 
398
414
  # 判断游戏状态
399
- if self.check_game_status() != GameStatus.Unset:
400
- break
415
+ self.check_game_status()
401
416
 
402
417
  # 公示存活玩家
403
418
  await self.send(f"当前存活玩家: \n\n{self.players.alive().show()}")
@@ -410,37 +425,49 @@ class Game:
410
425
  await self.send("讨论结束, 进入投票环节,限时1分钟\n请在私聊中进行投票交互")
411
426
  await self.run_vote()
412
427
 
413
- # 游戏结束
414
- winner = "好人" if self.check_game_status() == GameStatus.Good else "狼人"
428
+ # 判断游戏状态
429
+ self.check_game_status()
430
+
431
+ async def handle_game_finish(self, status: GameStatus) -> None:
432
+ match status:
433
+ case GameStatus.GoodGuy:
434
+ winner = "好人"
435
+ case GameStatus.Werewolf:
436
+ winner = "狼人"
437
+ case GameStatus.Joker:
438
+ winner = "小丑"
439
+
415
440
  msg = UniMessage.text(f"🎉游戏结束,{winner}获胜\n\n")
416
441
  for p in sorted(self.players, key=lambda p: (p.role.value, p.user_id)):
417
442
  msg.at(p.user_id).text(f": {p.role_name}\n")
418
443
  await self.send(msg)
419
- await self.send(f"玩家死亡报告:\n\n{self.show_killed_players()}")
444
+ await self.send(f"📌玩家死亡报告:\n\n{self.show_killed_players()}")
420
445
 
421
- def start(self):
422
- event = asyncio.Event()
446
+ def start(self) -> None:
447
+ finished = asyncio.Event()
423
448
  game_task = asyncio.create_task(self.run())
424
- game_task.add_done_callback(lambda _: event.set())
449
+ game_task.add_done_callback(lambda _: finished.set())
425
450
  dead_channel = asyncio.create_task(self.run_dead_channel())
426
451
 
427
- async def daemon():
428
- await event.wait()
452
+ async def daemon() -> None:
453
+ await finished.wait()
429
454
 
430
455
  try:
431
456
  game_task.result()
457
+ except asyncio.CancelledError:
458
+ logger.warning(f"{self.group.id} 的狼人杀游戏进程被取消")
459
+ except GameFinishedError as result:
460
+ await self.handle_game_finish(result.status)
432
461
  logger.info(f"{self.group.id} 的狼人杀游戏进程正常退出")
433
- except asyncio.CancelledError as err:
434
- logger.warning(f"{self.group.id} 的狼人杀游戏进程被取消: {err}")
435
462
  except Exception as err:
436
- msg = f"{self.group.id} 的狼人杀游戏进程出现错误: {err!r}"
463
+ msg = f"{self.group.id} 的狼人杀游戏进程出现未知错误: {err!r}"
437
464
  logger.opt(exception=err).error(msg)
438
465
  await self.send(msg)
439
466
  finally:
440
467
  dead_channel.cancel()
441
468
  running_games.pop(self.group.id, None)
442
469
 
443
- def daemon_callback(task: asyncio.Task[None]):
470
+ def daemon_callback(task: asyncio.Task[None]) -> None:
444
471
  if err := task.exception():
445
472
  logger.opt(exception=err).error(
446
473
  f"{self.group.id} 的狼人杀守护进程出现错误: {err!r}"
@@ -9,7 +9,7 @@ from nonebot.rule import to_me
9
9
  from nonebot_plugin_alconna import MsgTarget, UniMessage, UniMsg
10
10
  from nonebot_plugin_userinfo import EventUserInfo, UserInfo
11
11
 
12
- from .game import Game, running_games, starting_games
12
+ from .game import Game
13
13
  from .ob11_ext import ob11_ext_enabled
14
14
  from .utils import InputStore, is_group, prepare_game, rule_in_game, rule_not_in_game
15
15
 
@@ -36,9 +36,6 @@ async def handle_start(
36
36
  target: MsgTarget,
37
37
  admin_info: Annotated[UserInfo, EventUserInfo()],
38
38
  ) -> None:
39
- if target.id in running_games:
40
- await UniMessage.text("当前群聊内有正在进行的游戏,无法创建游戏").finish()
41
-
42
39
  admin_id = event.get_user_id()
43
40
  msg = (
44
41
  UniMessage.at(admin_id)
@@ -52,7 +49,7 @@ async def handle_start(
52
49
  msg.text("\n可使用戳一戳代替游戏交互中的 “/stop” 命令")
53
50
  await msg.text("\n\n游戏准备阶段限时5分钟,超时将自动结束").send()
54
51
 
55
- players = starting_games[target.id] = {admin_id: admin_info.user_name}
52
+ players = {admin_id: admin_info.user_name}
56
53
 
57
54
  try:
58
55
  async with asyncio.timeouts.timeout(5 * 60):
@@ -61,8 +58,6 @@ async def handle_start(
61
58
  raise
62
59
  except TimeoutError:
63
60
  await UniMessage.text("游戏准备超时,已自动结束").finish()
64
- finally:
65
- del starting_games[target.id]
66
61
 
67
- game = Game(bot=bot, group=target, players=players)
62
+ game = Game(bot, target, players)
68
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:
@@ -4,15 +4,20 @@ import asyncio
4
4
  import asyncio.timeouts
5
5
  from dataclasses import dataclass
6
6
  from typing import TYPE_CHECKING, ClassVar, TypeVar, final
7
- from typing_extensions import override
8
7
 
9
- from nonebot.adapters import Bot
8
+ from nonebot.log import logger
10
9
  from nonebot_plugin_alconna.uniseg import Receipt, Target, UniMessage
10
+ from typing_extensions import override
11
11
 
12
- from .constant import KillReason, Role, RoleGroup, role_name_conv
12
+ from .constant import GameStatus, KillReason, Role, RoleGroup, role_name_conv
13
+ from .exception import GameFinishedError
13
14
  from .utils import InputStore, check_index
14
15
 
15
16
  if TYPE_CHECKING:
17
+ from collections.abc import Callable
18
+
19
+ from nonebot.adapters import Bot
20
+
16
21
  from .game import Game
17
22
  from .player_set import PlayerSet
18
23
 
@@ -21,7 +26,7 @@ P = TypeVar("P", bound=type["Player"])
21
26
  PLAYER_CLASS: dict[Role, type[Player]] = {}
22
27
 
23
28
 
24
- def register_role(role: Role, role_group: RoleGroup, /):
29
+ def register_role(role: Role, role_group: RoleGroup, /) -> Callable[[P], P]:
25
30
  def decorator(cls: P, /) -> P:
26
31
  cls.role = role
27
32
  cls.role_group = role_group
@@ -46,7 +51,7 @@ class Player:
46
51
  user: Target
47
52
  name: str
48
53
  alive: bool = True
49
- killed: bool = False
54
+ killed: asyncio.Event
50
55
  kill_info: KillInfo | None = None
51
56
  selected: Player | None = None
52
57
 
@@ -56,6 +61,7 @@ class Player:
56
61
  self.game = game
57
62
  self.user = user
58
63
  self.name = name
64
+ self.killed = asyncio.Event()
59
65
 
60
66
  @final
61
67
  @classmethod
@@ -73,7 +79,7 @@ class Player:
73
79
  return PLAYER_CLASS[role](bot, game, user, name)
74
80
 
75
81
  def __repr__(self) -> str:
76
- return f"<{self.role_name}: user={self.user} alive={self.alive}>"
82
+ return f"<{self.role_name}: user={self.name!r} alive={self.alive}>"
77
83
 
78
84
  @property
79
85
  def user_id(self) -> str:
@@ -83,18 +89,32 @@ class Player:
83
89
  def role_name(self) -> str:
84
90
  return role_name_conv[self.role]
85
91
 
92
+ @final
93
+ def _log(self, text: str) -> None:
94
+ text = text.replace("\n", "\\n")
95
+ logger.opt(colors=True).info(
96
+ f"<b><e>{self.game.group.id}</e></b> | "
97
+ f"[<b><m>{self.role_name}</m></b>] "
98
+ f"<y>{self.name}</y>(<e>{self.user_id}</e>) | "
99
+ f"{text}",
100
+ )
101
+
86
102
  @final
87
103
  async def send(self, message: str | UniMessage) -> Receipt:
88
104
  if isinstance(message, str):
89
105
  message = UniMessage.text(message)
90
106
 
107
+ self._log(f"<g>Send</g> | {message}")
91
108
  return await message.send(target=self.user, bot=self.bot)
92
109
 
93
110
  @final
94
111
  async def receive(self, prompt: str | UniMessage | None = None) -> UniMessage:
95
112
  if prompt:
96
113
  await self.send(prompt)
97
- return await InputStore.fetch(self.user.id)
114
+
115
+ result = await InputStore.fetch(self.user.id)
116
+ self._log(f"<y>Recv</y> | {result}")
117
+ return result
98
118
 
99
119
  @final
100
120
  async def receive_text(self) -> str:
@@ -106,11 +126,7 @@ class Player:
106
126
  async def notify_role(self) -> None:
107
127
  await self.send(f"你的身份: {self.role_name}")
108
128
 
109
- async def kill(
110
- self,
111
- reason: KillReason,
112
- *killers: Player,
113
- ) -> bool:
129
+ async def kill(self, reason: KillReason, *killers: Player) -> bool:
114
130
  from .player_set import PlayerSet
115
131
 
116
132
  self.alive = False
@@ -118,7 +134,7 @@ class Player:
118
134
  return True
119
135
 
120
136
  async def post_kill(self) -> None:
121
- self.killed = True
137
+ self.killed.set()
122
138
 
123
139
  async def vote(self, players: PlayerSet) -> tuple[Player, Player] | None:
124
140
  await self.send(
@@ -184,7 +200,7 @@ class CanShoot(Player):
184
200
  text = await self.receive_text()
185
201
  if text == "/stop":
186
202
  await self.send("已取消技能")
187
- return
203
+ return None
188
204
  index = check_index(text, len(players))
189
205
  if index is not None:
190
206
  selected = index - 1
@@ -213,7 +229,7 @@ class Werewolf(Player):
213
229
  partners = players.select(RoleGroup.Werewolf).exclude(self)
214
230
 
215
231
  # 避免阻塞
216
- def broadcast(msg: str | UniMessage):
232
+ def broadcast(msg: str | UniMessage) -> asyncio.Task[None]:
217
233
  return asyncio.create_task(partners.broadcast(msg))
218
234
 
219
235
  msg = UniMessage()
@@ -234,8 +250,8 @@ class Werewolf(Player):
234
250
  selected = None
235
251
  finished = False
236
252
  while selected is None or not finished:
237
- input = await self.receive()
238
- text = input.extract_plain_text()
253
+ input_msg = await self.receive()
254
+ text = input_msg.extract_plain_text()
239
255
  index = check_index(text, len(players))
240
256
  if index is not None:
241
257
  selected = index - 1
@@ -249,14 +265,17 @@ class Werewolf(Player):
249
265
  broadcast(f"队友 {self.name} 结束当前回合")
250
266
  else:
251
267
  await self.send("当前未选择玩家,无法结束回合")
252
- broadcast(UniMessage.text(f"队友 {self.name}:\n") + input)
268
+ broadcast(UniMessage.text(f"队友 {self.name}:\n") + input_msg)
253
269
 
254
270
  self.selected = players[selected]
255
271
 
256
272
 
257
273
  @register_role(Role.WolfKing, RoleGroup.Werewolf)
258
274
  class WolfKing(CanShoot, Werewolf):
259
- pass
275
+ @override
276
+ async def notify_role(self) -> None:
277
+ await super().notify_role()
278
+ await self.send("作为狼王,你可以在死后射杀一名玩家")
260
279
 
261
280
 
262
281
  @register_role(Role.Prophet, RoleGroup.GoodGuy)
@@ -279,7 +298,7 @@ class Prophet(Player):
279
298
  await self.send("输入错误,请发送编号选择玩家")
280
299
 
281
300
  player = players[selected]
282
- result = role_name_conv[player.role_group]
301
+ result = "狼人" if player.role_group == RoleGroup.Werewolf else "好人"
283
302
  await self.send(f"玩家 {player.name} 的阵营是『{result}』")
284
303
 
285
304
 
@@ -288,24 +307,6 @@ class Witch(Player):
288
307
  antidote: int = 1
289
308
  poison: int = 1
290
309
 
291
- def set_state(
292
- self,
293
- *,
294
- antidote: Player | None = None,
295
- posion: Player | None = None,
296
- ):
297
- if antidote is not None:
298
- self.antidote = 0
299
- self.selected = antidote
300
- self.game.state.potion = (antidote, (True, False))
301
- elif posion is not None:
302
- self.poison = 0
303
- self.selected = posion
304
- self.game.state.potion = (posion, (False, True))
305
- else:
306
- self.selected = None
307
- self.game.state.potion = (None, (False, False))
308
-
309
310
  async def handle_killed(self) -> bool:
310
311
  msg = UniMessage()
311
312
  if (killed := self.game.state.killed) is not None:
@@ -324,13 +325,12 @@ class Witch(Player):
324
325
  text = await self.receive_text()
325
326
  if text == "1":
326
327
  self.antidote = 0
327
- self.set_state(antidote=killed)
328
+ self.game.state.antidote.add(killed)
328
329
  await self.send(f"你对 {killed.name} 使用了解药,回合结束")
329
330
  return True
330
- elif text == "/stop":
331
+ if text == "/stop":
331
332
  return False
332
- else:
333
- await self.send("输入错误: 请输入 “1” 或 “/stop”")
333
+ await self.send("输入错误: 请输入 “1” 或 “/stop”")
334
334
 
335
335
  @override
336
336
  async def interact(self) -> None:
@@ -339,7 +339,6 @@ class Witch(Player):
339
339
 
340
340
  if not self.poison:
341
341
  await self.send("你没有可以使用的药水,回合结束")
342
- self.set_state()
343
342
  return
344
343
 
345
344
  players = self.game.players.alive()
@@ -357,16 +356,14 @@ class Witch(Player):
357
356
  if index is not None:
358
357
  selected = index - 1
359
358
  break
360
- elif text == "/stop":
359
+ if text == "/stop":
361
360
  await self.send("你选择不使用毒药,回合结束")
362
- self.set_state()
363
361
  return
364
- else:
365
- await self.send("输入错误: 请发送玩家编号或 “/stop”")
362
+ await self.send("输入错误: 请发送玩家编号或 “/stop”")
366
363
 
367
364
  self.poison = 0
368
- self.selected = player = players[selected]
369
- self.set_state(posion=player)
365
+ player = players[selected]
366
+ self.game.state.poison.add((self, player))
370
367
  await self.send(f"当前回合选择对玩家 {player.name} 使用毒药\n回合结束")
371
368
 
372
369
 
@@ -379,9 +376,10 @@ class Hunter(CanShoot, Player):
379
376
  class Guard(Player):
380
377
  @override
381
378
  async def interact(self) -> None:
382
- players = self.game.players.alive().exclude(self)
379
+ players = self.game.players.alive()
383
380
  await self.send(
384
- UniMessage.text(f"请选择需要保护的玩家:\n{players.show()}")
381
+ UniMessage.text("请选择需要保护的玩家:\n")
382
+ .text(players.show())
385
383
  .text("\n\n发送编号选择玩家")
386
384
  .text("\n发送 “/stop” 结束回合")
387
385
  )
@@ -400,7 +398,8 @@ class Guard(Player):
400
398
  break
401
399
  await self.send("输入错误,请发送编号选择玩家")
402
400
 
403
- self.game.state.protected = self.selected = players[selected]
401
+ self.selected = players[selected]
402
+ self.game.state.protected.add(self.selected)
404
403
  await self.send(f"本回合保护的玩家: {self.selected.name}")
405
404
 
406
405
 
@@ -409,17 +408,20 @@ class Idiot(Player):
409
408
  voted: bool = False
410
409
 
411
410
  @override
412
- async def kill(
413
- self,
414
- reason: KillReason,
415
- *killers: Player,
416
- ) -> bool:
411
+ async def notify_role(self) -> None:
412
+ await super().notify_role()
413
+ await self.send(
414
+ "作为白痴,你可以在首次被投票放逐时免疫放逐,但在之后的投票中无法继续投票"
415
+ )
416
+
417
+ @override
418
+ async def kill(self, reason: KillReason, *killers: Player) -> bool:
417
419
  if reason == KillReason.Vote and not self.voted:
418
420
  self.voted = True
419
421
  await self.game.send(
420
422
  UniMessage.at(self.user_id)
421
423
  .text(" 的身份是白痴\n")
422
- .text("免疫本次投票放逐,且接下来无法参与投票")
424
+ .text("免疫本次投票放逐,且接下来无法参与投票"),
423
425
  )
424
426
  return False
425
427
  return await super().kill(reason, *killers)
@@ -432,6 +434,22 @@ class Idiot(Player):
432
434
  return await super().vote(players)
433
435
 
434
436
 
437
+ @register_role(Role.Joker, RoleGroup.Others)
438
+ class Joker(Player):
439
+ @override
440
+ async def notify_role(self) -> None:
441
+ await super().notify_role()
442
+ await self.send("你的胜利条件: 被投票放逐")
443
+
444
+ @override
445
+ async def kill(self, reason: KillReason, *killers: Player) -> bool:
446
+ await super().kill(reason, *killers)
447
+ if reason == KillReason.Vote:
448
+ self.game.killed_players.append(self)
449
+ raise GameFinishedError(GameStatus.Joker)
450
+ return True
451
+
452
+
435
453
  @register_role(Role.Civilian, RoleGroup.GoodGuy)
436
454
  class Civilian(Player):
437
455
  pass
@@ -2,12 +2,15 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import asyncio.timeouts
5
+ from typing import TYPE_CHECKING
5
6
 
6
- from nonebot_plugin_alconna.uniseg import UniMessage
7
-
8
- from .constant import Role, RoleGroup
9
7
  from .player import Player
10
8
 
9
+ if TYPE_CHECKING:
10
+ from nonebot_plugin_alconna.uniseg import UniMessage
11
+
12
+ from .constant import Role, RoleGroup
13
+
11
14
 
12
15
  class PlayerSet(set[Player]):
13
16
  @property
@@ -20,6 +23,9 @@ class PlayerSet(set[Player]):
20
23
  def dead(self) -> PlayerSet:
21
24
  return PlayerSet(p for p in self if not p.alive)
22
25
 
26
+ def killed(self) -> PlayerSet:
27
+ return PlayerSet(p for p in self if p.killed.is_set())
28
+
23
29
  def include(self, *types: Player | Role | RoleGroup) -> PlayerSet:
24
30
  return PlayerSet(
25
31
  player
@@ -6,11 +6,12 @@ from typing import Annotated, Any, ClassVar
6
6
 
7
7
  import nonebot_plugin_waiter as waiter
8
8
  from nonebot.adapters import Event
9
+ from nonebot.log import logger
9
10
  from nonebot.rule import to_me
10
11
  from nonebot_plugin_alconna import MsgTarget, UniMessage, UniMsg
11
12
  from nonebot_plugin_userinfo import EventUserInfo, UserInfo
12
13
 
13
- from .constant import role_preset
14
+ from .config import config
14
15
 
15
16
 
16
17
  def check_index(text: str, arrlen: int) -> int | None:
@@ -60,7 +61,7 @@ async def rule_in_game(event: Event, target: MsgTarget) -> bool:
60
61
  return False
61
62
  if target.private:
62
63
  return user_in_game(target.id, None)
63
- elif target.id in running_games:
64
+ if target.id in running_games:
64
65
  return user_in_game(event.get_user_id(), target.id)
65
66
  return False
66
67
 
@@ -104,8 +105,7 @@ async def _prepare_game_receive(
104
105
  async for user, name, text in wait(default=(None, "", "")):
105
106
  if user is None:
106
107
  continue
107
- name = re.sub(r"[\u2066-\u2069]", "", name)
108
- await queue.put((user, name, text))
108
+ await queue.put((user, re.sub(r"[\u2066-\u2069]", "", name), text))
109
109
 
110
110
 
111
111
  async def _prepare_game_handle(
@@ -113,13 +113,17 @@ async def _prepare_game_handle(
113
113
  players: dict[str, str],
114
114
  admin_id: str,
115
115
  ) -> None:
116
+ log = logger.opt(colors=True)
117
+
116
118
  while True:
117
119
  user, name, text = await queue.get()
118
120
  msg = UniMessage.at(user)
121
+ colored = f"<y>{name}</y>(<c>{user}</c>)"
119
122
 
120
123
  match (text, user == admin_id):
121
124
  case ("开始游戏", True):
122
125
  player_num = len(players)
126
+ role_preset = config.get_role_preset()
123
127
  if player_num < min(role_preset):
124
128
  await (
125
129
  msg.text(f"游戏至少需要 {min(role_preset)} 人, ")
@@ -140,12 +144,14 @@ async def _prepare_game_handle(
140
144
  )
141
145
  else:
142
146
  await msg.text("游戏即将开始...").send()
147
+ log.info(f"游戏发起者 {colored} 开始游戏")
143
148
  return
144
149
 
145
150
  case ("开始游戏", False):
146
151
  await msg.text("只有游戏发起者可以开始游戏").send()
147
152
 
148
153
  case ("结束游戏", True):
154
+ log.info(f"游戏发起者 {colored} 结束游戏")
149
155
  await msg.text("已结束当前游戏").finish()
150
156
 
151
157
  case ("结束游戏", False):
@@ -157,6 +163,7 @@ async def _prepare_game_handle(
157
163
  case ("加入游戏", False):
158
164
  if user not in players:
159
165
  players[user] = name
166
+ log.info(f"玩家 {colored} 加入游戏")
160
167
  await msg.text("成功加入游戏").send()
161
168
  else:
162
169
  await msg.text("你已经加入游戏了").send()
@@ -167,6 +174,7 @@ async def _prepare_game_handle(
167
174
  case ("退出游戏", False):
168
175
  if user in players:
169
176
  del players[user]
177
+ log.info(f"玩家 {colored} 退出游戏")
170
178
  await msg.text("成功退出游戏").send()
171
179
  else:
172
180
  await msg.text("你还没有加入游戏").send()
@@ -179,20 +187,16 @@ async def _prepare_game_handle(
179
187
 
180
188
 
181
189
  async def prepare_game(event: Event, players: dict[str, str]) -> None:
190
+ from .game import starting_games
191
+
192
+ group_id = UniMessage.get_target(event).id
193
+ starting_games[group_id] = players
194
+
182
195
  queue: asyncio.Queue[tuple[str, str, str]] = asyncio.Queue()
183
- task_receive = asyncio.create_task(
184
- _prepare_game_receive(
185
- queue,
186
- event,
187
- UniMessage.get_target(event).id,
188
- )
189
- )
196
+ task_receive = asyncio.create_task(_prepare_game_receive(queue, event, group_id))
190
197
 
191
198
  try:
192
- await _prepare_game_handle(
193
- queue,
194
- players,
195
- event.get_user_id(),
196
- )
199
+ await _prepare_game_handle(queue, players, event.get_user_id())
197
200
  finally:
198
201
  task_receive.cancel()
202
+ del starting_games[group_id]
@@ -1,15 +1,16 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nonebot-plugin-werewolf
3
- Version: 1.0.7
4
- Summary: Default template for PDM package
5
- Author-Email: wyf7685 <wyf7685@163.com>
3
+ Version: 1.1.1
4
+ Summary: 适用于 Nonebot2 的狼人杀插件
5
+ Author-email: wyf7685 <wyf7685@163.com>
6
6
  License: MIT
7
- Requires-Python: >=3.10
8
- Requires-Dist: nonebot2>=2.3.3
9
- Requires-Dist: nonebot-plugin-alconna>=0.52.1
10
- Requires-Dist: nonebot-plugin-userinfo>=0.2.6
11
- Requires-Dist: nonebot-plugin-waiter>=0.7.1
7
+ Requires-Python: >=3.11
12
8
  Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: nonebot2 >=2.3.3
11
+ Requires-Dist: nonebot-plugin-alconna >=0.52.1
12
+ Requires-Dist: nonebot-plugin-userinfo >=0.2.6
13
+ Requires-Dist: nonebot-plugin-waiter >=0.7.1
13
14
 
14
15
  <div align="center">
15
16
  <a href="https://v2.nonebot.dev/store">
@@ -25,13 +26,21 @@ _✨ 简单的狼人杀插件 ✨_
25
26
 
26
27
  [![license](https://img.shields.io/github/license/wyf7685/nonebot-plugin-werewolf.svg)](./LICENSE)
27
28
  [![pypi](https://img.shields.io/pypi/v/nonebot-plugin-werewolf?logo=python&logoColor=edb641)](https://pypi.python.org/pypi/nonebot-plugin-werewolf)
28
- [![python](https://img.shields.io/badge/python-3.10+-blue?logo=python&logoColor=edb641)](https://www.python.org/)
29
-
30
- [![pdm-managed](https://img.shields.io/endpoint?url=https%3A%2F%2Fcdn.jsdelivr.net%2Fgh%2Fpdm-project%2F.github%2Fbadge.json)](https://pdm-project.org)
29
+ [![python](https://img.shields.io/badge/python-3.11+-blue?logo=python&logoColor=edb641)](https://www.python.org/)
30
+ [![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)
31
+ [![ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
31
32
  [![isort](https://img.shields.io/badge/%20imports-isort-%231674b1)](https://pycqa.github.io/isort/)
32
33
  [![black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
33
34
  [![pyright](https://img.shields.io/badge/types-pyright-797952.svg?logo=python&logoColor=edb641)](https://github.com/Microsoft/pyright)
34
- [![ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
35
+
36
+ [![commits](https://img.shields.io/github/commit-activity/w/wyf7685/nonebot-plugin-werewolf)](https://github.com/wyf7685/nonebot-plugin-werewolf/commits)
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)
41
+
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)
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)
35
44
 
36
45
  </div>
37
46
 
@@ -41,6 +50,10 @@ _✨ 简单的狼人杀插件 ✨_
41
50
 
42
51
  ## 💿 安装
43
52
 
53
+ > [!note]
54
+ >
55
+ > 请确保 NoneBot2 使用的 Python 解释器版本 >=3.11
56
+
44
57
  <details open>
45
58
  <summary>使用 nb-cli 安装</summary>
46
59
  在 nonebot2 项目的根目录下打开命令行, 输入以下指令即可安装
@@ -86,7 +99,7 @@ _✨ 简单的狼人杀插件 ✨_
86
99
 
87
100
  ## ⚙️ 配置
88
101
 
89
- 在 nonebot2 项目的`.env`文件中添加下表中的必填配置
102
+ 在 nonebot2 项目的 `.env` 文件中添加如下配置
90
103
 
91
104
  | 配置项 | 必填 | 默认值 | 说明 |
92
105
  | :-----------------------------: | :--: | :----: | :-----------------------------------------------------------: |
@@ -94,6 +107,7 @@ _✨ 简单的狼人杀插件 ✨_
94
107
  | `werewolf__role_preset` | 否 | - | 覆写插件内置的职业预设 |
95
108
  | `werewolf__werewolf_priority` | 否 | - | 自定义狼人职业优先级 |
96
109
  | `werewolf__priesthood_proirity` | 否 | - | 自定义神职职业优先级 |
110
+ | `werewolf__joker_probability` | 否 | `0.0` | 小丑职业替换平民的概率, 范围`[0,1]` |
97
111
 
98
112
  `werewolf__role_preset`, `werewolf__werewolf_priority`, `werewolf__priesthood_proirity` 的配置格式请参考 [`游戏内容`](#游戏内容) 部分
99
113
 
@@ -192,7 +206,7 @@ werewolf__werewolf_priority=[1, 2, 1, 1]
192
206
 
193
207
  上述配置中,`[1, 2, 1, 1]` 表示狼人的职业优先级为 `狼人`, `狼王`, `狼人`, `狼人`
194
208
 
195
- #### 配置项 `werewolf__werewolf_priority`
209
+ #### 配置项 `werewolf__priesthood_proirity`
196
210
 
197
211
  ```env
198
212
  werewolf__priesthood_proirity=[11, 12, 13, 14, 15]
@@ -226,6 +240,18 @@ werewolf__priesthood_proirity=[11, 12, 13, 14, 15]
226
240
 
227
241
  <!-- CHANGELOG -->
228
242
 
243
+ - 2024.09.11 v1.1.1
244
+
245
+ - 修改 Python 需求为 `>=3.11`
246
+ - 优化交互结果处理 ~~_可以在一局游戏中加入多个女巫了_~~
247
+
248
+ - 2024.09.09 v1.1.0
249
+
250
+ - 新增职业 `小丑`
251
+ - 修复守卫无法保护自己的 bug
252
+ - 添加部分特殊职业的说明
253
+ - 添加游戏过程中的日志输出
254
+
229
255
  - 2024.09.04 v1.0.7
230
256
 
231
257
  - 优先使用群名片作为玩家名
@@ -0,0 +1,15 @@
1
+ nonebot_plugin_werewolf/__init__.py,sha256=I6lFyJgJ4Tz4t8gS2lrsx-7paN2h_PNoGjGR8ryvSeU,734
2
+ nonebot_plugin_werewolf/config.py,sha256=gl_ujiY-8fxQ5hN9_gjL1WpJJR5UOBIRBGofbLcQzaE,2900
3
+ nonebot_plugin_werewolf/constant.py,sha256=1XNTcTicil90WSviI2e33FX0LstWXl1aV2ugbhsDlIk,1909
4
+ nonebot_plugin_werewolf/exception.py,sha256=Ui8rB1sBg6_p8JHSvrjCpUaXJlgrM_9XsLJ09k8F1bg,391
5
+ nonebot_plugin_werewolf/game.py,sha256=Y9MQASaNdm2uuNw37IjdQEQzikWmbql_PWLHaL7lcB8,17414
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=ifqTBfNepQY5FCY7hZoUA6DcObkcFEiNmKEeRkitJqY,14749
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.1.dist-info/LICENSE,sha256=B_WbEqjGr6GYVNfEJPY31T1Opik7OtgOkhRs4Ig3e2M,1064
12
+ nonebot_plugin_werewolf-1.1.1.dist-info/METADATA,sha256=5ikh-mdZTIObaFs9w7qgzrgsx31lCxB5Ag--C8stl0g,10447
13
+ nonebot_plugin_werewolf-1.1.1.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
14
+ nonebot_plugin_werewolf-1.1.1.dist-info/top_level.txt,sha256=wLTfg8sTKbH9lLT9LtU118C9cTspEBJareLsrYM52YA,24
15
+ nonebot_plugin_werewolf-1.1.1.dist-info/RECORD,,
@@ -1,4 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: pdm-backend (2.3.3)
2
+ Generator: setuptools (74.1.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ nonebot_plugin_werewolf
@@ -1,13 +0,0 @@
1
- nonebot_plugin_werewolf-1.0.7.dist-info/METADATA,sha256=6Zjo-smpLFf6SX6TLAefU4OFCamMLTZmIKsI5QgzXOs,8479
2
- nonebot_plugin_werewolf-1.0.7.dist-info/WHEEL,sha256=rSwsxJWe3vzyR5HCwjWXQruDgschpei4h_giTm0dJVE,90
3
- nonebot_plugin_werewolf-1.0.7.dist-info/licenses/LICENSE,sha256=B_WbEqjGr6GYVNfEJPY31T1Opik7OtgOkhRs4Ig3e2M,1064
4
- nonebot_plugin_werewolf/__init__.py,sha256=sO1RtjlQkMIdgMEhS8bVwhJpCo8dd36Z_Fc5y-YpEoY,734
5
- nonebot_plugin_werewolf/config.py,sha256=VSXAWZa3u6_ehXzS0q_qYpPYr5GRqHzs-WR0zHEJiRI,437
6
- nonebot_plugin_werewolf/constant.py,sha256=w-KWhLTq1_uAVy0ALER-yCn0J1r5Wr07xYvSYyHNHsw,2859
7
- nonebot_plugin_werewolf/game.py,sha256=ZBOM_yK8Dux0nu6om7_PruH4Kr82-JVOPfpD5JcOFdA,15909
8
- nonebot_plugin_werewolf/matchers.py,sha256=nRnXYNjwM_Pp-Z1tBoMwd2TrxCPCB5du_qMHbXlZw3w,2373
9
- nonebot_plugin_werewolf/ob11_ext.py,sha256=I6bPCv5SgAStTJuvBl5F7wqgiksWeFkb4R7n06jXprA,2399
10
- nonebot_plugin_werewolf/player.py,sha256=sAT8qI-zGLgjQJqWwwq-ow0vaWFdFlpMP0sWEOj-ujg,13817
11
- nonebot_plugin_werewolf/player_set.py,sha256=WXmJtL3jbfkFCcKbzLCLu70elhBJE5DJwPA3ENNoKJM,2595
12
- nonebot_plugin_werewolf/utils.py,sha256=EhrlYgDLyNZbpRCGDgpcUnpp8RWJBiZ6l8rDdC5hHvM,6396
13
- nonebot_plugin_werewolf-1.0.7.dist-info/RECORD,,