nonebot-plugin-werewolf 1.0.1__py3-none-any.whl → 1.0.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.
@@ -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.1"
11
+ __version__ = "1.0.3"
12
12
  __plugin_meta__ = PluginMetadata(
13
13
  name="狼人杀",
14
14
  description="适用于 Nonebot2 的狼人杀插件",
@@ -44,6 +44,7 @@ class GameStatus(Enum):
44
44
 
45
45
  @dataclass
46
46
  class GameState:
47
+ day: int
47
48
  killed: "Player | None" = None
48
49
  shoot: tuple["Player", "Player"] | tuple[None, None] = (None, None)
49
50
  protected: "Player | None" = None
@@ -1,13 +1,16 @@
1
1
  import asyncio
2
2
  import asyncio.timeouts
3
3
  import random
4
- from collections.abc import Callable
5
4
 
6
5
  from nonebot.adapters import Bot
7
6
  from nonebot_plugin_alconna import Target, UniMessage
8
7
 
9
8
  from .constant import GameState, GameStatus, KillReason, Role, RoleGroup, player_preset
10
- from .player import Player, PlayerSet
9
+ from .player import Player
10
+ from .player_set import PlayerSet
11
+
12
+ starting_games: dict[str, dict[str, str]] = {}
13
+ running_games: dict[str, tuple["Game", asyncio.Task[None]]] = {}
11
14
 
12
15
 
13
16
  def init_players(bot: Bot, game: "Game", players: dict[str, str]) -> PlayerSet:
@@ -15,7 +18,7 @@ def init_players(bot: Bot, game: "Game", players: dict[str, str]) -> PlayerSet:
15
18
  if preset is None:
16
19
  raise ValueError(
17
20
  f"玩家人数不符: "
18
- f"应为{min(player_preset)}-{max(player_preset)}人, 传入{len(players)}人"
21
+ f"应为 {', '.join(map(str, player_preset))} 人, 传入{len(players)}人"
19
22
  )
20
23
 
21
24
  roles: list[Role] = []
@@ -50,20 +53,18 @@ class Game:
50
53
  group: Target
51
54
  players: PlayerSet
52
55
  state: GameState
53
- _on_exit: Callable[[], None]
56
+ killed_players: list[Player]
54
57
 
55
58
  def __init__(
56
59
  self,
57
60
  bot: Bot,
58
61
  group: Target,
59
62
  players: dict[str, str],
60
- on_exit: Callable[[], None],
61
63
  ) -> None:
62
64
  self.bot = bot
63
65
  self.group = group
64
66
  self.players = init_players(bot, self, players)
65
- self.state = GameState()
66
- self._on_exit = on_exit
67
+ self.state = GameState(0)
67
68
 
68
69
  async def send(self, message: str | UniMessage):
69
70
  if isinstance(message, str):
@@ -78,27 +79,49 @@ class Game:
78
79
 
79
80
  def check_game_status(self) -> GameStatus:
80
81
  players = self.players.alive()
81
-
82
82
  w = players.select(RoleGroup.狼人)
83
- if not w.size:
84
- return GameStatus.Good
85
-
86
83
  p = players.exclude(RoleGroup.狼人)
84
+
87
85
  if w.size >= p.size:
88
86
  return GameStatus.Bad
89
- if not players.select(Role.平民):
87
+ if not p.select(Role.平民):
90
88
  return GameStatus.Bad
91
- if not players.select(RoleGroup.好人).exclude(Role.平民):
89
+ if not p.exclude(Role.平民):
92
90
  return GameStatus.Bad
91
+ if not w.size:
92
+ return GameStatus.Good
93
93
 
94
94
  return GameStatus.Unset
95
95
 
96
+ def show_killed_players(self) -> str:
97
+ msg = ""
98
+
99
+ for player in self.killed_players:
100
+ if player.kill_info is None:
101
+ continue
102
+
103
+ msg += f"{player.name} 被 " + ", ".join(
104
+ p.name for p in player.kill_info.killers
105
+ )
106
+ match player.kill_info.reason:
107
+ case KillReason.Kill:
108
+ msg += " 刀了"
109
+ case KillReason.Poison:
110
+ msg += " 毒死"
111
+ case KillReason.Shoot:
112
+ msg += " 射杀"
113
+ case KillReason.Vote:
114
+ msg += " 投票放逐"
115
+ msg += "\n\n"
116
+
117
+ return msg.strip()
118
+
96
119
  async def notify_player_role(self) -> None:
97
120
  preset = player_preset[len(self.players)]
98
121
  await asyncio.gather(
99
122
  self.send(
100
123
  self.at_all()
101
- .text("\n正在分配职业,请注意查看私聊消息\n")
124
+ .text("\n\n正在分配职业,请注意查看私聊消息\n")
102
125
  .text(f"当前玩家数: {len(self.players)}\n")
103
126
  .text(f"职业分配: 狼人x{preset[0]}, 神职x{preset[1]}, 平民x{preset[2]}")
104
127
  ),
@@ -125,8 +148,10 @@ class Game:
125
148
  type_.role.name # Player
126
149
  if isinstance(type_, Player)
127
150
  else (
128
- type_.name if isinstance(type_, Role) else f"{type_.name}阵营" # Role
129
- ) # RoleGroup
151
+ type_.name # Role
152
+ if isinstance(type_, Role)
153
+ else f"{type_.name}阵营" # RoleGroup
154
+ )
130
155
  )
131
156
 
132
157
  await players.broadcast(f"{text}交互开始,限时 {timeout_secs/60:.2f} 分钟")
@@ -176,9 +201,10 @@ class Game:
176
201
  if not players:
177
202
  return
178
203
 
179
- for player in players:
204
+ for player in players.dead():
180
205
  await player.post_kill()
181
206
  await self.handle_new_dead(player)
207
+ self.killed_players.append(player)
182
208
 
183
209
  (shooter, shoot) = self.state.shoot
184
210
  if shooter is not None and shoot is not None:
@@ -189,27 +215,32 @@ class Game:
189
215
  .text("限时1分钟, 发送 “/stop” 结束发言")
190
216
  )
191
217
  await self.wait_stop(shoot, 60)
218
+ await self.post_kill(shoot)
192
219
  self.state.shoot = (None, None)
193
220
 
194
221
  async def run_vote(self) -> None:
195
222
  # 统计投票结果
196
- vote_result: dict[Player | None, int] = {}
223
+ players = self.players.alive()
224
+
225
+ # 被票玩家: [投票玩家]
226
+ vote_result: dict[Player, list[Player]] = await players.vote(60)
227
+ # 票数: [被票玩家]
197
228
  vote_reversed: dict[int, list[Player]] = {}
