A Diplomacy Engine for a Game in JavaScript

The code below, when run, provides a very simple simulation of diplomatic relations between various factions. Each of the factions can declare war on, offer peace to, or make alliances with any of the other factions. The results of each diplomatic exchange are determined by a simplistic model of self-interest, based on the relative strengths of the parties involved, their enemies, and their allies. For example, if faction A offers peace to faction B, faction B will only accept if the total strength of B’s enemies is greater than the total strength of its allies. If faction C proposes an alliance with faction D, faction D will only accept if the new enemies it makes by joining that alliance are less powerful than the added strength the alliance provides. And so on. If the strength of a faction is changed, obviously these value judgments will change as well.

In summary, everyone behaves very logically, and with complete knowledge of the other factions’ strengths, and with no way for one faction to offer incentives or disincentives to sway another’s decisions. Someday it is to be hoped that this program can be expanded to better simulate the irrational, emotional, resentful, mercantile, and uncertain atmosphere of real international diplomacy.

To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

Diplomacy


<html>
<body>
<div id="divMain"></div>
<script type="text/javascript">

// main

function main()
{
	var diplomaticActions = DiplomaticAction.Instances._All;

	var factions = 
	[
		new Faction("Red", 100),		
		new Faction("Orange", 100),
		new Faction("Yellow", 100),
		new Faction("Green", 100),
		new Faction("Blue", 100),
		new Faction("Violet", 100),
	];

	var universe = new Universe
	(
		diplomaticActions,
		factions
	);

	Globals.Instance.initialize(universe);
}

// extensions

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;
		}
	}

	Array.prototype.intersectionWith = function(other)
	{
		var returnValues = [];

		for (var i = 0; i < this.length; i++)
		{
			var item = this[i];

			if (other.indexOf(item) >= 0)
			{
				returnValues.push(item);
			}
		}

		return returnValues;
	}
}

// classes

