Cognoscan
2015-07-07, 05:05 PM
So I'm really late to the party on this, but a friend of mine introduced me to the Sacred Geometry (http://www.d20pfsrd.com/feats/general-feats/sacred-geometry) feat, and I just had to break it.
For those not familiar with the feat, it allows you to add 2 metamagic effects to a spell for free, without requiring you to take any metamagic feats. The only catch is that you have to solve a math problem:
1. Roll a number of d6 equal to your ranks in Knowledge(engineering)
2. Using addition, subtraction, multiplication, and division, get a single prime number from the resulting rolls.
3. The prime number desired varies depending on what level the spell would be if the metamagic effects were applied
There's another feat, Calculating Mind (http://www.d20pfsrd.com/feats/general-feats/calculating-mind), that changes this to use d8, but it's not really worth it, as I will show shortly.
I wrote a program to brute force solve any set of dice rolled for any target spell level. If there is a solution, my program can find it. There's an app on the android store that does much the same thing, but I wanted to write my own so I could see what the true probabilities are for finding a solution. I ended up running my brute force solver for every possible combination of dice, from 1 die up to 8 dice. So the results are exactly how likely you are to pull off casting a spell with a given number of dice.
Without taking "Calculating Mind", 6 ranks in Knowledge(Engineering) is enough to cast 9th level spells with a 92% success rate. Near 100% success occurs upon reaching 8 ranks. With "Calculating Mind", this occurs approximately 1 rank sooner, making the additional feat of little benefit.
This is, as we all knew, a completely broken feat. Either the player can use this program to always cast with 2 metamagic feats applied to a spell, or they can take forever finding a solution, knowing that one exists. Still, it's pretty cool that it requires at a bit of algorithm work to get your godlike powers.
My full probability results are below:
Using d6 for dice:
Dice | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
--------|-------|-------|-------|-------|-------|-------|-------|-------|
Level 1 | 0.333 | 0.556 | 0.917 | 1.000 | 1.000 | 1.000 | 1.000 | 1.000 |
Level 2 | 0.000 | 0.056 | 0.528 | 0.944 | 0.996 | 1.000 | 1.000 | 1.000 |
Level 3 | 0.000 | 0.000 | 0.250 | 0.779 | 0.983 | 0.999 | 1.000 | 1.000 |
Level 4 | 0.000 | 0.000 | 0.069 | 0.542 | 0.930 | 0.995 | 1.000 | 1.000 |
Level 5 | 0.000 | 0.000 | 0.000 | 0.361 | 0.864 | 0.985 | 0.999 | 1.000 |
Level 6 | 0.000 | 0.000 | 0.000 | 0.134 | 0.791 | 0.969 | 0.996 | 1.000 |
Level 7 | 0.000 | 0.000 | 0.000 | 0.063 | 0.674 | 0.959 | 0.996 | 0.999 |
Level 8 | 0.000 | 0.000 | 0.000 | 0.046 | 0.628 | 0.942 | 0.992 | 0.999 |
Level 9 | 0.000 | 0.000 | 0.000 | 0.046 | 0.532 | 0.918 | 0.987 | 0.998 |
Using d8 for dice:
Dice | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
--------|-------|-------|-------|-------|-------|-------|-------|
Level 1 | 0.375 | 0.500 | 0.898 | 0.999 | 1.000 | 1.000 | 1.000 |
Level 2 | 0.000 | 0.156 | 0.586 | 0.960 | 0.999 | 1.000 | 1.000 |
Level 3 | 0.000 | 0.000 | 0.311 | 0.853 | 0.992 | 1.000 | 1.000 |
Level 4 | 0.000 | 0.000 | 0.176 | 0.712 | 0.971 | 0.999 | 1.000 |
Level 5 | 0.000 | 0.000 | 0.105 | 0.595 | 0.950 | 0.997 | 1.000 |
Level 6 | 0.000 | 0.000 | 0.035 | 0.398 | 0.903 | 0.991 | 0.999 |
Level 7 | 0.000 | 0.000 | 0.006 | 0.283 | 0.866 | 0.991 | 0.999 |
Level 8 | 0.000 | 0.000 | 0.000 | 0.210 | 0.844 | 0.987 | 0.999 |
Level 9 | 0.000 | 0.000 | 0.000 | 0.131 | 0.754 | 0.974 | 0.998 |
As for the program itself, I've included the full source code, written in C, below. Just compile it and you too can have your own completely broken feat! Plus, as a bonus, it even adds parentheses to the equation, which the Android app does not do.
/*
* Program: SacredGeometry
* Author: Cognoscan
* Date: 2015-07-07
*
* Brute forces a solution to the Sacred Geometry feat in pathfinder, if a
* solution exists. Takes in a target level, and a set of dice rolls. It then
* runs through every unique permutation of the dice rolls and tries all
* possible operand sequences. If there is a solution, it will find it. May take
* a while for large dice pools, but honestly, I'm pretty sure it becomes
* exponentially more likely that a solution exists as dice pool size increases.
* So don't bother with a full 20 dice unless you need to.
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
const int goals[9][3] = {
{ 3, 5, 7},
{11, 13, 17},
{19, 23, 29},
{31, 37, 41},
{43, 47, 53},
{59, 61, 67},
{71, 73, 79},
{83, 89, 97},
{101, 103, 107}};
void swap(int*, int*);
void reverse(int* array, int length);
int intCompare (const void *a, const void *b);
void printResult (const int dice[], const int ops[], int length, int result);
bool nextPermutation(int data[], int length);
int computeTest(int working, int index, int lvl, const int data[], int ops[], int length);
int main(int argc, char *argv[]) {
int dice[20] = {0};
int lvl = 0;
int ops[20] = {0}; // 20 large so we can index into ops[19] without causing an exception
int count = 0;
int goal = -1;
// Grab Spell Level
printf("Pathfinder - Sacred Geometry\n");
printf("Feat Solution Finder\n");
printf("============================\n");
printf("Spell Level: ");
while(!scanf("%i", &lvl));
lvl = (lvl > 9) ? 9 : lvl;
lvl = (lvl < 1) ? 1 : lvl;
lvl--;
// Grab Dice Rolls
printf("Dice Rolls (spaces between, end with 0): ");
while(count < 20) {
if(!scanf("%i", &dice[count])) break;
if (dice[count] < 1 || dice[count] > 8) break;
count++;
}
// Permutation algorithm expects sorted array
qsort(dice, count, sizeof(int), intCompare);
// Run Recursive brute-force algorithm on each permutation. Stop when goal is
// found or when there are no more permutations
goal = computeTest(dice[0], 1, lvl, dice, ops, count);
while((goal < 0) && nextPermutation(dice, count)) {
goal = computeTest(dice[0], 1, lvl, dice, ops, count);
}
// If goal value was reached, print the solution
if (goal >= 0) {
printResult(dice, ops, count, goals[lvl][goal]);
} else {
printf("No valid result found!\n");
}
return 0;
}
// Swap two numbers
void swap(int* a, int* b) {
int temp = *b;
*b = *a;
*a = temp;
}
// Reverse a given array of some length
void reverse(int* array, int length) {
for(int i=0; i<length/2; i++) {
swap(&array[i], &array[length-1-i]);
}
}
// Integer compare function for qsort
int intCompare (const void *a, const void *b) {
if ( *(int*)a < *(int*)b ) return -1;
if ( *(int*)a > *(int*)b ) return 1;
return 0; // They are equal
}
// Get next permutation of data array, given an initially ordered array where
// a[k] <= a[k+1]. Returns false when all permutations have been done. If there
// are repeated values in the data, it handles them by only generating distinct
// permutations (no repeats due to repeated values).
bool nextPermutation(int data[], int length) {
int k = -1;
int l = 0;
// Find the largest index k such that a[k] < a[k+1]
for (int i=length-2; i>=0; i--) {
if (data[i] < data[i+1]) {
k = i;
break;
}
}
if (k == -1) return false;
// Find the largest index l greater than k such that a[k] < a[l].
for (int i=length-1; i>=k; i--) {
if (data[k] < data[i]) {
l = i;
break;
}
}
if (l <= k) return false; // This should never happen.
// Swap the value of a[k] with that of a[l].
swap(&data[k], &data[l]);
// Reverse the sequence from a[k + 1] up to and including the final element a[n]
reverse(&data[k+1], length-k-1);
return true;
}
// Print a Valid solution, with parentheses
void printResult (const int dice[], const int ops[], int length, int result) {
printf("Valid: %i = ", result);
// Print out opening parentheses
for (int i=0; i<length-1; i++) {
if (ops[i] < 2 && ops[i+1] >= 2) {
putchar('(');
}
}
printf("%i ", dice[0]);
for (int i=1; i < length; i++) {
switch(ops[i-1]) {
case 0: printf("+ "); break;
case 1: printf("- "); break;
case 2: printf("* "); break;
case 3: printf("/ "); break;
}
printf("%i", dice[i]);
if (ops[i-1] < 2 && ops[i] >= 2) {
putchar(')');
}
putchar(' ');
}
printf("\n");
}
/*
* Recursive Testing of all possible operand sequences on a given data set.
* Inputs:
* working - result of last operation. When first called, this should be data[0]
* index - Current index of data[] that is being used to perform next operation
* lvl - For indexing into goals array to see if valid solution was reached.
* data[] - The data being operated on
* ops[] - Array to record the operands used.
* length - Total length of data[] array
* Returns:
* -1 if no valid solution reached, otherwise the index of the goal value reached.
*
* Operand keys are:
* 0 - Addition
* 1 - Subtraction
* 2 - Multiplication
* 3 - Division
*/
int computeTest(int working, int index, int lvl, const int data[], int ops[], int length) {
int ret = -1;
// Final value computed. Test against target values
if (index == length) {
if (working == goals[lvl][0]) return 0;
if (working == goals[lvl][1]) return 1;
if (working == goals[lvl][2]) return 2;
return -1;
}
// Test with the 4 possible arithmetic operations
for (int i=0; i<4; i++) {
switch(i) {
case 0: ret = computeTest(working+data[index], index+1, lvl, data, ops, length); break;
case 1: ret = computeTest(working-data[index], index+1, lvl, data, ops, length); break;
case 2: ret = computeTest(working*data[index], index+1, lvl, data, ops, length); break;
case 3: if (working % data[index] == 0) {
ret = computeTest(working/data[index], index+1, lvl, data, ops, length);
} else {
ret = -1;
}
break;
}
// Check to see if we found a solution
if (ret >= 0) {
ops[index-1] = i; // Record the operand
break;
}
}
return ret;
}
For those not familiar with the feat, it allows you to add 2 metamagic effects to a spell for free, without requiring you to take any metamagic feats. The only catch is that you have to solve a math problem:
1. Roll a number of d6 equal to your ranks in Knowledge(engineering)
2. Using addition, subtraction, multiplication, and division, get a single prime number from the resulting rolls.
3. The prime number desired varies depending on what level the spell would be if the metamagic effects were applied
There's another feat, Calculating Mind (http://www.d20pfsrd.com/feats/general-feats/calculating-mind), that changes this to use d8, but it's not really worth it, as I will show shortly.
I wrote a program to brute force solve any set of dice rolled for any target spell level. If there is a solution, my program can find it. There's an app on the android store that does much the same thing, but I wanted to write my own so I could see what the true probabilities are for finding a solution. I ended up running my brute force solver for every possible combination of dice, from 1 die up to 8 dice. So the results are exactly how likely you are to pull off casting a spell with a given number of dice.
Without taking "Calculating Mind", 6 ranks in Knowledge(Engineering) is enough to cast 9th level spells with a 92% success rate. Near 100% success occurs upon reaching 8 ranks. With "Calculating Mind", this occurs approximately 1 rank sooner, making the additional feat of little benefit.
This is, as we all knew, a completely broken feat. Either the player can use this program to always cast with 2 metamagic feats applied to a spell, or they can take forever finding a solution, knowing that one exists. Still, it's pretty cool that it requires at a bit of algorithm work to get your godlike powers.
My full probability results are below:
Using d6 for dice:
Dice | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
--------|-------|-------|-------|-------|-------|-------|-------|-------|
Level 1 | 0.333 | 0.556 | 0.917 | 1.000 | 1.000 | 1.000 | 1.000 | 1.000 |
Level 2 | 0.000 | 0.056 | 0.528 | 0.944 | 0.996 | 1.000 | 1.000 | 1.000 |
Level 3 | 0.000 | 0.000 | 0.250 | 0.779 | 0.983 | 0.999 | 1.000 | 1.000 |
Level 4 | 0.000 | 0.000 | 0.069 | 0.542 | 0.930 | 0.995 | 1.000 | 1.000 |
Level 5 | 0.000 | 0.000 | 0.000 | 0.361 | 0.864 | 0.985 | 0.999 | 1.000 |
Level 6 | 0.000 | 0.000 | 0.000 | 0.134 | 0.791 | 0.969 | 0.996 | 1.000 |
Level 7 | 0.000 | 0.000 | 0.000 | 0.063 | 0.674 | 0.959 | 0.996 | 0.999 |
Level 8 | 0.000 | 0.000 | 0.000 | 0.046 | 0.628 | 0.942 | 0.992 | 0.999 |
Level 9 | 0.000 | 0.000 | 0.000 | 0.046 | 0.532 | 0.918 | 0.987 | 0.998 |
Using d8 for dice:
Dice | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
--------|-------|-------|-------|-------|-------|-------|-------|
Level 1 | 0.375 | 0.500 | 0.898 | 0.999 | 1.000 | 1.000 | 1.000 |
Level 2 | 0.000 | 0.156 | 0.586 | 0.960 | 0.999 | 1.000 | 1.000 |
Level 3 | 0.000 | 0.000 | 0.311 | 0.853 | 0.992 | 1.000 | 1.000 |
Level 4 | 0.000 | 0.000 | 0.176 | 0.712 | 0.971 | 0.999 | 1.000 |
Level 5 | 0.000 | 0.000 | 0.105 | 0.595 | 0.950 | 0.997 | 1.000 |
Level 6 | 0.000 | 0.000 | 0.035 | 0.398 | 0.903 | 0.991 | 0.999 |
Level 7 | 0.000 | 0.000 | 0.006 | 0.283 | 0.866 | 0.991 | 0.999 |
Level 8 | 0.000 | 0.000 | 0.000 | 0.210 | 0.844 | 0.987 | 0.999 |
Level 9 | 0.000 | 0.000 | 0.000 | 0.131 | 0.754 | 0.974 | 0.998 |
As for the program itself, I've included the full source code, written in C, below. Just compile it and you too can have your own completely broken feat! Plus, as a bonus, it even adds parentheses to the equation, which the Android app does not do.
/*
* Program: SacredGeometry
* Author: Cognoscan
* Date: 2015-07-07
*
* Brute forces a solution to the Sacred Geometry feat in pathfinder, if a
* solution exists. Takes in a target level, and a set of dice rolls. It then
* runs through every unique permutation of the dice rolls and tries all
* possible operand sequences. If there is a solution, it will find it. May take
* a while for large dice pools, but honestly, I'm pretty sure it becomes
* exponentially more likely that a solution exists as dice pool size increases.
* So don't bother with a full 20 dice unless you need to.
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
const int goals[9][3] = {
{ 3, 5, 7},
{11, 13, 17},
{19, 23, 29},
{31, 37, 41},
{43, 47, 53},
{59, 61, 67},
{71, 73, 79},
{83, 89, 97},
{101, 103, 107}};
void swap(int*, int*);
void reverse(int* array, int length);
int intCompare (const void *a, const void *b);
void printResult (const int dice[], const int ops[], int length, int result);
bool nextPermutation(int data[], int length);
int computeTest(int working, int index, int lvl, const int data[], int ops[], int length);
int main(int argc, char *argv[]) {
int dice[20] = {0};
int lvl = 0;
int ops[20] = {0}; // 20 large so we can index into ops[19] without causing an exception
int count = 0;
int goal = -1;
// Grab Spell Level
printf("Pathfinder - Sacred Geometry\n");
printf("Feat Solution Finder\n");
printf("============================\n");
printf("Spell Level: ");
while(!scanf("%i", &lvl));
lvl = (lvl > 9) ? 9 : lvl;
lvl = (lvl < 1) ? 1 : lvl;
lvl--;
// Grab Dice Rolls
printf("Dice Rolls (spaces between, end with 0): ");
while(count < 20) {
if(!scanf("%i", &dice[count])) break;
if (dice[count] < 1 || dice[count] > 8) break;
count++;
}
// Permutation algorithm expects sorted array
qsort(dice, count, sizeof(int), intCompare);
// Run Recursive brute-force algorithm on each permutation. Stop when goal is
// found or when there are no more permutations
goal = computeTest(dice[0], 1, lvl, dice, ops, count);
while((goal < 0) && nextPermutation(dice, count)) {
goal = computeTest(dice[0], 1, lvl, dice, ops, count);
}
// If goal value was reached, print the solution
if (goal >= 0) {
printResult(dice, ops, count, goals[lvl][goal]);
} else {
printf("No valid result found!\n");
}
return 0;
}
// Swap two numbers
void swap(int* a, int* b) {
int temp = *b;
*b = *a;
*a = temp;
}
// Reverse a given array of some length
void reverse(int* array, int length) {
for(int i=0; i<length/2; i++) {
swap(&array[i], &array[length-1-i]);
}
}
// Integer compare function for qsort
int intCompare (const void *a, const void *b) {
if ( *(int*)a < *(int*)b ) return -1;
if ( *(int*)a > *(int*)b ) return 1;
return 0; // They are equal
}
// Get next permutation of data array, given an initially ordered array where
// a[k] <= a[k+1]. Returns false when all permutations have been done. If there
// are repeated values in the data, it handles them by only generating distinct
// permutations (no repeats due to repeated values).
bool nextPermutation(int data[], int length) {
int k = -1;
int l = 0;
// Find the largest index k such that a[k] < a[k+1]
for (int i=length-2; i>=0; i--) {
if (data[i] < data[i+1]) {
k = i;
break;
}
}
if (k == -1) return false;
// Find the largest index l greater than k such that a[k] < a[l].
for (int i=length-1; i>=k; i--) {
if (data[k] < data[i]) {
l = i;
break;
}
}
if (l <= k) return false; // This should never happen.
// Swap the value of a[k] with that of a[l].
swap(&data[k], &data[l]);
// Reverse the sequence from a[k + 1] up to and including the final element a[n]
reverse(&data[k+1], length-k-1);
return true;
}
// Print a Valid solution, with parentheses
void printResult (const int dice[], const int ops[], int length, int result) {
printf("Valid: %i = ", result);
// Print out opening parentheses
for (int i=0; i<length-1; i++) {
if (ops[i] < 2 && ops[i+1] >= 2) {
putchar('(');
}
}
printf("%i ", dice[0]);
for (int i=1; i < length; i++) {
switch(ops[i-1]) {
case 0: printf("+ "); break;
case 1: printf("- "); break;
case 2: printf("* "); break;
case 3: printf("/ "); break;
}
printf("%i", dice[i]);
if (ops[i-1] < 2 && ops[i] >= 2) {
putchar(')');
}
putchar(' ');
}
printf("\n");
}
/*
* Recursive Testing of all possible operand sequences on a given data set.
* Inputs:
* working - result of last operation. When first called, this should be data[0]
* index - Current index of data[] that is being used to perform next operation
* lvl - For indexing into goals array to see if valid solution was reached.
* data[] - The data being operated on
* ops[] - Array to record the operands used.
* length - Total length of data[] array
* Returns:
* -1 if no valid solution reached, otherwise the index of the goal value reached.
*
* Operand keys are:
* 0 - Addition
* 1 - Subtraction
* 2 - Multiplication
* 3 - Division
*/
int computeTest(int working, int index, int lvl, const int data[], int ops[], int length) {
int ret = -1;
// Final value computed. Test against target values
if (index == length) {
if (working == goals[lvl][0]) return 0;
if (working == goals[lvl][1]) return 1;
if (working == goals[lvl][2]) return 2;
return -1;
}
// Test with the 4 possible arithmetic operations
for (int i=0; i<4; i++) {
switch(i) {
case 0: ret = computeTest(working+data[index], index+1, lvl, data, ops, length); break;
case 1: ret = computeTest(working-data[index], index+1, lvl, data, ops, length); break;
case 2: ret = computeTest(working*data[index], index+1, lvl, data, ops, length); break;
case 3: if (working % data[index] == 0) {
ret = computeTest(working/data[index], index+1, lvl, data, ops, length);
} else {
ret = -1;
}
break;
}
// Check to see if we found a solution
if (ret >= 0) {
ops[index-1] = i; // Record the operand
break;
}
}
return ret;
}