198
- for p in await self.players.alive().vote(60):
199
- vote_result[p] = vote_result.get(p, 0) + 1
229
+ # 收集到的总票数
230
+ total_votes = sum(map(len, vote_result.values()))
200
231
 
201
232
  # 投票结果公示
202
233
  msg = UniMessage.text("投票结果:\n")
203
234
  for p, v in sorted(vote_result.items(), key=lambda x: x[1], reverse=True):
204
235
  if p is not None:
205
236
  msg.at(p.user_id).text(f": {v} 票\n")
206
- vote_reversed[v] = [*vote_reversed.get(v, []), p]
207
- if v := vote_result.get(None, 0):
237
+ vote_reversed[len(v)] = [*vote_reversed.get(len(v), []), p]
238
+ if v := (len(players) - total_votes):
208
239
  msg.text(f"弃票: {v} 票\n")
209
240
  await self.send(msg)
210
241
 
211
242
  # 全员弃票 # 不是哥们?
212
- if not vote_reversed:
243
+ if total_votes == 0:
213
244
  await self.send("没有人被票出")
214
245
  return
215
246
 
@@ -224,7 +255,7 @@ class Game:
224
255
 
225
256
  # 仅有一名玩家票数最高
226
257
  voted = vs.pop()
227
- if not await voted.kill(KillReason.Vote):
258
+ if not await voted.kill(KillReason.Vote, *vote_result[voted]):
228
259
  # 投票放逐失败 (例: 白痴)
229
260
  return
230
261
 
@@ -267,7 +298,7 @@ class Game:
267
298
 
268
299
  while self.check_game_status() == GameStatus.Unset:
269
300
  # 重置游戏状态,进入下一夜
270
- self.state = GameState()
301
+ self.state = GameState(day_count)
271
302
  players = self.players.alive()
272
303
  await self.send("天黑请闭眼...")
273
304
 
@@ -290,19 +321,19 @@ class Game:
290
321
  if killed is not None:
291
322
  # 除非守卫保护或女巫使用解药,否则狼人正常击杀玩家
292
323
  if not ((killed is protected) or (antidote and potioned is killed)):
293
- await killed.kill(KillReason.Kill)
324
+ await killed.kill(KillReason.Kill, *players.select(RoleGroup.狼人))
294
325
  # 如果女巫使用毒药且守卫未保护,杀死该玩家
295
326
  if poison and (potioned is not None) and (potioned is not protected):
296
- await potioned.kill(KillReason.Poison)
327
+ await potioned.kill(KillReason.Poison, *players.select(Role.女巫))
297
328
 
298
329
  day_count += 1
299
- msg = UniMessage.text(f"【第{day_count}天】天亮了...\n")
330
+ msg = UniMessage.text(f"『第{day_count}天』天亮了...\n")
300
331
  # 没有玩家死亡,平安夜
301
332
  if not (dead := players.dead()):
302
333
  await self.send(msg.text("昨晚是平安夜"))
303
334
  # 有玩家死亡,执行死亡流程
304
335
  else:
305
- # 公开死者名单
336
+ # 公布死者名单
306
337
  msg.text("昨晚的死者是:")
307
338
  for p in dead.sorted():
308
339
  msg.text("\n").at(p.user_id)
@@ -336,8 +367,9 @@ class Game:
336
367
  # 游戏结束
337
368
  dead_channel.cancel()
338
369
  winner = "好人" if self.check_game_status() == GameStatus.Good else "狼人"
339
- msg = UniMessage.text(f"游戏结束,{winner}获胜\n\n")
340
- for p in sorted(self.players, key=lambda p: (p.role.name, p.user_id)):
370
+ msg = UniMessage.text(f"🎉游戏结束,{winner}获胜\n\n")
371
+ for p in sorted(self.players, key=lambda p: (p.role.value, p.user_id)):
341
372
  msg.at(p.user_id).text(f": {p.role.name}\n")
373
+ msg.text(f"\n{self.show_killed_players()}")
342
374
  await self.send(msg)
343
- self._on_exit()
375
+ running_games.pop(self.group.id, None)
@@ -1,47 +1,26 @@
1
1
  import asyncio
2
2
  import asyncio.timeouts
3
- import contextlib
4
3
  from typing import Annotated
5
4
 
6
- import nonebot_plugin_waiter as waiter
7
- from nonebot import on_command, on_message, on_type
5
+ from nonebot import on_command, on_message
8
6
  from nonebot.adapters import Bot, Event
9
7
  from nonebot.rule import to_me
10
8
  from nonebot_plugin_alconna import MsgTarget, UniMessage, UniMsg
11
9
  from nonebot_plugin_userinfo import EventUserInfo, UserInfo
12
10
 
13
- from .config import config
14
- from .game import Game, player_preset
15
- from .utils import InputStore
11
+ from .game import Game, running_games, starting_games
12
+ from .ob11_ext import ob11_ext_enabled
13
+ from .utils import InputStore, is_group, prepare_game, rule_in_game, rule_not_in_game
16
14
 
17
- starting_games: dict[str, dict[str, str]] = {}
18
- running_games: dict[str, tuple[Game, asyncio.Task[None]]] = {}
19
-
20
-
21
- def user_in_game(user_id: str, group_id: str | None) -> bool:
22
- if group_id is not None and group_id not in running_games:
23
- return False
24
- games = running_games.values() if group_id is None else [running_games[group_id]]
25
- for game, _ in games:
26
- return any(user_id == player.user_id for player in game.players)
27
- return False
28
-
29
-
30
- async def rule_in_game(event: Event, target: MsgTarget) -> bool:
31
- if not running_games:
32
- return False
33
- if target.private:
34
- return user_in_game(target.id, None)
35
- elif target.id in running_games:
36
- return user_in_game(event.get_user_id(), target.id)
37
- return False
38
-
39
-
40
- async def rule_not_in_game(event: Event, target: MsgTarget) -> bool:
41
- return not await rule_in_game(event, target)
15
+ in_game_message = on_message(rule=rule_in_game)
16
+ start_game = on_command(
17
+ "werewolf",
18
+ rule=to_me() & is_group & rule_not_in_game,
19
+ aliases={"狼人杀"},
20
+ )
42
21
 
43
22
 
44
- @on_message(rule=rule_in_game).handle()
23
+ @in_game_message.handle()
45
24
  async def handle_input(event: Event, target: MsgTarget, msg: UniMsg) -> None:
