nonebot-plugin-werewolf 1.1.1__py3-none-any.whl → 1.1.3__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.
Files changed (32) hide show
  1. nonebot_plugin_werewolf/__init__.py +8 -4
  2. nonebot_plugin_werewolf/_timeout.py +110 -0
  3. nonebot_plugin_werewolf/config.py +15 -18
  4. nonebot_plugin_werewolf/constant.py +18 -3
  5. nonebot_plugin_werewolf/exception.py +1 -1
  6. nonebot_plugin_werewolf/game.py +91 -110
  7. nonebot_plugin_werewolf/matchers/__init__.py +2 -0
  8. nonebot_plugin_werewolf/matchers/message_in_game.py +15 -0
  9. nonebot_plugin_werewolf/{ob11_ext.py → matchers/ob11_ext.py} +26 -20
  10. nonebot_plugin_werewolf/matchers/start_game.py +56 -0
  11. nonebot_plugin_werewolf/player_set.py +9 -7
  12. nonebot_plugin_werewolf/players/__init__.py +10 -0
  13. nonebot_plugin_werewolf/players/can_shoot.py +59 -0
  14. nonebot_plugin_werewolf/players/civilian.py +7 -0
  15. nonebot_plugin_werewolf/players/guard.py +37 -0
  16. nonebot_plugin_werewolf/players/hunter.py +8 -0
  17. nonebot_plugin_werewolf/players/idiot.py +44 -0
  18. nonebot_plugin_werewolf/players/joker.py +21 -0
  19. nonebot_plugin_werewolf/players/player.py +161 -0
  20. nonebot_plugin_werewolf/players/prophet.py +30 -0
  21. nonebot_plugin_werewolf/players/werewolf.py +67 -0
  22. nonebot_plugin_werewolf/players/witch.py +72 -0
  23. nonebot_plugin_werewolf/players/wolfking.py +14 -0
  24. nonebot_plugin_werewolf/utils.py +83 -65
  25. {nonebot_plugin_werewolf-1.1.1.dist-info → nonebot_plugin_werewolf-1.1.3.dist-info}/METADATA +20 -8
  26. nonebot_plugin_werewolf-1.1.3.dist-info/RECORD +29 -0
  27. {nonebot_plugin_werewolf-1.1.1.dist-info → nonebot_plugin_werewolf-1.1.3.dist-info}/WHEEL +1 -1
  28. nonebot_plugin_werewolf/matchers.py +0 -63
  29. nonebot_plugin_werewolf/player.py +0 -455
  30. nonebot_plugin_werewolf-1.1.1.dist-info/RECORD +0 -15
  31. {nonebot_plugin_werewolf-1.1.1.dist-info → nonebot_plugin_werewolf-1.1.3.dist-info}/LICENSE +0 -0
  32. {nonebot_plugin_werewolf-1.1.1.dist-info → nonebot_plugin_werewolf-1.1.3.dist-info}/top_level.txt +0 -0
@@ -1,34 +1,30 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import asyncio.timeouts
5
4
  import contextlib
6
5
  import secrets
7
- from typing import TYPE_CHECKING, NoReturn
6
+ from typing import TYPE_CHECKING, ClassVar, NoReturn
8
7
 
9
8
  from nonebot.log import logger
10
9
  from nonebot_plugin_alconna import At, Target, UniMessage
11
10
 
11
+ from ._timeout import timeout
12
12
  from .config import config
13
13
  from .constant import GameState, GameStatus, KillReason, Role, RoleGroup, role_name_conv
14
- from .exception import GameFinishedError
15
- from .player import Player
14
+ from .exception import GameFinished
16
15
  from .player_set import PlayerSet
16
+ from .players import Player
17
17
  from .utils import InputStore
18
18
 
19
19
  if TYPE_CHECKING:
20
20
  from nonebot.adapters import Bot
21
21
  from nonebot_plugin_alconna.uniseg.message import Receipt
22
22
 
23
- starting_games: dict[str, dict[str, str]] = {}
24
- running_games: dict[str, Game] = {}
25
-
26
23
 
27
24
  def init_players(bot: Bot, game: Game, players: dict[str, str]) -> PlayerSet:
