nonebot-plugin-werewolf 1.0.6__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.
@@ -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,role_group_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,9 +26,14 @@ P = TypeVar("P", bound=type["Player"])
21
26
  PLAYER_CLASS: dict[Role, type[Player]] = {}
22
27
 
23
28
 
24
- def register_role(cls: P) -> P:
25
- PLAYER_CLASS[cls.role] = cls
26
- return cls
29
+ def register_role(role: Role, role_group: RoleGroup, /) -> Callable[[P], P]:
30
+ def decorator(cls: P, /) -> P:
31
+ cls.role = role
32
+ cls.role_group = role_group
33
+ PLAYER_CLASS[role] = cls
34
+ return cls
35
+
36
+ return decorator
27
37
 
28
38
 
29
39
  @dataclass
@@ -41,7 +51,7 @@ class Player:
41
51
  user: Target
42
52
  name: str
43
53
  alive: bool = True
44
- killed: bool = False
54
+ killed: asyncio.Event
45
55
  kill_info: KillInfo | None = None
46
56
  selected: Player | None = None
47
57
 
@@ -51,6 +61,7 @@ class Player:
51
61
  self.game = game
52
62
  self.user = user
53
63
  self.name = name
64
+ self.killed = asyncio.Event()
54
65
 
55
66
  @final
56
67
  @classmethod
@@ -68,7 +79,7 @@ class Player:
68
79
  return PLAYER_CLASS[role](bot, game, user, name)
69
80
 
70
81
  def __repr__(self) -> str:
71
- return f"<{self.role_name}: user={self.user} alive={self.alive}>"
82
+ return f"<{self.role_name}: user={self.name!r} alive={self.alive}>"
72
83
 
73
84
  @property
74
85
  def user_id(self) -> str:
@@ -78,18 +89,32 @@ class Player:
78
89
  def role_name(self) -> str:
79
90
  return role_name_conv[self.role]
80
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
+
81
102
  @final
82
103
  async def send(self, message: str | UniMessage) -> Receipt:
83
104
  if isinstance(message, str):
84
105
  message = UniMessage.text(message)
85
106
 
107
+ self._log(f"<g>Send</g> | {message}")
86
108
  return await message.send(target=self.user, bot=self.bot)
87
109
 
88
110
  @final
89
111
  async def receive(self, prompt: str | UniMessage | None = None) -> UniMessage:
90
112
  if prompt:
91
113
  await self.send(prompt)
92
- 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
93
118
 
94
119
  @final
95
120
  async def receive_text(self) -> str:
@@ -101,11 +126,7 @@ class Player:
101
126
  async def notify_role(self) -> None:
102
127
  await self.send(f"你的身份: {self.role_name}")
103
128
 
104
- async def kill(
105
- self,
106
- reason: KillReason,
107
- *killers: Player,
108
- ) -> bool:
129
+ async def kill(self, reason: KillReason, *killers: Player) -> bool:
109
130
  from .player_set import PlayerSet
110
131
 
111
132
  self.alive = False
@@ -113,7 +134,7 @@ class Player:
113
134
  return True
114
135
 
115
136
  async def post_kill(self) -> None:
116
- self.killed = True
137
+ self.killed.set()
117
138
 
118
139
  async def vote(self, players: PlayerSet) -> tuple[Player, Player] | None:
119
140
  await self.send(
@@ -179,7 +200,7 @@ class CanShoot(Player):
179
200
  text = await self.receive_text()
180
201
  if text == "/stop":
181
202
  await self.send("已取消技能")
182
- return
203
+ return None
183
204
  index = check_index(text, len(players))
184
205
  if index is not None:
185
206
  selected = index - 1
@@ -190,11 +211,8 @@ class CanShoot(Player):
190
211
  return players[selected]
191
212
 
192
213
 
193
- @register_role
194
- class 狼人(Player):
195
- role: ClassVar[Role] = Role.Werewolf
196
- role_group: ClassVar[RoleGroup] = RoleGroup.Werewolf
197
-
214
+ @register_role(Role.Werewolf, RoleGroup.Werewolf)
215
+ class Werewolf(Player):
198
216
  @override
199
217
  async def notify_role(self) -> None:
200
218
  await super().notify_role()
@@ -211,7 +229,7 @@ class 狼人(Player):
211
229
  partners = players.select(RoleGroup.Werewolf).exclude(self)
212
230
 
213
231
  # 避免阻塞
214
- def broadcast(msg: str | UniMessage):
232
+ def broadcast(msg: str | UniMessage) -> asyncio.Task[None]:
215
233
  return asyncio.create_task(partners.broadcast(msg))
216
234
 
217
235
  msg = UniMessage()
@@ -232,8 +250,8 @@ class 狼人(Player):
232
250
  selected = None
233
251
  finished = False
234
252
  while selected is None or not finished:
235
- input = await self.receive()
236
- text = input.extract_plain_text()
253
+ input_msg = await self.receive()
254
+ text = input_msg.extract_plain_text()
237
255
  index = check_index(text, len(players))
238
256
  if index is not None:
239
257
  selected = index - 1
@@ -247,22 +265,21 @@ class 狼人(Player):
247
265
  broadcast(f"队友 {self.name} 结束当前回合")
248
266
  else:
249
267
  await self.send("当前未选择玩家,无法结束回合")
250
- broadcast(UniMessage.text(f"队友 {self.name}:\n") + input)
268
+ broadcast(UniMessage.text(f"队友 {self.name}:\n") + input_msg)
251
269
 
252
270
  self.selected = players[selected]
253
271
 
254
272
 
255
- @register_role
256
- class 狼王(CanShoot, 狼人):
257
- role: ClassVar[Role] = Role.WolfKing
258
- role_group: ClassVar[RoleGroup] = RoleGroup.Werewolf
259
-
273
+ @register_role(Role.WolfKing, RoleGroup.Werewolf)
274
+ class WolfKing(CanShoot, Werewolf):
275
+ @override
276
+ async def notify_role(self) -> None:
277
+ await super().notify_role()
278
+ await self.send("作为狼王,你可以在死后射杀一名玩家")
260
279
 
261
- @register_role
262
- class 预言家(Player):
263
- role: ClassVar[Role] = Role.Prophet
264
- role_group: ClassVar[RoleGroup] = RoleGroup.GoodGuy
265
280
 
281
+ @register_role(Role.Prophet, RoleGroup.GoodGuy)
282
+ class Prophet(Player):
266
283
  @override
267
284
  async def interact(self) -> None:
268
285
  players = self.game.players.alive().exclude(self)
@@ -281,14 +298,12 @@ class 预言家(Player):
281
298
  await self.send("输入错误,请发送编号选择玩家")
282
299
 
283
300
  player = players[selected]
284
- result = role_group_name_conv[player.role_group]
301
+ result = "狼人" if player.role_group == RoleGroup.Werewolf else "好人"
285
302
  await self.send(f"玩家 {player.name} 的阵营是『{result}』")
286
303
 
287
304
 
288
- @register_role
289
- class 女巫(Player):
290
- role: ClassVar[Role] = Role.Witch
291
- role_group: ClassVar[RoleGroup] = RoleGroup.GoodGuy
305
+ @register_role(Role.Witch, RoleGroup.GoodGuy)
306
+ class Witch(Player):
292
307
  antidote: int = 1
293
308
  poison: int = 1
294
309
 
@@ -297,7 +312,7 @@ class 女巫(Player):
297
312
  *,
298
313
  antidote: Player | None = None,
299
314
  posion: Player | None = None,
300
- ):
315
+ ) -> None:
301
316
  if antidote is not None:
302
317
  self.antidote = 0
303
318
  self.selected = antidote
@@ -331,10 +346,9 @@ class 女巫(Player):
331
346
  self.set_state(antidote=killed)
332
347
  await self.send(f"你对 {killed.name} 使用了解药,回合结束")
333
348
  return True
334
- elif text == "/stop":
349
+ if text == "/stop":
335
350
  return False
336
- else:
337
- await self.send("输入错误: 请输入 “1” 或 “/stop”")
351
+ await self.send("输入错误: 请输入 “1” 或 “/stop”")
338
352
 
339
353
  @override