46
25
  if target.private:
47
26
  InputStore.put(target.id, None, msg)
@@ -49,87 +28,16 @@ async def handle_input(event: Event, target: MsgTarget, msg: UniMsg) -> None:
49
28
  InputStore.put(event.get_user_id(), target.id, msg)
50
29
 
51
30
 
52
- async def is_group(target: MsgTarget) -> bool:
53
- return not target.private
54
-
55
-
56
- async def prepare_game(
57
- wait: waiter.Waiter[tuple[str, str, str]],
58
- players: dict[str, str],
59
- group_id: str,
60
- admin_id: str,
61
- ):
62
- async for user, name, text in wait(default=(None, "", "")):
63
- if user is None:
64
- continue
65
- msg = UniMessage.at(user)
66
-
67
- match (text, user == admin_id):
68
- case ("开始游戏", True):
69
- if len(players) < min(player_preset):
70
- await (
71
- msg.text(f"游戏至少需要 {min(player_preset)} 人, ")
72
- .text(f"当前已有 {len(players)} 人")
73
- .send()
74
- )
75
- elif len(players) > max(player_preset):
76
- await (
77
- msg.text(f"游戏最多需要 {max(player_preset)} 人, ")
78
- .text(f"当前已有 {len(players)} 人")
79
- .send()
80
- )
81
- else:
82
- await msg.text("游戏即将开始...").send()
83
- return
84
-
85
- case ("开始游戏", False):
86
- await msg.text("只有游戏发起者可以开始游戏").send()
87
-
88
- case ("结束游戏", True):
89
- del starting_games[group_id]
90
- await msg.text("已结束当前游戏").finish()
91
-
92
- case ("结束游戏", False):
93
- await msg.text("只有游戏发起者可以结束游戏").send()
94
-
95
- case ("加入游戏", True):
96
- await msg.text("游戏发起者已经加入游戏了").send()
97
-
98
- case ("加入游戏", False):
99
- if user not in players:
100
- players[user] = name
101
- await msg.text("成功加入游戏").send()
102
- else:
103
- await msg.text("你已经加入游戏了").send()
104
-
105
- case ("退出游戏", True):
106
- await msg.text("游戏发起者无法退出游戏").send()
107
-
108
- case ("退出游戏", False):
109
- if user in players:
110
- del players[user]
111
- await msg.text("成功退出游戏").send()
112
- else:
113
- await msg.text("你还没有加入游戏").send()
114
-
115
- case ("当前玩家", _):
116
- msg.text("\n当前玩家:\n")
117
- for name in players.values():
118
- msg.text(f"\n{name}")
119
- await msg.send()
120
-
121
-
122
- @on_command(
123
- "werewolf",
124
- rule=to_me() & is_group & rule_not_in_game,
125
- aliases={"狼人杀"},
126
- ).handle()
31
+ @start_game.handle()
127
32
  async def handle_start(
128
33
  bot: Bot,
129
34
  event: Event,
130
35
  target: MsgTarget,
131
36
  admin_info: Annotated[UserInfo, EventUserInfo()],
132
37
  ) -> None:
38
+ if target.id in running_games:
39
+ await UniMessage.text("当前群聊内有正在进行的游戏,无法创建游戏").finish()
40
+
133
41
  admin_id = event.get_user_id()