function DiplomaticAction(name, effect)
{
	this.name = name;
	this.effect = effect;
}
{
	function DiplomaticAction_Instances()
	{
		this.War = new DiplomaticAction
		(
			"Declare War on",
			function(universe, factionActing, factionReceiving) 
			{	
				var message;

				var stateExisting = factionActing.relationships[factionReceiving.name].state;
				if (stateExisting == Relationship.States.War)
				{
					message = "The " + factionActing.name + "s are already at war with the " + factionReceiving.name + "s.";
				}
				else
				{
					factionActing.relationships[factionReceiving.name].state = Relationship.States.War;
					factionReceiving.relationships[factionActing.name].state = Relationship.States.War;
					message = "The " + factionActing.name + "s have declared war on the " + factionReceiving.name + "s.";
				}


				alert(message);
			}
		),

		this.Peace = new DiplomaticAction
		(
			"Offer Peace to", 
			function(universe, factionActing, factionReceiving) 
			{
				var message;

				var stateExisting = factionActing.relationships[factionReceiving.name].state;
				if (stateExisting == Relationship.States.Alliance)
				{
					message = "The " + factionReceiving.name + "s are already allied with the " + factionActing.name + "s.";
				}
				else if (stateExisting == Relationship.States.Peace)
				{
					message = "The " + factionReceiving.name + "s are already at peace with the " + factionActing.name + "s.";
				}
				else // if (stateExisting == Relationship.States.War)
				{
					var strengthOfEnemies = Faction.strengthOfMany
					(
						factionReceiving.enemies()
					);
	
					var strengthOfSelfAndAllies = Faction.strengthOfMany
					(
						factionReceiving.selfAndAllies()
					);
			
					var strengthOfAlliesMinusEnemies = 
						strengthOfSelfAndAllies - strengthOfEnemies;
	
					if (strengthOfAlliesMinusEnemies <= 0)
					{
						factionActing.relationships[factionReceiving.name].state = Relationship.States.Peace;
						factionReceiving.relationships[factionActing.name].state = Relationship.States.Peace;
						message = "The " + factionReceiving.name + "s have accepted a peace offer from the " + factionActing.name + "s.";
					}
					else
					{
						message = "The " + factionReceiving.name + "s have rejected a peace offer from the " + factionActing.name + "s.";					
					}
				}

				alert(message);
			}
		);

		this.Alliance = new DiplomaticAction
		(
			"Propose Alliance with",
			// effect
			function(universe, factionActing, factionReceiving) 
			{
				var message;

				var stateExisting = factionActing.relationships[factionReceiving.name].state;
				if (stateExisting == Relationship.States.Alliance)
				{
					message = "The " + factionReceiving.name + "s are already allied with the " + factionActing.name + "s.";
				}
				else if (stateExisting == Relationship.States.War)
				{
					message = "The " + factionReceiving.name + "s are currently at war with the " + factionActing.name + "s.";
				}
				else
				{
					var factions = [ factionActing, factionReceiving ];
					var doAlliesAndEnemiesOfFactionsClash = false;

					var enemiesOfEitherFaction = [];

					for (var f = 0; f < factions.length; f++)
					{
						var factionThis = factions[f];
						var factionOther = factions[1 - f];

						var enemiesOfFactionThis = factionThis.enemies();
						enemiesOfEitherFaction = enemiesOfEitherFaction.concat(enemiesOfFactionThis);
						var alliesOfFactionOther = factionOther.allies();
						var intersection = enemiesOfFactionThis.intersectionWith(alliesOfFactionOther);

						if (intersection.length > 0)
						{
							doAlliesAndEnemiesOfFactionsClash = true;

							message = 
								"An alliance between the " 
								+ factionThis.name 
								+ "s and the " 
								+ factionOther.name
								+ "s is impossible because the " 
								+ factionThis.name 
								+ "s are at war with some allies of the "
								+ factionOther.name
								+ "s ("
								+ intersection.join(", ")
								+ ").";
						}
					}

					if (doAlliesAndEnemiesOfFactionsClash == false)
					{
						var factionsDeclaringWarOnActing = [];
						var factionsDeclaringWarOnReceiving = [];
						for (var i = 0; i < enemiesOfEitherFaction.length; i++)
						{
							var enemy = enemiesOfEitherFaction[i];
							if (factionActing.enemies().indexOf(enemy) == -1)
							{
								factionsDeclaringWarOnActing.push(enemy);
							}
							if (factionReceiving.enemies().indexOf() == -1)
							{
								factionsDeclaringWarOnReceiving.push(enemy);
							}			
						}

						var strengthOfNewEnemies = Faction.strengthOfMany(factionsDeclaringWarOnReceiving);
						if (strengthOfNewEnemies >= factionActing.strength)
						{
							message = 
								"The " 
								+ factionReceiving.name 
								+ "s have declined to join an alliance with the " 
								+ factionActing.name + "s.";
						}
						else
						{
							factionActing.relationships[factionReceiving.name].state = Relationship.States.Alliance;
							factionReceiving.relationships[factionActing.name].state = Relationship.States.Alliance;

							message = 
								"The " 
								+ factionReceiving.name 
								+ "s have joined an alliance with the " 
								+ factionActing.name + "s.";

							if (factionsDeclaringWarOnActing.length > 0)
							{
								message += 
									"  Some enemies of the " 
									+ factionReceiving.name
									+ "s (" 
									+ factionsDeclaringWarOnActing.join(", ") 
									+ ") have declared war on the "
									+ factionActing.name 
									+ "s."
							}

							if (factionsDeclaringWarOnReceiving.length > 0)
							{
								message += 
									"  Some enemies of the " 
									+ factionActing.name
									+ "s (" 
									+ factionsDeclaringWarOnReceiving.join(", ") 
									+ ") have declared war on the "
									+ factionReceiving.name 
									+ "s."
							}

						} // end if strengthOfNewEnemies >= factionActing.strength else

					} // end if doAlliesAndEnemiesOfFactionsClash

				} // end if stateExisting

				alert(message);
				
			} // end effect
		); // end new DiplomaticAction

		this._All = 
		[
			this.Peace,
			this.Alliance,
			this.War,
		];
	}

	DiplomaticAction.Instances = new DiplomaticAction_Instances();

}

function Faction(name, strength)
{
	this.name = name;
	this.strength = strength;

	this.relationships = [];
}
{
	// static methods

	Faction.strengthOfMany = function(factions)
	{
		var returnValue = 0;

		for (var i = 0; i < factions.length; i++)
		{
			var faction = factions[i];
			returnValue += faction.strength;
		}

		return returnValue;
	}

	// instance methods

	Faction.prototype.allies = function()
	{
		return this.factionsMatchingRelationshipState
		(
			Relationship.States.Alliance
		);
	}

	Faction.prototype.enemies = function()
	{
		return this.factionsMatchingRelationshipState
		(
			Relationship.States.War
		);
	}

	Faction.prototype.factionsMatchingRelationshipState = function(stateToMatch)
	{
		var returnValues = [];	

		for (var i = 0; i < this.relationships.length; i++)
		{
			var relationship = this.relationships[i];
			if (relationship.state == stateToMatch)
			{
				var factionOther = relationship.factionOther();
				returnValues.push(factionOther);
			}
		}

		return returnValues;
	}

	Faction.prototype.selfAndAllies = function()
	{
		var returnValues = this.factionsMatchingRelationshipState
		(
			Relationship.States.Alliance
		);

		returnValues.push(this);

		return returnValues;
	}

	Faction.prototype.toString = function()
	{
		return this.name;
	}
}

