nonebot-plugin-werewolf 1.0.4__py3-none-any.whl → 1.0.6__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.4"
11
+ __version__ = "1.0.6"
12
12
  __plugin_meta__ = PluginMetadata(
13
13
  name="狼人杀",
14
14
  description="适用于 Nonebot2 的狼人杀插件",
@@ -1,32 +1,36 @@
1
+ from __future__ import annotations
2
+
1
3
  from dataclasses import dataclass
2
4
  from enum import Enum, auto
3
5
  from typing import TYPE_CHECKING
4
6
 
7
+ from strenum import StrEnum
8
+
5
9
  from .config import config
6
10
 
7
11
  if TYPE_CHECKING:
8
12
  from .player import Player
9
13
 
10
14
 
11
- class Role(Enum):
15
+ class Role(StrEnum):
12
16
  # 狼人
13
- 狼人 = auto()
14
- 狼王 = auto()
17
+ Werewolf = auto()
18
+ WolfKing = auto()
15
19
 
16
20
  # 神职
17
- 预言家 = auto()
18
- 女巫 = auto()
19
- 猎人 = auto()
20
- 守卫 = auto()
21
- 白痴 = auto()
21
+ Prophet = auto()
22
+ Witch = auto()
23
+ Hunter = auto()
24
+ Guard = auto()
25
+ Idiot = auto()
22
26
 
23
27
  # 平民
24
- 平民 = auto()
28
+ Civilian = auto()
25
29
 
26
30
 
27
- class RoleGroup(Enum):
28
- 狼人 = auto()
29
- 好人 = auto()
31
+ class RoleGroup(StrEnum):
32
+ Werewolf = auto()
33
+ GoodGuy = auto()
30
34
 
31
35
 
32
36
  class KillReason(Enum):
@@ -45,16 +49,33 @@ class GameStatus(Enum):
45
49
  @dataclass
46
50
  class GameState:
47
51
  day: int
48
- killed: "Player | None" = None
49
- 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))
52
+ killed: Player | None = None
53
+ shoot: tuple[Player, Player] | tuple[None, None] = (None, None)
54
+ protected: Player | None = None
55
+ potion: tuple[Player | None, tuple[bool, bool]] = (None, (False, False))
56
+
57
+
58
+ role_name_conv: dict[Role, str] = {
59
+ Role.Werewolf: "狼人",
60
+ Role.WolfKing: "狼王",
61
+ Role.Prophet: "预言家",
62
+ Role.Witch: "女巫",
63
+ Role.Hunter: "猎人",
64
+ Role.Guard: "守卫",
65
+ Role.Idiot: "白痴",
66
+ Role.Civilian: "平民",
67
+ }
52
68
 
53
69
 
70
+ role_group_name_conv: dict[RoleGroup, str] = {
71
+ RoleGroup.Werewolf: "狼人",
72
+ RoleGroup.GoodGuy: "好人",
73
+ }
74
+
54
75
  player_preset: dict[int, tuple[int, int, int]] = {
55
76
  # 总人数: (狼, 神, 民)
56
- 6: (1, 3, 2),
57
- 7: (2, 3, 2),
77
+ 6: (1, 2, 3),
78
+ 7: (2, 2, 3),
58
79
  8: (2, 3, 3),
59
80
  9: (2, 4, 3),
60
81
  10: (3, 4, 3),
@@ -64,3 +85,4 @@ player_preset: dict[int, tuple[int, int, int]] = {
64
85
 
65
86
  if config.override_preset is not None:
66
87
  player_preset |= {i[0]: i[1:] for i in config.override_preset}
88
+ player_preset |= {i[0]: i[1:] for i in config.override_preset}
@@ -1,16 +1,20 @@
1
1
  import asyncio
2
2
  import asyncio.timeouts
3
+ import contextlib
3
4
  import random
5
+ import time
4
6
 
5
7
  from nonebot.adapters import Bot
8
+ from nonebot.log import logger
6
9
  from nonebot_plugin_alconna import Target, UniMessage
7
10
 
8
11
  from .constant import GameState, GameStatus, KillReason, Role, RoleGroup, player_preset
9
12
  from .player import Player
10
13
  from .player_set import PlayerSet
14
+ from .utils import InputStore
11
15
 
12
16
  starting_games: dict[str, dict[str, str]] = {}
13
- running_games: dict[str, tuple["Game", asyncio.Task[None]]] = {}
17
+ running_games: dict[str, tuple["Game", asyncio.Task[None], asyncio.Task[None]]] = {}
14
18
 
15
19
 
16
20
  def init_players(bot: Bot, game: "Game", players: dict[str, str]) -> PlayerSet:
@@ -22,11 +26,19 @@ def init_players(bot: Bot, game: "Game", players: dict[str, str]) -> PlayerSet:
22
26
  )
23
27
 
24
28
  roles: list[Role] = []
25
- roles.extend([Role.狼人, Role.狼人, Role.狼王, Role.狼人][: preset[0]])
26
- roles.extend([Role.预言家, Role.女巫, Role.猎人, Role.守卫, Role.白痴][: preset[1]])
27
- roles.extend([Role.平民] * preset[2])
29
+ roles.extend(
30
+ [Role.Werewolf, Role.Werewolf, Role.WolfKing, Role.Werewolf][: preset[0]]
31
+ )
32
+ roles.extend(
33
+ [Role.Prophet, Role.Witch, Role.Hunter, Role.Guard, Role.Idiot][: preset[1]]
34
+ )
35
+ roles.extend([Role.Civilian] * preset[2])
28
36
 
29
- random.shuffle(roles)
37
+ r = random.Random(time.time())
38
+ shuffled: list[Role] = []
39
+ for _ in range(len(players)):
40
+ idx = r.randint(0, len(roles) - 1)
41
+ shuffled.append(roles.pop(idx))
30
42
 
31
43
  async def selector(target_: Target, b: Bot):
32
44
  return target_.self_id == bot.self_id and b is bot
@@ -44,7 +56,7 @@ def init_players(bot: Bot, game: "Game", players: dict[str, str]) -> PlayerSet:
44
56
  ),
