Saturday, August 15, 2020

Realm of Darkness:

Work continues on the Farm dungeon. By my count, I have around 17 bad ends in the entire game completed, and I'm working on number 18. 

I'm trying to get at least one bad end done per animal variation, both male and female cows, horses, and pigs. I'm about half done with that... because I'm a little heavy on adding female cow bad endings as I enjoy writing them more.

Once the bad ends are done, the trick will be adding in sufficient events and connective tissue to actually get to these bad ends.

To that end, I've been working on an encounter system which will allow me to dynamically select events based on probability weights.

This is a common programming problem when doing such things as randomly selecting brokers to process data or another activity which requires selecting a particular value more often than other values, randomly.

Here's the code I've put together based on research I've done:

window.calculateEncounter = function(possibleEncounters){
	var i = 0;
	var j = 0;
	var totalWeight = 0;
	var keys = Object.keys(possibleEncounters);

	for (; i < keys.length; i++) {
	  totalWeight += possibleEncounters[keys[i]].weight;
	}

	var randomWeightNumber = Math.floor(Math.random() * totalWeight)
	var selectedKey = "";

	for (; j < keys.length; j++) {
	  if(randomWeightNumber < possibleEncounters[keys[j]].weight){
		selectedKey = keys[j];
		break;
	  }

	  randomWeightNumber = randomWeightNumber - possibleEncounters[keys[j]].weight;
	}

	return selectedKey;
}

window.calculateEncounterWithInclude = function(possibleEncounters){
	var selectedKey = calculateEncounter(possibleEncounters);

	return "<<include \"" + selectedKey + "\">>";	
}

window.calculateEncounterWithLink = function(optionNumber, optionName, linkText, possibleEncounters){
	var selectedKey = calculateEncounter(possibleEncounters);
	
	if(linkText){
		return "<span id=\"" + optionName + "Act\"><<link \"(" + optionNumber + ") " + linkText + "\" \"" + selectedKey + "\">><</link>></span>";			
	}else{
		return "<span id=\"" + optionName + "Act\"><<link \"(" + optionNumber + ") " + selectedKey + "\" \"" + selectedKey + "\">><</link>></span>";		
	}
}

The possibleEncounters input looks like the below object, specified in the 'StoryInit' passage so it will be run at the start of the game. A setup variable is used because these are static variables that do not need to be altered or tracked during the game.

<<set setup.testEncounters to {
  "Test Encounter A": {
    weight: 10
  },
  "Test Encounter B": {
    weight: 10
  },
  "Test Encounter C": {
    weight: 100
  },
}>>

The object consists of a passage name in the Twine game to select, and the 'weight' that passage is assigned.

The 'weight' is relative, which means in this case that 'Test Encounter C' would be selected as the passage to use ten times more often than the other passages.

The 'calculateEncounter' method iterates through the provided objects, getting the total weight of all encounters. Then, it randomly chooses a number between zero and the total weight. If the random number chosen is below the weight of the event being iterated, that event is selected. Otherwise, the weight of the event is subtracted from the random number, and the next event is inspected. This occurs until an event is chosen.

'calculateEncounterWithInclude' generates an include statement to be inserted into a Twine story passage. The include statement imports the contents of one passage into another passage.

'calculateEncounterWithLink' allows you to specify a link to the passage that has been chosen randomly. 'optionNumber' and 'optionName' are used for specifying a keyboard shortcut. 'linkText' is optional, but allows you to specify a name for the link different than the name of the destination passage.

Combined together, using this code in a passage would look like this:

This would be your generic location passage that needs a random encounter.

Then, you would have a randomly selected passage, weighted based on what is configured:

<<print calculateEncounterWithInclude(setup.testEncounters)>>

<<print calculateEncounterWithLink(1, "one", "This encounter is truly random.", setup.testEncounters)>>

The first print statement would be replaced with the randomly selected passage contents, while the second print statement would turn into a link. When this passage is visited, the code will execute and randomly choose an encounter based on the specified weight.

The code above would live in the 'Encounter Tester' passage, and when that passage is visited, one of the test encounter passages would be chosen to be included in that passage.

This is a pretty neat way to allow me to write standalone encounters and include them in the adventure pages. I will be using this logic pretty much everywhere in the story where random encounters occur.

In the Farm, there is a day loop, which means at the end of an encounter the game will need to increment the time and put you back in the right spot. I'll talk about my system for doing this another time. One major topic per post, I think!

This illustrates why it takes so much time to make a proper interactive game - there is a lot of logic under the covers like this that needs to be implemented to even make the story work - especially if you're trying to do more than a simple 'choose your own adventure' story!

With any luck, the next time I post, I'll be able to say that I have all of the main bad ends written! (And maybe even some of the encounters, too!)


2 comments:

  1. This is why I love your works so much. They're so thought out an polished, its fantastic

    ReplyDelete
  2. This looks wild! How is it coming along?

    ReplyDelete