Evert wondered if it is better to choose Agility over separate points into Ballistic Skill & Stealth? Ever thought that your current character has only Intellect but no points left for anything else?
This repo contains a Mixed-Integer Non-Linear Programming (MI-NLP) optimization solution to spending the optimal amount of XP on selected attributes, skills & traits in the role-playing game Wrath & Glory by Cubicle 7.
For simple character management, I recommend using the Character Forge @ Doctors of Doom. The XP optimizer can then be used on a created character to minimized the spent XP. Just pass in your desired target values (e.g. total value for Cunning, Tech & Deception and your desired Strength) and the optimizer will figure out the best distribution to use with minimal XP.
If you encounter any errors/wrong numbers please post your input & the expected values so I can debug them. If you have any recommendations, feel free to leave some comments.
The optimizer is written for the core ruleset v2.1. It does not take into account any species/archetype based bonuses/prerequisites - but these should be fine given that they use the same XP cost tables.
-
Install Python (3.8 or better) if not already done.
-
Switch to folder of this project, open terminal & install requirements
pip install -r requirements.txt
-
Try, if it works (shows help content)
python xpOptimizer.py -h
NOTE: You always have to specify the tier of your character!
The optimizer takes the tree-of-learning rule into account, but assigns the skill ratings randomly (within the min-xp constraint). Simply move the 1s around to your liking - the xp cost stay the same.
For few target properties it is best to use the command-line arguments, e.g. if you want to optimize your tier 1 character with Strength 3 and Max Wounds 5, type:
python xpOptimizer.py --Tier 1 --Strength 3 --MaxWounds 5
which will output the following markdown table:
## Tier
1
## Attributes
Name | Total | Target | Missed
---------- | ------ | ------ | ------
Agility | 1 | - | -
Fellowship | 1 | - | -
Initiative | 1 | - | -
Intellect | 1 | - | -
Strength | 3 | 3 | NO
Toughness | 3 | - | -
Willpower | 1 | - | -
## Skills
Name | Rating | Total | Target | Missed
-------------- | ------ | ------ | ------ | ------
Athletics | 0 | 3 | - | -
Awareness | 0 | 1 | - | -
BallisticSkill | 0 | 1 | - | -
Cunning | 0 | 1 | - | -
Deception | 0 | 1 | - | -
Insight | 0 | 1 | - | -
Intimidation | 0 | 1 | - | -
Investigation | 0 | 1 | - | -
Leadership | 0 | 1 | - | -
Medicae | 0 | 1 | - | -
Persuasion | 0 | 1 | - | -
Pilot | 0 | 1 | - | -
PsychicMastery | 0 | 1 | - | -
Scholar | 0 | 1 | - | -
Stealth | 0 | 1 | - | -
Survival | 0 | 1 | - | -
Tech | 0 | 1 | - | -
WeaponSkill | 0 | 1 | - | -
## Traits
Name | Total | Target | Missed
------------- | ------ | ------ | ------
Conviction | 1 | - | -
Defence | 0 | - | -
Determination | 3 | - | -
Influence | 0 | - | -
MaxShock | 2 | - | -
MaxWounds | 5 | 5 | NO
Resilience | 4 | - | -
Resolve | 0 | - | -
## XPCost
Name | Cost
---------- | ----
Attributes | 20
Skills | 0
Total | 20
If you prefer json instead of markdown, use the --return_json
flag:
python xpOptimizer.py --Tier 1 --Strength 3 --MaxWounds 5 --return_json
which will create:
{
"Tier": 1,
"Attributes": {
"Total": {
"Agility": 1,
"Fellowship": 1,
"Initiative": 1,
"Intellect": 1,
"Strength": 3,
"Toughness": 3,
"Willpower": 1
},
"Target": {
"Strength": 3
},
"Missed": []
},
"Skills": {
"Rating": {
"Athletics": 0,
"Awareness": 0,
"BallisticSkill": 0,
"Cunning": 0,
"Deception": 0,
"Insight": 0,
"Intimidation": 0,
"Investigation": 0,
"Leadership": 0,
"Medicae": 0,
"Persuasion": 0,
"Pilot": 0,
"PsychicMastery": 0,
"Scholar": 0,
"Stealth": 0,
"Survival": 0,
"Tech": 0,
"WeaponSkill": 0
},
"Total": {
"Athletics": 3,
"Awareness": 1,
"BallisticSkill": 1,
"Cunning": 1,
"Deception": 1,
"Insight": 1,
"Intimidation": 1,
"Investigation": 1,
"Leadership": 1,
"Medicae": 1,
"Persuasion": 1,
"Pilot": 1,
"PsychicMastery": 1,
"Scholar": 1,
"Stealth": 1,
"Survival": 1,
"Tech": 1,
"WeaponSkill": 1
},
"Target": {},
"Missed": []
},
"Traits": {
"Total": {
"Conviction": 1,
"Defence": 0,
"Determination": 3,
"Influence": 0,
"MaxShock": 2,
"MaxWounds": 5,
"Resilience": 4,
"Resolve": 0
},
"Target": {
"MaxWounds": 5
},
"Missed": []
},
"XPCost": {
"Attributes": 20,
"Skills": 0,
"Total": 20
}
}
If you have several properties you want to set, or simple want to keep single file for your character, use the json-file with the --file
command-line argument. The json file is a simple flat json with at least the tier field. For example if you want to optimize your tier 3 character with
- Agility 5
- BallisticSkill 11
- Cunning 7
- Deception 8
- Stealth 12
- Defence 6 (note the British spelling!)
- Max Wounds 10
then create a file (e.g. TestChar.json
) with the following content:
{
"Tier": 3,
"Agility": 5,
"BallisticSkill": 11,
"Cunning": 7,
"Deception": 8,
"Stealth": 12,
"Defence": 6,
"MaxWounds": 10
}
then call (assuming the file is in the same folder as the optimizer script):
python xpOptimizer.py --file TestChar.json
NOTE: The following section is best view in a latex-capable markdown viewer, otherwise the formulas will not be rendered and be readable only to the Tex-fetishist
Name | Abbreviation | Related Attribute | #Affected Skills/Attributes |
---|---|---|---|
Tier | Tier | - | 3 |
Attributes | |||
Agility | Agi | - | 3 |
Fellowship | Fel | - | 3 |
Initiative | Ini | - | 3 |
Intellect | Int | - | 3 |
Strength | Str | - | 3 |
Toughness | Tou | - | 3 |
Willpower | Wil | - | 3 |
Skills | |||
Athletics | Athl | Str | - |
Awareness | Awar | Int | 1 |
Ballistic Skill | BaSk | Agi | - |
Cunning | Cunn | Fel | - |
Deception | Dece | Fel | - |
Insight | Insi | Fel | - |
Intimidation | Inti | Wil | - |
Investigation | Inve | Int | - |
Leadership | Lead | Wil | - |
Medicae | Medi | Int | - |
Persuasion | Pers | Fel | - |
Pilot | Pilo | Agi | - |
Psychic Mastery | PsMa | Wil | - |
Scholar | Scho | Int | - |
Stealth | Stea | Agi | - |
Survival | Surv | Wil | - |
Tech | Tech | Int | - |
Weapon Skill | WeSk | Ini | - |
Traits | |||
Conviction | Conv | Wil | - |
Defence | Defe | Ini - 1 | - |
Determination | Dete | Tou | - |
Influence | Infl | Fel - 1 | - |
Max Shock | MaSh | Wil + Tier | - |
Max Wounds | MaWo | Tou + 2 * Tier | - |
Passive Awareness | PaAw | Awareness / 2 | - |
Resilience | Resi | Tou + 1 | - |
Resolve | Reso | Wil - 1 | - |
Wealth | Weal | Tier | - |
Name | Symbol |
---|---|
Attribute | |
Names | |
Rating | |
Ratings (Set) | |
Ratings (Vector) | $\vec{r}A = \left(r{Agi}, r_{Fel}, r_{Ini}, r_{Int}, r_{Str}, r_{Tou}, r_{Wil}\right)^T$ |
Skill | |
Names | |
Rating | |
Ratings (Set) | |
Ratings (Vector) | $\vec{r}S = \left(r{Athl}, r_{Awar}, r_{BaSk}, r_{Cunn}, r_{Dece}, r_{Insi}, r_{Inti}, r_{Inve}, r_{Lead}, r_{Medi}, r_{Pers}, r_{Pilo}, r_{PsMa}, r_{Scho}, r_{Stea}, r_{Surv}, r_{Tech}, r_{WeSk}\right)^T$ |
Overall | |
Target values |
|
XP Cost | Symbol | Formula | Range | Note |
---|---|---|---|---|
Attribute (cumulative) | See below | |||
Skill (cumulative) | Rule of Triangular Numbers |
The attribute cost is originally given as table:
1 | 0 | 0 | 0 |
2 | 4 | 4 | 4 |
3 | 10 | 6 | 2 |
4 | 20 | 10 | 4 |
5 | 35 | 15 | 5 |
6 | 55 | 20 | 5 |
7 | 80 | 25 | 5 |
8 | 110 | 30 | 5 |
9 | 145 | 35 | 5 |
10 | 185 | 40 | 5 |
11 | 230 | 45 | 5 |
12 | 280 | 50 | 5 |
The incremental attribute cost in tabular form can be rewritten as: $$ \Delta c_A\left(r_a\right) = \begin{cases} 0 & \text{if } r_a = 1 \ 2r_a & \text{if } r_a \in {2, 3} \ 5 \cdot (r_a - 2) & \text{if } r_a \geq 4 \ \end{cases} $$
In the following, we will use the rule for Triangular Numbers, which is given for the range $\left[1, b\right], b \in \mathbb{N}1$ by: $$ \sum^{b}{i = 1} i = \frac{b \cdot (b + 1)}{2} $$
and can be extended to arbitrary ranges $\left[a + 1, m = a + b\right], a \in \mathbb{N}0$ by: $$ \begin{aligned} \sum^{m}{i = a + 1} i &= \sum^{m}{i = 1} i &- &\sum^{a}{i = 1} i \ &= \frac{m \cdot (m + 1)}{2} &- &\frac{a \cdot (a + 1)}{2} \ \end{aligned} $$
which, when resolving
The switch criterion between the cases of the incremental attribute cost can be defined as: $$ k = \min({r_a, 3}) $$
The cumulative attribute cost is given by: $$ c_A\left(r_a\right) = \sum^{r_a}_{i = 1} \Delta c_A\left(r_a\right)\ $$
which, using the switch criterion and the rule for Triangular Numbers, can be rewritten asy: $$ \begin{aligned} c_A\left(r_a\right) &= 0 + \sum^{k}{i = 2} 2i &+ &\sum^{r_a}{i = k + 1} 5 \cdot (r_a - 2) \ &= 0 + 2\sum^{1 + (k - 1)}{i = 1 + 1} i &+ &5 \cdot \left(\sum^{k + (r_a - k)}{i = k + 1} r_a - \sum^{r_a}_{i = k + 1} 2 \right) \ &= 2 \cdot \frac{(k - 1) \cdot ((k - 1) + 2 + 1)}{2} &+ &5 \cdot \left(\frac{(r_a - k) \cdot ((r_a - k) + 2k + 1)}{2} - 2 \cdot (r_a - k) \right) \ &= (k - 1) \cdot (k + 2) &+ &2.5 \cdot (r_a - k) \cdot \left(r_a + k - 3 \right) \end{aligned} $$
which, when resolving the switch criterion, results in the alternate form: $$ c_A\left(r_a\right) = \begin{cases} (r_a - 1) \cdot (r_a + 2) & \text{if } r_a \leq 2 \Rightarrow k = r_a \ 2.5 \cdot \left( r_a \cdot (r_a - 3) + 4 \right) & \text{if } r_a \geq 3 \Rightarrow k = 3 \ \end{cases} $$
The total cost is given by: $$ \begin{aligned} C\left(\vec{r}A, \vec{r}S\right) &= \sum^{|A|}{a = 1} c_A\left(r_a\right) &+ &\sum^{|S|}{s = 1} c_S\left(r_s\right) \ &= \sum^{|A|}{a = 1} \left( (k - 1) \cdot (k + 2) + 2.5 \cdot (r_a - k) \cdot \left(r_a + k - 3 \right) \right) &+ &\sum^{|S|}{s = 1} r_s \cdot (r_s + 1)\ \end{aligned} $$
$$ \begin{aligned} \text{minimize } & C\left(\vec{r}_A, \vec{r}_S\right) \ \text{subject to } & M_A \cdot \vec{r}_A + M_S \cdot \vec{r}_S + \vec{c}=\vec{b} & &\text{target values} \ \text{and } & | \vec{r}_S |_0 \geq | \vec{r}S |\infty & &\text{"tree of learning"} \ \end{aligned} $$
with the maximal set of constraints for the target values given by: $$ \begin{matrix} Athl \ Awar \ BaSk \ Cunn \ Dece \ Insi \ Inti \ Inve \ Lead \ Medi \ Pers \ Pilo \ PsMa \ Scho \ Stea \ Surv \ Tech \ WeSk \ \ Conv \ Defe \ Dete \ Infl \ MaSh \ MaWo \ Resi \ Reso \ \end{matrix} \left( \begin{matrix} 0 & 0 & 0 & 0 & 1 & 0 & 0 \ 0 & 0 & 0 & 1 & 0 & 0 & 0 \ 1 & 0 & 0 & 0 & 0 & 0 & 0 \ 0 & 1 & 0 & 0 & 0 & 0 & 0 \ 0 & 1 & 0 & 0 & 0 & 0 & 0 \ 0 & 1 & 0 & 0 & 0 & 0 & 0 \ 0 & 0 & 0 & 0 & 0 & 0 & 1 \ 0 & 0 & 0 & 1 & 0 & 0 & 0 \ 0 & 0 & 0 & 0 & 0 & 0 & 1 \ 0 & 0 & 0 & 1 & 0 & 0 & 0 \ 0 & 1 & 0 & 0 & 0 & 0 & 0 \ 1 & 0 & 0 & 0 & 0 & 0 & 0 \ 0 & 0 & 0 & 0 & 0 & 0 & 1 \ 0 & 0 & 0 & 1 & 0 & 0 & 0 \ 1 & 0 & 0 & 0 & 0 & 0 & 0 \ 0 & 0 & 0 & 0 & 0 & 0 & 1 \ 0 & 0 & 0 & 1 & 0 & 0 & 0 \ 0 & 0 & 1 & 0 & 0 & 0 & 0 \ \ 0 & 0 & 0 & 0 & 0 & 0 & 1 \ 0 & 0 & 1 & 0 & 0 & 0 & 0 \ 0 & 0 & 0 & 0 & 0 & 1 & 0 \ 0 & 1 & 0 & 0 & 0 & 0 & 0 \ 0 & 0 & 0 & 0 & 0 & 0 & 1 \ 0 & 0 & 0 & 0 & 0 & 1 & 0 \ 0 & 0 & 0 & 0 & 0 & 1 & 0 \ 0 & 0 & 0 & 0 & 0 & 0 & 1 \ \end{matrix} \right) \cdot \vec{r}{A} + \left( \begin{matrix} \ \ \ \ \ \ \ \ \ \mathbf{I}{|S|}\ \ \ \ \ \ \ \ \ \ \ \ \ \mathbf{0}\ \ \ \ \ \end{matrix} \right) \cdot \vec{r}{S} + \left( \begin{matrix} \ \ \ \ \ \ \ \ \ \vec{0} \ \ \ \ \ \ \ \ \ \ 0 \ -1 \ 0 \ -1 \ Tier \ 2 \cdot Tier \ 1 \ -1 \ \end{matrix} \right) = \left( \begin{matrix} b{Athl} \ b_{Awar} \ b_{BaSk} \ b_{Cunn} \ b_{Dece} \ b_{Insi} \ b_{Inti} \ b_{Inve} \ b_{Lead} \ b_{Medi} \ b_{Pers} \ b_{Pilo} \ b_{PsMa} \ b_{Scho} \ b_{Stea} \ b_{Surv} \ b_{Tech} \ b_{WeSk} \ \ b_{Conv} \ b_{Defe} \ b_{Dete} \ b_{Infl} \ b_{MaSh} \ b_{MaWo} \ b_{Resi} \ b_{Reso} \ \end{matrix} \right) $$