nonebot-plugin-werewolf 1.0.7__py3-none-any.whl → 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@ require("nonebot_plugin_waiter")
8
8
  from . import matchers as matchers
9
9
  from .config import Config
10
10
 
11
- __version__ = "1.0.7"
11
+ __version__ = "1.1.0"
12
12
  __plugin_meta__ = PluginMetadata(
13
13
  name="狼人杀",
14
14
  description="适用于 Nonebot2 的狼人杀插件",
@@ -1,14 +1,81 @@
1
- from nonebot import get_plugin_config
2
- from pydantic import BaseModel
1
+ from typing import Literal, 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
+ from typing_extensions import Self
7
+
8
+ from .constant import (
9
+ Role,
10
+ default_priesthood_proirity,
11
+ default_role_preset,
12
+ default_werewolf_priority,
13
+ )
14
+
15
+ if PYDANTIC_V2:
16
+ from pydantic import model_validator as model_validator
17
+ else:
18
+ from pydantic import root_validator
19
+
20
+ @overload
21
+ def model_validator(*, mode: Literal["before"]): ... # noqa: ANN201
22
+
23
+ @overload
24
+ def model_validator(*, mode: Literal["after"]): ... # noqa: ANN201
25
+
26
+ def model_validator(*, mode: Literal["before", "after"]):
27
+ return root_validator(
28
+ pre=mode == "before", # pyright: ignore[reportArgumentType]
29
+ allow_reuse=True,
30
+ )
5
31
 
6
32
 
7
33
  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
34
+ enable_poke: bool = Field(default=True)
35
+ role_preset: list[tuple[int, int, int, int]] | dict[int, tuple[int, int, int]] = (
36
+ Field(default_factory=default_role_preset.copy)
37
+ )
38
+ werewolf_priority: list[Role] = Field(
39
+ default_factory=default_werewolf_priority.copy
40
+ )
41
+ priesthood_proirity: list[Role] = Field(
42
+ default_factory=default_priesthood_proirity.copy
43
+ )
44
+ joker_probability: float = Field(default=0.0, ge=0.0, le=1.0)
45
+
46
+ @model_validator(mode="after")
47
+ def _validate(self) -> Self:
48
+ if isinstance(self.role_preset, list):
49
+ for preset in self.role_preset:
50
+ if preset[0] != sum(preset[1:]):
51
+ raise RuntimeError(
52
+ "配置项 `role_preset` 错误: "
53
+ f"预设总人数为 {preset[0]}, 实际总人数为 {sum(preset[1:])} "
54
+ f"({', '.join(map(str, preset[1:]))})"
55
+ )
56
+ self.role_preset = default_role_preset | {
57
+ i[0]: i[1:] for i in self.role_preset
58
+ }
59
+ logger.debug(f"覆写配置 role_preset: {self.role_preset}")
60
+
61
+ min_length = max(i[0] for i in self.role_preset.values())
62
+ if len(self.werewolf_priority) < min_length:
63
+ raise RuntimeError(
64
+ f"配置项 `werewolf_priority` 错误: 应至少为 {min_length} 项"
65
+ )
66
+
67
+ min_length = max(i[1] for i in self.role_preset.values())
68
+ if len(self.priesthood_proirity) < min_length:
69
+ raise RuntimeError(
70
+ f"配置项 `priesthood_proirity` 错误: 应至少为 {min_length} 项"
71
+ )
72
+
73
+ return self
74
+
75
+ def get_role_preset(self) -> dict[int, tuple[int, int, int]]:
76
+ if isinstance(self.role_preset, list):
77
+ self.role_preset = {i[0]: i[1:] for i in self.role_preset}
78
+ return self.role_preset
12
79
 
13
80
 
14
81
  class Config(BaseModel):
@@ -16,3 +83,4 @@ class Config(BaseModel):
16
83
 
17
84
 
18
85
  config = get_plugin_config(Config).werewolf
86
+ logger.debug(f"加载插件配置: {config}")
@@ -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,19 +30,21 @@ 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()
44
+ GoodGuy = auto()
45
+ Werewolf = auto()
42
46
  Unset = auto()
47
+ Joker = auto()
43
48
 
44
49
 
45
50
  @dataclass
@@ -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
@@ -5,30 +5,29 @@ import asyncio.timeouts
5
5
  import contextlib
6
6
  import random
7
7
  import time
8
+ from typing import TYPE_CHECKING, NoReturn
8
9
 
9
- from nonebot.adapters import Bot
10
10
  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
- )
11
+ from nonebot_plugin_alconna import At, Target, UniMessage
12
+
13
+ from .config import config
14
+ from .constant import GameState, GameStatus, KillReason, Role, RoleGroup, role_name_conv
15
+ from .exception import GameFinishedError
23
16
  from .player import Player