28
25
  logger.opt(colors=True).debug(f"初始化 <c>{game.group.id}</c> 的玩家职业")
29
26
  role_preset = config.get_role_preset()
30
- preset = role_preset.get(len(players))
31
- if preset is None:
27
+ if (preset := role_preset.get(len(players))) is None:
32
28
  raise ValueError(
33
29
  f"玩家人数不符: "
34
30
  f"应为 {', '.join(map(str, role_preset))} 人, 传入{len(players)}人"
@@ -44,34 +40,22 @@ def init_players(bot: Bot, game: Game, players: dict[str, str]) -> PlayerSet:
44
40
  roles.remove(Role.Civilian)
45
41
  roles.append(Role.Joker)
46
42
 
47
- shuffled: list[Role] = []
48
- while roles:
49
- idx = secrets.randbelow(len(roles))
50
- shuffled.append(roles.pop(idx))
51
-
52
- logger.debug(f"职业分配: {shuffled}")
53
-
54
- async def selector(target_: Target, b: Bot) -> bool:
55
- return target_.self_id == bot.self_id and b is bot
56
-
57
- return PlayerSet(
58
- Player.new(
59
- role,
60
- bot,
61
- game,
62
- Target(
63
- user_id,
64
- private=True,
65
- self_id=bot.self_id,
66
- selector=selector,
67
- ),
68
- players[user_id],
69
- )
70
- for user_id, role in zip(players, shuffled, strict=True)
43
+ def _select_role() -> Role:
44
+ return roles.pop(secrets.randbelow(len(roles)))
45
+
46
+ player_set = PlayerSet(
47
+ Player.new(_select_role(), bot, game, user_id, players[user_id])
48
+ for user_id in players
71
49
  )
50
+ logger.debug(f"职业分配完成: {player_set}")
51
+
52
+ return player_set
72
53
 
73
54
 
74
55
  class Game:
56
+ starting_games: ClassVar[dict[Target, dict[str, str]]] = {}
57
+ running_games: ClassVar[set[Game]] = set()
58
+
75
59
  bot: Bot
76
60
  group: Target
77
61
  players: PlayerSet
@@ -112,92 +96,82 @@ class Game:
112
96
 
113
97
  # 狼人数量大于其他职业数量
114
98
  if w.size >= p.size:
115
- raise GameFinishedError(GameStatus.Werewolf)
99
+ raise GameFinished(GameStatus.Werewolf)
116
100
  # 屠边-村民/中立全灭
117
101
  if not p.select(Role.Civilian, RoleGroup.Others).size:
118
- raise GameFinishedError(GameStatus.Werewolf)
102
+ raise GameFinished(GameStatus.Werewolf)
119
103
  # 屠边-神职全灭
120
104
  if not p.exclude(Role.Civilian).size:
121
- raise GameFinishedError(GameStatus.Werewolf)
105
+ raise GameFinished(GameStatus.Werewolf)
122
106
  # 狼人全灭
123
107
  if not w.size:
124
- raise GameFinishedError(GameStatus.GoodGuy)
108
+ raise GameFinished(GameStatus.GoodGuy)
125
109
 
126
110
  def show_killed_players(self) -> str:
127
- msg = ""
111
+ result: list[str] = []
128
112
 
129
113
  for player in self.killed_players:
130
114
  if player.kill_info is None:
131
115
  continue
132
116
 
133
- msg += f"{player.name} 被 " + ", ".join(
117
+ line = f"{player.name} 被 " + ", ".join(
134
118
  p.name for p in player.kill_info.killers
135
119
  )
136
120
  match player.kill_info.reason:
137
121
  case KillReason.Werewolf:
138
- msg += " 刀了"
122
+ line = f"🔪 {line} 刀了"
139
123
  case KillReason.Poison:
140
- msg += " 毒死"
124
+ line = f"🧪 {line} 毒死"
141
125
  case KillReason.Shoot:
142
- msg += " 射杀"
126
+ line = f"🔫 {line} 射杀"
143
127
  case KillReason.Vote:
144
- msg += " 票出"
145
- msg += "\n\n"
128
+ line = f"🗳️ {line} 票出"
129
+ result.append(line)
146
130
 
147
- return msg.strip()
131
+ return "\n\n".join(result)
148
132
 
149
133
  async def notify_player_role(self) -> None:
150
134
  preset = config.get_role_preset()[len(self.players)]
151
135
  await asyncio.gather(
152
136
  self.send(
153
137
  self.at_all()
154
- .text("\n\n正在分配职业,请注意查看私聊消息\n")
138
+ .text("\n\n📝正在分配职业,请注意查看私聊消息\n")
155
139
  .text(f"当前玩家数: {len(self.players)}\n")
156
140
  .text(f"职业分配: 狼人x{preset[0]}, 神职x{preset[1]}, 平民x{preset[2]}")
157
141
  ),
158
142
  *[p.notify_role() for p in self.players],
159
143
  )
160
144
 
161
- async def wait_stop(
162
- self,
163
- players: Player | PlayerSet,
164
- timeout_secs: float,
165
- ) -> None:
166
- if isinstance(players, Player):
167
- players = PlayerSet([players])
168
-
145
+ async def wait_stop(self, *players: Player, timeout_secs: float) -> None:
169
146
  async def wait(p: Player) -> None:
170
147
  while True:
171
148
  msg = await InputStore.fetch(p.user_id, self.group.id)
172
- if msg.extract_plain_text() == "/stop":
149
+ if msg.extract_plain_text().strip() == "/stop":
173
150
  break
174
151
 
175
152
  with contextlib.suppress(TimeoutError):
176
- async with asyncio.timeouts.timeout(timeout_secs):
153
+ async with timeout(timeout_secs):
177
154
  await asyncio.gather(*[wait(p) for p in players])
178
155
 
179
156
  async def interact(
180
157
  self,
181
- type_: Player | Role | RoleGroup,
158
+ player_type: Player | Role | RoleGroup,
182
159
  timeout_secs: float,
183
160
  ) -> None:
184
- players = self.players.alive().select(type_)
185
- text = (
186
- type_.role_name # Player
187
- if isinstance(type_, Player)
188
- else (
189
- role_name_conv[type_] # Role
190
- if isinstance(type_, Role)
191
- else f"{role_name_conv[type_]}阵营" # RoleGroup
192
- )
193
- )
194
-
195
- await players.broadcast(f"{text}交互开始,限时 {timeout_secs/60:.2f} 分钟")
161
+ players = self.players.alive().select(player_type)
162
+ if isinstance(player_type, Player):
163
+ text = player_type.role_name
164
+ elif isinstance(player_type, Role):
165
+ text = role_name_conv[player_type]
166
+ else: # RoleGroup
167
+ text = f"{role_name_conv[player_type]}阵营"
168
+
169
+ await players.broadcast(f"✏️{text}交互开始,限时 {timeout_secs/60:.2f} 分钟")
196
170
  try:
197
171
  await players.interact(timeout_secs)
198
172
  except TimeoutError:
199
- logger.opt(colors=True).debug(f"{text}交互超时 (<y>{timeout_secs}</y>s)")
200
- await players.broadcast(f"{text}交互时间结束")
173
+ logger.opt(colors=True).debug(f"⚠️{text}交互超时 (<y>{timeout_secs}</y>s)")
174
+ await players.broadcast(f"ℹ️{text}交互时间结束")
201
175
 
202
176
  async def select_killed(self) -> None:
203
177
  players = self.players.alive()
@@ -207,9 +181,9 @@ class Game:
207
181
  await self.interact(RoleGroup.Werewolf, 120)
208
182
  if (s := w.player_selected()).size == 1:
209
183
  self.state.killed = s.pop()
210
- await w.broadcast(f"今晚选择的目标为: {self.state.killed.name}")
184
+ await w.broadcast(f"🔪今晚选择的目标为: {self.state.killed.name}")
211
185
  else:
212
- await w.broadcast("狼人阵营意见未统一,此晚空刀")
186
+ await w.broadcast("⚠️狼人阵营意见未统一,此晚空刀")
213
187
 
214
188
  # 如果女巫存活,正常交互,限时1分钟
215
189
  if players.include(Role.Witch):
@@ -226,12 +200,12 @@ class Game:
226
200
 
227
201
  await asyncio.gather(
228
202
  players.broadcast(
229
- "你已加入死者频道,请勿在群内继续发言\n"
203
+ "ℹ️你已加入死者频道,请勿在群内继续发言\n"
230
204
  "私聊发送消息将转发至其他已死亡玩家"
231
205
  ),
232
206
  self.players.dead()
233
207
  .exclude(*players)
234
- .broadcast(f"玩家 {', '.join(p.name for p in players)} 加入了死者频道"),
208
+ .broadcast(f"ℹ️玩家 {', '.join(p.name for p in players)} 加入了死者频道"),
235
209
  )
236
210
 
237
211
  async def post_kill(self, players: Player | PlayerSet) -> None:
@@ -253,7 +227,7 @@ class Game:
253
227
  .text(f" 被{shooter.role_name}射杀, 请发表遗言\n")
254
228
  .text("限时1分钟, 发送 “/stop” 结束发言")
255
229
  )
256
- await self.wait_stop(shoot, 60)
230
+ await self.wait_stop(shoot, timeout_secs=60)
257
231
  self.state.shoot = (None, None)
258
232
  await self.post_kill(shoot)
259
233
 
@@ -271,34 +245,35 @@ class Game:
271
245
  logger.debug(f"投票结果: {vote_result}")
272
246
 
273
247
  # 投票结果公示
274
- msg = UniMessage.text("投票结果:\n")
248
+ msg = UniMessage.text("📊投票结果:\n")
275
249
  for p, v in sorted(vote_result.items(), key=lambda x: len(x[1]), reverse=True):
276
250
  if p is not None:
277
251
  msg.at(p.user_id).text(f": {len(v)} 票\n")
278
252
  vote_reversed[len(v)] = [*vote_reversed.get(len(v), []), p]
279
253
  if v := (len(players) - total_votes):
280
- msg.text(f"弃票: {v} 票\n")
281
- await self.send(msg)
254
+ msg.text(f"弃票: {v} 票\n\n")
282
255
 
283
256
  # 全员弃票 # 不是哥们?
284
257
  if total_votes == 0:
285
- await self.send("没有人被票出")
258
+ await self.send(msg.text("🔨没有人被票出"))
286
259
  return
287
260
 
288
261
  # 弃票大于最高票
289
262
  if (len(players) - total_votes) >= max(vote_reversed.keys()):
290
- await self.send("弃票数大于最高票数, 没有人被票出")
263
+ await self.send(msg.text("🔨弃票数大于最高票数, 没有人被票出"))
291
264
  return
292
265
 
293
266
  # 平票
294
267
  if len(vs := vote_reversed[max(vote_reversed.keys())]) != 1:
295
268
  await self.send(
296
- UniMessage.text("玩家 ")
269
+ msg.text("🔨玩家 ")
297
270
  .text(", ".join(p.name for p in vs))
298
271
  .text(" 平票, 没有人被票出")
299
272
  )
300
273
  return
301
274
 
275
+ await self.send(msg)
276
+
302
277
  # 仅有一名玩家票数最高
303
278
  voted = vs.pop()
304
279
  if not await voted.kill(KillReason.Vote, *vote_result[voted]):
@@ -307,15 +282,15 @@ class Game:
307
282
 
308
283
  # 遗言
309
284
  await self.send(
310
- UniMessage.text("玩家 ")
285
+ UniMessage.text("🔨玩家 ")
311
286
  .at(voted.user_id)
312
287
  .text(" 被投票放逐, 请发表遗言\n")
313
288
  .text("限时1分钟, 发送 “/stop” 结束发言")
314
289
  )
315
- await self.wait_stop(voted, 60)
290
+ await self.wait_stop(voted, timeout_secs=60)
316
291
  await self.post_kill(voted)
317
292
 
318
- async def run_dead_channel(self) -> None:
293
+ async def run_dead_channel(self) -> NoReturn:
319
294
  loop = asyncio.get_event_loop()
320
295
  queue: asyncio.Queue[tuple[Player, UniMessage]] = asyncio.Queue()
321
296
 
@@ -342,7 +317,7 @@ class Game:
342
317
  await queue.put((player, msg))
343
318
  loop.call_later(60, decrease)
344
319
  else:
345
- await player.send("发言频率超过限制, 该消息被屏蔽")
320
+ await player.send("❌发言频率超过限制, 该消息被屏蔽")
346
321
 
347
322
  await asyncio.gather(send(), *[recv(p) for p in self.players])
348
323
 
@@ -357,22 +332,22 @@ class Game:
357
332
  # 重置游戏状态,进入下一夜
358
333
  self.state = GameState(day_count)
359
334
  players = self.players.alive()
360
- await self.send("天黑请闭眼...")
335
+ await self.send("🌙天黑请闭眼...")
361
336
 
362
337
  # 狼人、预言家、守卫 同时交互,女巫在狼人后交互
363
338
  await asyncio.gather(
364
339
  self.select_killed(),
365
340
  self.interact(Role.Prophet, 60),
366
341
  self.interact(Role.Guard, 60),
367
- players.select(Role.Witch).broadcast("请等待狼人决定目标..."),
342
+ players.select(Role.Witch).broadcast("ℹ️请等待狼人决定目标..."),
368
343
  players.exclude(
369
344
  RoleGroup.Werewolf, Role.Prophet, Role.Witch, Role.Guard
370
- ).broadcast("请等待其他玩家结束交互..."),
345
+ ).broadcast("ℹ️请等待其他玩家结束交互..."),
371
346
  )
372
347
 
373
348
  # 狼人击杀目标
374
349
  if (
375
- (killed := self.state.killed) # 狼人未空刀
350
+ (killed := self.state.killed) is not None # 狼人未空刀
376
351
  and killed not in self.state.protected # 守卫保护
377
352
  and killed not in self.state.antidote # 女巫使用解药
378
353
  ):
@@ -383,46 +358,52 @@ class Game:
383
358
  )