45
57
  players[user_id],
46
58
  )
47
- for user_id, role in zip(players, roles)
59
+ for user_id, role in zip(players, shuffled)
48
60
  )
49
61
 
50
62
 
@@ -65,6 +77,7 @@ class Game:
65
77
  self.group = group
66
78
  self.players = init_players(bot, self, players)
67
79
  self.state = GameState(0)
80
+ self.killed_players = []
68
81
 
69
82
  async def send(self, message: str | UniMessage):
70
83
  if isinstance(message, str):
@@ -73,20 +86,20 @@ class Game:
73
86
 
74
87
  def at_all(self) -> UniMessage:
75
88
  msg = UniMessage()
76
- for p in sorted(self.players, key=lambda p: (p.role.name, p.user_id)):
89
+ for p in sorted(self.players, key=lambda p: (p.role_name, p.user_id)):
77
90
  msg.at(p.user_id)
78
91
  return msg
79
92
 
80
93
  def check_game_status(self) -> GameStatus:
81
94
  players = self.players.alive()
82
- w = players.select(RoleGroup.狼人)
83
- p = players.exclude(RoleGroup.狼人)
95
+ w = players.select(RoleGroup.Werewolf)
96
+ p = players.exclude(RoleGroup.Werewolf)
84
97
 
85
98
  if w.size >= p.size:
86
99
  return GameStatus.Bad
87
- if not p.select(Role.平民):
100
+ if not p.select(Role.Civilian):
88
101
  return GameStatus.Bad
89
- if not p.exclude(Role.平民):
102
+ if not p.exclude(Role.Civilian):
90
103
  return GameStatus.Bad
91
104
  if not w.size:
92
105
  return GameStatus.Good
@@ -111,7 +124,7 @@ class Game:
111
124
  case KillReason.Shoot:
112
125
  msg += " 射杀"
113
126
  case KillReason.Vote:
114
- msg += " 投票放逐"
127
+ msg += " 票出"
115
128
  msg += "\n\n"
116
129
 
117
130
  return msg.strip()
@@ -136,7 +149,15 @@ class Game:
136
149
  if isinstance(players, Player):
137
150
  players = PlayerSet([players])
138
151
 
139
- await players.wait_group_stop(self.group.id, timeout_secs)
152
+ async def wait(p: Player):
153
+ while True:
154
+ msg = await InputStore.fetch(p.user_id, self.group.id)
155
+ if msg.extract_plain_text() == "/stop":
156
+ break
157
+
158
+ with contextlib.suppress(TimeoutError):
159
+ async with asyncio.timeouts.timeout(timeout_secs):
160
+ await asyncio.gather(*[wait(p) for p in players])
140
161
 
141
162
  async def interact(
142
163
  self,
@@ -145,7 +166,7 @@ class Game:
145
166
  ) -> None:
146
167
  players = self.players.alive().select(type_)