24
17
  from .player_set import PlayerSet
25
18
  from .utils import InputStore
26
19
 
20
+ if TYPE_CHECKING:
21
+ from nonebot.adapters import Bot
22
+ from nonebot_plugin_alconna.uniseg.message import Receipt
23
+
27
24
  starting_games: dict[str, dict[str, str]] = {}
28
25
  running_games: dict[str, Game] = {}
29
26
 
30
27
 
31
28
  def init_players(bot: Bot, game: Game, players: dict[str, str]) -> PlayerSet:
29
+ logger.opt(colors=True).debug(f"初始化 <c>{game.group.id}</c> 的玩家职业")
30
+ role_preset = config.get_role_preset()
32
31
  preset = role_preset.get(len(players))
33
32
  if preset is None:
34
33
  raise ValueError(
@@ -37,17 +36,24 @@ def init_players(bot: Bot, game: Game, players: dict[str, str]) -> PlayerSet:
37
36
  )
38
37
 
39
38
  roles: list[Role] = []
40
- roles.extend(werewolf_priority[: preset[0]])
41
- roles.extend(priesthood_proirity[: preset[1]])
39
+ roles.extend(config.werewolf_priority[: preset[0]])
40
+ roles.extend(config.priesthood_proirity[: preset[1]])
42
41
  roles.extend([Role.Civilian] * preset[2])
43
42
 
44
- r = random.Random(time.time())
43
+ r = random.Random(time.time()) # noqa: S311
44
+
45
+ if roles.count(Role.Civilian) >= 2 and r.random() <= config.joker_probability:
46
+ roles.remove(Role.Civilian)
47
+ roles.append(Role.Joker)
48
+
45
49
  shuffled: list[Role] = []
46
50
  for _ in range(len(players)):
47
51
  idx = r.randint(0, len(roles) - 1)
48
52
  shuffled.append(roles.pop(idx))
49
53
 
50
- async def selector(target_: Target, b: Bot):
54
+ logger.debug(f"职业分配: {shuffled}")
55
+
56
+ async def selector(target_: Target, b: Bot) -> bool:
51
57
  return target_.self_id == bot.self_id and b is bot
52
58
 
53
59
  return PlayerSet(
@@ -63,7 +69,7 @@ def init_players(bot: Bot, game: Game, players: dict[str, str]) -> PlayerSet:
63
69
  ),
64
70
  players[user_id],
65
71
  )
66
- for user_id, role in zip(players, shuffled)
72
+ for user_id, role in zip(players, shuffled, strict=True)
67
73
  )
68
74
 
69
75
 
@@ -71,24 +77,28 @@ class Game:
71
77
  bot: Bot
72
78
  group: Target
73
79
  players: PlayerSet
80
+ _player_map: dict[str, Player]
74
81
  state: GameState
75
82
  killed_players: list[Player]
76
83
 
77
- def __init__(
78
- self,
79
- bot: Bot,
80
- group: Target,
81
- players: dict[str, str],
82
- ) -> None:
84
+ def __init__(self, bot: Bot, group: Target, players: dict[str, str]) -> None:
83
85
  self.bot = bot
84
86
  self.group = group
85
87
  self.players = init_players(bot, self, players)
88
+ self._player_map = {p.user_id: p for p in self.players}
86
89
  self.state = GameState(0)
87
90
  self.killed_players = []
88
91
 
89
- async def send(self, message: str | UniMessage):
92
+ async def send(self, message: str | UniMessage) -> Receipt:
90
93
  if isinstance(message, str):
91
94
  message = UniMessage.text(message)
95
+ text = f"<b><e>{self.group.id}</e></b> | <g>Send</g> | "
96
+ for seg in message:
97
+ if isinstance(seg, At):
98
+ text += f"<y>@{self._player_map[seg.target].name}</y>"
99
+ else:
100
+ text += str(seg)
101
+ logger.opt(colors=True).info(text.replace("\n", "\\n"))
92
102
  return await message.send(self.group, self.bot)
93
103
 
94
104
  def at_all(self) -> UniMessage:
@@ -102,14 +112,18 @@ class Game:
102
112
  w = players.select(RoleGroup.Werewolf)