384
359
 
385
360
  # 女巫操作目标
386
- for witch, potioned in self.state.poison:
387
- if potioned not in self.state.protected: # 守卫未保护
361
+ for witch in self.state.poison:
362
+ if witch.selected is None:
363
+ continue
364
+ if witch.selected not in self.state.protected: # 守卫未保护
388
365
  # 女巫毒杀玩家
389
- await potioned.kill(KillReason.Poison, witch)
366
+ await witch.selected.kill(KillReason.Poison, witch)
390
367
 
391
368
  day_count += 1
392
- msg = UniMessage.text(f"『第{day_count}天』天亮了...\n")
369
+ msg = UniMessage.text(f"『第{day_count}天』☀️天亮了...\n")
393
370
  # 没有玩家死亡,平安夜
394
371
  if not (dead := players.dead()):
395
372
  await self.send(msg.text("昨晚是平安夜"))
396
373
  # 有玩家死亡,公布死者名单
397
374
  else:
398
- msg.text("昨晚的死者是:")
399
- for p in dead.sorted():
375
+ msg.text("☠️昨晚的死者是:")
376
+ for p in dead.sorted:
400
377
  msg.text("\n").at(p.user_id)
401
378
  await self.send(msg)
402
379
 
403
380
  # 第一晚被狼人杀死的玩家发表遗言