340
354
  async def interact(self) -> None:
@@ -361,12 +375,11 @@ class 女巫(Player):
361
375
  if index is not None:
362
376
  selected = index - 1
363
377
  break
364
- elif text == "/stop":
378
+ if text == "/stop":
365
379
  await self.send("你选择不使用毒药,回合结束")
366
380
  self.set_state()
367
381
  return
368
- else:
369
- await self.send("输入错误: 请发送玩家编号或 “/stop”")
382
+ await self.send("输入错误: 请发送玩家编号或 “/stop”")
370
383
 
371
384
  self.poison = 0
372
385
  self.selected = player = players[selected]
@@ -374,22 +387,19 @@ class 女巫(Player):
374
387
  await self.send(f"当前回合选择对玩家 {player.name} 使用毒药\n回合结束")
375
388
 
376
389
 
377
- @register_role
378
- class 猎人(CanShoot, Player):
379
- role: ClassVar[Role] = Role.Hunter
380
- role_group: ClassVar[RoleGroup] = RoleGroup.GoodGuy
390
+ @register_role(Role.Hunter, RoleGroup.GoodGuy)
391
+ class Hunter(CanShoot, Player):
392
+ pass
381
393
 
382
394
 
383
- @register_role
384
- class 守卫(Player):
385
- role: ClassVar[Role] = Role.Guard
386
- role_group: ClassVar[RoleGroup] = RoleGroup.GoodGuy
387
-
395
+ @register_role(Role.Guard, RoleGroup.GoodGuy)
396
+ class Guard(Player):
388
397
  @override
389
398
  async def interact(self) -> None:
390
- players = self.game.players.alive().exclude(self)
399
+ players = self.game.players.alive()
391
400
  await self.send(
392
- UniMessage.text(f"请选择需要保护的玩家:\n{players.show()}")
401
+ UniMessage.text("请选择需要保护的玩家:\n")
402
+ .text(players.show())
393
403
  .text("\n\n发送编号选择玩家")
394
404
  .text("\n发送 “/stop” 结束回合")
395
405
  )
@@ -412,24 +422,25 @@ class 守卫(Player):
412
422
  await self.send(f"本回合保护的玩家: {self.selected.name}")
413
423
 
414
424
 
415
- @register_role
416
- class 白痴(Player):
417
- role: ClassVar[Role] = Role.Idiot
418
- role_group: ClassVar[RoleGroup] = RoleGroup.GoodGuy
425
+ @register_role(Role.Idiot, RoleGroup.GoodGuy)
426
+ class Idiot(Player):
419
427
  voted: bool = False
420
428
 
421
429
  @override
422
- async def kill(
423
- self,
424
- reason: KillReason,
425
- *killers: Player,
426
- ) -> 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:
427
438
  if reason == KillReason.Vote and not self.voted:
428
439
  self.voted = True
429
440
  await self.game.send(
430
441
  UniMessage.at(self.user_id)
431
442
  .text(" 的身份是白痴\n")
432
- .text("免疫本次投票放逐,且接下来无法参与投票")
443
+ .text("免疫本次投票放逐,且接下来无法参与投票"),
433
444
  )
434
445
  return False
435
446
  return await super().kill(reason, *killers)
@@ -442,7 +453,22 @@ class 白痴(Player):
442
453
  return await super().vote(players)
443
454
 
444
455
 
445
- @register_role
446
- class 平民(Player):
447
- role: ClassVar[Role] = Role.Civilian
448
- role_group: ClassVar[RoleGroup] = RoleGroup.GoodGuy
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
+
472
+ @register_role(Role.Civilian, RoleGroup.GoodGuy)
473
+ class Civilian(Player):
474
+ pass
@@ -1,34 +1,42 @@
1
+ from __future__ import annotations
2
+
1
3
  import asyncio
2
4
  import asyncio.timeouts
5
+ from typing import TYPE_CHECKING
3
6
 
4
- from nonebot_plugin_alconna.uniseg import UniMessage
5
-
6
- from .constant import Role, RoleGroup
7
7
  from .player import Player
8
8
 