function Globals()
{}
{
	Globals.Instance = new Globals();

	Globals.prototype.initialize = function(universe)
	{
		this.universe = universe;	
		this.universe.initialize();
	}
}

function Relationship(factionNameOther, state)
{
	this.factionNameOther = factionNameOther;
	this.state = state;
}
{
	function Relationship_States()
	{
		this.Alliance = "Alliance";
		this.Peace = "Peace";
		this.War = "War";
	}

	Relationship.States = new Relationship_States();

	Relationship.prototype.factionOther = function()
	{
		return Globals.Instance.universe.factions[this.factionNameOther];
	}

	Relationship.prototype.toString = function()
	{
		var returnValue = this.factionNameOther + ":" + this.state;
		return returnValue;
	}
}

function Universe(diplomaticActions, factions)
{
	this.diplomaticActions = diplomaticActions;
	this.diplomaticActions.addLookups("name");

	this.factions = factions;
	this.factions.addLookups("name")
}
{
	Universe.prototype.initialize = function()
	{
		for (var f = 0; f < this.factions.length; f++)
		{
			var factionThis = this.factions[f];

			for (var g = 0; g < f; g++)
			{
				var factionOther = this.factions[g];

				factionThis.relationships.push
				(
					new Relationship
					(
						factionOther.name, 
						Relationship.States.Peace
					)
				);
				factionOther.relationships.push
				(
					new Relationship
					(
						factionThis.name, 
						Relationship.States.Peace
					)
				);
			}
		}

		for (var f = 0; f < this.factions.length; f++)
		{
			var faction = this.factions[f];
			faction.relationships.addLookups("factionNameOther");
		}		

		this.domElementUpdate();
	}

	// dom

	Universe.prototype.domElementUpdate = function()
	{
		if (this.domElement == null)
		{
			var divSession = document.createElement("div");

			// uncomment for 1-on-1 // for (var f = 0; f < 2; f++)
			for (var f = 0; f < this.factions.length; f++)
			{
				var faction = this.factions[f]; // comment out for 1-on-1

				var divFaction = document.createElement("div");
				divFaction.faction = faction;
				divFaction.indexWithinParent = f;
				divFaction.style.border = "1px solid";

				var labelFaction = document.createElement("label");
				labelFaction.innerHTML = "Faction:"
				divFaction.appendChild(labelFaction);
	
				var selectFaction = document.createElement("select");
				selectFaction.className = "selectFaction";
				var optionForNoSelection = document.createElement("option");
				optionForNoSelection.innerHTML = "[none]";
				selectFaction.appendChild(optionForNoSelection);
				for (var i = 0; i < this.factions.length; i++)
				{
					var factionAvailable = this.factions[i];
					var optionForFaction = document.createElement("option");
					optionForFaction.innerHTML = factionAvailable.name;
					optionForFaction.faction = factionAvailable;
					selectFaction.appendChild(optionForFaction);
				}
				selectFaction.onchange = this.selectFactionChanged.bind(this);
				selectFaction.disabled = true; // comment out for 1-on-1
				selectFaction.value = faction.name;
				divFaction.appendChild(selectFaction);

				var divStrength = document.createElement("div");

				var labelStrength = document.createElement("label");
				labelStrength.innerHTML = "Strength:"
				divStrength.appendChild(labelStrength);
	
				var textStrength = document.createElement("input");
				textStrength.className = "textStrength";
				textStrength.type = "number";
				textStrength.value = 0;
				textStrength.style.width = "50px";
				textStrength.onchange = this.textStrengthChanged.bind(this);
				divStrength.appendChild(textStrength);

				divFaction.appendChild(divStrength);

				var divRelationships = document.createElement("div");
				
				var labelRelationships = document.createElement("label");
				labelRelationships.innerHTML = "Relationships:"
				divRelationships.appendChild(labelRelationships);
	
				var textRelationships = document.createElement("input");
				textRelationships.className = "textRelationships";
				textRelationships.disabled = true;
				textRelationships.style.width = "400px";
				divRelationships.appendChild(textRelationships);

				divFaction.appendChild(divRelationships);

				var divAction = document.createElement("div");

				var labelAction = document.createElement("label");
				labelAction.innerHTML = "Action to Take:"
				divAction.appendChild(labelAction);

				var selectAction = document.createElement("select");
				selectAction.className = "selectAction";
				for (var a = 0; a < this.diplomaticActions.length; a++)
				{
					var action = this.diplomaticActions[a];
					var optionForAction = document.createElement("option");
					optionForAction.innerHTML = action.name;
					optionForAction.action = action;
					selectAction.appendChild(optionForAction);
				}
				divAction.appendChild(selectAction);

				var selectActionTarget = document.createElement("select");
				selectActionTarget.className = "selectActionTarget";
				for (var i = 0; i < this.factions.length; i++)
				{
					var factionOther = this.factions[i];
					if (factionOther != faction)
					{
						var optionForFaction = document.createElement("option");
						optionForFaction.innerHTML = factionOther.name;
						optionForFaction.faction = factionOther;
						selectActionTarget.appendChild(optionForFaction);
					}
				}
				divAction.appendChild(selectActionTarget);

				var buttonActionPerform = document.createElement("button");
				buttonActionPerform.innerHTML = "Perform Action";
				buttonActionPerform.onclick = this.buttonActionPerformClicked.bind(this);
				divAction.appendChild(buttonActionPerform);

				divFaction.appendChild(divAction);

				divSession.appendChild(divFaction);
			}

			var divMain = document.getElementById("divMain");

			divMain.appendChild(divSession);
			
			this.domElement = divSession;
		}

		for (var f = 0; f < this.factions.length; f++)
		{
			var divSession = this.domElement;
			var divFaction = divSession.children[f];
			var faction = divFaction.faction;

			var textStrength = divFaction.getElementsByClassName("textStrength")[0];
			var textRelationships = divFaction.getElementsByClassName("textRelationships")[0];

			if (faction == null)
			{
				textStrength.value = 0;
				textRelationships.value = "";
			}
			else
			{				
				textStrength.value = faction.strength;
				textRelationships.value = faction.relationships.join(";");
			}
		}
	}

	// events

	Universe.prototype.buttonActionPerformClicked = function(event)
	{
		var divFaction = event.srcElement.parentElement;
		var selectFaction = divFaction.getElementsByClassName("selectFaction")[0];
		var factionName = selectFaction.value;
		var faction = this.factions[factionName];

		// uncomment for 1-on-1

		//var divSession = divFaction.parentElement;
		//var indexOfFactionInSession = divFaction.indexWithinParent;
		//var divFactionOther = divSession.children[1 - indexOfFactionInSession];
		//var selectFactionOther = divFactionOther.getElementsByClassName("selectFaction")[0];
		//var factionNameOther = selectFactionOther.value;
		
		// comment out for 1-on-1

		var selectActionTarget = divFaction.getElementsByClassName("selectActionTarget")[0];
		var factionNameOther = selectActionTarget.value;
		var factionOther = this.factions[factionNameOther];		

		if (faction == null || factionOther == null)
		{
			return;
		}

		var selectAction = divFaction.getElementsByClassName("selectAction")[0];
		var nameOfActionToPerform = selectAction.value;
		var actionToPerform = this.diplomaticActions[nameOfActionToPerform];
		if (actionToPerform != null)
		{
			actionToPerform.effect(this, faction, factionOther);
		}

		this.domElementUpdate();
	}

	Universe.prototype.buttonTurnAdvanceClicked = function(event)
	{
		alert("todo");
	}

	Universe.prototype.selectFactionChanged = function(event)
	{
		var selectFaction = event.srcElement;
		var factionName = selectFaction.value;
		var faction = this.factions[factionName];

		var divFaction = selectFaction.parentElement;
		divFaction.faction = faction;

		this.domElementUpdate();
	}

	Universe.prototype.textStrengthChanged = function(event)
	{
		var textStrengthChanged = event.srcElement;
		var divFaction = textStrengthChanged.parentElement;
		var selectFaction = divFaction.getElementsByClassName("selectFaction")[0];
		var factionName = selectFaction.value;
		var faction = this.factions[factionName];

		if (faction != null)
		{
			faction.strength = parseInt(textStrengthChanged.value);
		}
	}
}

// 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