147
168
  text = (
148
- type_.role.name # Player
169
+ type_.role_name # Player
149
170
  if isinstance(type_, Player)
150
171
  else (
151
172
  type_.name # Role
@@ -164,8 +185,8 @@ class Game:
164
185
  players = self.players.alive()
165
186
  self.state.killed = None
166
187
 
167
- w = players.select(RoleGroup.狼人)
168
- await self.interact(RoleGroup.狼人, 120)
188
+ w = players.select(RoleGroup.Werewolf)
189
+ await self.interact(RoleGroup.Werewolf, 120)
169
190
  if (s := w.player_selected()).size == 1:
170
191
  self.state.killed = s.pop()
171
192
  await w.broadcast(f"今晚选择的目标为: {self.state.killed.name}")
@@ -173,8 +194,8 @@ class Game:
173
194
  await w.broadcast("狼人阵营意见未统一,此晚空刀")
174
195
 
175
196
  # 如果女巫存活,正常交互,限时1分钟
176
- if players.include(Role.女巫):
177
- await self.interact(Role.女巫, 60)
197
+ if players.include(Role.Witch):
198
+ await self.interact(Role.Witch, 60)
178
199
  # 否则等待 5-20s
179
200
  else:
180
201
  await asyncio.sleep(random.uniform(5, 20))
@@ -211,15 +232,15 @@ class Game:
211
232
  await self.send(
212
233
  UniMessage.text("玩家 ")
213
234
  .at(shoot.user_id)
214
- .text(f" 被{shooter.role.name}射杀, 请发表遗言\n")
235
+ .text(f" 被{shooter.role_name}射杀, 请发表遗言\n")
215
236
  .text("限时1分钟, 发送 “/stop” 结束发言")
216
237
  )
217
238
  await self.wait_stop(shoot, 60)
239
+ self.state.shoot = (None, None)
218
240
  await self.post_kill(shoot)
219
- self.state.shoot = (None, None)
220
241
 
221
242
  async def run_vote(self) -> None:
222
- # 统计投票结果
243
+ # 筛选当前存活玩家
223
244
  players = self.players.alive()
224
245
 
225
246
  # 被票玩家: [投票玩家]
@@ -231,9 +252,9 @@ class Game:
231
252
 
232
253
  # 投票结果公示
233
254
  msg = UniMessage.text("投票结果:\n")
234
- for p, v in sorted(vote_result.items(), key=lambda x: x[1], reverse=True):
255
+ for p, v in sorted(vote_result.items(), key=lambda x: len(x[1]), reverse=True):
235
256
  if p is not None:
236
- msg.at(p.user_id).text(f": {v} 票\n")
257
+ msg.at(p.user_id).text(f": {len(v)} 票\n")
237
258
  vote_reversed[len(v)] = [*vote_reversed.get(len(v), []), p]
238
259
  if v := (len(players) - total_votes):
239
260
  msg.text(f"弃票: {v} 票\n")
@@ -244,6 +265,11 @@ class Game:
244
265
  await self.send("没有人被票出")
245
266
  return
246
267
 
268
+ # 弃票大于最高票
269
+ if (len(players) - total_votes) >= max(vote_reversed.keys()):
270
+ await self.send("弃票数大于最高票数, 没有人被票出")
271
+ return
272
+
247
273
  # 平票
248
274
  if len(vs := vote_reversed[max(vote_reversed.keys())]) != 1:
249
275
  await self.send(
@@ -270,6 +296,7 @@ class Game:
270
296
  await self.post_kill(voted)
271
297
 
272
298
  async def run_dead_channel(self) -> None:
299
+ loop = asyncio.get_event_loop()
273
300
  queue: asyncio.Queue[tuple[Player, UniMessage]] = asyncio.Queue()
274
301
 
275
302
  async def send():
@@ -279,20 +306,29 @@ class Game:
279
306
  await self.players.dead().exclude(player).broadcast(msg)
280
307
 
281
308
  async def recv(player: Player):
309
+ counter = 0
310
+
311
+ def decrease():
312
+ nonlocal counter
313
+ counter -= 1
314
+
282
315
  while True:
283
316
  if not player.killed:
284
317
  await asyncio.sleep(1)
285
318
  continue
286
319
  msg = await player.receive()
287
- await queue.put((player, msg))
320
+ counter += 1
321
+ if counter <= 10:
322
+ await queue.put((player, msg))
323
+ loop.call_later(60, decrease)
324
+ else:
325
+ await player.send("发言频率超过限制, 该消息被屏蔽")
288
326
 
289
327
  await asyncio.gather(send(), *[recv(p) for p in self.players])
290
328
 
291
329
  async def run(self) -> None:
292
330
  # 告知玩家角色信息
293
331
  await self.notify_player_role()
294
- # 死者频道
295
- dead_channel = asyncio.create_task(self.run_dead_channel())
296
332
  # 天数记录 主要用于第一晚狼人击杀的遗言
297
333
  day_count = 0
298
334
 
@@ -305,9 +341,10 @@ class Game:
305
341
  # 狼人、预言家、守卫 同时交互,女巫在狼人后交互
306
342
  await asyncio.gather(
307
343
  self.select_killed(),
308
- players.select(Role.女巫).broadcast("请等待狼人决定目标..."),
309
- self.interact(Role.预言家, 60),
310
- self.interact(Role.守卫, 60),
344
+ players.select(Role.Witch).broadcast("请等待狼人决定目标..."),
345
+ players.select(Role.Civilian).broadcast("请等待其他玩家结束交互..."),
346
+ self.interact(Role.Prophet, 60),
347
+ self.interact(Role.Guard, 60),
311
348
  )
312
349
 
313
350
  # 狼人击杀目标
@@ -321,40 +358,42 @@ class Game:
321
358
  if killed is not None:
322
359
  # 除非守卫保护或女巫使用解药,否则狼人正常击杀玩家
323
360
  if not ((killed is protected) or (antidote and potioned is killed)):
324
- await killed.kill(KillReason.Kill, *players.select(RoleGroup.狼人))
361
+ await killed.kill(
362
+ KillReason.Kill, *players.select(RoleGroup.Werewolf)
363
+ )
325
364
  # 如果女巫使用毒药且守卫未保护,杀死该玩家
326
365
  if poison and (potioned is not None) and (potioned is not protected):
327
- await potioned.kill(KillReason.Poison, *players.select(Role.女巫))
366
+ await potioned.kill(KillReason.Poison, *players.select(Role.Witch))
328
367
 
329
368
  day_count += 1
330
369
  msg = UniMessage.text(f"『第{day_count}天』天亮了...\n")
331
370
  # 没有玩家死亡,平安夜
332
371
  if not (dead := players.dead()):
333
372
  await self.send(msg.text("昨晚是平安夜"))
334
- # 有玩家死亡,执行死亡流程
373
+ # 有玩家死亡,公布死者名单
335
374
  else:
336
- # 公布死者名单
337
375
  msg.text("昨晚的死者是:")
338
376
  for p in dead.sorted():
339
377
  msg.text("\n").at(p.user_id)
340
378
  await self.send(msg)
341
- await self.post_kill(dead)
342
-
343
- # 判断游戏状态
344
- if self.check_game_status() != GameStatus.Unset:
345
- break
346
-
347
- # 公示存活玩家
348
- await self.send(f"当前存活玩家: \n\n{self.players.alive().show()}")
349
379
 
350
380
  # 第一晚被狼人杀死的玩家发表遗言
351
381
  if day_count == 1 and killed is not None and not killed.alive:
352
382
  await self.send(
353
383
  UniMessage.text("当前为第一天\n请被狼人杀死的 ")
354
384
  .at(killed.user_id)
355
- .text(" 发表遗言\n限时1分钟, 发送 “/stop” 结束发言")
385
+ .text(" 发表遗言\n")
386
+ .text("限时1分钟, 发送 “/stop” 结束发言")
356
387
  )
357
388
  await self.wait_stop(killed, 60)
389
+ await self.post_kill(dead)
390
+
391
+ # 判断游戏状态
392
+ if self.check_game_status() != GameStatus.Unset:
393
+ break
394
+
395
+ # 公示存活玩家
396
+ await self.send(f"当前存活玩家: \n\n{self.players.alive().show()}")
358
397
 
359
398
  # 开始自由讨论
360
399
  await self.send("接下来开始自由讨论\n限时2分钟, 全员发送 “/stop” 结束发言")
@@ -365,11 +404,31 @@ class Game:
365
404
  await self.run_vote()
366
405
 
367
406
  # 游戏结束
368
- dead_channel.cancel()
369
407
  winner = "好人" if self.check_game_status() == GameStatus.Good else "狼人"
370
408
  msg = UniMessage.text(f"🎉游戏结束,{winner}获胜\n\n")
371
409
  for p in sorted(self.players, key=lambda p: (p.role.value, p.user_id)):
372
- msg.at(p.user_id).text(f": {p.role.name}\n")
373
- msg.text(f"\n{self.show_killed_players()}")
410
+ msg.at(p.user_id).text(f": {p.role_name}\n")
374
411
  await self.send(msg)
375
- running_games.pop(self.group.id, None)
412
+ await self.send(f"玩家死亡报告:\n\n{self.show_killed_players()}")
413
+
414
+ def start(self):
415
+ task = asyncio.create_task(self.run())
416
+ dead_channel = asyncio.create_task(self.run_dead_channel())
417
+
418
+ async def daemon():
419
+ while not task.done(): # noqa: ASYNC110
420
+ await asyncio.sleep(1)
421
+
422
+ try:
423
+ task.result()
424
+ except asyncio.CancelledError as err:
425
+ logger.warning(f"狼人杀游戏进程被取消: {err}")
426
+ except Exception as err:
427
+ msg = f"狼人杀游戏进程出现错误: {err!r}"
428
+ logger.opt(exception=err).error(msg)
429
+ await self.send(msg)
430
+ finally:
431
+ dead_channel.cancel()
432
+ running_games.pop(self.group.id, None)
433
+
434
+ running_games[self.group.id] = (self, task, asyncio.create_task(daemon()))
@@ -62,5 +62,4 @@ async def handle_start(
62
62
  del starting_games[target.id]
63
63
 
64
64
  game = Game(bot=bot, group=target, players=players)
65
- task = asyncio.create_task(game.run())
66
- running_games[target.id] = (game, task)
65
+ game.start()
@@ -1,13 +1,15 @@
1
+ from __future__ import annotations
2
+
1
3
  import asyncio
2
4
  import asyncio.timeouts
3
5
  from dataclasses import dataclass
4
- from typing import TYPE_CHECKING, ClassVar, TypeVar
6
+ from typing import TYPE_CHECKING, ClassVar, TypeVar, final
5
7
  from typing_extensions import override
6
8
 
7
9
  from nonebot.adapters import Bot
8
10
  from nonebot_plugin_alconna.uniseg import Receipt, Target, UniMessage
9
11
 
10
- from .constant import KillReason, Role, RoleGroup
12
+ from .constant import KillReason, Role, RoleGroup, role_name_conv,role_group_name_conv
11
13
  from .utils import InputStore, check_index
12
14
 
13
15
  if TYPE_CHECKING:
@@ -16,7 +18,7 @@ if TYPE_CHECKING:
16
18
 
17
19
 
18
20
  P = TypeVar("P", bound=type["Player"])
19
- PLAYER_CLASS: dict[Role, type["Player"]] = {}
21
+ PLAYER_CLASS: dict[Role, type[Player]] = {}
20
22
 
21
23
 
22
24
  def register_role(cls: P) -> P:
@@ -27,7 +29,7 @@ def register_role(cls: P) -> P:
27
29
  @dataclass
28
30
  class KillInfo:
29
31
  reason: KillReason
30
- killers: "PlayerSet"
32
+ killers: PlayerSet
31
33
 
32
34
 
33
35
  class Player:
@@ -35,52 +37,61 @@ class Player:
35
37
  role_group: ClassVar[RoleGroup]
36
38
 
37
39
  bot: Bot
38
- game: "Game"
40
+ game: Game
39
41
  user: Target
40
42
  name: str
41
43
  alive: bool = True
42
44
  killed: bool = False
43
45
  kill_info: KillInfo | None = None
44
- selected: "Player | None" = None
46
+ selected: Player | None = None
45
47
 
46
- def __init__(self, bot: Bot, game: "Game", user: Target, name: str) -> None:
48
+ @final
49
+ def __init__(self, bot: Bot, game: Game, user: Target, name: str) -> None:
47
50
  self.bot = bot
48
51
  self.game = game
49
52
  self.user = user
50
53
  self.name = name
51
54
 
55
+ @final
52
56
  @classmethod
53
57
  def new(
54
58
  cls,
55
59
  role: Role,
56
60
  bot: Bot,
57
- game: "Game",
61
+ game: Game,
58
62
  user: Target,
59
63
  name: str,
60
- ) -> "Player":
64
+ ) -> Player:
61
65
  if role not in PLAYER_CLASS:
62
66
  raise ValueError(f"Unexpected role: {role!r}")
63
67
 
64
68
  return PLAYER_CLASS[role](bot, game, user, name)
65
69
 
66
70
  def __repr__(self) -> str:
67
- return f"<{self.role.name}: user={self.user} alive={self.alive}>"
71
+ return f"<{self.role_name}: user={self.user} alive={self.alive}>"
68
72
 
69
73
  @property
70
74
  def user_id(self) -> str:
71
75
  return self.user.id
72
76
 
77
+ @property
78
+ def role_name(self) -> str:
79
+ return role_name_conv[self.role]
80
+
81
+ @final
73
82
  async def send(self, message: str | UniMessage) -> Receipt:
74
83
  if isinstance(message, str):
75
84
  message = UniMessage.text(message)
76
85
 
77
86
  return await message.send(target=self.user, bot=self.bot)
78
87
 
88
+ @final
79
89
  async def receive(self, prompt: str | UniMessage | None = None) -> UniMessage:
80
90
  if prompt:
81
91
  await self.send(prompt)
82
92
  return await InputStore.fetch(self.user.id)
83
93
 
94
+ @final
84
95
  async def receive_text(self) -> str:
85
96
  return (await self.receive()).extract_plain_text()
86
97
 
@@ -88,13 +99,15 @@ class Player:
88
99
  return
89
100
 
90
101
  async def notify_role(self) -> None:
91
- await self.send(f"你的身份: {self.role.name}")
102
+ await self.send(f"你的身份: {self.role_name}")
92
103
 
93
104
  async def kill(
94
105
  self,
95
106
  reason: KillReason,
96
- *killers: "Player",
107
+ *killers: Player,
97
108
  ) -> bool:
109
+ from .player_set import PlayerSet
110
+
98
111
  self.alive = False
99
112
  self.kill_info = KillInfo(reason, PlayerSet(killers))
100
113
  return True
@@ -102,15 +115,16 @@ class Player:
102
115
  async def post_kill(self) -> None:
103
116
  self.killed = True
104
117
 
105
- async def vote(self, players: "PlayerSet") -> "tuple[Player, Player] | None":
118
+ async def vote(self, players: PlayerSet) -> tuple[Player, Player] | None:
106
119
  await self.send(
107
120
  f"请选择需要投票的玩家:\n{players.show()}"
108
121
  "\n\n发送编号选择玩家\n发送 “/stop” 弃票"
109
122
  )
110
123
 
111
124
  while True:
112
- text = (await self.receive()).extract_plain_text()
125
+ text = await self.receive_text()
113
126
  if text == "/stop":
127
+ await self.send("你选择了弃票")
114
128
  return None
115
129
  index = check_index(text, len(players))
116
130
  if index is not None:
@@ -131,9 +145,9 @@ class CanShoot(Player):
131
145
  return await super().post_kill()
132
146
 
133
147
  await self.game.send(
134
- UniMessage.text(f"{self.role.name} ")
148
+ UniMessage.text(f"{self.role_name} ")
135
149
  .at(self.user_id)
136
- .text(f" 死了\n请{self.role.name}决定击杀目标...")
150
+ .text(f" 死了\n请{self.role_name}决定击杀目标...")
137
151
  )
138
152
 
139
153
  self.game.state.shoot = (None, None)
@@ -142,14 +156,14 @@ class CanShoot(Player):
142
156
  if shoot is not None:
143
157
  self.game.state.shoot = (self, shoot)
144
158
  await self.send(
145
- UniMessage.text(f"{self.role.name} ")
159
+ UniMessage.text(f"{self.role_name} ")
146
160
  .at(self.user_id)
147
161
  .text(" 射杀了玩家 ")
148
162
  .at(shoot.user_id)
149
163
  )
150
164
  await shoot.kill(KillReason.Shoot, self)
151
165
  else:
152
- await self.send(f"{self.role.name}选择了取消技能")
166
+ await self.send(f"{self.role_name}选择了取消技能")
153
167
  return await super().post_kill()
154
168
 
155
169
  async def shoot(self) -> Player | None:
@@ -178,20 +192,23 @@ class CanShoot(Player):
178
192
 
179
193
  @register_role
180
194
  class 狼人(Player):
181
- role: ClassVar[Role] = Role.狼人
182
- role_group: ClassVar[RoleGroup] = RoleGroup.狼人
195
+ role: ClassVar[Role] = Role.Werewolf
196
+ role_group: ClassVar[RoleGroup] = RoleGroup.Werewolf
183
197
 
184
198
  @override
185
199
  async def notify_role(self) -> None:
186
200
  await super().notify_role()
187
- partners = self.game.players.alive().select(RoleGroup.狼人).exclude(self)
188
- msg = "你的队友:\n" + "\n".join(f" {p.role.name}: {p.name}" for p in partners)
189
- await self.send(msg)
201
+ partners = self.game.players.alive().select(RoleGroup.Werewolf).exclude(self)
202
+ if partners:
203
+ await self.send(
204
+ "你的队友:\n"
205
+ + "\n".join(f" {p.role_name}: {p.name}" for p in partners)
206
+ )
190
207
 
191
208
  @override
192
209
  async def interact(self) -> None:
193
210
  players = self.game.players.alive()
194
- partners = players.select(RoleGroup.狼人).exclude(self)
211
+ partners = players.select(RoleGroup.Werewolf).exclude(self)
195
212
 
196
213
  # 避免阻塞
197
214
  def broadcast(msg: str | UniMessage):
@@ -201,7 +218,7 @@ class 狼人(Player):
201
218
  if partners:
202
219
  msg = (
203
220
  msg.text("你的队友:\n")
204
- .text("\n".join(f" {p.role.name}: {p.name}" for p in partners))
221
+ .text("\n".join(f" {p.role_name}: {p.name}" for p in partners))
205
222
  .text("\n所有私聊消息将被转发至队友\n\n")
206
223
  )
207
224
  await self.send(
@@ -221,7 +238,7 @@ class 狼人(Player):
221
238
  if index is not None:
222
239
  selected = index - 1
223
240
  msg = f"当前选择玩家: {players[selected].name}"
224
- await self.send(msg)
241
+ await self.send(f"{msg}\n发送 “/stop” 结束回合")
225
242
  broadcast(f"队友 {self.name} {msg}")
226
243
  if text == "/stop":
227
244
  if selected is not None:
@@ -237,14 +254,14 @@ class 狼人(Player):
237
254
 
238
255
  @register_role
239
256
  class 狼王(CanShoot, 狼人):
240
- role: ClassVar[Role] = Role.狼王
241
- role_group: ClassVar[RoleGroup] = RoleGroup.狼人
257
+ role: ClassVar[Role] = Role.WolfKing
258
+ role_group: ClassVar[RoleGroup] = RoleGroup.Werewolf
242
259
 
243
260
 
244
261
  @register_role
245
262
  class 预言家(Player):
246
- role: ClassVar[Role] = Role.预言家
247
- role_group: ClassVar[RoleGroup] = RoleGroup.好人
263
+ role: ClassVar[Role] = Role.Prophet
264
+ role_group: ClassVar[RoleGroup] = RoleGroup.GoodGuy
248
265
 
249
266
  @override
250
267
  async def interact(self) -> None:
@@ -264,14 +281,14 @@ class 预言家(Player):
264
281
  await self.send("输入错误,请发送编号选择玩家")
265
282
 
266
283
  player = players[selected]
267
- result = "狼人" if player.role == Role.狼人 else "好人"
284
+ result = role_group_name_conv[player.role_group]
268
285
  await self.send(f"玩家 {player.name} 的阵营是『{result}』")
269
286
 
270
287
 
271
288
  @register_role
272
289
  class 女巫(Player):
273
- role: ClassVar[Role] = Role.女巫
274
- role_group: ClassVar[RoleGroup] = RoleGroup.好人
290
+ role: ClassVar[Role] = Role.Witch
291
+ role_group: ClassVar[RoleGroup] = RoleGroup.GoodGuy
275
292
  antidote: int = 1
276
293
  poison: int = 1
277
294
 
@@ -296,7 +313,7 @@ class 女巫(Player):
296
313
  async def handle_killed(self) -> bool:
297
314
  msg = UniMessage()
298
315
  if (killed := self.game.state.killed) is not None:
299
- msg.text(f"今晚 {killed} 被刀了\n\n")
316
+ msg.text(f"今晚 {killed.name} 被刀了\n\n")
300
317
  else:
301
318
  await self.send("今晚没有人被刀")
302
319
  return False
@@ -359,14 +376,14 @@ class 女巫(Player):
359
376
 
360
377
  @register_role
361
378
  class 猎人(CanShoot, Player):
362
- role: ClassVar[Role] = Role.猎人
363
- role_group: ClassVar[RoleGroup] = RoleGroup.好人
379
+ role: ClassVar[Role] = Role.Hunter
380
+ role_group: ClassVar[RoleGroup] = RoleGroup.GoodGuy
364
381
 
365
382
 
366
383
  @register_role
367
384
  class 守卫(Player):
368
- role: ClassVar[Role] = Role.守卫
369
- role_group: ClassVar[RoleGroup] = RoleGroup.好人
385
+ role: ClassVar[Role] = Role.Guard
386
+ role_group: ClassVar[RoleGroup] = RoleGroup.GoodGuy
370
387
 
371
388
  @override
372
389
  async def interact(self) -> None:
@@ -397,8 +414,8 @@ class 守卫(Player):
397
414
 
398
415
  @register_role
399
416
  class 白痴(Player):
400
- role: ClassVar[Role] = Role.白痴
401
- role_group: ClassVar[RoleGroup] = RoleGroup.好人
417
+ role: ClassVar[Role] = Role.Idiot
418
+ role_group: ClassVar[RoleGroup] = RoleGroup.GoodGuy
402
419
  voted: bool = False
403
420
 
404
421
  @override
@@ -418,7 +435,7 @@ class 白痴(Player):
418
435
  return await super().kill(reason, *killers)
419
436
 
420
437
  @override
421
- async def vote(self, players: "PlayerSet") -> "tuple[Player, Player] | None":
438
+ async def vote(self, players: PlayerSet) -> tuple[Player, Player] | None:
422
439
  if self.voted:
423
440
  await self.send("你已经发动过白痴身份的技能,无法参与本次投票")
424
441
  return None
@@ -427,5 +444,5 @@ class 白痴(Player):
427
444
 
428
445
  @register_role
429
446
  class 平民(Player):
430
- role: ClassVar[Role] = Role.平民
431
- role_group: ClassVar[RoleGroup] = RoleGroup.好人
447
+ role: ClassVar[Role] = Role.Civilian
448
+ role_group: ClassVar[RoleGroup] = RoleGroup.GoodGuy
@@ -1,12 +1,10 @@
1
1
  import asyncio
2
2
  import asyncio.timeouts
3
- import contextlib
4
3
 
5
4
  from nonebot_plugin_alconna.uniseg import UniMessage
6
5
 
7
6
  from .constant import Role, RoleGroup
8
7
  from .player import Player
9
- from .utils import InputStore
10
8
 
11
9
 
12
10
  class PlayerSet(set[Player]):
@@ -52,12 +50,13 @@ class PlayerSet(set[Player]):
52
50
  await asyncio.gather(*[p.interact() for p in self.alive()])
53
51
 
54
52
  async def vote(self, timeout_secs: float = 60) -> dict[Player, list[Player]]:
55
- async def vote(player: Player) -> "tuple[Player, Player] | None":
53
+ async def vote(player: Player) -> tuple[Player, Player] | None:
56
54
  try:
57
55
  async with asyncio.timeouts.timeout(timeout_secs):
58
56
  return await player.vote(self)
59
57
  except TimeoutError:
60
58
  await player.send("投票超时,将视为弃票")
59
+ return None
61
60
 
62
61
  result: dict[Player, list[Player]] = {}
63
62
  for item in await asyncio.gather(*[vote(p) for p in self.alive()]):
@@ -69,17 +68,6 @@ class PlayerSet(set[Player]):
69
68
  async def broadcast(self, message: str | UniMessage) -> None:
70
69
  await asyncio.gather(*[p.send(message) for p in self])
71
70
 
72
- async def wait_group_stop(self, group_id: str, timeout_secs: float) -> None:
73
- async def wait(p: Player):
74
- while True:
75
- msg = await InputStore.fetch(p.user_id, group_id)
76
- if msg.extract_plain_text() == "/stop":
77
- break
78
-
79
- with contextlib.suppress(TimeoutError):
80
- async with asyncio.timeouts.timeout(timeout_secs):
81
- await asyncio.gather(*[wait(p) for p in self])
82
-
83
71
  def show(self) -> str:
84
72
  return "\n".join(f"{i}. {p.name}" for i, p in enumerate(self.sorted(), 1))
85
73
 
@@ -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 player_preset
12
+ from .constant import player_preset
13
13
 
14
14
 
15
15
  def check_index(text: str, arrlen: int) -> int | None:
@@ -47,7 +47,7 @@ def user_in_game(user_id: str, group_id: str | None) -> bool:
47
47
  if group_id is not None and group_id not in running_games:
48
48
  return False
49
49
  games = running_games.values() if group_id is None else [running_games[group_id]]
50
- for game, _ in games:
50
+ for game, *_ in games:
51
51
  return any(user_id == player.user_id for player in game.players)
52
52
  return False
53
53
 
@@ -173,12 +173,12 @@ async def _prepare_game_handle(
173
173
 
174
174
 
175
175
  async def prepare_game(event: Event, players: dict[str, str]) -> None:
176
- queue = asyncio.Queue()
176
+ queue: asyncio.Queue[tuple[str, str, str]] = asyncio.Queue()
177
177
  task_receive = asyncio.create_task(
178
178
  _prepare_game_receive(
179
179
  queue,
180
180
  event,
181
- UniMessage.get_target().id,
181
+ UniMessage.get_target(event).id,
182
182
  )
183
183
  )
184
184
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nonebot-plugin-werewolf
3
- Version: 1.0.4
3
+ Version: 1.0.6
4
4
  Summary: Default template for PDM package
5
5
  Author-Email: wyf7685 <wyf7685@163.com>
6
6
  License: MIT
@@ -9,6 +9,7 @@ Requires-Dist: nonebot2>=2.3.3
9
9
  Requires-Dist: nonebot-plugin-alconna>=0.52.1
10
10
  Requires-Dist: nonebot-plugin-userinfo>=0.2.6
11
11
  Requires-Dist: nonebot-plugin-waiter>=0.7.1
12
+ Requires-Dist: StrEnum>=0.4.15
12
13
  Description-Content-Type: text/markdown
13
14
 
14
15
  <div align="center">
@@ -116,9 +117,18 @@ _✨ 简单的狼人杀插件 ✨_
116
117
 
117
118
  ### 指令表
118
119
 
119
- | 指令 | 权限 | 需要@ | 范围 | 说明 |
120
- | :-----------------: | :--: | :---: | :--: | :------: |
121
- | `werewolf`/`狼人杀` | 群员 | 是 | 群聊 | 发起游戏 |
120
+ | 指令 | 权限 | 需要@ | 范围 | 说明 |
121
+ | :-----------------: | :--------: | :---: | :--: | :---------------------------------: |
122
+ | `werewolf`/`狼人杀` | 群员 | 是 | 群聊 | 发起游戏 (进入准备阶段) |
123
+ | `开始游戏` | 游戏发起者 | 是 | 群聊 | _[准备阶段]_ 游戏发起者开始游戏 |
124
+ | `结束游戏` | 游戏发起者 | 是 | 群聊 | _[准备阶段]_ 游戏发起者结束游戏 |
125
+ | `当前玩家` | 群员 | 是 | 群聊 | _[准备阶段]_ 列出参与游戏的玩家列表 |
126
+ | `加入游戏` | 群员 | 是 | 群聊 | _[准备阶段]_ 玩家加入游戏 |
127
+ | `退出游戏` | 群员 | 是 | 群聊 | _[准备阶段]_ 玩家退出游戏 |
128
+
129
+ _其他交互参考游戏内提示_
130
+
131
+ 对于 `OneBot V11` 适配器, 启用配置项 `werewolf__enable_poke` 后, 可以使用戳一戳代替 _准备阶段_ 的 `加入游戏` 操作 和 游戏内的 `/stop` 命令
122
132
 
123
133
  ### 游戏内容
124
134
 
@@ -132,8 +142,8 @@ _✨ 简单的狼人杀插件 ✨_
132
142
 
133
143
  | 总人数 | 狼人 | 神职 | 平民 |
134
144
  | ------ | ---- | ---- | ---- |
135
- | 6     | 1  | 3   | 2   |
136
- | 7     | 2  | 3   | 2   |
145
+ | 6     | 1  | 2   | 3   |
146
+ | 7     | 2  | 2   | 3   |
137
147
  | 8     | 2  | 3   | 3   |
138
148
  | 9     | 2  | 4   | 3   |
139
149
  | 10   | 3  | 4   | 3   |
@@ -171,9 +181,14 @@ werewolf__override_preset='
171
181
  <details>
172
182
  <summary>更新日志</summary>
173
183
 
174
- - 2024.09.01 v1.0.4
184
+ - 2024.09.03 v1.0.6
185
+
186
+ - 修复预言家查验狼王返回好人的 bug
187
+
188
+ - 2024.09.03 v1.0.5
175
189
 
176
190
  - 优化玩家交互体验
191
+ - 添加游戏结束后死亡报告
177
192
 
178
193
  - 2024.08.31 v1.0.1
179
194
 
@@ -0,0 +1,13 @@
1
+ nonebot_plugin_werewolf-1.0.6.dist-info/METADATA,sha256=VEQ_yjPRs3bB28Khz98EJA5XTlJ6fMrP_YNZBBp9PQc,6839
2
+ nonebot_plugin_werewolf-1.0.6.dist-info/WHEEL,sha256=rSwsxJWe3vzyR5HCwjWXQruDgschpei4h_giTm0dJVE,90
3
+ nonebot_plugin_werewolf-1.0.6.dist-info/licenses/LICENSE,sha256=B_WbEqjGr6GYVNfEJPY31T1Opik7OtgOkhRs4Ig3e2M,1064
4
+ nonebot_plugin_werewolf/__init__.py,sha256=WEwQN8NtEtOfTrH23_MysaaS4OnCjxtGb-_brG9bGTQ,734
5
+ nonebot_plugin_werewolf/config.py,sha256=3O63P1pjvJEwgOwxApAHbKQzvCR1zoG5UjTClZ_OJts,315
6
+ nonebot_plugin_werewolf/constant.py,sha256=k4r0vKYtePDzAqNOdpAlSXnP37YvJSgmJvmGVoJDdeI,1746
7
+ nonebot_plugin_werewolf/game.py,sha256=oGuqV4zmuX95CH7zjtx7wMFzQjzObWXbnnO8PHSnzYk,15525
8
+ nonebot_plugin_werewolf/matchers.py,sha256=D8F2FsV03YhBfXCUeUJmpSFvMk9Z7F0lKeFx-lRN9To,2281
9
+ nonebot_plugin_werewolf/ob11_ext.py,sha256=I6bPCv5SgAStTJuvBl5F7wqgiksWeFkb4R7n06jXprA,2399
10
+ nonebot_plugin_werewolf/player.py,sha256=KjGtfMT7mZG-ACfn5z2z0UJDlLH6t1-6mls_H4UxtWU,14175
11
+ nonebot_plugin_werewolf/player_set.py,sha256=pEXyjmA7INog23i5-TmuJA559y5eKUehrPxHcnzWCTU,2571
12
+ nonebot_plugin_werewolf/utils.py,sha256=8ddVr_PyObxZ8PfWry_-IXnH2dys2bTowKmHfqVbhPE,6233
13
+ nonebot_plugin_werewolf-1.0.6.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- nonebot_plugin_werewolf-1.0.4.dist-info/METADATA,sha256=AkymAo7r7BJckqlRFyjnFBGYCKhpyREppGHPGFowtx8,5815
2
- nonebot_plugin_werewolf-1.0.4.dist-info/WHEEL,sha256=rSwsxJWe3vzyR5HCwjWXQruDgschpei4h_giTm0dJVE,90
3
- nonebot_plugin_werewolf-1.0.4.dist-info/licenses/LICENSE,sha256=B_WbEqjGr6GYVNfEJPY31T1Opik7OtgOkhRs4Ig3e2M,1064
4
- nonebot_plugin_werewolf/__init__.py,sha256=Ia0RlpR6eFLWMI-0OyLGMbBp5W7Oh6s96r45rGWGtDw,734
5
- nonebot_plugin_werewolf/config.py,sha256=3O63P1pjvJEwgOwxApAHbKQzvCR1zoG5UjTClZ_OJts,315
6
- nonebot_plugin_werewolf/constant.py,sha256=kMn5DHXdYuOXeGpJjnG_WX9AD-OytLKXSjrww0T5OYg,1234
7
- nonebot_plugin_werewolf/game.py,sha256=PI6j-T70Logax87fpmf8HBS3LihF7J40nespvoNFyus,13449
8
- nonebot_plugin_werewolf/matchers.py,sha256=o0LdO7AOUjTPcNaNTHbmw5d0CcnMc7mX8oko0JT8Np4,2351
9
- nonebot_plugin_werewolf/ob11_ext.py,sha256=I6bPCv5SgAStTJuvBl5F7wqgiksWeFkb4R7n06jXprA,2399
10
- nonebot_plugin_werewolf/player.py,sha256=z_yVjqfDOxnQlDtDxSbdmvEapIQikkho2hCjb46zqKY,13799
11
- nonebot_plugin_werewolf/player_set.py,sha256=ptNZb13H8DgcjQ9tm3uT2ISXnnWioQG1W4R506HlaMU,3057
12
- nonebot_plugin_werewolf/utils.py,sha256=B8YipTx96k2Y-trrCDfzRQnmHs-yAMcU5-g8FUrRQ64,6186
13
- nonebot_plugin_werewolf-1.0.4.dist-info/RECORD,,