404
381
  if day_count == 1 and killed is not None and not killed.alive:
405
382
  await self.send(
406
- UniMessage.text("当前为第一天\n请被狼人杀死的 ")
383
+ UniMessage.text("⚙️当前为第一天\n请被狼人杀死的 ")
407
384
  .at(killed.user_id)
408
385
  .text(" 发表遗言\n")
409
386
  .text("限时1分钟, 发送 “/stop” 结束发言")
410
387
  )
411
- await self.wait_stop(killed, 60)
388
+ await self.wait_stop(killed, timeout_secs=60)
412
389
  await self.post_kill(dead)
413
390
 
414
391
  # 判断游戏状态
415
392
  self.check_game_status()
416
393
 
417
394
  # 公示存活玩家
418
- await self.send(f"当前存活玩家: \n\n{self.players.alive().show()}")
395
+ await self.send(f"📝当前存活玩家: \n\n{self.players.alive().show()}")
419
396
 
420
397
  # 开始自由讨论
421
- await self.send("接下来开始自由讨论\n限时2分钟, 全员发送 “/stop” 结束发言")
422
- await self.wait_stop(self.players.alive(), 120)
398
+ await self.send(
399
+ "💬接下来开始自由讨论\n限时2分钟, 全员发送 “/stop” 结束发言"
400
+ )
401
+ await self.wait_stop(*self.players.alive(), timeout_secs=120)
423
402
 