134
42
  msg = (
135
43
  UniMessage.at(admin_id)
@@ -139,102 +47,20 @@ async def handle_start(
139
47
  .text("游戏发起者 @我 发送 “结束游戏” 可结束当前游戏\n")
140
48
  .text("玩家均加入后,游戏发起者请 @我 发送 “开始游戏”\n")
141
49
  )
142
- if (
143
- config.enable_poke
144
- and OneBotV11Available
145
- and bot.adapter.get_name() == "OneBot V11"
146
- ):
50
+ if ob11_ext_enabled():
147
51
  msg.text("\n可使用戳一戳代替游戏交互中的 “/stop” 命令")
148
52
  await msg.text("\n\n游戏准备阶段限时5分钟,超时将自动结束").send()
149
53
 
150
- async def rule(target_: MsgTarget) -> bool:
151
- return not target_.private and target_.id == target.id
152
-
153
- @waiter.waiter(
154
- waits=[event.get_type()],
155
- keep_session=False,
156
- rule=to_me() & rule & rule_not_in_game,
157
- )
158
- def wait(
159
- event: Event,
160
- info: Annotated[UserInfo | None, EventUserInfo()],
161
- msg: UniMsg,
162
- ):
163
- return (
164
- event.get_user_id(),
165
- info.user_name if info is not None else event.get_user_id(),
166
- msg.extract_plain_text().strip(),
167
- )
168
-
169
- starting_games[target.id] = players = {admin_id: admin_info.user_name}
54
+ players = starting_games[target.id] = {admin_id: admin_info.user_name}
170
55
 
171
56
  try:
172
57
  async with asyncio.timeouts.timeout(5 * 60):
173
- await prepare_game(wait, players, target.id, admin_id)
58
+ await prepare_game(event, players)
174
59
  except TimeoutError:
175
- del starting_games[target.id]
176
60
  await UniMessage.text("游戏准备超时,已自动结束").finish()
61
+ finally:
62
+ del starting_games[target.id]
177
63
 
178
- game = Game(
179
- bot=bot,
180
- group=target,
181
- players=players,
182
- on_exit=lambda: running_games.pop(target.id, None) and None,
183
- )
64
+ game = Game(bot=bot, group=target, players=players)
184
65
  task = asyncio.create_task(game.run())
185
66
  running_games[target.id] = (game, task)
186
- del starting_games[target.id]
187
-
188
-
189
- # OneBot V11 扩展
190
- OneBotV11Available = False
191
- with contextlib.suppress(ImportError, RuntimeError):
192
- if not config.enable_poke:
193
- raise RuntimeError
194
-
195
- from nonebot.adapters.onebot.v11 import Bot as V11Bot
196
- from nonebot.adapters.onebot.v11 import MessageSegment
197
- from nonebot.adapters.onebot.v11.event import PokeNotifyEvent
198
-
199
- OneBotV11Available = True
200
-
201
- # 游戏内戳一戳等效 "/stop"
202
- async def _rule_poke_1(event: PokeNotifyEvent) -> bool:
203
- user_id = str(event.user_id)
204
- group_id = str(event.group_id) if event.group_id is not None else None
205
- return (event.target_id == event.self_id) and user_in_game(user_id, group_id)
206
-
207
- @on_type(PokeNotifyEvent, rule=_rule_poke_1).handle()
208
- async def handle_poke_1(event: PokeNotifyEvent) -> None:
209
- InputStore.put(
210
- user_id=str(event.user_id),
211
- group_id=str(event.group_id) if event.group_id is not None else None,
212
- msg=UniMessage.text("/stop"),
213
- )
214
-
215
- # 准备阶段戳一戳等效加入游戏
216
- async def _rule_poke_2(event: PokeNotifyEvent) -> bool:
217
- if event.group_id is None:
218
- return False
219
-
220
- user_id = str(event.user_id)
221
- group_id = str(event.group_id)
222
- return (
223
- (event.target_id == event.self_id)
224
- and not user_in_game(user_id, group_id)
225
- and group_id in starting_games
226
- )
227
-
228
- @on_type(PokeNotifyEvent, rule=_rule_poke_2).handle()
229
- async def handle_poke_2(bot: V11Bot, event: PokeNotifyEvent) -> None:
230
- user_id = str(event.user_id)
231
- group_id = str(event.group_id)
232
- players = starting_games[group_id]
233
-
234
- if user_id not in players:
235
- res: dict[str, str] = await bot.get_group_member_info(
236
- group_id=int(group_id),
237
- user_id=int(user_id),
238
- )
239
- players[user_id] = res.get("nickname") or user_id
240
- await bot.send(event, MessageSegment.at(user_id) + "成功加入游戏")
@@ -0,0 +1,72 @@
1
+ import contextlib
2
+
3
+ from nonebot import on_type
4
+ from nonebot.internal.matcher import current_bot
5
+ from nonebot_plugin_alconna import UniMessage
6
+
7
+ from .config import config
8
+ from .game import starting_games
9
+ from .utils import InputStore, user_in_game
10
+
11
+
12
+ def ob11_ext_enabled() -> bool:
13
+ return False
14
+
15
+
16
+ with contextlib.suppress(ImportError):
17
+ from nonebot.adapters.onebot.v11 import Bot, MessageSegment
18
+ from nonebot.adapters.onebot.v11.event import PokeNotifyEvent
19
+
20
+ # 游戏内戳一戳等效 "/stop"
21
+ async def _rule_poke_1(event: PokeNotifyEvent) -> bool:
22
+ if not config.enable_poke:
23
+ return False
24
+
25
+ user_id = str(event.user_id)
26
+ group_id = str(event.group_id) if event.group_id is not None else None
27
+ return (
28
+ config.enable_poke
29
+ and (event.target_id == event.self_id)
30
+ and user_in_game(user_id, group_id)
31
+ )
32
+
33
+ @on_type(PokeNotifyEvent, rule=_rule_poke_1).handle()
34
+ async def handle_poke_1(event: PokeNotifyEvent) -> None:
35
+ InputStore.put(
36
+ user_id=str(event.user_id),
37
+ group_id=str(event.group_id) if event.group_id is not None else None,
38
+ msg=UniMessage.text("/stop"),
39
+ )
40
+
41
+ # 准备阶段戳一戳等效加入游戏
42
+ async def _rule_poke_2(event: PokeNotifyEvent) -> bool:
43
+ if not config.enable_poke or event.group_id is None:
44
+ return False
45
+
46
+ user_id = str(event.user_id)
47
+ group_id = str(event.group_id)
48
+ return (
49
+ (event.target_id == event.self_id)
50
+ and not user_in_game(user_id, group_id)
51
+ and group_id in starting_games
52
+ )
53
+
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]
59
+
60
+ if user_id not in players:
61
+ res: dict[str, str] = await bot.get_group_member_info(
62
+ group_id=int(group_id),
63
+ user_id=int(user_id),
64
+ )
65
+ players[user_id] = res.get("nickname") or user_id
66
+ await bot.send(event, MessageSegment.at(user_id) + "成功加入游戏")
67
+
68
+ def ob11_ext_enabled() -> bool:
69
+ if not config.enable_poke:
70
+ return False
71
+
72
+ return isinstance(current_bot.get(), Bot)
@@ -1,7 +1,7 @@
1
1
  import asyncio
2
2
  import asyncio.timeouts
3
- import contextlib
4
- from typing import TYPE_CHECKING, ClassVar, Literal
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, ClassVar, TypeVar
5
5
  from typing_extensions import override
6
6
 
7
7
  from nonebot.adapters import Bot
@@ -12,15 +12,24 @@ from .utils import InputStore, check_index
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from .game import Game
15
+ from .player_set import PlayerSet
15
16
 
16
- PlayerClass: dict[Role, type["Player"]] = {}
17
17
 
18
+ P = TypeVar("P", bound=type["Player"])
19
+ PLAYER_CLASS: dict[Role, type["Player"]] = {}
18
20
 
19
- def register_role(cls: type["Player"]) -> type["Player"]:
20
- PlayerClass[cls.role] = cls
21
+
22
+ def register_role(cls: P) -> P:
23
+ PLAYER_CLASS[cls.role] = cls
21
24
  return cls
22
25
 
23
26
 
27
+ @dataclass
28
+ class KillInfo:
29
+ reason: KillReason
30
+ killers: "PlayerSet"
31
+
32
+
24
33
  class Player:
25
34
  role: ClassVar[Role]
26
35
  role_group: ClassVar[RoleGroup]
@@ -31,9 +40,15 @@ class Player:
31
40
  name: str
32
41
  alive: bool = True
33
42
  killed: bool = False
34
- kill_reason: KillReason | None = None
43
+ kill_info: KillInfo | None = None
35
44
  selected: "Player | None" = None
36
45
 
46
+ def __init__(self, bot: Bot, game: "Game", user: Target, name: str) -> None:
47
+ self.bot = bot
48
+ self.game = game
49
+ self.user = user
50
+ self.name = name
51
+
37
52
  @classmethod
38
53
  def new(
39
54
  cls,
@@ -43,20 +58,18 @@ class Player:
43
58
  user: Target,
44
59
  name: str,
45
60
  ) -> "Player":
46
- if role not in PlayerClass:
61
+ if role not in PLAYER_CLASS:
47
62
  raise ValueError(f"Unexpected role: {role!r}")
48
63
 
49
- player = PlayerClass[role]()
50
- player.bot = bot
51
- player.game = game
52
- player.user = user
53
- player.name = name
54
- player.game = game
55
- return player
64
+ return PLAYER_CLASS[role](bot, game, user, name)
56
65
 
57
66
  def __repr__(self) -> str:
58
67
  return f"<{self.role.name}: user={self.user} alive={self.alive}>"
59
68
 
