Old Card Cost Theory has been useful but I have often mentioned that it was likely flawed due to its skyhook nature. Now I have found a ground from which a foundation can be built.
I created an optimization simulator.
Spoiler for Hidden:
Code is in Java
import java.util.Random;
public class EtgPseudocode
{
public static void main(String[] args)
{
for(int lifespan=7; lifespan<=7; lifespan = lifespan+2)
{
for(int cost=0; cost<=4; cost = cost+2)
{
for(int turnOfNote=1; turnOfNote<=24; turnOfNote++)
{
optimize(turnOfNote, lifespan, cost);
}
}
}
}
private static void optimize(int turnOfNote, int lifespan, int cost)
{
int numOfPills = 0;
double bestAverage = 0;
double averageTotalDamage = 0;
double totalDamage;
for(int ii=0; ii<=30; ii++)
{
totalDamage = 0;
for(int jj=0; jj<500; jj++)
{
totalDamage += simulate(ii,turnOfNote,lifespan,cost);
}
averageTotalDamage = totalDamage/1000;//each of the 500 simulations returns 2 games
if(averageTotalDamage > bestAverage)
{
numOfPills = ii;
bestAverage = averageTotalDamage;
}
}
System.out.println("Turn of Note:" + turnOfNote + ", Lifespan:" + lifespan
+ ", Casting Cost:" + cost + ", # of Pillars:" + numOfPills + ", Average Total Damage:" + bestAverage);
}
private static int simulate(int numOfPills, int turnOfNote, int lifespan, int cost)
{
int[] deck = new int[30];
int[] creatures = new int[30];
int deckIndex = 0;
int creatureIndex;
int pillars;
int creaturesInHand;
int quanta;
int totalDamage = 0;
int openingHandLength = 7;
if(turnOfNote == 24)//if going second
{
openingHandLength = 6;
//on the 24th turn going second would deckout so going second was ignored
}
for(int jj=0; jj<2; jj++)//once for going second(7+draw), once for going first(6+draw)
{
//Field strts empty
for(int ii=0; ii<30; ii++)
{
creatures[ii]=0;
}
//Deck starts with 30 cards
for(int ii=0; ii<30; ii++)//for each slot in the deck
{
if(ii<numOfPills)//put all the pillars in front and
{
deck[ii]=0; //set as a pillar
}
else
{
deck[ii]=1; //set as a creature
}
}
//Deck is shuffled
int tempValue = 0;
int randIndex;
Random rnd = new Random();
for(int ii=30; ii>0; ii--)
{
tempValue = deck[ii-1];
randIndex = rnd.nextInt(ii);
deck[ii-1] = deck[randIndex];
deck[randIndex] = tempValue;
}
pillars = 0;
creaturesInHand = 0;
creatureIndex = 0;
//Draw opening hand
for(int ii=0; ii<openingHandLength; ii++)//populate hand (draw 6/7 cards)
{
if(deck[ii] == 0)//if card is a pillar
{
pillars++;
}
else//if card is a creature
{
creaturesInHand++;
}
}
deckIndex = openingHandLength;//keeps track of next card to draw
quanta = 0;
//Run each turn
for(int ii=0; ii<turnOfNote; ii++)//for each turn
{
//draw a card
if(deck[deckIndex] == 0)//if card is a pillar
{
pillars++;
}
else//if card is a creature
{
creaturesInHand++;
}
deckIndex++;
//play creatures
while(quanta >= cost)
{
if(creaturesInHand == 0)
{
break;
}
quanta = quanta - cost;
creatures[creatureIndex] = lifespan;
creatureIndex++;
creaturesInHand--;
}
//combat
for(int kk=0; kk<creatures.length; kk++)
{
if(creatures[kk] > 0)
{
totalDamage++;
creatures[kk] = creatures[kk] - 1;
}
}
//end turn
quanta = quanta + 1 + pillars;//generate quanta from mark and pillars
if(creaturesInHand > 7)//discard if necessary
{
creaturesInHand = 7;
}
}
openingHandLength = 6;
}
return totalDamage;
}
}
Spoiler for copy of code:
import java.util.Random;
public class EtgPseudocode
{
public static void main(String[] args)
{
for(int lifespan=7; lifespan<=7; lifespan = lifespan+2)
{
for(int cost=0; cost<=4; cost = cost+2)
{
for(int turnOfNote=1; turnOfNote<=24; turnOfNote++)
{
optimize(turnOfNote, lifespan, cost);
}
}
}
}
private static void optimize(int turnOfNote, int lifespan, int cost)
{
int numOfPills = 0;
double bestAverage = 0;
double averageTotalDamage = 0;
double totalDamage;
for(int ii=0; ii<=30; ii++)
{
totalDamage = 0;
for(int jj=0; jj<500; jj++)
{
totalDamage += simulate(ii,turnOfNote,lifespan,cost);
}
averageTotalDamage = totalDamage/1000;//each of the 500 simulations returns 2 games
if(averageTotalDamage > bestAverage)
{
numOfPills = ii;
bestAverage = averageTotalDamage;
}
}
System.out.println("Turn of Note:" + turnOfNote + ", Lifespan:" + lifespan
+ ", Casting Cost:" + cost + ", # of Pillars:" + numOfPills + ", Average Total Damage:" + bestAverage);
}
private static int simulate(int numOfPills, int turnOfNote, int lifespan, int cost)
{
int[] deck = new int[30];
int[] creatures = new int[30];
int deckIndex = 0;
int creatureIndex;
int pillars;
int creaturesInHand;
int quanta;
int totalDamage = 0;
int openingHandLength = 7;
if(turnOfNote == 24)//if going second
{
openingHandLength = 6;
//on the 24th turn going second would deckout so going second was ignored
}
for(int jj=0; jj<2; jj++)//once for going second(7+draw), once for going first(6+draw)
{
//Field strts empty
for(int ii=0; ii<30; ii++)
{
creatures[ii]=0;
}
//Deck starts with 30 cards
for(int ii=0; ii<30; ii++)//for each slot in the deck
{
if(ii<numOfPills)//put all the pillars in front and
{
deck[ii]=0; //set as a pillar
}
else
{
deck[ii]=1; //set as a creature
}
}
//Deck is shuffled
int tempValue = 0;
int randIndex;
Random rnd = new Random();
for(int ii=30; ii>0; ii--)
{
tempValue = deck[ii-1];
randIndex = rnd.nextInt(ii);
deck[ii-1] = deck[randIndex];
deck[randIndex] = tempValue;
}
pillars = 0;
creaturesInHand = 0;
creatureIndex = 0;
//Draw opening hand
for(int ii=0; ii<openingHandLength; ii++)//populate hand (draw 6/7 cards)
{
if(deck[ii] == 0)//if card is a pillar
{
pillars++;
}
else//if card is a creature
{
creaturesInHand++;
}
}
deckIndex = openingHandLength;//keeps track of next card to draw
quanta = 0;
//Run each turn
for(int ii=0; ii<turnOfNote; ii++)//for each turn
{
//draw a card
if(deck[deckIndex] == 0)//if card is a pillar
{
pillars++;
}
else//if card is a creature
{
creaturesInHand++;
}
deckIndex++;
//play creatures
while(quanta >= cost)
{
if(creaturesInHand == 0)
{
break;
}
quanta = quanta - cost;
creatures[creatureIndex] = lifespan;
creatureIndex++;
creaturesInHand--;
}
//combat
for(int kk=0; kk<creatures.length; kk++)
{
if(creatures[kk] > 0)
{
totalDamage++;
creatures[kk] = creatures[kk] - 1;
}
}
//end turn
quanta = quanta + 1 + pillars;//generate quanta from mark and pillars
if(creaturesInHand > 7)//discard if necessary
{
creaturesInHand = 7;
}
}
openingHandLength = 6;
}
return totalDamage;
}
}
Sorry for my sparse commenting. Edit the Main method loops to set the values for the hypothetical creature you are testing.
That simulator would take 30 card decks comprised only of pillars and hypothetical creature A(attack 1, cost X, lives N turns) in an ideal ratio. The ideal ratio ignored card number limits and was calculated individually for every turn and every hypothetical creature examined. I collected the data
here. I find the resulting graphs(sheet 2) to be most interesting.
Now those graphs are a bit intimidating. However we can simplify that with an example. Let's take a metagame balanced around turn 10 where our creature in question would survive about 7 turns on average before falling to CC. If our creature cost 8, then we would expect to see 29 turns of action during a game in a deck optimized for its cost. If it cost 2 we would expect to see 59 turns of action during a game in a deck optimized for its cost. If the creature gives a linear contribution(say attacking) then a 2 cost version should be half as powerful as an 8 cost version since it would see twice as many turns. If it cost 4 it would see 44 turns and thus should be 2/3rds as powerful as the 8 cost version and 4/3rds as powerful as the 2 cost version. (See the non linear nature)
Now the first 3 charts have a flaw. They do not already account for the cost of the CC the opponent is theoretically using to shorten the lives of our creatures. That cost would need to be taken into account when making judgement calls about specific creature balancing.
Finally I will admit that this is more of a data dump than a conclusion dump. I am expecting many brains will be able to decipher the data better than just my own perspective. Plus data has this nice objective taste to it.
PS: Post 10,000! Yeah!