424
403
  # 开始投票
425
- await self.send("讨论结束, 进入投票环节,限时1分钟\n请在私聊中进行投票交互")
404
+ await self.send(
405
+ "🗳️讨论结束, 进入投票环节,限时1分钟\n请在私聊中进行投票交互"
406
+ )
426
407
  await self.run_vote()
427
408
 
428
409
  # 判断游戏状态
@@ -456,22 +437,22 @@ class Game:
456
437
  game_task.result()
457
438
  except asyncio.CancelledError:
458
439
  logger.warning(f"{self.group.id} 的狼人杀游戏进程被取消")
459
- except GameFinishedError as result:
440
+ except GameFinished as result:
460
441
  await self.handle_game_finish(result.status)
461
442
  logger.info(f"{self.group.id} 的狼人杀游戏进程正常退出")
462
443
  except Exception as err:
463
444
  msg = f"{self.group.id} 的狼人杀游戏进程出现未知错误: {err!r}"
464
445
  logger.opt(exception=err).error(msg)
465
- await self.send(msg)
446
+ await self.send(f"❌狼人杀游戏进程出现未知错误: {err!r}")
466
447
  finally:
467
448
  dead_channel.cancel()
468
- running_games.pop(self.group.id, None)
449
+ self.running_games.discard(self)
469
450
 