69
+ @property
70
+ def user_id(self) -> str:
71
+ return self.user.id
72
+
60
73
  async def send(self, message: str | UniMessage) -> Receipt:
61
74
  if isinstance(message, str):
62
75
  message = UniMessage.text(message)
@@ -68,21 +81,28 @@ class Player:
68
81
  await self.send(prompt)
69
82
  return await InputStore.fetch(self.user.id)
70
83
 
84
+ async def receive_text(self) -> str:
85
+ return (await self.receive()).extract_plain_text()
86
+
71
87
  async def interact(self) -> None:
72
88
  return
73
89
 
74
90
  async def notify_role(self) -> None:
75
91
  await self.send(f"你的身份: {self.role.name}")
76
92
 
77
- async def kill(self, reason: KillReason) -> bool:
93
+ async def kill(
94
+ self,
95
+ reason: KillReason,
96
+ *killers: "Player",
97
+ ) -> bool:
78
98
  self.alive = False
79
- self.kill_reason = reason
99
+ self.kill_info = KillInfo(reason, PlayerSet(killers))
80
100
  return True
81
101
 
82
102
  async def post_kill(self) -> None:
83
103
  self.killed = True
84
104
 
85
- async def vote(self, players: "PlayerSet") -> "Player | None":
105
+ async def vote(self, players: "PlayerSet") -> "tuple[Player, Player] | None":
86
106
  await self.send(
87
107
  f"请选择需要投票的玩家:\n{players.show()}"
88
108
  "\n\n发送编号选择玩家\n发送 “/stop” 弃票"
@@ -100,17 +120,13 @@ class Player:
100
120
 
101
121
  player = players[selected]
102
122
  await self.send(f"投票的玩家: {player.name}")
103
- return player
104
-
105
- @property
106
- def user_id(self) -> str:
107
- return self.user.id
123
+ return self, player
108
124
 
109
125
 
110
126
  class CanShoot(Player):
111
127
  @override
112
128
  async def post_kill(self) -> None:
113
- if self.kill_reason == KillReason.Poison:
129
+ if self.kill_info and self.kill_info.reason == KillReason.Poison:
114
130
  await self.send("你昨晚被女巫毒杀,无法使用技能")
115
131
  return await super().post_kill()
116
132
 
@@ -122,18 +138,16 @@ class CanShoot(Player):
122
138
 
123
139
  self.game.state.shoot = (None, None)
124
140
  shoot = await self.shoot()
125
- if shoot is not None:
126
- self.game.state.shoot = (self, shoot)
127
141
 
128
142
  if shoot is not None:
143
+ self.game.state.shoot = (self, shoot)
129
144
  await self.send(
130
145
  UniMessage.text(f"{self.role.name} ")
131
146
  .at(self.user_id)
132
147
  .text(" 射杀了玩家 ")
133
148
  .at(shoot.user_id)
134
149
  )
135
- await shoot.kill(KillReason.Shoot)
136
- await shoot.post_kill()
150
+ await shoot.kill(KillReason.Shoot, self)
137
151
  else:
138
152
  await self.send(f"{self.role.name}选择了取消技能")
139
153
  return await super().post_kill()
@@ -148,7 +162,7 @@ class CanShoot(Player):
148
162
  )
149
163
 
150
164
  while True:
151
- text = (await self.receive()).extract_plain_text()
165
+ text = await self.receive_text()
152
166
  if text == "/stop":
153
167
  await self.send("已取消技能")
154
168
  return
@@ -242,7 +256,7 @@ class 预言家(Player):
242
256
  )
243
257
 
244
258
  while True:
245
- text = (await self.receive()).extract_plain_text()
259
+ text = await self.receive_text()
246
260
  index = check_index(text, len(players))
247
261
  if index is not None:
248
262
  selected = index - 1
@@ -262,7 +276,10 @@ class 女巫(Player):
262
276
  poison: int = 1
263
277
 
264
278
  def set_state(
265
- self, *, antidote: Player | None = None, posion: Player | None = None
279
+ self,
280
+ *,
281
+ antidote: Player | None = None,
282
+ posion: Player | None = None,
266
283
  ):
267
284
  if antidote is not None:
268
285
  self.antidote = 0
@@ -273,33 +290,29 @@ class 女巫(Player):
273
290
  self.selected = posion
274
291
  self.game.state.potion = (posion, (False, True))
275
292
  else:
293
+ self.selected = None
276
294
  self.game.state.potion = (None, (False, False))
277
295
 
278
- @staticmethod
279
- def potion_str(potion: Literal[1, 2]) -> str:
280
- return "解药" if potion == 1 else "毒药"
281
-
282
296
  async def handle_killed(self) -> bool:
283
- if self.game.state.killed is not None:
284
- await self.send(f"今晚 {self.game.state.killed.name} 被刀了")
297
+ msg = UniMessage()
298
+ if (killed := self.game.state.killed) is not None:
299
+ msg.text(f"今晚 {killed} 被刀了\n\n")
285
300
  else:
286
301
  await self.send("今晚没有人被刀")
287
302
  return False
288
303
 
289
304
  if not self.antidote:
290
- await self.send("你已经用过解药了")
305
+ await self.send(msg.text("你已经用过解药了"))
291
306
  return False
292
307
 
293
- await self.send("使用解药请发送 “1”\n不使用解药请发送 “/stop”")
308
+ await self.send(msg.text("使用解药请发送 “1”\n不使用解药请发送 “/stop”"))
294
309
 
295
310
  while True:
296
- text = (await self.receive()).extract_plain_text()
311
+ text = await self.receive_text()
297
312
  if text == "1":
298
313
  self.antidote = 0
299
- self.set_state(antidote=self.game.state.killed)
300
- await self.send(
301
- f"你对 {self.game.state.killed.name} 使用了解药,回合结束"
302
- )
314
+ self.set_state(antidote=killed)
315
+ await self.send(f"你对 {killed.name} 使用了解药,回合结束")
303
316
  return True
304
317
  elif text == "/stop":
305
318
  return False
@@ -326,7 +339,7 @@ class 女巫(Player):
326
339
  )
327
340
 
328
341
  while True:
329
- text = (await self.receive()).extract_plain_text().strip()
342
+ text = await self.receive_text()
330
343
  index = check_index(text, len(players))
331
344
  if index is not None:
332
345
  selected = index - 1
@@ -365,7 +378,7 @@ class 守卫(Player):
365
378
  )
366
379
 
367
380
  while True:
368
- text = (await self.receive()).extract_plain_text()
381
+ text = await self.receive_text()
369
382
  if text == "/stop":