9
+ if TYPE_CHECKING:
10
+ from nonebot_plugin_alconna.uniseg import UniMessage
11
+
12
+ from .constant import Role, RoleGroup
13
+
9
14
 
10
15
  class PlayerSet(set[Player]):
11
16
  @property
12
17
  def size(self) -> int:
13
18
  return len(self)
14
19
 
15
- def alive(self) -> "PlayerSet":
20
+ def alive(self) -> PlayerSet:
16
21
  return PlayerSet(p for p in self if p.alive)
17
22
 
18
- def dead(self) -> "PlayerSet":
23
+ def dead(self) -> PlayerSet:
19
24
  return PlayerSet(p for p in self if not p.alive)
20
25
 
21
- def include(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
26
+ def killed(self) -> PlayerSet:
27
+ return PlayerSet(p for p in self if p.killed.is_set())
28
+
29
+ def include(self, *types: Player | Role | RoleGroup) -> PlayerSet:
22
30
  return PlayerSet(
23
31
  player
24
32
  for player in self
25
33
  if (player in types or player.role in types or player.role_group in types)
26
34
  )
27
35
 
28
- def select(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
36
+ def select(self, *types: Player | Role | RoleGroup) -> PlayerSet:
29
37
  return self.include(*types)
30
38
 
31
- def exclude(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
39
+ def exclude(self, *types: Player | Role | RoleGroup) -> PlayerSet:
32
40
  return PlayerSet(
33
41
  player
34
42
  for player in self
@@ -39,7 +47,7 @@ class PlayerSet(set[Player]):
39
47
  )
40
48
  )
41
49
 
42
- def player_selected(self) -> "PlayerSet":
50
+ def player_selected(self) -> PlayerSet:
43
51
  return PlayerSet(p.selected for p in self.alive() if p.selected is not None)
44
52
 
45
53
  def sorted(self) -> list[Player]:
@@ -1,15 +1,17 @@
1
1
  import asyncio
2
2
  import asyncio.timeouts
3
+ import re
3
4
  from collections import defaultdict
4
5
  from typing import Annotated, Any, ClassVar
5
6
 
6
7
  import nonebot_plugin_waiter as waiter
7
8
  from nonebot.adapters import Event
9
+ from nonebot.log import logger
8
10
  from nonebot.rule import to_me
9
11
  from nonebot_plugin_alconna import MsgTarget, UniMessage, UniMsg
10
12
  from nonebot_plugin_userinfo import EventUserInfo, UserInfo
11
13
 
12
- from .constant import player_preset
14
+ from .config import config
13
15
 
14
16
 
15
17
  def check_index(text: str, arrlen: int) -> int | None:
@@ -47,7 +49,7 @@ def user_in_game(user_id: str, group_id: str | None) -> bool:
47
49
  if group_id is not None and group_id not in running_games:
48
50
  return False
49
51
  games = running_games.values() if group_id is None else [running_games[group_id]]
50
- for game, *_ in games:
52
+ for game in games:
51
53
  return any(user_id == player.user_id for player in game.players)
52
54
  return False
53
55
 
@@ -59,7 +61,7 @@ async def rule_in_game(event: Event, target: MsgTarget) -> bool:
59
61
  return False
60
62
  if target.private:
61
63
  return user_in_game(target.id, None)
62
- elif target.id in running_games:
64
+ if target.id in running_games:
63
65
  return user_in_game(event.get_user_id(), target.id)
64
66
  return False
65
67
 
@@ -92,14 +94,18 @@ async def _prepare_game_receive(
92
94
  ) -> tuple[str, str, str]:
93
95
  return (
94
96
  event.get_user_id(),
95
- info.user_name if info is not None else event.get_user_id(),
97
+ (
98
+ (info.user_displayname or info.user_name)
99
+ if info is not None
100
+ else event.get_user_id()
101
+ ),
96
102
  msg.extract_plain_text().strip(),
97
103
  )
98
104
 
99
105
  async for user, name, text in wait(default=(None, "", "")):
100
106
  if user is None:
101
107
  continue
102
- await queue.put((user, name, text))
108
+ await queue.put((user, re.sub(r"[\u2066-\u2069]", "", name), text))
103
109
 
104
110
 
105
111
  async def _prepare_game_handle(
@@ -107,26 +113,30 @@ async def _prepare_game_handle(
107
113
  players: dict[str, str],
108
114
  admin_id: str,
109
115
  ) -> None:
116
+ log = logger.opt(colors=True)
117
+
110
118
  while True:
111
119
  user, name, text = await queue.get()
112
120
  msg = UniMessage.at(user)
121
+ colored = f"<y>{name}</y>(<c>{user}</c>)"
113
122
 
114
123
  match (text, user == admin_id):
115
124
  case ("开始游戏", True):
116
125
  player_num = len(players)
117
- if player_num < min(player_preset):
126
+ role_preset = config.get_role_preset()
127
+ if player_num < min(role_preset):
118
128
  await (
119
- msg.text(f"游戏至少需要 {min(player_preset)} 人, ")
129
+ msg.text(f"游戏至少需要 {min(role_preset)} 人, ")
120
130
  .text(f"当前已有 {player_num} 人")
121
131
  .send()
122
132
  )
123
- elif player_num > max(player_preset):
133
+ elif player_num > max(role_preset):
124
134
  await (
125
- msg.text(f"游戏最多需要 {max(player_preset)} 人, ")
135
+ msg.text(f"游戏最多需要 {max(role_preset)} 人, ")
126
136
  .text(f"当前已有 {player_num} 人")
127
137
  .send()
128
138
  )
129
- elif player_num not in player_preset:
139
+ elif player_num not in role_preset:
130
140
  await (
131
141
  msg.text(f"不存在总人数为 {player_num} 的预设, ")
132
142
  .text("无法开始游戏")
@@ -134,12 +144,14 @@ async def _prepare_game_handle(
134
144
  )
135
145
  else:
136
146
  await msg.text("游戏即将开始...").send()
147
+ log.info(f"游戏发起者 {colored} 开始游戏")
137
148
  return
138
149
 
139
150
  case ("开始游戏", False):
140
151
  await msg.text("只有游戏发起者可以开始游戏").send()
141
152
 
142
153
  case ("结束游戏", True):
154
+ log.info(f"游戏发起者 {colored} 结束游戏")
143
155
  await msg.text("已结束当前游戏").finish()
144
156
 
145
157
  case ("结束游戏", False):
@@ -151,6 +163,7 @@ async def _prepare_game_handle(
151
163
  case ("加入游戏", False):
152
164
  if user not in players:
153
165
  players[user] = name
166
+ log.info(f"玩家 {colored} 加入游戏")
154
167
  await msg.text("成功加入游戏").send()
155
168
  else:
156
169
  await msg.text("你已经加入游戏了").send()
@@ -161,32 +174,29 @@ async def _prepare_game_handle(
161
174
  case ("退出游戏", False):
162
175
  if user in players:
163
176
  del players[user]
177
+ log.info(f"玩家 {colored} 退出游戏")
164
178
  await msg.text("成功退出游戏").send()
165
179
  else:
166
180
  await msg.text("你还没有加入游戏").send()
167
181
 
168
182
  case ("当前玩家", _):
169
183
  msg.text("\n当前玩家:\n")
170
- for name in players.values():
171
- msg.text(f"\n{name}")
184
+ for idx, name in enumerate(players.values(), 1):
185
+ msg.text(f"\n{idx}. {name}")
172
186
  await msg.send()
173
187
 
174
188
 
175
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
+
176
195
  queue: asyncio.Queue[tuple[str, str, str]] = asyncio.Queue()
177
- task_receive = asyncio.create_task(
178
- _prepare_game_receive(
179
- queue,
180
- event,
181
- UniMessage.get_target(event).id,
182
- )
183
- )
196
+ task_receive = asyncio.create_task(_prepare_game_receive(queue, event, group_id))
184
197
 
185
198
  try:
186
- await _prepare_game_handle(
187
- queue,
188
- players,
189
- event.get_user_id(),
190
- )
199
+ await _prepare_game_handle(queue, players, event.get_user_id())
191
200
  finally:
192
201
  task_receive.cancel()
202
+ del starting_games[group_id]