An Alchemy Engine for a Game in JavaScript

The code below implements an alchemy engine for a video game in JavaScript. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

The player clicks the left and right arrow buttons to select three ingredients to combine, then clicks the “Combine” button to combine them into potions. If the three ingredients all share a common property, then they will be transfomed into a potion named for that shared property. If, on the other hand, no such common property exists, then the ingredients are consumed without any potion being created.

Specifically, you can combine the ingredients named A, B, and C to make a Potion of Strength, or the ingredients B, D and F to make a Potion of Intelligence. A, D, and E combine to make a Potion of Dexterity, and C plus E plus F yields a potion of Charisma. All other combinations fizzle.

The idea was to approximate the way that alchemy works in the Elder Scrolls games by Bethesda Softworks.  Obviously, six ingredients and four potion types don’t make for a very exciting experience in and of themselves, so you’d need to add more to make things really interesting.

AlchemySession

<html>
<body>

<div id="divMain"></div>

<script type="text/javascript">

// main

function main()
{
	var itemDefns = 
	[
		new ItemDefn("A", true, [ "Strength", "Dexterity" ] ),
		new ItemDefn("B", true, [ "Strength", "Intelligence"] ),
		new ItemDefn("C", true, [ "Strength", "Charisma"] ),
		new ItemDefn("D", true, [ "Dexterity", "Intelligence" ]),
		new ItemDefn("E", true, [ "Dexterity", "Charisma" ]),
		new ItemDefn("F", true, [ "Intelligence", "Charisma" ]),
	];

	var itemsIngredients = 
	[
		new Item("A", 5),
		new Item("B", 5),
		new Item("C", 5),
		new Item("D", 5),
		new Item("E", 5),
		new Item("F", 5),
	];

	var alchemySession = new AlchemySession
	(
		itemDefns,
		itemsIngredients
	);
	alchemySession.domElementUpdate();
}

// classes