103
113
  p = players.exclude(RoleGroup.Werewolf)
104
114
 
115
+ # 狼人数量大于其他职业数量
105
116
  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
117
+ raise GameFinishedError(GameStatus.Werewolf)
118
+ # 屠边-村民/中立全灭
119
+ if not p.select(Role.Civilian, RoleGroup.Others).size:
120
+ raise GameFinishedError(GameStatus.Werewolf)
121
+ # 屠边-神职全灭
122
+ if not p.exclude(Role.Civilian).size:
123
+ raise GameFinishedError(GameStatus.Werewolf)
124
+ # 狼人全灭
111
125
  if not w.size:
112
- return GameStatus.Good
126
+ raise GameFinishedError(GameStatus.GoodGuy)
113
127
 
114
128
  return GameStatus.Unset
115
129
 
@@ -124,7 +138,7 @@ class Game:
124
138
  p.name for p in player.kill_info.killers
125
139
  )
126
140
  match player.kill_info.reason:
127
- case KillReason.Kill:
141
+ case KillReason.Werewolf:
128
142
  msg += " 刀了"
129
143
  case KillReason.Poison:
130
144
  msg += " 毒死"
@@ -137,7 +151,7 @@ class Game:
137
151
  return msg.strip()
138
152
 
139
153
  async def notify_player_role(self) -> None:
140
- preset = role_preset[len(self.players)]
154
+ preset = config.get_role_preset()[len(self.players)]
141
155
  await asyncio.gather(
142
156
  self.send(
143
157
  self.at_all()
@@ -156,7 +170,7 @@ class Game:
156
170
  if isinstance(players, Player):
157
171
  players = PlayerSet([players])
158
172
 
159
- async def wait(p: Player):
173
+ async def wait(p: Player) -> None:
160
174
  while True:
161
175
  msg = await InputStore.fetch(p.user_id, self.group.id)
162
176
  if msg.extract_plain_text() == "/stop":
@@ -176,9 +190,9 @@ class Game:
176
190
  type_.role_name # Player
177
191
  if isinstance(type_, Player)
178
192
  else (
179
- type_.name # Role
193
+ role_name_conv[type_] # Role
180
194
  if isinstance(type_, Role)
181
- else f"{type_.name}阵营" # RoleGroup
195
+ else f"{role_name_conv[type_]}阵营" # RoleGroup
182
196
  )
183
197
  )
184
198
 
@@ -186,6 +200,7 @@ class Game:
186
200
  try:
187
201
  await players.interact(timeout_secs)
188
202
  except TimeoutError:
203
+ logger.opt(colors=True).debug(f"{text}交互超时 (<y>{timeout_secs}</y>s)")
189
204
  await players.broadcast(f"{text}交互时间结束")
190
205
 
191
206
  async def select_killed(self) -> None:
@@ -205,7 +220,7 @@ class Game:
205
220
  await self.interact(Role.Witch, 60)
206
221
  # 否则等待 5-20s
207
222
  else:
208
- await asyncio.sleep(random.uniform(5, 20))
223
+ await asyncio.sleep(random.uniform(5, 20)) # noqa: S311
209
224
 
210
225
  async def handle_new_dead(self, players: Player | PlayerSet) -> None:
211
226
  if isinstance(players, Player):
@@ -257,6 +272,8 @@ class Game:
257
272
  # 收集到的总票数
258
273
  total_votes = sum(map(len, vote_result.values()))
259
274
 
275
+ logger.debug(f"投票结果: {vote_result}")
276
+
260
277
  # 投票结果公示
261
278
  msg = UniMessage.text("投票结果:\n")
262
279
  for p, v in sorted(vote_result.items(), key=lambda x: len(x[1]), reverse=True):
@@ -306,23 +323,23 @@ class Game:
306
323
  loop = asyncio.get_event_loop()
307
324
  queue: asyncio.Queue[tuple[Player, UniMessage]] = asyncio.Queue()
308
325
 
309
- async def send():
326
+ async def send() -> NoReturn:
310
327
  while True:
311
328
  player, msg = await queue.get()
312
329
  msg = f"玩家 {player.name}:\n" + msg
313
- await self.players.dead().exclude(player).broadcast(msg)
330
+ await self.players.killed().exclude(player).broadcast(msg)
331
+ queue.task_done()
332
+
333
+ async def recv(player: Player) -> NoReturn:
334
+ await player.killed.wait()
314
335
 
315
- async def recv(player: Player):
316
336
  counter = 0
317
337
 
318
- def decrease():
338
+ def decrease() -> None:
319
339
  nonlocal counter
320
340
  counter -= 1
321
341
 
322
342
  while True:
323
- if not player.killed:
324
- await asyncio.sleep(1)
325
- continue
326
343
  msg = await player.receive()
327
344
  counter += 1
328
345
  if counter <= 10:
@@ -333,13 +350,14 @@ class Game:
333
350
 
334
351
  await asyncio.gather(send(), *[recv(p) for p in self.players])
335
352
 
336
- async def run(self) -> None:
353
+ async def run(self) -> NoReturn:
337
354
  # 告知玩家角色信息
338
355
  await self.notify_player_role()
339
356
  # 天数记录 主要用于第一晚狼人击杀的遗言
340
357
  day_count = 0
341
358
 
342
- while self.check_game_status() == GameStatus.Unset:
359
+ # 游戏主循环
360
+ while True:
343
361
  # 重置游戏状态,进入下一夜
344
362
  self.state = GameState(day_count)
345
363
  players = self.players.alive()
@@ -348,10 +366,12 @@ class Game:
348
366
  # 狼人、预言家、守卫 同时交互,女巫在狼人后交互
349
367
  await asyncio.gather(
350
368
  self.select_killed(),
351
- players.select(Role.Witch).broadcast("请等待狼人决定目标..."),
352
- players.select(Role.Civilian).broadcast("请等待其他玩家结束交互..."),
353
369
  self.interact(Role.Prophet, 60),
354
370
  self.interact(Role.Guard, 60),
371
+ players.select(Role.Witch).broadcast("请等待狼人决定目标..."),
372
+ players.select(Role.Civilian, RoleGroup.Others).broadcast(
373
+ "请等待其他玩家结束交互..."
374
+ ),
355
375
  )
356
376
 
357
377
  # 狼人击杀目标
@@ -361,13 +381,13 @@ class Game:
361
381
  # 女巫的操作目标和内容
362
382
  potioned, (antidote, poison) = self.state.potion
363
383
 
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
- )
384
+ # 狼人未空刀,除非守卫保护或女巫使用解药,否则狼人正常击杀玩家
385
+ if killed is not None and (
386
+ not ((killed is protected) or (antidote and potioned is killed))
387
+ ):
388
+ await killed.kill(
389
+ KillReason.Werewolf, *players.select(RoleGroup.Werewolf)
390
+ )
371
391
  # 如果女巫使用毒药且守卫未保护,杀死该玩家