370
383
  await self.send("你选择了取消,回合结束")
371
384
  return
@@ -389,7 +402,11 @@ class 白痴(Player):
389
402
  voted: bool = False
390
403
 
391
404
  @override
392
- async def kill(self, reason: KillReason) -> bool:
405
+ async def kill(
406
+ self,
407
+ reason: KillReason,
408
+ *killers: Player,
409
+ ) -> bool:
393
410
  if reason == KillReason.Vote and not self.voted:
394
411
  self.voted = True
395
412
  await self.game.send(
@@ -398,10 +415,10 @@ class 白痴(Player):
398
415
  .text("免疫本次投票放逐,且接下来无法参与投票")
399
416
  )
400
417
  return False
401
- return await super().kill(reason)
418
+ return await super().kill(reason, *killers)
402
419
 
403
420
  @override
404
- async def vote(self, players: "PlayerSet") -> "Player | None":
421
+ async def vote(self, players: "PlayerSet") -> "tuple[Player, Player] | None":
405
422
  if self.voted:
406
423
  await self.send("你已经发动过白痴身份的技能,无法参与本次投票")
407
424
  return None
@@ -412,83 +429,3 @@ class 白痴(Player):
412
429
  class 平民(Player):
413
430
  role: ClassVar[Role] = Role.平民
414
431
  role_group: ClassVar[RoleGroup] = RoleGroup.好人