function AlchemySession(itemDefns, itemsIngredients)
{
	this.itemDefns = itemDefns;
	this.itemDefns.addLookups("name");
	this.itemHolderIngredients = new ItemHolder
	(
		"Ingredients", itemsIngredients
	);
	this.itemHolderStage = new ItemHolder
	(
		"Stage", []
	);
	this.itemHolderProducts = new ItemHolder
	(
		"Products", []
	);
}
{
	AlchemySession.prototype.combine = function()
	{
		var ingredients = this.itemHolderStage.items;

		var numberOfIngredientsRequired = 3;
		if (ingredients.length != numberOfIngredientsRequired)
		{
			alert("You need exactly " + numberOfIngredientsRequired + " ingredients.");
		}
		else
		{
			this.combine_ProcessIngredients
			(
				ingredients, 
				// numberOfIngredientsForEffect
				numberOfIngredientsRequired 
			);

		} // end if ingredients.length == numberOfIngredientsRequired
	}

	AlchemySession.prototype.combine_ProcessIngredients = function
	(
		ingredients, numberOfIngredientsForEffect
	)
	{
		var propertyNamesInEffect = this.combine_ProcessIngredients_FindEffects
		(
			ingredients,
			numberOfIngredientsForEffect
		);

		this.combine_ProcessIngredients_ConsumeIngredients(ingredients);
		
		if (propertyNamesInEffect.length > 0)
		{
			this.combine_ProcessIngredients_GenerateProduct(propertyNamesInEffect);
		}
	}

	AlchemySession.prototype.combine_ProcessIngredients_ConsumeIngredients = function(ingredients)
	{
		var itemsToRemove = ingredients.slice();
	
		for (var i = 0; i < itemsToRemove.length; i++)
		{
			var item = itemsToRemove[i];
			var itemDefnName = item.defnName;
			this.itemHolderStage.itemAddOrRemove(new Item(itemDefnName, -1))
		}
	}

	AlchemySession.prototype.combine_ProcessIngredients_FindEffects = function
	(
		ingredients, 
		numberOfIngredientsForEffect
	)
	{
		var propertyNamesSharedLookup = [];

		for (var i = 0; i < ingredients.length; i++)
		{
			var ingredient = ingredients[i];
			var ingredientPropertyNames = ingredient.defn(this).propertyNames;
			for (var p = 0; p < ingredientPropertyNames.length; p++)
			{
				var propertyName = ingredientPropertyNames[p];
				var numberOfIngredientsWithProperty = propertyNamesSharedLookup[propertyName];
				if (numberOfIngredientsWithProperty == null)
				{
					numberOfIngredientsWithProperty = 0;
				}
				numberOfIngredientsWithProperty++;
				propertyNamesSharedLookup[propertyName] = numberOfIngredientsWithProperty;
			}
		}

		var propertyNamesInEffect = [];
		
		for (var propertyName in propertyNamesSharedLookup)
		{
			var numberOfIngredientsWithProperty = propertyNamesSharedLookup[propertyName];
			if (numberOfIngredientsWithProperty >= numberOfIngredientsForEffect)
			{
				propertyNamesInEffect.push(propertyName);
			}
		}

		return propertyNamesInEffect;
	}

	AlchemySession.prototype.combine_ProcessIngredients_GenerateProduct = function(propertyNamesInEffect)
	{
		var itemDefnNameForProduct = "Potion of " + propertyNamesInEffect.join(" and ");
		var itemDefnForProduct = this.itemDefns[itemDefnNameForProduct];
		if (itemDefnForProduct == null)
		{
			itemDefnForProduct = new ItemDefn
			(
				itemDefnNameForProduct,
				false, // isIngredient
				propertyNamesInEffect
			);
			this.itemDefns.push(itemDefnForProduct);
			this.itemDefns[itemDefnForProduct.name] = itemDefnForProduct;
		}
	
		var itemProduct = new Item(itemDefnForProduct.name, 1);
		this.itemHolderProducts.itemAddOrRemove(itemProduct);
	}

	AlchemySession.prototype.itemTransfer = function(itemDefnName, holderFrom, holderTo)
	{
		var itemToMove = holderFrom.items[itemDefnName];
		holderTo.itemAddOrRemove(itemToMove);
		holderFrom.itemAddOrRemove(new Item(itemDefnName, 0 - itemToMove.quantity));
	}


	// dom

	AlchemySession.prototype.domElementUpdate = function()
	{
		var divSession = this.domElement;

		if (divSession == null)
		{
			divSession = document.createElement("div");
			//divSession.style.display = "table";

			var divItemIngredients = document.createElement("div");
			divItemIngredients.style.height = "100%";
			divItemIngredients.style.verticalAlign = "middle";
			divItemIngredients.style.display = "table-cell";
			var labelItemIngredients = document.createElement("label");
			labelItemIngredients.innerHTML = "Ingredients in Inventory:";
			divItemIngredients.appendChild(labelItemIngredients);
			var selectItemIngredients = document.createElement("select");
			selectItemIngredients.id = "selectItemIngredients"; 
			selectItemIngredients.size = 6;
			selectItemIngredients.style.width = "100%";
			divItemIngredients.appendChild(selectItemIngredients);
			divSession.appendChild(divItemIngredients);

			var divTransfer = document.createElement("div");
			divTransfer.style.height = "100%";
			divTransfer.style.verticalAlign = "middle";
			divTransfer.style.display = "table-cell";
			var buttonItemStage = document.createElement("button");
			buttonItemStage.innerHTML = ">";
			buttonItemStage.onclick = this.buttonStage_Click.bind(this);
			divTransfer.appendChild(buttonItemStage);
			var buttonItemUnstage = document.createElement("button");
			buttonItemUnstage.innerHTML = "<";
			buttonItemUnstage.onclick = this.buttonUnstage_Click.bind(this);
			divTransfer.appendChild(buttonItemUnstage);
			divSession.appendChild(divTransfer);

			var divItemsToCombine = document.createElement("div");
			divItemsToCombine.style.display = "table-cell";
			var labelItemsToCombine = document.createElement("label");
			labelItemsToCombine.innerHTML = "Ingredients to Combine:";
			divItemsToCombine.appendChild(labelItemsToCombine);
			var selectItemsToCombine = document.createElement("select");
			selectItemsToCombine.id = "selectItemsToCombine"; 
			selectItemsToCombine.size = 6;
			selectItemsToCombine.style.width = "100%";
			divItemsToCombine.appendChild(selectItemsToCombine);
			divSession.appendChild(divItemsToCombine);

			var divCombine = document.createElement("div");
			divCombine.style.height = "100%";
			divCombine.style.verticalAlign = "middle";
			divCombine.style.display = "table-cell";
			var buttonCombine = document.createElement("button");
			buttonCombine.innerHTML = "Combine";
			buttonCombine.onclick = this.buttonCombine_Click.bind(this);
			divCombine.appendChild(buttonCombine);
			divSession.appendChild(divCombine);

			var divItemProducts = document.createElement("div");
			divItemProducts.style.height = "100%";
			divItemProducts.style.verticalAlign = "middle";
			divItemProducts.style.display = "table-cell";
			var labelItemProducts = document.createElement("label");
			labelItemProducts.innerHTML = "Products:";
			divItemProducts.appendChild(labelItemProducts);
			var selectItemProducts = document.createElement("select");
			selectItemProducts.id = "selectItemProducts"; 
			selectItemProducts.size = 6;
			selectItemProducts.style.width = "100%";
			divItemProducts.appendChild(selectItemProducts);
			divSession.appendChild(divItemProducts);

			this.domElement = divSession;
			var divMain = document.getElementById("divMain");
			divMain.appendChild(divSession);
		}

		var selectItemIngredients = document.getElementById("selectItemIngredients");
		selectItemIngredients.innerHTML = "";
		var itemIngredients = this.itemHolderIngredients.items;

		for (var i = 0; i < itemIngredients.length; i++)
		{
			var item = itemIngredients[i];
			var optionItemHeld = document.createElement("option");
			optionItemHeld.value = item.defnName;
			optionItemHeld.innerHTML = item.defnName + "(" + item.quantity + ")";
			selectItemIngredients.appendChild(optionItemHeld);
		}

		var selectItemStaged = document.getElementById("selectItemsToCombine");
		selectItemStaged.innerHTML = "";
		var itemsToCombine = this.itemHolderStage.items;

		for (var i = 0; i < itemsToCombine.length; i++)
		{
			var item = itemsToCombine[i];
			var optionItemHeld = document.createElement("option");
			optionItemHeld.value = item.defnName;
			optionItemHeld.innerHTML = item.defnName + "(" + item.quantity + ")";
			selectItemStaged.appendChild(optionItemHeld);
		}

		var selectItemProducts = document.getElementById("selectItemProducts");
		selectItemProducts.innerHTML = "";
		var itemProducts = this.itemHolderProducts.items;

		for (var i = 0; i < itemProducts.length; i++)
		{
			var item = itemProducts[i];
			var optionItemHeld = document.createElement("option");
			optionItemHeld.value = item.defnName;
			optionItemHeld.innerHTML = item.defnName + "(" + item.quantity + ")";
			selectItemProducts.appendChild(optionItemHeld);
		}
	}

	// dom events

	AlchemySession.prototype.buttonCombine_Click = function(event)
	{
		this.combine();
		this.domElementUpdate();
	}

	AlchemySession.prototype.buttonStage_Click = function(event)
	{
		var selectItemsHeld = document.getElementById("selectItemIngredients");
		var selectedOptions = selectItemsHeld.selectedOptions;
		if (selectedOptions.length > 0)
		{
			var itemDefnName = selectedOptions[0].value;
			this.itemTransfer
			(
				itemDefnName, 
				this.itemHolderIngredients, 
				this.itemHolderStage
			);
		}

		this.domElementUpdate();
	}

	AlchemySession.prototype.buttonUnstage_Click = function(event)
	{
		var selectItemsStaged = document.getElementById("selectItemsStaged");
		var selectedOptions = selectItemsToCombine.selectedOptions;
		if (selectedOptions.length > 0)
		{
			var itemDefnName = selectedOptions[0].value;
			this.itemTransfer
			(
				itemDefnName, 
				this.itemHolderStage, 
				this.itemHolderIngredients
			);
		}

		this.domElementUpdate();
	}
}