372
392
  if poison and (potioned is not None) and (potioned is not protected):
373
393
  await potioned.kill(KillReason.Poison, *players.select(Role.Witch))
@@ -396,8 +416,7 @@ class Game:
396
416
  await self.post_kill(dead)
397
417
 
398
418
  # 判断游戏状态
399
- if self.check_game_status() != GameStatus.Unset:
400
- break
419
+ self.check_game_status()
401
420
 
402
421
  # 公示存活玩家
403
422
  await self.send(f"当前存活玩家: \n\n{self.players.alive().show()}")
@@ -410,37 +429,51 @@ class Game:
410
429
  await self.send("讨论结束, 进入投票环节,限时1分钟\n请在私聊中进行投票交互")
411
430
  await self.run_vote()
412
431
 
413
- # 游戏结束
414
- winner = "好人" if self.check_game_status() == GameStatus.Good else "狼人"
432
+ # 判断游戏状态
433
+ self.check_game_status()
434
+
435
+ async def handle_game_finish(self, status: GameStatus) -> None:
436
+ match status:
437
+ case GameStatus.GoodGuy:
438
+ winner = "好人"
439
+ case GameStatus.Werewolf:
440
+ winner = "狼人"
441
+ case GameStatus.Joker:
442
+ winner = "小丑"
443
+ case GameStatus.Unset:
444
+ raise RuntimeError(f"错误的游戏状态: {status!r}")
445
+
415
446
  msg = UniMessage.text(f"🎉游戏结束,{winner}获胜\n\n")
416
447
  for p in sorted(self.players, key=lambda p: (p.role.value, p.user_id)):
417
448
  msg.at(p.user_id).text(f": {p.role_name}\n")
418
449
  await self.send(msg)
419
- await self.send(f"玩家死亡报告:\n\n{self.show_killed_players()}")
450
+ await self.send(f"📌玩家死亡报告:\n\n{self.show_killed_players()}")
420
451
 
