今天要來開始寫程式了!
有了昨天列出來的使用者故事 (需求),接下來就是把它們實作出來了。今天會依序介紹專案架構、主程式,最後會開始實作一點簡單的功能。
複習一下昨天整理出來的使用者故事。
補充:「在頻道內輸入線索」簡稱為「報線索」
除了上面比較偏向功能面的需求之外,在實作上也希望有以下幾個要求:
以下是我的專案架構:
Repo
├─cogs
│ ├─__init__.py
│ ├─clue.py # 線索管理
│ ├─exchange.py # 計算最佳做法功能
│ ├─remind.py # 定時提醒
│ └─tutorial.py # 使用指南與教學
├─utils
│ ├─__init__.py
│ ├─crud_clues.py # 線索資訊的增刪查改
│ ├─calculate.py # 計算最佳化做法
│ ├─optimize.exe # 計算最佳化做法的主程式
│ ├─clues.txt # 儲存線索資訊
│ └─result.txt # 儲存計算結果
├─.env
├─example.env
├─main.py # 主程式
├─players.json # 紀錄 Discord ID 與遊戲暱稱
└─requirements.txt
其中,cogs
資料夾內放的是個各類分類的指令。分成幾類:
utils
資料夾內放的則是其他與 discord 本身無關的,例如計算最佳化結果的相關程式。
.env
與 example.env
就會是存放環境變數,例如伺服器 ID、報線索頻道的 ID 等。
player.json 紀錄的是 Discord ID 與暱稱的對照表,在結果呈現上會使用到。
接下來,來看一下主程式。
# main.py
from typing import Optional
import discord
from discord.ext import commands
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
token: str
guild_id: Optional[int] = None
class MyBot(commands.Bot):
def __init__(
self,
*args,
initial_extensions: list[str],
guild_id: Optional[int] = None,
**kwargs,
):
super().__init__(*args, **kwargs)
self.guild_id = guild_id
self.initial_extensions = initial_extensions
async def setup_hook(self) -> None:
for extension in self.initial_extensions:
await self.load_extension(extension)
if self.guild_id:
guild = discord.Object(self.guild_id)
self.tree.copy_global_to(guild=guild)
await self.tree.sync(guild=guild)
settings = Settings()
exts = [
"cogs.clues",
"cogs.exchange",
"cogs.remind",
"cogs.tutorial",
]
intents = discord.Intents.default()
intents.message_content = True
bot = MyBot(
initial_extensions=exts,
guild_id=settings.guild_id,
command_prefix="",
intents=intents,
)
bot.run(settings.token)
相較於之前的範例,這個稍微複雜了一些,讓我們來看一下這兩個部分:
這邊我選擇使用 Pydantic 的 Pydantic Settings 來取得 .env
中的環境變數 (預設路徑就是 .env
)。
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
token: str
guild_id: Optional[int] = None
settings = Settings()
之後就可以輕鬆地從 settings
中取得 .env
中的環境變數了,例如:settings.token
。
我個人蠻喜歡用 Pydantic 來取得環境變數,因為它可以幫忙做型別轉換、驗證、設定預設值。如果各位對於取得環境變數有其他比較習慣的做法,都可以進行替換。
class MyBot(commands.Bot):
def __init__(
self,
*args,
initial_extensions: list[str],
guild_id: Optional[int] = None,
**kwargs,
):
super().__init__(*args, **kwargs)
self.guild_id = guild_id
self.initial_extensions = initial_extensions
async def setup_hook(self) -> None:
for extension in self.initial_extensions:
await self.load_extension(extension)
if self.guild_id:
guild = discord.Object(self.guild_id)
self.tree.copy_global_to(guild=guild)
await self.tree.sync(guild=guild)
由於指令都是放在 cogs
資料夾內,所以找個時機點使用 load_extension
,那些指令才會生效。另外,我偏好還是要逐一列出要載入的 extension (要稱為 cog 也可以),而不是整包資料夾都送進去。
應用指令也有類似的狀況,為了避免等太久才生效,需要找個時機 sync 到指定的伺服器。不過,還是有保留一點彈性,把它設為 Optional 的參數。
而上面這兩個步驟的最佳時機點就是 setup_hook
(再次強調,絕對不是 on_ready
)。
今天先從簡單的功能開始做,讓大家熟悉這個專案架構。而最簡單的功能就是:
這個比較簡單,就直接來看程式碼。
# cogs/tutorial.py
from discord.ext import commands
class TutorialCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.hybrid_command()
async def tutorial(self, ctx: commands.Context):
await ctx.send("使用教學")
async def setup(bot: commands.Bot):
await bot.add_cog(TutorialCog(bot))
為了節省版面,之後就只會呈現中間 Cog 的部分
這個功能的第一直覺應該是使用 help
來觸發指令,但是 help
是預設指令,所以這邊改成選擇使用 tutorial
來當作觸發的指令。
首先,要思考的是,要用什麼關鍵字來當作指令?由於 help
是預設指令,所以這邊選擇使用 tutorial
來當作觸發的指令。
可以看到目前只有一個指令:
tutorial
至於「使用教學」的內容... 我目前還沒寫XD
考量到內容應該頗長,這時候可以考慮直接放一個網址就好 (e.g. HackMD 筆記),或者是使用嵌入式內容 (請參考 Day 13) 做一個重點說明,並附上詳細說明的連結。
@commands.hybrid_command()
async def tutorial(self, ctx: commands.Context):
embed = discord.Embed(
title="使用教學",
url="https://ithelp.ithome.com.tw/users/20162280/ironman/7781",
)
embed.add_field(name="tutorial", value="查看使用教學", inline=False)
embed.add_field(name="exchange", value="開始計算最佳做法", inline=False)
await ctx.send(embed=embed)
效果如下:
因為是使用 Hybrid Command,所以會同時設定兩種指令。
今天介紹了專案架構和主程式,並開始實作功能。明天會繼續實作其他功能~