2. 規則和骰子滾動¶
在_EvAdventure_中我們決定使用Knave RPG 規則集。這是商業性的,但在 Creative Commons 4.0 下發布,這意味著可以分享和 使_Knave_適應任何目的,甚至商業用途。如果你不想買但仍關注 另外,您可以在這裡找到免費粉絲版本。
2.2. 製作規則模組¶
建立一個新模組mygame/evadventure/rules.py
對於大多數 RPGS 來說,有三組廣泛的規則:
角色生成規則,通常僅在角色建立過程中使用
常規遊戲規則 - 擲骰子並解決遊戲狀況
角色提升 - 獲取並花費經驗來提升角色
我們希望我們的 rules 模組能夠涵蓋我們原本必須查詢的盡可能多的方面
在規則手冊中。
2.3. 擲骰子¶
我們將從製作一個骰子開始。讓我們將所有骰子組合成這樣的結構 (還不是功能程式碼):
class EvAdventureRollEngine:
def roll(...):
# get result of one generic roll, for any type and number of dice
def roll_with_advantage_or_disadvantage(...)
# get result of normal d20 roll, with advantage/disadvantage (or not)
def saving_throw(...):
# do a saving throw against a specific target number
def opposed_saving_throw(...):
# do an opposed saving throw against a target's defense
def roll_random_table(...):
# make a roll against a random table (loaded elsewere)
def morale_check(...):
# roll a 2d6 morale check for a target
def heal_from_rest(...):
# heal 1d8 when resting+eating, but not more than max value.
def roll_death(...):
# roll to determine penalty when hitting 0 HP.
dice = EvAdventureRollEngine()
這種結構(稱為 singleton)意味著我們將所有骰子分為一個類,然後啟動該類
到模組末尾的變數 dice 中。這意味著我們可以從其他地方做以下事情
模組:
from .rules import dice
dice.roll("1d8")
2.3.1. 通用骰子滾輪¶
我們希望能夠執行 roll("1d20") 並從擲骰中獲得隨機結果。
# in mygame/evadventure/rules.py
from random import randint
class EvAdventureRollEngine:
def roll(self, roll_string):
"""
Roll XdY dice, where X is the number of dice
and Y the number of sides per die.
Args:
roll_string (str): A dice string on the form XdY.
Returns:
int: The result of the roll.
"""
# split the XdY input on the 'd' one time
number, diesize = roll_string.split("d", 1)
# convert from string to integers
number = int(number)
diesize = int(diesize)
# make the roll
return sum(randint(1, diesize) for _ in range(number))
randint 標準 Python 函式庫模組產生一個隨機整數
在特定範圍內。線路
sum(randint(1, diesize) for _ in range(number))
工作原理如下:
對於特定的
number次……建立
1和diesize之間的隨機整數……和
sum所有這些整數在一起。
您可以像這樣不那麼緊湊地編寫相同的內容:
rolls = []
for _ in range(number):
random_result = randint(1, diesize)
rolls.append(random_result)
return sum(rolls)
我們不希望終端使用者呼叫此方法;如果我們這樣做,我們將必須驗證輸入
更多 - 我們必須確保 number 或 diesize 是有效輸入,而不是
太瘋狂了,所以迴圈需要永遠!
2.3.2. 滾動優勢¶
現在我們有了通用滾筒,我們可以開始使用它來進行更複雜的滾動。
# in mygame/evadventure/rules.py
# ...
class EvAdventureRollEngine:
def roll(roll_string):
# ...
def roll_with_advantage_or_disadvantage(self, advantage=False, disadvantage=False):
if not (advantage or disadvantage) or (advantage and disadvantage):
# normal roll - advantage/disadvantage not set or they cancel
# each other out
return self.roll("1d20")
elif advantage:
# highest of two d20 rolls
return max(self.roll("1d20"), self.roll("1d20"))
else:
# disadvantage - lowest of two d20 rolls
return min(self.roll("1d20"), self.roll("1d20"))
min() 和 max() 函式是取得最大/最小的標準 Python 函式
兩個引數。
2.3.3. 豁免檢定¶
我們希望豁免檢定本身能夠確定它是否成功。這意味著它需要知道
能力加值(如STR +1)。如果我們可以直接傳遞實體的話會很方便
對此方法進行儲存丟擲,告訴它需要什麼型別的儲存,然後
讓它弄清楚事情:
result, quality = dice.saving_throw(character, Ability.STR)
如果透過,返回值將是布林值 True/False,以及告訴我們是否透過的 quality
是否有完美的失敗/成功。
為了使儲存方法變得如此聰明,我們需要更多地考慮如何儲存我們的 有關角色的資料。
就我們的目的而言,我們將使用 屬性 來儲存聽起來很合理
能力得分。為了方便起見,我們將它們命名為與
我們在上一課中設定的列舉值。所以如果我們有
列舉 STR = "strength",我們希望將角色的能力儲存為 Attribute strength。
從Attribute檔案中,我們可以看到我們可以使用AttributeProperty來使其
Attribute 可用作 character.strength,這就是我們要做的。
因此,簡而言之,我們將建立儲存丟擲方法,假設我們能夠執行以下操作
character.strength、character.constitution、character.charisma 等以獲得相關能力。
# in mygame/evadventure/rules.py
# ...
from .enums import Ability
class EvAdventureRollEngine:
def roll(...)
# ...
def roll_with_advantage_or_disadvantage(...)
# ...
def saving_throw(self, character, bonus_type=Ability.STR, target=15,
advantage=False, disadvantage=False):
"""
Do a saving throw, trying to beat a target.
Args:
character (Character): A character (assumed to have Ability bonuses
stored on itself as Attributes).
bonus_type (Ability): A valid Ability bonus enum.
target (int): The target number to beat. Always 15 in Knave.
advantage (bool): If character has advantage on this roll.
disadvantage (bool): If character has disadvantage on this roll.
Returns:
tuple: A tuple (bool, Ability), showing if the throw succeeded and
the quality is one of None or Ability.CRITICAL_FAILURE/SUCCESS
"""
# make a roll
dice_roll = self.roll_with_advantage_or_disadvantage(advantage, disadvantage)
# figure out if we had critical failure/success
quality = None
if dice_roll == 1:
quality = Ability.CRITICAL_FAILURE
elif dice_roll == 20:
quality = Ability.CRITICAL_SUCCESS
# figure out bonus
bonus = getattr(character, bonus_type.value, 1)
# return a tuple (bool, quality)
return (dice_roll + bonus) > target, quality
getattr(obj, attrname, default) 函式是一個非常有用的 Python 工具,用於取得 attribute
如果未定義 attribute,則關閉物件並取得預設值。
2.3.4. 反對豁免¶
使用我們已經建立的建置區塊,此方法很簡單。請記住,你擁有的防禦力
在_Knave_中擊敗始終是相關獎金+10。所以如果敵人用STR +3防禦,你必須
滾動高於13。
# in mygame/evadventure/rules.py
from .enums import Ability
class EvAdventureRollEngine:
def roll(...):
# ...
def roll_with_advantage_or_disadvantage(...):
# ...
def saving_throw(...):
# ...
def opposed_saving_throw(self, attacker, defender,
attack_type=Ability.STR, defense_type=Ability.ARMOR,
advantage=False, disadvantage=False):
defender_defense = getattr(defender, defense_type.value, 1) + 10
result, quality = self.saving_throw(attacker, bonus_type=attack_type,
target=defender_defense,
advantage=advantage, disadvantage=disadvantage)
return result, quality
2.3.5. 士氣檢查¶
我們將假設 morale 值可以從生物中簡單地獲得
monster.morale - 我們需要記住稍後再做!
在_Knave_中,生物的2d6士氣等於或低於其士氣時,不會逃跑或投降
當事情向南發展時。標準士氣值為 9。
# in mygame/evadventure/rules.py
class EvAdventureRollEngine:
# ...
def morale_check(self, defender):
return self.roll("2d6") <= getattr(defender, "morale", 9)
2.3.6. 滾動治療¶
為了能夠處理治癒,我們需要對如何儲存做出更多假設
遊戲實體的健康狀況。我們將需要hp_max(可用總量HP)和hp
(當前健康值)。我們再次假設這些將作為 obj.hp 和 obj.hp_max 提供。
根據規則,角色在吃完口糧並睡了一整夜後,會恢復
1d8 + CON HP。
# in mygame/evadventure/rules.py
from .enums import Ability
class EvAdventureRollEngine:
# ...
def heal_from_rest(self, character):
"""
A night's rest retains 1d8 + CON HP
"""
con_bonus = getattr(character, Ability.CON.value, 1)
character.heal(self.roll("1d8") + con_bonus)
我們在這裡做出另一個假設 - character.heal() 是一個東西。我們告訴這個函式如何
角色應該要治癒很多,並且它會這樣做,並確保治癒的量不會超過其最大值
HP數量
知道角色上有什麼可用的以及我們需要什麼規則,這有點像先有雞還是先有蛋的問題 問題。我們將確保下一課實現匹配的 Character 類別。
2.3.7. 在桌子上打滾¶
我們有時需要在“桌子”上滾動——一系列選擇。有兩種主要的表型別 我們需要支援:
只需表格的每一行一個元素(獲得每個結果的機率相同)。
結果 |
|---|
專案1 |
專案2 |
專案3 |
專案4 |
我們將簡單地表示為一個簡單的列表
["item1", "item2", "item3", "item4"]
每個專案的範圍(每個結果的賠率不同):
範圍 |
結果 |
|---|---|
1-5 |
專案1 |
6-15 |
專案2 |
16-19 |
專案3 |
20 |
專案4 |
我們將其表示為元組列表:
[("1-5", "item1"), ("6-15", "item2"), ("16-19", "item4"), ("20", "item5")]
我們還需要知道要擲什麼骰子才能得到結果(可能並非總是如此) 顯而易見,在某些遊戲中,您可能會被要求擲較低的骰子才能獲得 例如,早期的表格結果)。
# in mygame/evadventure/rules.py
from random import randint, choice
class EvAdventureRollEngine:
# ...
def roll_random_table(self, dieroll, table_choices):
"""
Args:
dieroll (str): A die roll string, like "1d20".
table_choices (iterable): A list of either single elements or
of tuples.
Returns:
Any: A random result from the given list of choices.
Raises:
RuntimeError: If rolling dice giving results outside the table.
"""
roll_result = self.roll(dieroll)
if isinstance(table_choices[0], (tuple, list)):
# the first element is a tuple/list; treat as on the form [("1-5", "item"),...]
for (valrange, choice) in table_choices:
minval, *maxval = valrange.split("-", 1)
minval = abs(int(minval))
maxval = abs(int(maxval[0]) if maxval else minval)
if minval <= roll_result <= maxval:
return choice
# if we get here we must have set a dieroll producing a value
# outside of the table boundaries - raise error
raise RuntimeError("roll_random_table: Invalid die roll")
else:
# a simple regular list
roll_result = max(1, min(len(table_choices), roll_result))
return table_choices[roll_result - 1]
檢查您是否理解它的作用。
這可能會令人困惑:
minval, *maxval = valrange.split("-", 1)
minval = abs(int(minval))
maxval = abs(int(maxval[0]) if maxval else minval)
如果 valrange 是字串 1-5,則 valrange.split("-", 1) 將產生元組 ("1", "5")。
但如果字串實際上只是 "20"(可能是 RPG 表中的單一條目),這將
導致錯誤,因為它只會分裂出一個元素 - 而我們期望兩個。
透過使用 *maxval (與 * 一起),maxval 被告知元組中需要 0 個或更多 元素。
因此 1-5 的結果將是 ("1", ("5",)),而 20 的結果將變為 ("20", ())。在行
maxval = abs(int(maxval[0]) if maxval else minval)
我們檢查 maxval 實際上是否有值 ("5",) 或它是否為空 ()。結果是
"5" 或 minval 的值。
2.3.8. 滾動死亡¶
雖然原始的無賴建議擊中 0 HP 意味著立即死亡,但我們將從“美化”無賴的可選規則中獲取可選的“死亡表”,以使其懲罰減輕一些。我們還將 2 的結果更改為“死亡”,因為我們在本教學中不模擬“肢解”:
卷 |
結果 |
-1d4 喪失能力 |
|---|---|---|
1-2 |
1-2 死了 |
- |
3 |
削弱 |
STR |
4 |
不穩定的 |
DEX |
5 |
病態的 |
CON |
6 |
糊塗的 |
INT |
7 |
驚慌的 |
WIS |
8 |
毀容的 |
CHA |
所有非死亡值都對映到六種能力之一的 1d4 損失(但你會得到 HP 恢復)。我們需要從上表中對映回這一點。一個人的能力加值也不能低於-10,如果這樣做,你也會死。
# in mygame/evadventure/rules.py
death_table = (
("1-2", "dead"),
("3", "strength"),
("4", "dexterity"),
("5", "constitution"),
("6", "intelligence"),
("7", "wisdom"),
("8", "charisma"),
)
class EvAdventureRollEngine:
# ...
def roll_random_table(...)
# ...
def roll_death(self, character):
ability_name = self.roll_random_table("1d8", death_table)
if ability_name == "dead":
# TODO - kill the character!
pass
else:
loss = self.roll("1d4")
current_ability = getattr(character, ability_name)
current_ability -= loss
if current_ability < -10:
# TODO - kill the character!
pass
else:
# refresh 1d4 health, but suffer 1d4 ability loss
self.heal(character, self.roll("1d4"))
setattr(character, ability_name, current_ability)
character.msg(
"You survive your brush with death, and while you recover "
f"some health, you permanently lose {loss} {ability_name} instead."
)
dice = EvAdventureRollEngine()
在這裡,我們根據規則滾動“死亡表”,看看會發生什麼。我們賦予角色 如果他們倖存,就會收到一條訊息,讓他們知道發生了什麼事。
我們還不知道「殺死角色」在技術上意味著什麼,所以我們將其標記為 TODO 並在後面的課程中返回它。我們只知道我們需要在這裡做點什麼來殺死這個角色!
2.4. 測試¶
建立一個新模組
mygame/evadventure/tests/test_rules.py
測試rules模組也會在測試時展示一些非常有用的工具。
# mygame/evadventure/tests/test_rules.py
from unittest.mock import patch
from evennia.utils.test_resources import BaseEvenniaTest
from .. import rules
class TestEvAdventureRuleEngine(BaseEvenniaTest):
def setUp(self):
"""Called before every test method"""
super().setUp()
self.roll_engine = rules.EvAdventureRollEngine()
@patch("evadventure.rules.randint")
def test_roll(self, mock_randint):
mock_randint.return_value = 4
self.assertEqual(self.roll_engine.roll("1d6"), 4)
self.assertEqual(self.roll_engine.roll("2d6"), 2 * 4)
# test of the other rule methods below ...
和以前一樣,執行特定測試
evennia test --settings settings.py evadventure.tests.test_rules
2.4.1. 模擬和修補¶
setUp方法是測試類別的特殊方法。 It will be run before every
測試方法。我們使用 super().setUp() 來確定該方法的父類別版本
總是火。 Then we create a fresh EvAdventureRollEngine we can test with.
在我們的測試中,我們從 unittest.mock 庫匯入 patch。這是一個非常有用的測試工具。
通常我們在rules中匯入的randint函式會傳回一個隨機值。這很難測試,因為每次測試的值都會不同。
使用@patch(這稱為_decorator_),我們暫時用「模擬」(虛擬實體)取代rules.randint。該模擬被傳遞到測試方法中。然後我們將mock_randint設定為.return_value = 4。
將 return_value 新增到模擬中意味著每次呼叫此模擬時,它將返回 4。在測試期間,我們現在可以使用 self.assertEqual 檢查我們的 roll 方法是否始終傳回結果,就好像隨機結果是 4 一樣。
有【很多瞭解mock的資源】(https://realpython.com/python-mock-library/),參考 他們尋求進一步的幫助。
EvAdventureRollEngine有很多方法可以測試。我們將此作為額外練習!
2.5. 概括¶
這總結了 Knave 的所有核心規則機制 - 遊戲過程中使用的規則。我們在這裡注意到,我們很快就需要確定我們的 Character 實際上如何儲存資料。所以我們接下來會解決這個問題。