421
- def start(self):
422
- event = asyncio.Event()
452
+ def start(self) -> None:
453
+ finished = asyncio.Event()
423
454
  game_task = asyncio.create_task(self.run())
424
- game_task.add_done_callback(lambda _: event.set())
455
+ game_task.add_done_callback(lambda _: finished.set())
425
456
  dead_channel = asyncio.create_task(self.run_dead_channel())
426
457
 
427
- async def daemon():
428
- await event.wait()
458
+ async def daemon() -> None:
459
+ await finished.wait()
429
460
 
430
461
  try:
431
462
  game_task.result()
463
+ except asyncio.CancelledError:
464
+ logger.warning(f"{self.group.id} 的狼人杀游戏进程被取消")
465
+ except GameFinishedError as result:
466
+ await self.handle_game_finish(result.status)
432
467
  logger.info(f"{self.group.id} 的狼人杀游戏进程正常退出")
433
- except asyncio.CancelledError as err:
434
- logger.warning(f"{self.group.id} 的狼人杀游戏进程被取消: {err}")
435
468
  except Exception as err:
436
- msg = f"{self.group.id} 的狼人杀游戏进程出现错误: {err!r}"
469
+ msg = f"{self.group.id} 的狼人杀游戏进程出现未知错误: {err!r}"
437
470
  logger.opt(exception=err).error(msg)
438
471
  await self.send(msg)
439
472
  finally:
440
473
  dead_channel.cancel()
441
474
  running_games.pop(self.group.id, None)
442
475
 
443
- def daemon_callback(task: asyncio.Task[None]):
476
+ def daemon_callback(task: asyncio.Task[None]) -> None:
444
477
  if err := task.exception():
445
478
  logger.opt(exception=err).error(
446
479
  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
 
@@ -293,7 +312,7 @@ class Witch(Player):
293
312
  *,
294
313
  antidote: Player | None = None,
295
314
  posion: Player | None = None,
296
- ):
315
+ ) -> None:
297
316
  if antidote is not None:
298
317
  self.antidote = 0
299
318
  self.selected = antidote
@@ -327,10 +346,9 @@ class Witch(Player):
327
346
  self.set_state(antidote=killed)
328
347
  await self.send(f"你对 {killed.name} 使用了解药,回合结束")
329
348
  return True
330
- elif text == "/stop":
349
+ if text == "/stop":
331
350
  return False
332
- else:
333
- await self.send("输入错误: 请输入 “1” 或 “/stop”")
351
+ await self.send("输入错误: 请输入 “1” 或 “/stop”")
334
352
 
335
353
  @override
336
354
  async def interact(self) -> None:
@@ -357,12 +375,11 @@ class Witch(Player):
357
375
  if index is not None:
358
376
  selected = index - 1
359
377
  break
360
- elif text == "/stop":
378
+ if text == "/stop":
361
379
  await self.send("你选择不使用毒药,回合结束")
362
380
  self.set_state()
363
381
  return
364
- else:
365
- await self.send("输入错误: 请发送玩家编号或 “/stop”")
382
+ await self.send("输入错误: 请发送玩家编号或 “/stop”")
366
383
 
367
384
  self.poison = 0
368
385
  self.selected = player = players[selected]
@@ -379,9 +396,10 @@ class Hunter(CanShoot, Player):
379
396
  class Guard(Player):
380
397
  @override
381
398
  async def interact(self) -> None:
382
- players = self.game.players.alive().exclude(self)
399
+ players = self.game.players.alive()
383
400
  await self.send(
384
- UniMessage.text(f"请选择需要保护的玩家:\n{players.show()}")
401
+ UniMessage.text("请选择需要保护的玩家:\n")
402
+ .text(players.show())
385
403
  .text("\n\n发送编号选择玩家")
386
404
  .text("\n发送 “/stop” 结束回合")
387
405
  )
@@ -409,17 +427,20 @@ class Idiot(Player):
409
427
  voted: bool = False
410
428
 
411
429
  @override
412
- async def kill(
413
- self,
414
- reason: KillReason,
415
- *killers: Player,
416
- ) -> bool:
430
+ async def notify_role(self) -> None:
431
+ await super().notify_role()
432
+ await self.send(
433
+ "作为白痴,你可以在首次被投票放逐时免疫放逐,但在之后的投票中无法继续投票"
434
+ )
435
+
436
+ @override
437
+ async def kill(self, reason: KillReason, *killers: Player) -> bool:
417
438
  if reason == KillReason.Vote and not self.voted:
418
439
  self.voted = True
419
440
  await self.game.send(
420
441
  UniMessage.at(self.user_id)
421
442
  .text(" 的身份是白痴\n")
422
- .text("免疫本次投票放逐,且接下来无法参与投票")
443
+ .text("免疫本次投票放逐,且接下来无法参与投票"),
423
444
  )
424
445
  return False
425
446
  return await super().kill(reason, *killers)
@@ -432,6 +453,22 @@ class Idiot(Player):
432
453
  return await super().vote(players)
433
454
 
434
455
 
456
+ @register_role(Role.Joker, RoleGroup.Others)
457
+ class Joker(Player):
458
+ @override
459
+ async def notify_role(self) -> None:
460
+ await super().notify_role()
461
+ await self.send("你的胜利条件: 被投票放逐")
462
+
463
+ @override
464
+ async def kill(self, reason: KillReason, *killers: Player) -> bool:
465
+ result = await super().kill(reason, *killers)
466
+ if reason == KillReason.Vote:
467
+ self.game.killed_players.append(self)
468
+ raise GameFinishedError(GameStatus.Joker)
469
+ return result
470
+
471
+
435
472
  @register_role(Role.Civilian, RoleGroup.GoodGuy)
436
473
  class Civilian(Player):
437
474
  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.0
4
+ Summary: 适用于 Nonebot2 的狼人杀插件
5
+ Author-email: wyf7685 <wyf7685@163.com>
6
6
  License: MIT
7
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
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">
@@ -27,12 +28,15 @@ _✨ 简单的狼人杀插件 ✨_
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
29
  [![python](https://img.shields.io/badge/python-3.10+-blue?logo=python&logoColor=edb641)](https://www.python.org/)
29
30
 
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)
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)
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
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)
35
36
 
37
+ [![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
+ [![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)
39
+
36
40
  </div>
37
41
 
38
42
  ## 📖 介绍
@@ -86,7 +90,7 @@ _✨ 简单的狼人杀插件 ✨_
86
90
 
87
91
  ## ⚙️ 配置
88
92
 
89
- 在 nonebot2 项目的`.env`文件中添加下表中的必填配置
93
+ 在 nonebot2 项目的 `.env` 文件中添加如下配置
90
94
 
91
95
  | 配置项 | 必填 | 默认值 | 说明 |
92
96
  | :-----------------------------: | :--: | :----: | :-----------------------------------------------------------: |
@@ -94,6 +98,7 @@ _✨ 简单的狼人杀插件 ✨_
94
98
  | `werewolf__role_preset` | 否 | - | 覆写插件内置的职业预设 |
95
99
  | `werewolf__werewolf_priority` | 否 | - | 自定义狼人职业优先级 |
96
100
  | `werewolf__priesthood_proirity` | 否 | - | 自定义神职职业优先级 |
101
+ | `werewolf__joker_probability` | 否 | `0.0` | 小丑职业替换平民的概率, 范围`[0,1]` |
97
102
 
98
103
  `werewolf__role_preset`, `werewolf__werewolf_priority`, `werewolf__priesthood_proirity` 的配置格式请参考 [`游戏内容`](#游戏内容) 部分
99
104
 
@@ -192,7 +197,7 @@ werewolf__werewolf_priority=[1, 2, 1, 1]
192
197
 
193
198
  上述配置中,`[1, 2, 1, 1]` 表示狼人的职业优先级为 `狼人`, `狼王`, `狼人`, `狼人`
194
199
 
195
- #### 配置项 `werewolf__werewolf_priority`
200
+ #### 配置项 `werewolf__priesthood_proirity`
196
201
 
197
202
  ```env
198
203
  werewolf__priesthood_proirity=[11, 12, 13, 14, 15]
@@ -226,6 +231,13 @@ werewolf__priesthood_proirity=[11, 12, 13, 14, 15]
226
231
 
227
232
  <!-- CHANGELOG -->
228
233
 
234
+ - 2024.09.09 v1.1.0
235
+
236
+ - 新增职业 `小丑`
237
+ - 修复守卫无法保护自己的 bug
238
+ - 添加部分特殊职业的说明
239
+ - 添加游戏过程中的日志输出
240
+
229
241
  - 2024.09.04 v1.0.7
230
242
 
231
243
  - 优先使用群名片作为玩家名
@@ -0,0 +1,15 @@
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,,
@@ -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,,