415
-
416
-
417
- class PlayerSet(set[Player]):
418
- @property
419
- def size(self) -> int:
420
- return len(self)
421
-
422
- def alive(self) -> "PlayerSet":
423
- return PlayerSet(p for p in self if p.alive)
424
-
425
- def dead(self) -> "PlayerSet":
426
- return PlayerSet(p for p in self if not p.alive)
427
-
428
- def include(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
429
- return PlayerSet(
430
- player
431
- for player in self
432
- if (player in types or player.role in types or player.role_group in types)
433
- )
434
-
435
- def select(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
436
- return self.include(*types)
437
-
438
- def exclude(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
439
- return PlayerSet(
440
- player
441
- for player in self
442
- if (
443
- player not in types
444
- and player.role not in types
445
- and player.role_group not in types
446
- )
447
- )
448
-
449
- def player_selected(self) -> "PlayerSet":
450
- return PlayerSet(p.selected for p in self.alive() if p.selected is not None)
451
-
452
- def sorted(self) -> list[Player]:
453
- return sorted(self, key=lambda p: p.user_id)
454
-
455
- async def interact(self, timeout_secs: float = 60) -> None:
456
- async with asyncio.timeouts.timeout(timeout_secs):
457
- await asyncio.gather(*[p.interact() for p in self.alive()])
458
-
459
- async def vote(self, timeout_secs: float = 60) -> list[Player]:
460
- async def vote(player: Player) -> "Player | None":
461
- try:
462
- async with asyncio.timeouts.timeout(timeout_secs):
463
- return await player.vote(self)
464
- except TimeoutError:
465
- await player.send("投票超时,将自动弃票")
466
-
467
- return [
468
- p
469
- for p in await asyncio.gather(*[vote(p) for p in self.alive()])
470
- if p is not None
471
- ]
472
-
473
- async def post_kill(self) -> None:
474
- await asyncio.gather(*[p.post_kill() for p in self])
475
-
476
- async def broadcast(self, message: str | UniMessage) -> None:
477
- await asyncio.gather(*[p.send(message) for p in self])
478
-
479
- async def wait_group_stop(self, group_id: str, timeout_secs: float) -> None:
480
- async def wait(p: Player):
481
- while True:
482
- msg = await InputStore.fetch(p.user_id, group_id)
483
- if msg.extract_plain_text() == "/stop":
484
- break
485
-
486
- with contextlib.suppress(TimeoutError):
487
- async with asyncio.timeouts.timeout(timeout_secs):
488
- await asyncio.gather(*[wait(p) for p in self])
489
-
490
- def show(self) -> str:
491
- return "\n".join(f"{i}. {p.name}" for i, p in enumerate(self.sorted(), 1))
492
-
493
- def __getitem__(self, __index: int) -> Player:
494
- return self.sorted()[__index]
@@ -0,0 +1,87 @@
1
+ import asyncio
2
+ import asyncio.timeouts
3
+ import contextlib
4
+
5
+ from nonebot_plugin_alconna.uniseg import UniMessage
6
+
7
+ from .constant import Role, RoleGroup
8
+ from .player import Player
9
+ from .utils import InputStore
10
+
11
+
12
+ class PlayerSet(set[Player]):
13
+ @property
14
+ def size(self) -> int:
15
+ return len(self)
16
+
17
+ def alive(self) -> "PlayerSet":
18
+ return PlayerSet(p for p in self if p.alive)
19
+
20
+ def dead(self) -> "PlayerSet":
21
+ return PlayerSet(p for p in self if not p.alive)
22
+
23
+ def include(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
24
+ return PlayerSet(
25
+ player
26
+ for player in self
27
+ if (player in types or player.role in types or player.role_group in types)
28
+ )
29
+
30
+ def select(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
31
+ return self.include(*types)
32
+
33
+ def exclude(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
34
+ return PlayerSet(
35
+ player
36
+ for player in self
37
+ if (
38
+ player not in types
39
+ and player.role not in types
40
+ and player.role_group not in types
41
+ )
42
+ )
43
+
44
+ def player_selected(self) -> "PlayerSet":
45
+ return PlayerSet(p.selected for p in self.alive() if p.selected is not None)
46
+
47
+ def sorted(self) -> list[Player]:
48
+ return sorted(self, key=lambda p: p.user_id)
49
+
50
+ async def interact(self, timeout_secs: float = 60) -> None:
51
+ async with asyncio.timeouts.timeout(timeout_secs):
52
+ await asyncio.gather(*[p.interact() for p in self.alive()])
53
+
54
+ async def vote(self, timeout_secs: float = 60) -> dict[Player, list[Player]]:
55
+ async def vote(player: Player) -> "tuple[Player, Player] | None":
56
+ try:
57
+ async with asyncio.timeouts.timeout(timeout_secs):
58
+ return await player.vote(self)
59
+ except TimeoutError:
60
+ await player.send("投票超时,将视为弃票")
61
+
62
+ result: dict[Player, list[Player]] = {}
63
+ for item in await asyncio.gather(*[vote(p) for p in self.alive()]):
64
+ if item is not None:
65
+ player, voted = item
66
+ result[voted] = [*result.get(voted, []), player]
67
+ return result
68
+
69
+ async def broadcast(self, message: str | UniMessage) -> None:
70
+ await asyncio.gather(*[p.send(message) for p in self])
71
+
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
+ def show(self) -> str:
84
+ return "\n".join(f"{i}. {p.name}" for i, p in enumerate(self.sorted(), 1))
85
+
86
+ def __getitem__(self, __index: int) -> Player:
87
+ return self.sorted()[__index]
@@ -1,8 +1,15 @@
1
1
  import asyncio
2
+ import asyncio.timeouts
2
3
  from collections import defaultdict
3
- from typing import ClassVar
4
+ from typing import Annotated, Any, ClassVar
4
5
 
5
- from nonebot_plugin_alconna import UniMessage
6
+ import nonebot_plugin_waiter as waiter
7
+ from nonebot.adapters import Event
8
+ from nonebot.rule import to_me
9
+ from nonebot_plugin_alconna import MsgTarget, UniMessage, UniMsg
10
+ from nonebot_plugin_userinfo import EventUserInfo, UserInfo
11
+
12
+ from .game import player_preset, running_games
6
13
 
7
14
 
8
15
  def check_index(text: str, arrlen: int) -> int | None:
@@ -18,7 +25,7 @@ class InputStore:
18
25
  futures: ClassVar[dict[str, asyncio.Future[UniMessage]]] = {}
19
26
 
20
27
  @classmethod
21
- async def fetch(cls, user_id: str, group_id: str | None = None):
28
+ async def fetch(cls, user_id: str, group_id: str | None = None) -> UniMessage[Any]:
22
29
  key = f"{group_id}_{user_id}"
23
30
  async with cls.locks[key]:
24
31
  cls.futures[key] = asyncio.get_event_loop().create_future()
@@ -28,7 +35,154 @@ class InputStore:
28
35
  del cls.futures[key]
29
36
 
30
37
  @classmethod
31
- def put(cls, user_id: str, group_id: str | None, msg: UniMessage):
38
+ def put(cls, user_id: str, group_id: str | None, msg: UniMessage) -> None:
32
39
  key = f"{group_id}_{user_id}"
33
40
  if future := cls.futures.get(key):
34
41
  future.set_result(msg)
42
+
43
+
44
+ def user_in_game(user_id: str, group_id: str | None) -> bool:
45
+ if group_id is not None and group_id not in running_games:
46
+ return False
47
+ games = running_games.values() if group_id is None else [running_games[group_id]]
48
+ for game, _ in games:
49
+ return any(user_id == player.user_id for player in game.players)
50
+ return False
51
+
52
+
53
+ async def rule_in_game(event: Event, target: MsgTarget) -> bool:
54
+ if not running_games:
55
+ return False
56
+ if target.private:
57
+ return user_in_game(target.id, None)
58
+ elif target.id in running_games:
59
+ return user_in_game(event.get_user_id(), target.id)
60
+ return False
61
+
62
+
63
+ async def rule_not_in_game(event: Event, target: MsgTarget) -> bool:
64
+ return not await rule_in_game(event, target)
65
+
66
+
67
+ async def is_group(target: MsgTarget) -> bool:
68
+ return not target.private
69
+
70
+
71
+ async def _prepare_game_receive(
72
+ queue: asyncio.Queue[tuple[str, str, str]],
73
+ event: Event,
74
+ group_id: str,
75
+ ) -> None:
76
+ async def rule(target_: MsgTarget) -> bool:
77
+ return not target_.private and target_.id == group_id
78
+
79
+ @waiter.waiter(
80
+ waits=[event.get_type()],
81
+ keep_session=False,
82
+ rule=to_me() & rule & rule_not_in_game,
83
+ )
84
+ def wait(
85
+ event: Event,
86
+ info: Annotated[UserInfo | None, EventUserInfo()],
87
+ msg: UniMsg,
88
+ ) -> tuple[str, str, str]:
89
+ return (
90
+ event.get_user_id(),
91
+ info.user_name if info is not None else event.get_user_id(),
92
+ msg.extract_plain_text().strip(),
93
+ )
94
+
95
+ async for user, name, text in wait(default=(None, "", "")):
96
+ if user is None:
97
+ continue
98
+ await queue.put((user, name, text))
99
+
100
+
101
+ async def _prepare_game_handle(
102
+ queue: asyncio.Queue[tuple[str, str, str]],
103
+ players: dict[str, str],
104
+ admin_id: str,
105
+ ) -> None:
106
+ while True:
107
+ user, name, text = await queue.get()
108
+ msg = UniMessage.at(user)
109
+
110
+ match (text, user == admin_id):
111
+ case ("开始游戏", True):
112
+ player_num = len(players)
113
+ if player_num < min(player_preset):
114
+ await (
115
+ msg.text(f"游戏至少需要 {min(player_preset)} 人, ")
116
+ .text(f"当前已有 {player_num} 人")
117
+ .send()
118
+ )
119
+ elif player_num > max(player_preset):
120
+ await (
121
+ msg.text(f"游戏最多需要 {max(player_preset)} 人, ")
122
+ .text(f"当前已有 {player_num} 人")
123
+ .send()
124
+ )
125
+ elif player_num not in player_preset:
126
+ await (
127
+ msg.text(f"不存在总人数为 {player_num} 的预设, ")
128
+ .text("无法开始游戏")
129
+ .send()
130
+ )
131
+ else:
132
+ await msg.text("游戏即将开始...").send()
133
+ return
134
+
135
+ case ("开始游戏", False):
136
+ await msg.text("只有游戏发起者可以开始游戏").send()
137
+
138
+ case ("结束游戏", True):
139
+ await msg.text("已结束当前游戏").finish()
140
+
141
+ case ("结束游戏", False):
142
+ await msg.text("只有游戏发起者可以结束游戏").send()
143
+
144
+ case ("加入游戏", True):
145
+ await msg.text("游戏发起者已经加入游戏了").send()
146
+
147
+ case ("加入游戏", False):
148
+ if user not in players:
149
+ players[user] = name
150
+ await msg.text("成功加入游戏").send()
151
+ else:
152
+ await msg.text("你已经加入游戏了").send()
153
+
154
+ case ("退出游戏", True):
155
+ await msg.text("游戏发起者无法退出游戏").send()
156
+
157
+ case ("退出游戏", False):
158
+ if user in players:
159
+ del players[user]
160
+ await msg.text("成功退出游戏").send()
161
+ else:
162
+ await msg.text("你还没有加入游戏").send()
163
+
164
+ case ("当前玩家", _):
165
+ msg.text("\n当前玩家:\n")
166
+ for name in players.values():
167
+ msg.text(f"\n{name}")
168
+ await msg.send()
169
+
170
+
171
+ async def prepare_game(event: Event, players: dict[str, str]) -> None:
172
+ queue = asyncio.Queue()
173
+ task_receive = asyncio.create_task(
174
+ _prepare_game_receive(
175
+ queue,
176
+ event,
177
+ UniMessage.get_target().id,
178
+ )
179
+ )
180
+
181
+ try:
182
+ await _prepare_game_handle(
183
+ queue,
184
+ players,
185
+ event.get_user_id(),
186
+ )
187
+ finally:
188
+ task_receive.cancel()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nonebot-plugin-werewolf
3
- Version: 1.0.1
3
+ Version: 1.0.3
4
4
  Summary: Default template for PDM package
5
5
  Author-Email: wyf7685 <wyf7685@163.com>
6
6
  License: MIT
@@ -12,9 +12,9 @@ Requires-Dist: nonebot-plugin-waiter>=0.7.1
12
12
  Description-Content-Type: text/markdown
13
13
 
14
14
  <div align="center">
15
- <a href="https://v2.nonebot.dev/store"><img src="https://github.com/A-kirami/nonebot-plugin-template/blob/resources/nbp_logo.png" width="180" height="180" alt="NoneBotPluginLogo"></a>
16
- <br>
17
- <p><img src="https://github.com/A-kirami/nonebot-plugin-template/blob/resources/NoneBotPlugin.svg" width="240" alt="NoneBotPluginText"></p>
15
+ <a href="https://v2.nonebot.dev/store">
16
+ <img src="https://github.com/wyf7685/wyf7685/blob/main/assets/NoneBotPlugin.svg" width="300" alt="logo">
17
+ </a>
18
18
  </div>
19
19
 
20
20
  <div align="center">
@@ -24,7 +24,7 @@ Description-Content-Type: text/markdown
24
24
  _✨ 简单的狼人杀插件 ✨_
25
25
 
26
26
  [![license](https://img.shields.io/github/license/wyf7685/nonebot-plugin-werewolf.svg)](./LICENSE)
27
- [![pypi](https://img.shields.io/pypi/v/nonebot-plugin-werewolf?logo=python&logoColor=edb641)](https://pypi.python.org/pypi/nonebot-plugin-exe-code)
27
+ [![pypi](https://img.shields.io/pypi/v/nonebot-plugin-werewolf?logo=python&logoColor=edb641)](https://pypi.python.org/pypi/nonebot-plugin-werewolf)
28
28
  [![python](https://img.shields.io/badge/python-3.10+-blue?logo=python&logoColor=edb641)](https://www.python.org/)
29
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)
@@ -143,7 +143,9 @@ _✨ 简单的狼人杀插件 ✨_
143
143
  职业预设可以通过配置项 `werewolf__override_preset` 修改
144
144
 
145
145
  <details>
146
- <summary>配置项 `werewolf__override_preset` 示例</summary>
146
+ <summary>示例</summary>
147
+
148
+ 配置项 `werewolf__override_preset`
147
149
 
148
150
  ```env
149
151
  werewolf__override_preset='
@@ -169,6 +171,10 @@ werewolf__override_preset='
169
171
  <details>
170
172
  <summary>更新日志</summary>
171
173
 
174
+ - 2024.09.01 v1.0.3
175
+
176
+ - 优化玩家交互体验
177
+
172
178
  - 2024.08.31 v1.0.1
173
179
 
174
180
  - 允许通过配置项修改职业预设
@@ -0,0 +1,13 @@
1
+ nonebot_plugin_werewolf-1.0.3.dist-info/METADATA,sha256=0co-Cgj6W-98W7ZyDOR8u7phucnJUIstiq0vtwQbLf8,5815
2
+ nonebot_plugin_werewolf-1.0.3.dist-info/WHEEL,sha256=rSwsxJWe3vzyR5HCwjWXQruDgschpei4h_giTm0dJVE,90
3
+ nonebot_plugin_werewolf-1.0.3.dist-info/licenses/LICENSE,sha256=B_WbEqjGr6GYVNfEJPY31T1Opik7OtgOkhRs4Ig3e2M,1064
4
+ nonebot_plugin_werewolf/__init__.py,sha256=CajWxyaRax2kotzc4w5IcJbdmc07ysCbSNGKve5wIvw,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=KfCBdiy5pxkOf1k1EOg8NLe5-viiCCm72vPP6lobudQ,6127
13
+ nonebot_plugin_werewolf-1.0.3.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- nonebot_plugin_werewolf-1.0.1.dist-info/METADATA,sha256=tQZAz__0XgsrQTiovdGfV-glhlS6TESEMfwhxvYF1sc,5944
2
- nonebot_plugin_werewolf-1.0.1.dist-info/WHEEL,sha256=rSwsxJWe3vzyR5HCwjWXQruDgschpei4h_giTm0dJVE,90
3
- nonebot_plugin_werewolf-1.0.1.dist-info/licenses/LICENSE,sha256=B_WbEqjGr6GYVNfEJPY31T1Opik7OtgOkhRs4Ig3e2M,1064
4
- nonebot_plugin_werewolf/__init__.py,sha256=dGaXi6VOQUYCjbe2yfwTLWbHvRRRq11xUeDCgPqnjPE,734
5
- nonebot_plugin_werewolf/config.py,sha256=3O63P1pjvJEwgOwxApAHbKQzvCR1zoG5UjTClZ_OJts,315
6
- nonebot_plugin_werewolf/constant.py,sha256=Q1MKe5hSzg88jL8LQ5SvC2Pr6QwJ9PHrrj1qpOSYCZg,1221
7
- nonebot_plugin_werewolf/game.py,sha256=KD57WxN16VcLJbflxxPw4EXGfjEaXvvBAB8DiK4RMwY,12322
8
- nonebot_plugin_werewolf/matchers.py,sha256=8bGM1gESjL9JMFQRv7611YGM0TZFfg0hQjlOrzS3zRg,8336
9
- nonebot_plugin_werewolf/player.py,sha256=ckviPuXv9JvgcwzD9xcHSVBj7JdRtqGge7WFpynonVY,16355
10
- nonebot_plugin_werewolf/utils.py,sha256=mzFVuzkayfY-8wXnFASPqo2dtJkNOhycfoceIyRGuZg,1023
11
- nonebot_plugin_werewolf-1.0.1.dist-info/RECORD,,