Eden Generation

From Binding of Isaac: Rebirth Wiki
Jump to: navigation, search

This is a technical analysis of how the game randomly generates the various characteristics of the Eden character. Note that this analysis was performed by BLouBLue on version 1.05 (pre-Afterbirth). Thus, it may be obsolete now.

Launch[edit | edit source]

When launching the game, the main thing to know is the game will derive a bunch of subseeds that will be used by RNG instances dedicated to items, trinkets, pickups, enemies, level generation and character management.

This allows you to play your run slightly differently, yet still get the same items at the same place (for the most part - that's not entirely true).

One specific "character" subseed is used for Eden generation. This RNG instance is used during two phases:

  1. generate hearts, pickups and attributes, before level initialization;
  2. randomly pick cards, pills, trinkets and items, after level initialization.

It is reset to the character subseed before either phase. This means the same random numbers could come out of the RNG in the same order for both phases.

Hearts[edit | edit source]

Hearts are the first thing to be generated. The game tries its best to give you 3 full hearts, red and soul hearts altogether:

  1. the game randomly picks x = 0 to 3 red hearts;
  2. it will randomly pick between 0 and (3 - x) soul hearts;
  3. if no red hearts were picked at the first step, the game guarantees the player at least 2 soul hearts.

There cannot be any black hearts from this phase. Collectible items given afterward may exceed those limits.

Keys, Bombs and Coins[edit | edit source]

Eden has a 1/3 chance to start with either keys, bombs or coins. If you've been granted that chance, here are the possible outcomes:

  1. Eden starts with 1 key;
  2. Eden starts with 1 or 2 bombs;
  3. Eden starts with 1 to 5 coins.

Collectible active/passive items given afterward may give additional pickups, bringing the total beyond those limits.

The chance to start with a pickup is calculated as follows:

/* rand(min,max) returns a random integer between min (inclusive) and max (exclusive) */
if( (rand(0,3) == 0)
{  // 1/3 chance that the player will get a pickup
    tmp = rand(0, 3); // either 0, 1 or 2
    if(tmp == 0)
        give 1 key
    else if(tmp == 1)
        give 1 + rand(0,2) bombs
    else
        give 1 + rand(0,5) coins
}

Attributes[edit | edit source]

Attributes are based off Isaac's base attributes. An additive modifier (float value) is randomly chosen for each of the 6 attributes:

Attribute Eden's modifier range
damage -1.00 to +1.00
speed -0.15 to +0.15
tear delay -0.75 to +0.75
tear height -5.00 to +5.00
shot speed -0.25 to +0.25
luck -1.00 to +1.00

Here's what the attribute initialization part of the routine looks like:

// characterRng.Random() returns a float value between 0.0 and 1.0
Player->attributeModifierDamage       = (characterRng.Random() * 2.0f) + -1.0f;
Player->attributeModifierSpeed        = (characterRng.Random() * 0.3f) + -0.15f;
Player->attributeModifierTears        = (characterRng.Random() * 1.5f) + -0.75f;
Player->attributeModifierHeightRange  = (characterRng.Random() * 10.0f) + -5.0f;
Player->attributeModifierShotSpeed    = (characterRng.Random() * 0.5f) + -0.25f;
Player->attributeModifierLuck         = (characterRng.Random() * 2.0f) + -1.0f;

Pocket Items[edit | edit source]

Pills, cards, trinkets and items are picked after level generation. Two phases occur:

  1. give the player a chance to have either a pill, card or trinket;
  2. attempt to pick 1 active item and 1 passive item.

So, when consuming an Eden token, you have:

  • 1/3 chance to get a trinket;
  • 1/6 chance to get a pill;
  • 1/6 chance to get a card;
  • 1/3 chance to get none of the above.

These options are mutually exclusive. You cannot get a trinket and a card or pill. Here's what the actual routine looks like:

if(characterRng.Random(3) > 0) { // 2/3 chance to get pill, card or nothing
    if(characterRng.Random(2) > 0) { // 2/3 * 1/2 chance to get nothing
        give nothing
    }
    else { // 2/3 * 1/2 chance to get a pill or a card
        if(characterRng.Random(2) > 0) { // 1/3 * 1/2 chance to generate a pill
            give a random pill
        }
        else { // 1/3 * 1/2 chance to generate a card
            give a random card
        }
    }
}
else { // 1/3 chance to get a trinket
    give a random trinket
}

More about how pocket items of each type are selected:

  • pill colors and pill effects are shuffled based on a subseed derived from the main seed. 9 pill effects are selected and randomly affecter to each of the 9 flavors. If entitled to, Eden will randomly get one of those 9 pills;
  • trinkets are randomly chosen among the 62 trinkets. There are 32 random repicks, and another pass if unsuccessful. The game may give up, if the player hasn't unlocked any trinket;
  • Eden cannot start with the following cards/runes:
    • any rune,
    • 2 of clubs, 2 of diamonds, 2 of spades, 2 of hearts, Joker;
  • 2 sets of cards are created:
    • a special card set containing: Chaos card, Credit card, Rules card, A card against humanity and Suicide King,
    • a global set containing every other card except those that are forbidden,
    • Eden has a 1/25 chance to randomly get a card from the special set, and a 24/25 chance to get one from the global set.

Here's how the card selection is done, in pseudo-code:

// both isRunesAllowed and isSpecialCardsAllowed are set to false when initializing Eden.
// Card IDs can be found in the game's xml files
// cardRng.Random(min,max) returns a random integer between min (inclusive) and max (exclusive)
// cardRng.Random(max) returns a random integer between 0 (inclusive) and max (exclusive, so max-1 is the upper bound)
if(cardRng.Random(25) > 0) // 24/25 chance 
{
    if(cardRng.Random(10) || !isRunesAllowed) 
    {
        bool tmp = (cardRng.Random(5) != 0) || !isSpecialCardsAllowed;
        if(tmp == false) 
        {
            card_first = CARD_2_OF_CLUBS;
            card_last = CARD_JOKER;
        }
        else
        {
            card_first = CARD_0_THE_FOOL;
            card_last = CARD_XXI_THE_WORLD;
        }
    }
    else
    {
        card_first = RUNE_DESTRUCTION_HAGALAZ;
        card_last = RUNE_RESISTANCE_ALGIZ;
    }
}
else 
{
    card_first = CARD_CHAOS_CARD;
    card_last = CARD_SUICIDE_KING;
}

return cardRng.Random(card_first, card_last + 1);

Collectibles[edit | edit source]

The game does 101 attempts to fill both the active item slot and one passive item slot. At each attempt, it will randomly pick an item ID between 1 and 346.

There are a few disabled item IDs which cause the game to repick a random ID and go on to the next attempt:

  • #43: "Pills here!", an active item that does nothing and looks like a pill.
  • #59: "Tarot Card", a passive item that looks like a card and does nothing.
  • #61: Literally nothing, and is not defined in items.xml.
  • #235: Not Available
  • #238: Key Piece 1
  • #239: Key Piece 2
  • #263: Not Available

The first valid candidate (unlocked item) for the passive or active slot is selected, and will not be overwritten by subsequent attempts (there will be 101 attempts no matter what).

The player is given those items after 101 attempts, whether both slots are filled or not.

Therefore, Eden may start with up to 1 active item and up to 1 passive item. It is highly unlikely that a seed exists which gives Eden only 1 or no item.

As nothing weighs some items specifically, it means each item combo probably has about a 0.005% chance of being your reward. Testing material: 1000 Eden seeds

C# Code[edit | edit source]

Code for calculating Eden's items for a given seed. This is missing trinket, pill and stats calculations.

void Main()
{
	var seeds = new List<uint>();
	for(uint i = 0; i < uint.MaxValue; i++)
	{
		var startSeed = (uint)i;
		var dropSeed = CalculatePlayerSeed(startSeed);
		var items = CalculateEdenItems(dropSeed);

		if (items.Active == 186 && (items.Passive == 276 || items.Passive == 108 || items.Passive == 301) && items.Card == 5)
		{
			seeds.Add(startSeed);	
			SeedToString(startSeed).Dump();
		}
		if (i % 100000000 == 0)
			(":" + i).Dump();
	}
	seeds.Select(s => SeedToString(s)).ToArray().Dump();
}

//ItemCount impacts the item pick RNG even though eden can't start with an item over id 552
public static EdenItems CalculateEdenItems(uint dropSeed, int itemCount = 552)
{
	var rng = new Rng(dropSeed, 0x1, 0x5, 0x13);
	
	var trinket = 0;
	var card = 0;
	var pill = 0;
	var hearts = 0;
	var soulHearts = 0;	
	if ((rng.Next() % 3) == 0) 
	{
		//trinket
	}
	else if ((rng.Next() & 1) == 0) 
	{
		if ((rng.Next() & 1) == 0) 
		{
			card = GetCard(rng.Next());
		}
		else
		{
			//pill
			var pillSeed = rng.Next();
		}
	}
	
	var activeId = 0;
	var passiveId = 0;
	for (var i = 0; i < 100; i++) 
	{
		int itemId = (int)(rng.Next() % itemCount) + 1;
		
		//BlackList
		if (itemId > 552)
			continue;
		switch(itemId){
			case 0x3B:
			case 0xEB:
			case 0x107:
			case 0x2b:
			case 0x3d:
			case 0xee:
			case 0xef:
			case 0x226:
			case 0x227:
			case 0x228:
				continue;
			default:
				break;
		}
		
		var itemType = ItemConfig[itemId];
		if (itemType == ItemType.Active)
		{
			activeId = activeId != 0 ? activeId : itemId;
		} 
		else if (itemType == ItemType.Passive || itemType == ItemType.Familiar) 
		{
			passiveId = passiveId != 0 ? passiveId : itemId;
		}
		//Isaac doesn't exit early :thinking:
		if (activeId != 0 && passiveId != 0)
			break;

	}
	
	//Hearts and SoulHearts are actually done in Player::Init
	var healthRng = new Rng(dropSeed, 0x1, 0x5, 0x13);
	var halfHearts = (int)healthRng.Next() & 3;
	hearts = halfHearts * 2;
	soulHearts = ((int)healthRng.Next() % (4 - halfHearts)) * 2;
	if (hearts == 0 && soulHearts < 4)
	 	soulHearts = 4;
		
	
	
	return new EdenItems(hearts, soulHearts, activeId, passiveId, trinket, card, pill);
}
public static int GetCard(uint seed, bool playing = false)
{
	var cardRng = new Rng(seed, 0x5, 0x9, 0x7);
	if (cardRng.Next() % 25 == 0) {
		return (int)(cardRng.Next() % 13) + 42;
	}
	if (cardRng.Next() % 10 == 0)
	{
		//Rune
	}
	if (cardRng.Next() % 5 == 0 && playing)
	{
		//Playing card
		return (int)(cardRng.Next() % 9) + 23;
	}
	return (int)(cardRng.Next() % 22) + 1;
}


public static uint CalculatePlayerInitSeed(uint startSeed)
{
	var startRng = new Rng(startSeed, 0x3, 0x17, 0x19);
	//Stage Seeds
	for (var i = 0; i < 0xD; i++)
		startRng.Next();
	return startRng.Next(); //Seeds::PlayerInitSeed
}

public static uint CalculatePlayerSeed(uint startSeed)
{
	var playerInitSeed = CalculatePlayerInitSeed(startSeed);

	//These happen inside Player::Init
	var playerInitRng = new Rng(playerInitSeed, 0x1, 0xB, 0x10);
	//CollectiblesRNG seed
	playerInitRng.Next();
	playerInitRng.Next();
	playerInitRng.Next();
	//CardsRNG seed
	playerInitRng.Next();
	
	return playerInitRng.Next(); //Entity::DropSeed
}

public class EdenItems
{
	public int Hearts;
	public int SoulHearts;
	public int Active;
	public int Passive;
	public int Trinket;
	public int Card;
	public int Pill;
	public EdenItems(int hearts, int soulHearts, int active, int passive, int trinket, int card, int pill) 
	{
		Hearts = hearts;
		SoulHearts = soulHearts;
		Active = active;
		Passive = passive;
		Trinket = trinket;
		Card = card;
		Pill = pill;
	}
}

static string SeedToString(uint num)
{
	const string chars = "ABCDEFGHJKLMNPQRSTWXYZ01234V6789";
	byte x = 0;
	var tnum = num;
	while (tnum != 0)
	{
		x += ((byte)tnum);
		x += (byte)(x + (x >> 7));
		tnum >>= 5;
	}
	num ^= 0x0FEF7FFD;
	tnum = (num) << 8 | x;

	var ret = new char[8];
	for (int i = 0; i < 6; i++)
	{
		ret[i] = chars[(int)(num >> (27 - (i * 5)) & 0x1F)];
	}
	ret[6] = chars[(int)(tnum >> 5 & 0x1F)];
	ret[7] = chars[(int)(tnum & 0x1F)];

	return new string(ret);
}

public class Rng
{
	public uint Seed;
	public int Shift1;
	public int Shift2;
	public int Shift3;
	public uint Next()
	{
		var num = Seed;
		num ^= num >> Shift1;
		num ^= num << Shift2;
		num ^= num >> Shift3;
		Seed = num;
		return num;
	}
	
	public Rng(uint seed, int s1, int s2, int s3) {
		this.Seed = seed;
		Shift1 = s1;
		Shift2 = s2;
		Shift3 = s3;
	}
};

public enum ItemType {
	Null = 0,
	Passive = 1,
	Trinket = 2,
	Active = 3,
	Familiar = 4,
}

public static ItemType[] ItemConfig = GetItemConfig();
public static ItemType[] GetItemConfig() {
	var itemConfig = new ItemType[553];
	return itemConfig;
}

The full code can be found here Blade's GitHub Gist.