470
451
  def daemon_callback(task: asyncio.Task[None]) -> None:
471
452
  if err := task.exception():
472
- logger.opt(exception=err).error(
473
- f"{self.group.id} 的狼人杀守护进程出现错误: {err!r}"
474
- )
453
+ msg = f"{self.group.id} 的狼人杀守护进程出现错误: {err!r}"
454
+ logger.opt(exception=err).error(msg)
475
455
 
476
- running_games[self.group.id] = self
477
- asyncio.create_task(daemon()).add_done_callback(daemon_callback)
456
+ self.running_games.add(self)
457
+ daemon_task = asyncio.create_task(daemon())
458
+ daemon_task.add_done_callback(daemon_callback)
@@ -0,0 +1,2 @@
1
+ from . import message_in_game as message_in_game
2
+ from . import start_game as start_game
@@ -0,0 +1,15 @@
1
+ from nonebot import on_message
2
+ from nonebot.adapters import Event
3
+ from nonebot_plugin_alconna import MsgTarget, UniMsg
4
+
5
+ from ..utils import InputStore, rule_in_game
6
+
7
+ message_in_game = on_message(rule=rule_in_game)
8
+
9
+
10
+ @message_in_game.handle()
11
+ async def handle_input(event: Event, target: MsgTarget, msg: UniMsg) -> None:
12
+ if target.private:
13
+ InputStore.put(msg, target.id)
14
+ else:
15
+ InputStore.put(msg, event.get_user_id(), target.id)
@@ -1,12 +1,12 @@
1
1
  import contextlib
2
2
 
3
- from nonebot import on_type
3
+ from nonebot import on_notice
4
4
  from nonebot.internal.matcher import current_bot
5
- from nonebot_plugin_alconna import UniMessage
5
+ from nonebot_plugin_alconna import MsgTarget, UniMessage
6
6
 
7
- from .config import config
8
- from .game import starting_games
9
- from .utils import InputStore, user_in_game
7
+ from ..config import config
8
+ from ..game import Game
9
+ from ..utils import InputStore, user_in_game
10
10
 
11
11
 
12
12
  def ob11_ext_enabled() -> bool:
@@ -14,11 +14,11 @@ def ob11_ext_enabled() -> bool:
14
14
 
15
15
 
16
16
  with contextlib.suppress(ImportError):
17
- from nonebot.adapters.onebot.v11 import Bot, MessageSegment
17
+ from nonebot.adapters.onebot.v11 import Bot
18
18
  from nonebot.adapters.onebot.v11.event import PokeNotifyEvent
19
19
 
20
20
  # 游戏内戳一戳等效 "/stop"
21
- async def _rule_poke_1(event: PokeNotifyEvent) -> bool:
21
+ async def _rule_poke_stop(bot: Bot, event: PokeNotifyEvent) -> bool:
22
22
  if not config.enable_poke:
