nonebot-plugin-werewolf 1.0.5__tar.gz → 1.0.7__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nonebot-plugin-werewolf
3
- Version: 1.0.5
3
+ Version: 1.0.7
4
4
  Summary: Default template for PDM package
5
5
  Author-Email: wyf7685 <wyf7685@163.com>
6
6
  License: MIT
@@ -88,10 +88,14 @@ _✨ 简单的狼人杀插件 ✨_
88
88
 
89
89
  在 nonebot2 项目的`.env`文件中添加下表中的必填配置
90
90
 
91
- | 配置项 | 必填 | 默认值 | 说明 |
92
- | :-------------------------: | :--: | :----: | :-----------------------------------------------------------: |
93
- | `werewolf__enable_poke` | 否 | `True` | 是否使用戳一戳简化操作流程<br/>仅在 `OneBot V11` 适配器下生效 |
94
- | `werewolf__override_preset` | 否 | - | 覆写插件内置的职业预设 |
91
+ | 配置项 | 必填 | 默认值 | 说明 |
92
+ | :-----------------------------: | :--: | :----: | :-----------------------------------------------------------: |
93
+ | `werewolf__enable_poke` | 否 | `True` | 是否使用戳一戳简化操作流程<br/>仅在 `OneBot V11` 适配器下生效 |
94
+ | `werewolf__role_preset` | 否 | - | 覆写插件内置的职业预设 |
95
+ | `werewolf__werewolf_priority` | 否 | - | 自定义狼人职业优先级 |
96
+ | `werewolf__priesthood_proirity` | 否 | - | 自定义神职职业优先级 |
97
+
98
+ `werewolf__role_preset`, `werewolf__werewolf_priority`, `werewolf__priesthood_proirity` 的配置格式请参考 [`游戏内容`](#游戏内容) 部分
95
99
 
96
100
  ## 🎉 使用
97
101
 
@@ -149,15 +153,15 @@ _其他交互参考游戏内提示_
149
153
  | 11   | 3  | 5   | 3   |
150
154
  | 12   | 4  | 5   | 3   |
151
155
 
152
- 职业预设可以通过配置项 `werewolf__override_preset` 修改
156
+ 职业预设可以通过配置项 `werewolf__role_preset` 修改
153
157
 
154
158
  <details>
155
159
  <summary>示例</summary>
156
160
 
157
- 配置项 `werewolf__override_preset`
161
+ 配置项 `werewolf__role_preset`
158
162
 
159
163
  ```env
160
- werewolf__override_preset='
164
+ werewolf__role_preset='
161
165
  [
162
166
  [6, 1, 3, 2],
163
167
  [7, 2, 3, 2]
@@ -170,16 +174,67 @@ werewolf__override_preset='
170
174
  </details>
171
175
  <br/>
172
176
 
173
- 对于`狼人`和`神职`的职业分配,有如下优先级:
177
+ 对于`狼人`和`神职`的职业分配,默认有如下优先级:
174
178
 
175
179
  - `狼人`: `狼人`, `狼人`, `狼王`, `狼人`
176
- - `神职`: `预言家`, `女巫`, `猎人`, `守卫`, `白痴`
180
+ - `神职`: `女巫`, `预言家`, `猎人`, `守卫`, `白痴`
181
+
182
+ 职业分配优先级可以通过配置项 `werewolf__werewolf_priority` 和 `werewolf__priesthood_proirity` 修改
183
+
184
+ <details>
185
+ <summary>示例</summary>
186
+
187
+ #### 配置项 `werewolf__werewolf_priority`
188
+
189
+ ```env
190
+ werewolf__werewolf_priority=[1, 2, 1, 1]
191
+ ```
192
+
193
+ 上述配置中,`[1, 2, 1, 1]` 表示狼人的职业优先级为 `狼人`, `狼王`, `狼人`, `狼人`
194
+
195
+ #### 配置项 `werewolf__werewolf_priority`
196
+
197
+ ```env
198
+ werewolf__priesthood_proirity=[11, 12, 13, 14, 15]
199
+ ```
200
+
201
+ 上述配置中,`[11, 12, 13, 14, 15]` 表示神职的职业优先级为 `预言家`, `女巫`, `猎人`, `守卫`, `白痴`
202
+
203
+ #### 职业与数字的对应关系
204
+
205
+ 上述配置示例中有大量~~意义不明的~~数字, 其对应的是 [`这里`](./nonebot_plugin_werewolf/constant.py) 的枚举类 `Role` 的值
206
+
207
+ 以下列出目前的枚举值供参考
208
+
209
+ | 职业 | 枚举值 |
210
+ | -------- | ------ |
211
+ | `狼人` | `1` |
212
+ | `狼王` | `2` |
213
+ | `预言家` | `11` |
214
+ | `女巫` | `12` |
215
+ | `猎人` | `13` |
216
+ | `守卫` | `14` |
217
+ | `白痴` | `15` |
218
+ | `平民` | `0` |
219
+
220
+ </details>
177
221
 
178
222
  ## 📝 更新日志
179
223
 
180
224
  <details>
181
225
  <summary>更新日志</summary>
182
226
 
227
+ <!-- CHANGELOG -->
228
+
229
+ - 2024.09.04 v1.0.7
230
+
231
+ - 优先使用群名片作为玩家名
232
+ - 支持通过配置项修改职业分配优先级
233
+
234
+ - 2024.09.03 v1.0.6
235
+
236
+ - 修复预言家查验狼王返回好人的 bug
237
+
183
238
  - 2024.09.03 v1.0.5
184
239
 
185
240
  - 优化玩家交互体验
@@ -187,7 +242,7 @@ werewolf__override_preset='
187
242
 
188
243
  - 2024.08.31 v1.0.1
189
244
 
190
- - 允许通过配置项修改职业预设
245
+ - 支持通过配置项修改职业预设
191
246
 
192
247
  - 2024.08.31 v1.0.0
193
248
 
@@ -201,3 +256,4 @@ werewolf__override_preset='
201
256
  - [`nonebot/plugin-alconna`](https://github.com/nonebot/plugin-alconna): 跨平台的消息处理接口
202
257
  - [`noneplugin/nonebot-plugin-userinfo`](https://github.com/noneplugin/nonebot-plugin-userinfo): 用户信息获取
203
258
  - [`RF-Tar-Railt/nonebot-plugin-waiter`](https://github.com/RF-Tar-Railt/nonebot-plugin-waiter): 灵活获取用户输入
259
+ - `热心群友`: 协助测试插件
@@ -75,10 +75,14 @@ _✨ 简单的狼人杀插件 ✨_
75
75
 
76
76
  在 nonebot2 项目的`.env`文件中添加下表中的必填配置
77
77
 
78
- | 配置项 | 必填 | 默认值 | 说明 |
79
- | :-------------------------: | :--: | :----: | :-----------------------------------------------------------: |
80
- | `werewolf__enable_poke` | 否 | `True` | 是否使用戳一戳简化操作流程<br/>仅在 `OneBot V11` 适配器下生效 |
81
- | `werewolf__override_preset` | 否 | - | 覆写插件内置的职业预设 |
78
+ | 配置项 | 必填 | 默认值 | 说明 |
79
+ | :-----------------------------: | :--: | :----: | :-----------------------------------------------------------: |
80
+ | `werewolf__enable_poke` | 否 | `True` | 是否使用戳一戳简化操作流程<br/>仅在 `OneBot V11` 适配器下生效 |
81
+ | `werewolf__role_preset` | 否 | - | 覆写插件内置的职业预设 |
82
+ | `werewolf__werewolf_priority` | 否 | - | 自定义狼人职业优先级 |
83
+ | `werewolf__priesthood_proirity` | 否 | - | 自定义神职职业优先级 |
84
+
85
+ `werewolf__role_preset`, `werewolf__werewolf_priority`, `werewolf__priesthood_proirity` 的配置格式请参考 [`游戏内容`](#游戏内容) 部分
82
86
 
83
87
  ## 🎉 使用
84
88
 
@@ -136,15 +140,15 @@ _其他交互参考游戏内提示_
136
140
  | 11   | 3  | 5   | 3   |
137
141
  | 12   | 4  | 5   | 3   |
138
142
 
139
- 职业预设可以通过配置项 `werewolf__override_preset` 修改
143
+ 职业预设可以通过配置项 `werewolf__role_preset` 修改
140
144
 
141
145
  <details>
142
146
  <summary>示例</summary>
143
147
 
144
- 配置项 `werewolf__override_preset`
148
+ 配置项 `werewolf__role_preset`
145
149
 
146
150
  ```env
147
- werewolf__override_preset='
151
+ werewolf__role_preset='
148
152
  [
149
153
  [6, 1, 3, 2],
150
154
  [7, 2, 3, 2]
@@ -157,16 +161,67 @@ werewolf__override_preset='
157
161
  </details>
158
162
  <br/>
159
163
 
160
- 对于`狼人`和`神职`的职业分配,有如下优先级:
164
+ 对于`狼人`和`神职`的职业分配,默认有如下优先级:
161
165
 
162
166
  - `狼人`: `狼人`, `狼人`, `狼王`, `狼人`
163
- - `神职`: `预言家`, `女巫`, `猎人`, `守卫`, `白痴`
167
+ - `神职`: `女巫`, `预言家`, `猎人`, `守卫`, `白痴`
168
+
169
+ 职业分配优先级可以通过配置项 `werewolf__werewolf_priority` 和 `werewolf__priesthood_proirity` 修改
170
+
171
+ <details>
172
+ <summary>示例</summary>
173
+
174
+ #### 配置项 `werewolf__werewolf_priority`
175
+
176
+ ```env
177
+ werewolf__werewolf_priority=[1, 2, 1, 1]
178
+ ```
179
+
180
+ 上述配置中,`[1, 2, 1, 1]` 表示狼人的职业优先级为 `狼人`, `狼王`, `狼人`, `狼人`
181
+
182
+ #### 配置项 `werewolf__werewolf_priority`
183
+
184
+ ```env
185
+ werewolf__priesthood_proirity=[11, 12, 13, 14, 15]
186
+ ```
187
+
188
+ 上述配置中,`[11, 12, 13, 14, 15]` 表示神职的职业优先级为 `预言家`, `女巫`, `猎人`, `守卫`, `白痴`
189
+
190
+ #### 职业与数字的对应关系
191
+
192
+ 上述配置示例中有大量~~意义不明的~~数字, 其对应的是 [`这里`](./nonebot_plugin_werewolf/constant.py) 的枚举类 `Role` 的值
193
+
194
+ 以下列出目前的枚举值供参考
195
+
196
+ | 职业 | 枚举值 |
197
+ | -------- | ------ |
198
+ | `狼人` | `1` |
199
+ | `狼王` | `2` |
200
+ | `预言家` | `11` |
201
+ | `女巫` | `12` |
202
+ | `猎人` | `13` |
203
+ | `守卫` | `14` |
204
+ | `白痴` | `15` |
205
+ | `平民` | `0` |
206
+
207
+ </details>
164
208
 
165
209
  ## 📝 更新日志
166
210
 
167
211
  <details>
168
212
  <summary>更新日志</summary>
169
213
 
214
+ <!-- CHANGELOG -->
215
+
216
+ - 2024.09.04 v1.0.7
217
+
218
+ - 优先使用群名片作为玩家名
219
+ - 支持通过配置项修改职业分配优先级
220
+
221
+ - 2024.09.03 v1.0.6
222
+
223
+ - 修复预言家查验狼王返回好人的 bug
224
+
170
225
  - 2024.09.03 v1.0.5
171
226
 
172
227
  - 优化玩家交互体验
@@ -174,7 +229,7 @@ werewolf__override_preset='
174
229
 
175
230
  - 2024.08.31 v1.0.1
176
231
 
177
- - 允许通过配置项修改职业预设
232
+ - 支持通过配置项修改职业预设
178
233
 
179
234
  - 2024.08.31 v1.0.0
180
235
 
@@ -188,3 +243,4 @@ werewolf__override_preset='
188
243
  - [`nonebot/plugin-alconna`](https://github.com/nonebot/plugin-alconna): 跨平台的消息处理接口
189
244
  - [`noneplugin/nonebot-plugin-userinfo`](https://github.com/noneplugin/nonebot-plugin-userinfo): 用户信息获取
190
245
  - [`RF-Tar-Railt/nonebot-plugin-waiter`](https://github.com/RF-Tar-Railt/nonebot-plugin-waiter): 灵活获取用户输入
246
+ - `热心群友`: 协助测试插件
@@ -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.5"
11
+ __version__ = "1.0.7"
12
12
  __plugin_meta__ = PluginMetadata(
13
13
  name="狼人杀",
14
14
  description="适用于 Nonebot2 的狼人杀插件",
@@ -1,10 +1,14 @@
1
1
  from nonebot import get_plugin_config
2
2
  from pydantic import BaseModel
3
3
 
4
+ from .constant import Role
5
+
4
6
 
5
7
  class PluginConfig(BaseModel):
6
8
  enable_poke: bool = True
7
- override_preset: list[tuple[int, int, int, int]] | None = None
9
+ role_preset: list[tuple[int, int, int, int]] | None = None
10
+ werewolf_priority: list[Role] | None = None
11
+ priesthood_proirity: list[Role] | None = None
8
12
 
9
13
 
10
14
  class Config(BaseModel):
@@ -0,0 +1,124 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum, auto
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from .player import Player
9
+
10
+
11
+ class Role(Enum):
12
+ # 狼人
13
+ Werewolf = 1
14
+ WolfKing = 2
15
+
16
+ # 神职
17
+ Prophet = 11
18
+ Witch = 12
19
+ Hunter = 13
20
+ Guard = 14
21
+ Idiot = 15
22
+
23
+ # 平民
24
+ Civilian = 0
25
+
26
+
27
+ class RoleGroup(Enum):
28
+ Werewolf = auto()
29
+ GoodGuy = auto()
30
+
31
+
32
+ class KillReason(Enum):
33
+ Kill = auto()
34
+ Poison = auto()
35
+ Shoot = auto()
36
+ Vote = auto()
37
+
38
+
39
+ class GameStatus(Enum):
40
+ Good = auto()
41
+ Bad = auto()
42
+ Unset = auto()
43
+
44
+
45
+ @dataclass
46
+ class GameState:
47
+ 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
+
53
+
54
+ role_name_conv: dict[Role | RoleGroup, str] = {
55
+ Role.Werewolf: "狼人",
56
+ Role.WolfKing: "狼王",
57
+ Role.Prophet: "预言家",
58
+ Role.Witch: "女巫",
59
+ Role.Hunter: "猎人",
60
+ Role.Guard: "守卫",
61
+ Role.Idiot: "白痴",
62
+ Role.Civilian: "平民",
63
+ RoleGroup.Werewolf: "狼人",
64
+ RoleGroup.GoodGuy: "好人",
65
+ }
66
+
67
+ role_preset: dict[int, tuple[int, int, int]] = {
68
+ # 总人数: (狼, 神, 民)
69
+ 6: (1, 2, 3),
70
+ 7: (2, 2, 3),
71
+ 8: (2, 3, 3),
72
+ 9: (2, 4, 3),
73
+ 10: (3, 4, 3),
74
+ 11: (3, 5, 3),
75
+ 12: (4, 5, 3),
76
+ }
77
+
78
+ werewolf_priority: list[Role] = [
79
+ Role.Werewolf,
80
+ Role.Werewolf,
81
+ Role.WolfKing,
82
+ Role.Werewolf,
83
+ ]
84
+ priesthood_proirity: list[Role] = [
85
+ Role.Witch,
86
+ Role.Prophet,
87
+ Role.Hunter,
88
+ Role.Guard,
89
+ Role.Idiot,
90
+ ]
91
+
92
+
93
+ def __apply_config():
94
+ from .config import config
95
+
96
+ global role_preset, werewolf_priority, priesthood_proirity
97
+
98
+ if config.role_preset is not None:
99
+ for preset in config.role_preset:
100
+ if preset[0] != preset[1:]:
101
+ raise RuntimeError(
102
+ "配置项 `role_preset` 错误: "
103
+ f"预设总人数为 {preset[0]}, 实际总人数为 {sum(preset[1:])}"
104
+ )
105
+ role_preset |= {i[0]: i[1:] for i in config.role_preset}
106
+
107
+ if (priority := config.werewolf_priority) is not None:
108
+ min_length = max(i[0] for i in role_preset.values())
109
+ if len(priority) < min_length:
110
+ raise RuntimeError(
111
+ f"配置项 `werewolf_priority` 错误: 应至少为 {min_length} 项"
112
+ )
113
+ werewolf_priority = priority
114
+
115
+ if (priority := config.priesthood_proirity) is not None:
116
+ min_length = max(i[1] for i in role_preset.values())
117
+ if len(priority) < min_length:
118
+ raise RuntimeError(
119
+ f"配置项 `priesthood_proirity` 错误: 应至少为 {min_length} 项"
120
+ )
121
+ priesthood_proirity = priority
122
+
123
+
124
+ __apply_config()
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import asyncio
2
4
  import asyncio.timeouts
3
5
  import contextlib
@@ -8,27 +10,36 @@ from nonebot.adapters import Bot
8
10
  from nonebot.log import logger
9
11
  from nonebot_plugin_alconna import Target, UniMessage
10
12
 
11
- from .constant import GameState, GameStatus, KillReason, Role, RoleGroup, player_preset
13
+ from .constant import (
14
+ GameState,
15
+ GameStatus,
16
+ KillReason,
17
+ Role,
18
+ RoleGroup,
19
+ priesthood_proirity,
20
+ role_preset,
21
+ werewolf_priority,
22
+ )
12
23
  from .player import Player
13
24
  from .player_set import PlayerSet
14
25
  from .utils import InputStore
15
26
 
16
27
  starting_games: dict[str, dict[str, str]] = {}
17
- running_games: dict[str, tuple["Game", asyncio.Task[None], asyncio.Task[None]]] = {}
28
+ running_games: dict[str, Game] = {}
18
29
 
19
30
 
20
- def init_players(bot: Bot, game: "Game", players: dict[str, str]) -> PlayerSet:
21
- preset = player_preset.get(len(players))
31
+ def init_players(bot: Bot, game: Game, players: dict[str, str]) -> PlayerSet:
32
+ preset = role_preset.get(len(players))
22
33
  if preset is None:
23
34
  raise ValueError(
24
35
  f"玩家人数不符: "
25
- f"应为 {', '.join(map(str, player_preset))} 人, 传入{len(players)}人"
36
+ f"应为 {', '.join(map(str, role_preset))} 人, 传入{len(players)}人"
26
37
  )
27
38
 
28
39
  roles: list[Role] = []
29
- roles.extend([Role.狼人, Role.狼人, Role.狼王, Role.狼人][: preset[0]])
30
- roles.extend([Role.预言家, Role.女巫, Role.猎人, Role.守卫, Role.白痴][: preset[1]])
31
- roles.extend([Role.平民] * preset[2])
40
+ roles.extend(werewolf_priority[: preset[0]])
41
+ roles.extend(priesthood_proirity[: preset[1]])
42
+ roles.extend([Role.Civilian] * preset[2])
32
43
 
33
44
  r = random.Random(time.time())
34
45
  shuffled: list[Role] = []
@@ -82,20 +93,20 @@ class Game:
82
93
 
83
94
  def at_all(self) -> UniMessage:
84
95
  msg = UniMessage()
85
- for p in sorted(self.players, key=lambda p: (p.role.name, p.user_id)):
96
+ for p in sorted(self.players, key=lambda p: (p.role_name, p.user_id)):
86
97
  msg.at(p.user_id)
87
98
  return msg
88
99
 
89
100
  def check_game_status(self) -> GameStatus:
90
101
  players = self.players.alive()
91
- w = players.select(RoleGroup.狼人)
92
- p = players.exclude(RoleGroup.狼人)
102
+ w = players.select(RoleGroup.Werewolf)
103
+ p = players.exclude(RoleGroup.Werewolf)
93
104
 
94
105
  if w.size >= p.size:
95
106
  return GameStatus.Bad
96
- if not p.select(Role.平民):
107
+ if not p.select(Role.Civilian):
97
108
  return GameStatus.Bad
98
- if not p.exclude(Role.平民):
109
+ if not p.exclude(Role.Civilian):
99
110
  return GameStatus.Bad
100
111
  if not w.size:
101
112
  return GameStatus.Good
@@ -126,7 +137,7 @@ class Game:
126
137
  return msg.strip()
127
138
 
128
139
  async def notify_player_role(self) -> None:
129
- preset = player_preset[len(self.players)]
140
+ preset = role_preset[len(self.players)]
130
141
  await asyncio.gather(
131
142
  self.send(
132
143
  self.at_all()
@@ -162,7 +173,7 @@ class Game:
162
173
  ) -> None:
163
174
  players = self.players.alive().select(type_)
164
175
  text = (
165
- type_.role.name # Player
176
+ type_.role_name # Player
166
177
  if isinstance(type_, Player)
167
178
  else (
168
179
  type_.name # Role
@@ -181,8 +192,8 @@ class Game:
181
192
  players = self.players.alive()
182
193
  self.state.killed = None
183
194
 
184
- w = players.select(RoleGroup.狼人)
185
- await self.interact(RoleGroup.狼人, 120)
195
+ w = players.select(RoleGroup.Werewolf)
196
+ await self.interact(RoleGroup.Werewolf, 120)
186
197
  if (s := w.player_selected()).size == 1:
187
198
  self.state.killed = s.pop()
188
199
  await w.broadcast(f"今晚选择的目标为: {self.state.killed.name}")
@@ -190,8 +201,8 @@ class Game:
190
201
  await w.broadcast("狼人阵营意见未统一,此晚空刀")
191
202
 
192
203
  # 如果女巫存活,正常交互,限时1分钟
193
- if players.include(Role.女巫):
194
- await self.interact(Role.女巫, 60)
204
+ if players.include(Role.Witch):
205
+ await self.interact(Role.Witch, 60)
195
206
  # 否则等待 5-20s
196
207
  else:
197
208
  await asyncio.sleep(random.uniform(5, 20))
@@ -228,7 +239,7 @@ class Game:
228
239
  await self.send(
229
240
  UniMessage.text("玩家 ")
230
241
  .at(shoot.user_id)
231
- .text(f" 被{shooter.role.name}射杀, 请发表遗言\n")
242
+ .text(f" 被{shooter.role_name}射杀, 请发表遗言\n")
232
243
  .text("限时1分钟, 发送 “/stop” 结束发言")
233
244
  )
234
245
  await self.wait_stop(shoot, 60)
@@ -337,10 +348,10 @@ class Game:
337
348
  # 狼人、预言家、守卫 同时交互,女巫在狼人后交互
338
349
  await asyncio.gather(
339
350
  self.select_killed(),
340
- players.select(Role.女巫).broadcast("请等待狼人决定目标..."),
341
- players.select(Role.平民).broadcast("请等待其他玩家结束交互..."),
342
- self.interact(Role.预言家, 60),
343
- self.interact(Role.守卫, 60),
351
+ players.select(Role.Witch).broadcast("请等待狼人决定目标..."),
352
+ players.select(Role.Civilian).broadcast("请等待其他玩家结束交互..."),
353
+ self.interact(Role.Prophet, 60),
354
+ self.interact(Role.Guard, 60),
344
355
  )
345
356
 
346
357
  # 狼人击杀目标
@@ -354,10 +365,12 @@ class Game:
354
365
  if killed is not None:
355
366
  # 除非守卫保护或女巫使用解药,否则狼人正常击杀玩家
356
367
  if not ((killed is protected) or (antidote and potioned is killed)):
357
- await killed.kill(KillReason.Kill, *players.select(RoleGroup.狼人))
368
+ await killed.kill(
369
+ KillReason.Kill, *players.select(RoleGroup.Werewolf)
370
+ )
358
371
  # 如果女巫使用毒药且守卫未保护,杀死该玩家
359
372
  if poison and (potioned is not None) and (potioned is not protected):
360
- await potioned.kill(KillReason.Poison, *players.select(Role.女巫))
373
+ await potioned.kill(KillReason.Poison, *players.select(Role.Witch))
361
374
 
362
375
  day_count += 1
363
376
  msg = UniMessage.text(f"『第{day_count}天』天亮了...\n")
@@ -401,28 +414,37 @@ class Game:
401
414
  winner = "好人" if self.check_game_status() == GameStatus.Good else "狼人"
402
415
  msg = UniMessage.text(f"🎉游戏结束,{winner}获胜\n\n")
403
416
  for p in sorted(self.players, key=lambda p: (p.role.value, p.user_id)):
404
- msg.at(p.user_id).text(f": {p.role.name}\n")
417
+ msg.at(p.user_id).text(f": {p.role_name}\n")
405
418
  await self.send(msg)
406
419
  await self.send(f"玩家死亡报告:\n\n{self.show_killed_players()}")
407
420
 
408
421
  def start(self):
409
- task = asyncio.create_task(self.run())
422
+ event = asyncio.Event()
423
+ game_task = asyncio.create_task(self.run())
424
+ game_task.add_done_callback(lambda _: event.set())
410
425
  dead_channel = asyncio.create_task(self.run_dead_channel())
411
426
 
412
427
  async def daemon():
413
- while not task.done(): # noqa: ASYNC110
414
- await asyncio.sleep(1)
428
+ await event.wait()
415
429
 
416
430
  try:
417
- task.result()
431
+ game_task.result()
432
+ logger.info(f"{self.group.id} 的狼人杀游戏进程正常退出")
418
433
  except asyncio.CancelledError as err:
419
- logger.warning(f"狼人杀游戏进程被取消: {err}")
434
+ logger.warning(f"{self.group.id} 的狼人杀游戏进程被取消: {err}")
420
435
  except Exception as err:
421
- msg = f"狼人杀游戏进程出现错误: {err!r}"
436
+ msg = f"{self.group.id} 的狼人杀游戏进程出现错误: {err!r}"
422
437
  logger.opt(exception=err).error(msg)
423
438
  await self.send(msg)
424
439
  finally:
425
440
  dead_channel.cancel()
426
441
  running_games.pop(self.group.id, None)
427
442
 
428
- running_games[self.group.id] = (self, task, asyncio.create_task(daemon()))
443
+ def daemon_callback(task: asyncio.Task[None]):
444
+ if err := task.exception():
445
+ logger.opt(exception=err).error(
446
+ f"{self.group.id} 的狼人杀守护进程出现错误: {err!r}"
447
+ )
448
+
449
+ running_games[self.group.id] = self
450
+ asyncio.create_task(daemon()).add_done_callback(daemon_callback)
@@ -4,6 +4,7 @@ from typing import Annotated
4
4
 
5
5
  from nonebot import on_command, on_message
6
6
  from nonebot.adapters import Bot, Event
7
+ from nonebot.exception import FinishedException
7
8
  from nonebot.rule import to_me
8
9
  from nonebot_plugin_alconna import MsgTarget, UniMessage, UniMsg
9
10
  from nonebot_plugin_userinfo import EventUserInfo, UserInfo
@@ -56,6 +57,8 @@ async def handle_start(
56
57
  try:
57
58
  async with asyncio.timeouts.timeout(5 * 60):
58
59
  await prepare_game(event, players)
60
+ except FinishedException:
61
+ raise
59
62
  except TimeoutError:
60
63
  await UniMessage.text("游戏准备超时,已自动结束").finish()
61
64
  finally:
@@ -3,13 +3,13 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import asyncio.timeouts
5
5
  from dataclasses import dataclass
6
- from typing import TYPE_CHECKING, ClassVar, TypeVar
6
+ from typing import TYPE_CHECKING, ClassVar, TypeVar, final
7
7
  from typing_extensions import override
8
8
 
9
9
  from nonebot.adapters import Bot
10
10
  from nonebot_plugin_alconna.uniseg import Receipt, Target, UniMessage
11
11
 
12
- from .constant import KillReason, Role, RoleGroup
12
+ from .constant import KillReason, Role, RoleGroup, role_name_conv
13
13
  from .utils import InputStore, check_index
14
14
 
15
15
  if TYPE_CHECKING:
@@ -21,9 +21,14 @@ P = TypeVar("P", bound=type["Player"])
21
21
  PLAYER_CLASS: dict[Role, type[Player]] = {}
22
22
 
23
23
 
24
- def register_role(cls: P) -> P:
25
- PLAYER_CLASS[cls.role] = cls
26
- return cls
24
+ def register_role(role: Role, role_group: RoleGroup, /):
25
+ def decorator(cls: P, /) -> P:
26
+ cls.role = role
27
+ cls.role_group = role_group
28
+ PLAYER_CLASS[role] = cls
29
+ return cls
30
+
31
+ return decorator
27
32
 
28
33
 
29
34
  @dataclass
@@ -45,12 +50,14 @@ class Player:
45
50
  kill_info: KillInfo | None = None
46
51
  selected: Player | None = None
47
52
 
53
+ @final
48
54
  def __init__(self, bot: Bot, game: Game, user: Target, name: str) -> None:
49
55
  self.bot = bot
50
56
  self.game = game
51
57
  self.user = user
52
58
  self.name = name
53
59
 
60
+ @final
54
61
  @classmethod
55
62
  def new(
56
63
  cls,
@@ -66,23 +73,30 @@ class Player:
66
73
  return PLAYER_CLASS[role](bot, game, user, name)
67
74
 
68
75
  def __repr__(self) -> str:
69
- return f"<{self.role.name}: user={self.user} alive={self.alive}>"
76
+ return f"<{self.role_name}: user={self.user} alive={self.alive}>"
70
77
 
71
78
  @property
72
79
  def user_id(self) -> str:
73
80
  return self.user.id
74
81
 
82
+ @property
83
+ def role_name(self) -> str:
84
+ return role_name_conv[self.role]
85
+
86
+ @final
75
87
  async def send(self, message: str | UniMessage) -> Receipt:
76
88
  if isinstance(message, str):
77
89
  message = UniMessage.text(message)
78
90
 
79
91
  return await message.send(target=self.user, bot=self.bot)
80
92
 
93
+ @final
81
94
  async def receive(self, prompt: str | UniMessage | None = None) -> UniMessage:
82
95
  if prompt:
83
96
  await self.send(prompt)
84
97
  return await InputStore.fetch(self.user.id)
85
98
 
99
+ @final
86
100
  async def receive_text(self) -> str:
87
101
  return (await self.receive()).extract_plain_text()
88
102
 
@@ -90,7 +104,7 @@ class Player:
90
104
  return
91
105
 
92
106
  async def notify_role(self) -> None:
93
- await self.send(f"你的身份: {self.role.name}")
107
+ await self.send(f"你的身份: {self.role_name}")
94
108
 
95
109
  async def kill(
96
110
  self,
@@ -136,9 +150,9 @@ class CanShoot(Player):
136
150
  return await super().post_kill()
137
151
 
138
152
  await self.game.send(
139
- UniMessage.text(f"{self.role.name} ")
153
+ UniMessage.text(f"{self.role_name} ")
140
154
  .at(self.user_id)
141
- .text(f" 死了\n请{self.role.name}决定击杀目标...")
155
+ .text(f" 死了\n请{self.role_name}决定击杀目标...")
142
156
  )
143
157
 
144
158
  self.game.state.shoot = (None, None)
@@ -147,14 +161,14 @@ class CanShoot(Player):
147
161
  if shoot is not None:
148
162
  self.game.state.shoot = (self, shoot)
149
163
  await self.send(
150
- UniMessage.text(f"{self.role.name} ")
164
+ UniMessage.text(f"{self.role_name} ")
151
165
  .at(self.user_id)
152
166
  .text(" 射杀了玩家 ")
153
167
  .at(shoot.user_id)
154
168
  )
155
169
  await shoot.kill(KillReason.Shoot, self)
156
170
  else:
157
- await self.send(f"{self.role.name}选择了取消技能")
171
+ await self.send(f"{self.role_name}选择了取消技能")
158
172
  return await super().post_kill()
159
173
 
160
174
  async def shoot(self) -> Player | None:
@@ -181,25 +195,22 @@ class CanShoot(Player):
181
195
  return players[selected]
182
196
 
183
197
 
184
- @register_role
185
- class 狼人(Player):
186
- role: ClassVar[Role] = Role.狼人
187
- role_group: ClassVar[RoleGroup] = RoleGroup.狼人
188
-
198
+ @register_role(Role.Werewolf, RoleGroup.Werewolf)
199
+ class Werewolf(Player):
189
200
  @override
190
201
  async def notify_role(self) -> None:
191
202
  await super().notify_role()
192
- partners = self.game.players.alive().select(RoleGroup.狼人).exclude(self)
203
+ partners = self.game.players.alive().select(RoleGroup.Werewolf).exclude(self)
193
204
  if partners:
194
205
  await self.send(
195
206
  "你的队友:\n"
196
- + "\n".join(f" {p.role.name}: {p.name}" for p in partners)
207
+ + "\n".join(f" {p.role_name}: {p.name}" for p in partners)
197
208
  )
198
209
 
199
210
  @override
200
211
  async def interact(self) -> None:
201
212
  players = self.game.players.alive()
202
- partners = players.select(RoleGroup.狼人).exclude(self)
213
+ partners = players.select(RoleGroup.Werewolf).exclude(self)
203
214
 
204
215
  # 避免阻塞
205
216
  def broadcast(msg: str | UniMessage):
@@ -209,7 +220,7 @@ class 狼人(Player):
209
220
  if partners:
210
221
  msg = (
211
222
  msg.text("你的队友:\n")
212
- .text("\n".join(f" {p.role.name}: {p.name}" for p in partners))
223
+ .text("\n".join(f" {p.role_name}: {p.name}" for p in partners))
213
224
  .text("\n所有私聊消息将被转发至队友\n\n")
214
225
  )
215
226
  await self.send(
@@ -243,17 +254,13 @@ class 狼人(Player):
243
254
  self.selected = players[selected]
244
255
 
245
256
 
246
- @register_role
247
- class 狼王(CanShoot, 狼人):
248
- role: ClassVar[Role] = Role.狼王
249
- role_group: ClassVar[RoleGroup] = RoleGroup.狼人
250
-
257
+ @register_role(Role.WolfKing, RoleGroup.Werewolf)
258
+ class WolfKing(CanShoot, Werewolf):
259
+ pass
251
260
 
252
- @register_role
253
- class 预言家(Player):
254
- role: ClassVar[Role] = Role.预言家
255
- role_group: ClassVar[RoleGroup] = RoleGroup.好人
256
261
 
262
+ @register_role(Role.Prophet, RoleGroup.GoodGuy)
263
+ class Prophet(Player):
257
264
  @override
258
265
  async def interact(self) -> None:
259
266
  players = self.game.players.alive().exclude(self)
@@ -272,14 +279,12 @@ class 预言家(Player):
272
279
  await self.send("输入错误,请发送编号选择玩家")
273
280
 
274
281
  player = players[selected]
275
- result = "狼人" if player.role == Role.狼人 else "好人"
282
+ result = role_name_conv[player.role_group]
276
283
  await self.send(f"玩家 {player.name} 的阵营是『{result}』")
277
284
 
278
285
 
279
- @register_role
280
- class 女巫(Player):
281
- role: ClassVar[Role] = Role.女巫
282
- role_group: ClassVar[RoleGroup] = RoleGroup.好人
286
+ @register_role(Role.Witch, RoleGroup.GoodGuy)
287
+ class Witch(Player):
283
288
  antidote: int = 1
284
289
  poison: int = 1
285
290
 
@@ -365,17 +370,13 @@ class 女巫(Player):
365
370
  await self.send(f"当前回合选择对玩家 {player.name} 使用毒药\n回合结束")
366
371
 
367
372
 
368
- @register_role
369
- class 猎人(CanShoot, Player):
370
- role: ClassVar[Role] = Role.猎人
371
- role_group: ClassVar[RoleGroup] = RoleGroup.好人
372
-
373
+ @register_role(Role.Hunter, RoleGroup.GoodGuy)
374
+ class Hunter(CanShoot, Player):
375
+ pass
373
376
 
374
- @register_role
375
- class 守卫(Player):
376
- role: ClassVar[Role] = Role.守卫
377
- role_group: ClassVar[RoleGroup] = RoleGroup.好人
378
377
 
378
+ @register_role(Role.Guard, RoleGroup.GoodGuy)
379
+ class Guard(Player):
379
380
  @override
380
381
  async def interact(self) -> None:
381
382
  players = self.game.players.alive().exclude(self)
@@ -403,10 +404,8 @@ class 守卫(Player):
403
404
  await self.send(f"本回合保护的玩家: {self.selected.name}")
404
405
 
405
406
 
406
- @register_role
407
- class 白痴(Player):
408
- role: ClassVar[Role] = Role.白痴
409
- role_group: ClassVar[RoleGroup] = RoleGroup.好人
407
+ @register_role(Role.Idiot, RoleGroup.GoodGuy)
408
+ class Idiot(Player):
410
409
  voted: bool = False
411
410
 
412
411
  @override
@@ -433,7 +432,6 @@ class 白痴(Player):
433
432
  return await super().vote(players)
434
433
 
435
434
 
436
- @register_role
437
- class 平民(Player):
438
- role: ClassVar[Role] = Role.平民
439
- role_group: ClassVar[RoleGroup] = RoleGroup.好人
435
+ @register_role(Role.Civilian, RoleGroup.GoodGuy)
436
+ class Civilian(Player):
437
+ pass
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import asyncio
2
4
  import asyncio.timeouts
3
5
 
@@ -12,23 +14,23 @@ class PlayerSet(set[Player]):
12
14
  def size(self) -> int:
13
15
  return len(self)
14
16
 
15
- def alive(self) -> "PlayerSet":
17
+ def alive(self) -> PlayerSet:
16
18
  return PlayerSet(p for p in self if p.alive)
17
19
 
18
- def dead(self) -> "PlayerSet":
20
+ def dead(self) -> PlayerSet:
19
21
  return PlayerSet(p for p in self if not p.alive)
20
22
 
21
- def include(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
23
+ def include(self, *types: Player | Role | RoleGroup) -> PlayerSet:
22
24
  return PlayerSet(
23
25
  player
24
26
  for player in self
25
27
  if (player in types or player.role in types or player.role_group in types)
26
28
  )
27
29
 
28
- def select(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
30
+ def select(self, *types: Player | Role | RoleGroup) -> PlayerSet:
29
31
  return self.include(*types)
30
32
 
31
- def exclude(self, *types: Player | Role | RoleGroup) -> "PlayerSet":
33
+ def exclude(self, *types: Player | Role | RoleGroup) -> PlayerSet:
32
34
  return PlayerSet(
33
35
  player
34
36
  for player in self
@@ -39,7 +41,7 @@ class PlayerSet(set[Player]):
39
41
  )
40
42
  )
41
43
 
42
- def player_selected(self) -> "PlayerSet":
44
+ def player_selected(self) -> PlayerSet:
43
45
  return PlayerSet(p.selected for p in self.alive() if p.selected is not None)
44
46
 
45
47
  def sorted(self) -> list[Player]:
@@ -1,5 +1,6 @@
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
 
@@ -9,7 +10,7 @@ from nonebot.rule import to_me
9
10
  from nonebot_plugin_alconna import MsgTarget, UniMessage, UniMsg
10
11
  from nonebot_plugin_userinfo import EventUserInfo, UserInfo
11
12
 
12
- from .constant import player_preset
13
+ from .constant import role_preset
13
14
 
14
15
 
15
16
  def check_index(text: str, arrlen: int) -> int | None:
@@ -47,7 +48,7 @@ def user_in_game(user_id: str, group_id: str | None) -> bool:
47
48
  if group_id is not None and group_id not in running_games:
48
49
  return False
49
50
  games = running_games.values() if group_id is None else [running_games[group_id]]
50
- for game, *_ in games:
51
+ for game in games:
51
52
  return any(user_id == player.user_id for player in game.players)
52
53
  return False
53
54
 
@@ -92,13 +93,18 @@ async def _prepare_game_receive(
92
93
  ) -> tuple[str, str, str]:
93
94
  return (
94
95
  event.get_user_id(),
95
- info.user_name if info is not None else event.get_user_id(),
96
+ (
97
+ (info.user_displayname or info.user_name)
98
+ if info is not None
99
+ else event.get_user_id()
100
+ ),
96
101
  msg.extract_plain_text().strip(),
97
102
  )
98
103
 
99
104
  async for user, name, text in wait(default=(None, "", "")):
100
105
  if user is None:
101
106
  continue
107
+ name = re.sub(r"[\u2066-\u2069]", "", name)
102
108
  await queue.put((user, name, text))
103
109
 
104
110
 
@@ -114,19 +120,19 @@ async def _prepare_game_handle(
114
120
  match (text, user == admin_id):
115
121
  case ("开始游戏", True):
116
122
  player_num = len(players)
117
- if player_num < min(player_preset):
123
+ if player_num < min(role_preset):
118
124
  await (
119
- msg.text(f"游戏至少需要 {min(player_preset)} 人, ")
125
+ msg.text(f"游戏至少需要 {min(role_preset)} 人, ")
120
126
  .text(f"当前已有 {player_num} 人")
121
127
  .send()
122
128
  )
123
- elif player_num > max(player_preset):
129
+ elif player_num > max(role_preset):
124
130
  await (
125
- msg.text(f"游戏最多需要 {max(player_preset)} 人, ")
131
+ msg.text(f"游戏最多需要 {max(role_preset)} 人, ")
126
132
  .text(f"当前已有 {player_num} 人")
127
133
  .send()
128
134
  )
129
- elif player_num not in player_preset:
135
+ elif player_num not in role_preset:
130
136
  await (
131
137
  msg.text(f"不存在总人数为 {player_num} 的预设, ")
132
138
  .text("无法开始游戏")
@@ -167,8 +173,8 @@ async def _prepare_game_handle(
167
173
 
168
174
  case ("当前玩家", _):
169
175
  msg.text("\n当前玩家:\n")
170
- for name in players.values():
171
- msg.text(f"\n{name}")
176
+ for idx, name in enumerate(players.values(), 1):
177
+ msg.text(f"\n{idx}. {name}")
172
178
  await msg.send()
173
179
 
174
180
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nonebot-plugin-werewolf"
3
- version = "1.0.5"
3
+ version = "1.0.7"
4
4
  description = "Default template for PDM package"
5
5
  authors = [
6
6
  { name = "wyf7685", email = "wyf7685@163.com" },
@@ -26,6 +26,9 @@ build-backend = "pdm.backend"
26
26
  [tool.pdm]
27
27
  distribution = true
28
28
 
29
+ [tool.pdm.scripts]
30
+ updver = "pdm run python ./scripts/update_version.py"
31
+
29
32
  [tool.pdm.scripts.lint]
30
33
  composite = [
31
34
  "isort .",
@@ -96,3 +99,13 @@ pythonPlatform = "All"
96
99
  typeCheckingMode = "standard"
97
100
  reportShadowedImports = false
98
101
  disableBytesTypePromotions = true
102
+
103
+ [tool.nonebot]
104
+ adapters = [
105
+ { name = "OneBot V11", module_name = "nonebot.adapters.onebot.v11" },
106
+ ]
107
+ plugins = [
108
+ "nonebot_plugin_werewolf",
109
+ ]
110
+ plugin_dirs = []
111
+ builtin_plugins = []
@@ -1,68 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from enum import Enum, auto
5
- from typing import TYPE_CHECKING
6
-
7
- from .config import config
8
-
9
- if TYPE_CHECKING:
10
- from .player import Player
11
-
12
-
13
- class Role(Enum):
14
- # 狼人
15
- 狼人 = auto()
16
- 狼王 = auto()
17
-
18
- # 神职
19
- 预言家 = auto()
20
- 女巫 = auto()
21
- 猎人 = auto()
22
- 守卫 = auto()
23
- 白痴 = auto()
24
-
25
- # 平民
26
- 平民 = auto()
27
-
28
-
29
- class RoleGroup(Enum):
30
- 狼人 = auto()
31
- 好人 = auto()
32
-
33
-
34
- class KillReason(Enum):
35
- Kill = auto()
36
- Poison = auto()
37
- Shoot = auto()
38
- Vote = auto()
39
-
40
-
41
- class GameStatus(Enum):
42
- Good = auto()
43
- Bad = auto()
44
- Unset = auto()
45
-
46
-
47
- @dataclass
48
- class GameState:
49
- day: int
50
- killed: Player | None = None
51
- shoot: tuple[Player, Player] | tuple[None, None] = (None, None)
52
- protected: Player | None = None
53
- potion: tuple[Player | None, tuple[bool, bool]] = (None, (False, False))
54
-
55
-
56
- player_preset: dict[int, tuple[int, int, int]] = {
57
- # 总人数: (狼, 神, 民)
58
- 6: (1, 2, 3),
59
- 7: (2, 2, 3),
60
- 8: (2, 3, 3),
61
- 9: (2, 4, 3),
62
- 10: (3, 4, 3),
63
- 11: (3, 5, 3),
64
- 12: (4, 5, 3),
65
- }
66
-
67
- if config.override_preset is not None:
68
- player_preset |= {i[0]: i[1:] for i in config.override_preset}