function ArrayExtensions()
{
	// do nothing
}
{
	Array.prototype.addLookups = function(keyName)
	{
		for (var i = 0; i < this.length; i++)
		{
			var item = this[i];
			var key = item[keyName];
			this[key] = item;
		}
	}
}

function Item(defnName, quantity)
{
	this.defnName = defnName;
	this.quantity = quantity;
}
{
	Item.prototype.defn = function(session)
	{
		return session.itemDefns[this.defnName];	
	}
}

function ItemDefn(name, isIngredient, propertyNames)
{
	this.name = name;
	this.isIngredient = isIngredient;
	this.propertyNames = propertyNames;
}

function ItemHolder(name, items)
{
	this.name = name;
	this.items = items;
	this.items.addLookups("defnName");
}
{
	ItemHolder.prototype.itemAddOrRemove = function(itemToAdd)
	{
		var itemDefnName = itemToAdd.defnName;
		var itemAlreadyHeld = this.items[itemDefnName];

		if (itemAlreadyHeld == null)
		{
			itemAlreadyHeld = new Item(itemDefnName, 0);
			this.items.push(itemAlreadyHeld);
			this.items[itemDefnName] = itemAlreadyHeld;
		}		

		itemAlreadyHeld.quantity += itemToAdd.quantity;
		if (itemAlreadyHeld.quantity <= 0)
		{
			this.items.splice
			(
				this.items.indexOf(itemAlreadyHeld), 1
			);
			delete this.items[itemDefnName];
		}	
	}
}

// run

main();

</script>
</body>
</html>

Advertisements
This entry was posted in Uncategorized and tagged , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s