23
23
  return False
24
24
 
@@ -27,19 +27,21 @@ with contextlib.suppress(ImportError):
27
27
  return (
28
28
  config.enable_poke
29
29
  and (event.target_id == event.self_id)
30
- and user_in_game(user_id, group_id)
30
+ and user_in_game(bot.self_id, user_id, group_id)
31
31
  )
32
32
 
33
- @on_type(PokeNotifyEvent, rule=_rule_poke_1).handle()
34
- async def handle_poke_1(event: PokeNotifyEvent) -> None:
33
+ @on_notice(rule=_rule_poke_stop).handle()
34
+ async def handle_poke_stop(event: PokeNotifyEvent) -> None:
35
35
  InputStore.put(
36
+ msg=UniMessage.text("/stop"),
36
37
  user_id=str(event.user_id),
37
38
  group_id=str(event.group_id) if event.group_id is not None else None,
38
- msg=UniMessage.text("/stop"),
39
39
  )
40
40
 
41
41
  # 准备阶段戳一戳等效加入游戏
42
- async def _rule_poke_2(event: PokeNotifyEvent) -> bool:
42
+ async def _rule_poke_join(
43
+ bot: Bot, event: PokeNotifyEvent, target: MsgTarget
44
+ ) -> bool:
43
45
  if not config.enable_poke or event.group_id is None:
44
46
  return False
45
47
 
@@ -47,15 +49,19 @@ with contextlib.suppress(ImportError):
47
49
  group_id = str(event.group_id)
48
50
  return (
49
51
  (event.target_id == event.self_id)
50
- and not user_in_game(user_id, group_id)
51
- and group_id in starting_games
52
+ and not user_in_game(bot.self_id, user_id, group_id)
53
+ and any(target.verify(group) for group in Game.starting_games)
52
54
  )
53
55
 
54
- @on_type(PokeNotifyEvent, rule=_rule_poke_2).handle()
55
- async def handle_poke_2(bot: Bot, event: PokeNotifyEvent) -> None:
56
- user_id = str(event.user_id)
57
- group_id = str(event.group_id)
58
- players = starting_games[group_id]
56
+ @on_notice(rule=_rule_poke_join).handle()
57
+ async def handle_poke_join(
58
+ bot: Bot,
59
+ event: PokeNotifyEvent,
60
+ target: MsgTarget,
61
+ ) -> None:
62
+ user_id = event.get_user_id()
63
+ group_id = target.id
64
+ players = next(p for g, p in Game.starting_games.items() if target.verify(g))
59
65
 
60
66
  if user_id not in players:
61
67
  res: dict[str, str] = await bot.get_group_member_info(
@@ -63,7 +69,7 @@ with contextlib.suppress(ImportError):
63
69
  user_id=int(user_id),
64
70
  )
65
71
  players[user_id] = res.get("card") or res.get("nickname") or user_id
66
- await bot.send(event, MessageSegment.at(user_id) + "成功加入游戏")
72
+ await UniMessage.at(user_id).text("\n✅成功加入游戏").send(target, bot)
67
73
 
68
74
  def ob11_ext_enabled() -> bool:
69
75
  if not config.enable_poke:
@@ -0,0 +1,56 @@
1
+ from nonebot import on_command
2
+ from nonebot.adapters import Bot, Event
3
+ from nonebot.rule import to_me
4
+ from nonebot_plugin_alconna import MsgTarget, UniMessage
5
+ from nonebot_plugin_uninfo import Uninfo
6
+
7
+ from .._timeout import timeout
8
+ from ..game import Game
9
+ from ..utils import prepare_game, rule_not_in_game
10
+ from .ob11_ext import ob11_ext_enabled
11
+
12
+ start_game = on_command(
13
+ "werewolf",
14
+ rule=to_me() & rule_not_in_game,
15
+ aliases={"狼人杀"},
16
+ )
17
+
18
+
19
+ @start_game.handle()
20
+ async def handle_start_warning(target: MsgTarget) -> None:
21
+ if target.private:
22
+ await UniMessage("⚠️请在群组中创建新游戏").finish(reply_to=True)
23
+
24
+
25
+ @start_game.handle()
26
+ async def handle_start(
27
+ bot: Bot,
28
+ event: Event,
29
+ target: MsgTarget,
30
+ session: Uninfo,
31
+ ) -> None:
32
+ admin_id = event.get_user_id()
33
+ msg = (
34
+ UniMessage.at(admin_id)
35
+ .text("\n🎉成功创建游戏\n\n")
36
+ .text(" 玩家请 @我 发送 “加入游戏”、“退出游戏”\n")
37
+ .text(" 玩家 @我 发送 “当前玩家” 可查看玩家列表\n")
38
+ .text(" 游戏发起者 @我 发送 “结束游戏” 可结束当前游戏\n")
39
+ .text(" 玩家均加入后,游戏发起者请 @我 发送 “开始游戏”\n")
40
+ )
41
+ if ob11_ext_enabled():
42
+ msg.text("\n可使用戳一戳代替游戏交互中的 “/stop” 命令\n")
43
+ await msg.text("\n游戏准备阶段限时5分钟,超时将自动结束").send()
44
+
45
+ admin_name = session.user.nick or session.user.name or admin_id
46
+ if session.member:
47
+ admin_name = session.member.nick or admin_name
48
+ players = {admin_id: admin_name}
49
+
50
+ try:
51
+ async with timeout(5 * 60):
52
+ await prepare_game(event, players)
53
+ except TimeoutError:
54
+ await UniMessage.text("⚠️游戏准备超时,已自动结束").finish()
55
+
56
+ Game(bot, target, players).start()
@@ -1,10 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import asyncio.timeouts
4
+ import functools
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- from .player import Player
7
+ from ._timeout import timeout
8
+ from .players import Player
8
9
 
9
10
  if TYPE_CHECKING:
10
11
  from nonebot_plugin_alconna.uniseg import UniMessage
@@ -50,20 +51,21 @@ class PlayerSet(set[Player]):
50
51
  def player_selected(self) -> PlayerSet:
51
52
  return PlayerSet(p.selected for p in self.alive() if p.selected is not None)
52
53
 
54
+ @functools.cached_property
53
55
  def sorted(self) -> list[Player]:
54
56
  return sorted(self, key=lambda p: p.user_id)
55
57
 
56
58
  async def interact(self, timeout_secs: float = 60) -> None:
57
- async with asyncio.timeouts.timeout(timeout_secs):
59
+ async with timeout(timeout_secs):
58
60
  await asyncio.gather(*[p.interact() for p in self.alive()])
59
61
 
60
62
  async def vote(self, timeout_secs: float = 60) -> dict[Player, list[Player]]:
61
63
  async def vote(player: Player) -> tuple[Player, Player] | None:
62
64
  try:
63
- async with asyncio.timeouts.timeout(timeout_secs):
65
+ async with timeout(timeout_secs):
64
66
  return await player.vote(self)
65
67
  except TimeoutError:
66
- await player.send("投票超时,将视为弃票")
68
+ await player.send("⚠️投票超时,将视为弃票")
67
69
  return None
68
70
 
69
71
  result: dict[Player, list[Player]] = {}
@@ -77,7 +79,7 @@ class PlayerSet(set[Player]):
77
79
  await asyncio.gather(*[p.send(message) for p in self])
78
80
 
79
81
  def show(self) -> str:
80
- return "\n".join(f"{i}. {p.name}" for i, p in enumerate(self.sorted(), 1))
82
+ return "\n".join(f"{i}. {p.name}" for i, p in enumerate(self.sorted, 1))
81
83
 
82
84
  def __getitem__(self, __index: int) -> Player:
83
- return self.sorted()[__index]
85
+ return self.sorted[__index]
@@ -0,0 +1,10 @@
1
+ from .civilian import Civilian as Civilian
2
+ from .guard import Guard as Guard
3
+ from .hunter import Hunter as Hunter
4
+ from .idiot import Idiot as Idiot
5
+ from .joker import Joker as Joker
6
+ from .player import Player as Player
7
+ from .prophet import Prophet as Prophet
8
+ from .werewolf import Werewolf as Werewolf
9
+ from .witch import Witch as Witch
10
+ from .wolfking import WolfKing as WolfKing