#!/usr/bin/python3
# Create island descriptions using the Judges Guild Island Book 1 tables as a guide
# Jerry Stratton, astoundingscripts.com

import random

types = [
	['barren rocks'],
	['basalt cay', 'c'],
	['sparse key', 'c', 't'],
	['sparse ait', 'c', 'r', 't', 'p'],
	['sparse isle', 2, 'h', 'c', 'r', 'p'],
	['meager isle', 3, 'h', 'c', 's', 't', 'p'],
	['rugged isle', 4, 'v', 'h', 'c', 'r', 't'],
	['sandy island', 6, 'm', 'h', 'c', 't'],
	['terrible island', 10, 'v', 'm', 'h', 'p', 'c', 't'],
	['monstrous island', 2, 6, 'v', 'h', 'p', 'c', 't'],
	['sleepy island', 'r', 'f', 'p'],
	['peaceful island', 'h', 's', 'f', 'p'],
	['atoll ring reef', 'c'],
	['plentiful island', 'h', 's', 'f', 'r', 'p'],
	['ample island', 2, 'v', 'h', 's', 'f', 'r', 'p'],
	['rich island', 3, 'm', 'h', 's', 'r', 'p', 'f'],
	['teeming island', 4, 'v', 'm', 'h', 's', 'r', 'c', 'p'],
	['lush island', 6, 'm', 'h', 's', 'f', 'r', 'p'],
	['luxuriant island', 10, 'm', 'h', 's', 'f', 'r', 'p'],
	['paradisal island', 2, 6, 'v', 'm', 'h', 's', 'f', 'r', 'p'],
]

#d20 tables
islandFeatures = ['waterfall', 'pond', 'pool', 'tarn', 'lakelet', 'mare', 'delta', 'swamp', 'lake', 'cove', 'loch', 'cascade', 'bog', 'bank', 'marsh', 'vale', 'strand', 'peninsula', 'bay', 'promontory']
islandLandmarks = ['beach', 'rocky slope', 'dell', 'dense thicket', 'set of boulders', 'swampy morass', 'cliff', 'track', 'trail', 'hillock', 'ravine', 'hill', 'cul-de-sac', 'hill', 'crevice', 'ridge', 'vale', 'mountain peak', 'gully', 'cave entrance']
islandWeathers = ['clear', 'cloudy', 'overcast', 'misty', 'fog', 'dense fog', 'drizzle', 'heavy rain', 'downpour', 'torrent', 'muggy', 'cloudy', 'heat lightning', 'light breeze', 'blowing rain', 'gale', 'torrent', 'peeper frog fall', 'sticky downpour', 'oily drizzle']
islandSounds = ['deathly silence', 'chirking', 'cawing', 'clipping', 'crunching', 'whistling', 'slicking', 'thumping', 'moaning', 'wailing', 'scream', 'trilling', 'splashing', 'slurping', 'walking', 'snap', 'howling', 'grunt', 'screech', 'roar']
islandVolcanoes = ['extinct shield', 'extinct cinder cone', 'extinct composite cone', 'extinct dome (collapses if crossed)', 'extinct fissure', 'extinct Maar crater', 'dormant shield', 'dormant cinder cone', 'dormant composite cone', 'dormant dome', 'dormant fissure', 'active shield', 'active cinder cone', 'active composite cone', 'active dome', 'active fissure', 'erupting shield', 'erupting cinder cone', 'erupting composite cone', 'erupting dome']
volcanoEruptions = ['gentle outpour', 'pumice cloud', 'lava flood', 'ash flows', 'thin flows', 'hot ash cloud', 'mud flow', 'cinder fall', 'fire-broken rock', 'splatter', 'block & ash fount', 'obsidian fall', 'steam fumeroles', 'sulphure fumeroles', 'carbon dioxide fumeroles', 'methane fumeroles', 'boiling rain', 'lava fountain', 'pancake bombs', 'glowing avalanche']
islandTraps = ['quicksand', 'hidden pit', 'falling tree', 'landslide', 'rockslide', 'clashing rocks', 'lightning attraction', 'giant lodestone', 'mirage', 'distortion cave', 'spring trap', 'deadfall trap', 'snare trap', 'spider web', 'giant clam', 'tangle vines', 'ambush', 'gas fissure', 'explosive runes', 'dazzling mirror']
islandCreatures = ['giant waterbug', 'giant octopus', 'giant leeches', 'giant slugs', 'water spider', 'water rat', 'giant pigs', 'giant crabs', 'water naga', 'catoblepas', 'giant crocodile', 'paleocincus', 'black dragon', 'giant frog', 'nymph', 'sea hag', 'giant sea snake', 'giant toad', 'giant sea turtle', 'will-o-wisp']
islandProvisions = ['barren', 'salt spray', 'salt potholes', ('poisonous rivulet', 100), ('hot spring', 50), ('warm spring', 40), ('porous lava spring', 30), ('hillside spring', 20), ('artesian spring', 10), ('limestone spring', 0), ('geysers', 50), ('caldera lake', 20), 'roots', 'fruit', 'vegetables', 'nuts', 'game', 'wreck', 'abandoned habitation', 'inhabited']
waterPoisons = ['yellow fever (20% chance)', 'nausea for 1-6 turns (bitter)', 'orange coloration (2d12 days)', 'protruding eyes (1-6 days)', 'purple blotches (1-6 days)', 'stunned (1-6 turns)', 'saps 1d6 strength (1-6 turns)', 'contaminated with oil', 'dysentery (4% chance)', 'grippe (5% chance)', 'lose all hair (1-6 months)', 'lose all teeth', 'blind (1-6 turns)', 'lose hearing (1-6 turns)', 'sleep (10-60 turns)', 'dehydrate (1-6 turns)', 'poison class 1', 'poison class 2', 'poison class 3', 'poison class 4']
provisionsGame = ['duck', 'mallard', 'teal', 'pigeon', 'parrot', 'flamingo', 'toucan', 'pelican', 'hyena', 'python', 'raccoon', 'rodent', 'goat', 'hare', 'dog', 'lizard', 'tortoise', 'toad', 'wart hog', 'big cat']
provisionsHabitation = ['cave', 'cavern', 'lean-to', 'covered pit', 'hollowed tree', 'sail tent', 'giant shell', 'pole house', 'castle', 'temple', 'ruins', 'tower', 'manor', 'stone house', 'log cabin', 'grass hut', 'stockade', 'tree house', 'hovel', 'village']

habitantRecluses = ['happy hermit', 'mourning loss of fortune', 'rejected lover', 'exiled noble', 'studious sage', 'hideous outcast', 'researching alchemist', 'hiding from enemy', 'paranoid collector', 'monkly vows', 'cursed extrovert', 'exiled godling', 'prospector', 'artifact hunter', 'loathes speech', 'escaped slave', 'insane wizard', 'mad scientist', 'rotting disease victim', 'black plague victim']
habitantCastaways = ['pirate', 'buccaneer', 'engineer', 'alchemist', 'trainer', 'sage', 'ranger', 'fighter', 'thief', 'captain', 'merchant', 'noble', 'Amazon', 'monk', 'assassin', 'druid', 'illusionist', 'mage', 'bard', 'craftsman']
habitantGarrisons = ['naval station', 'merchant’s trading post', 'pirate stronghold', 'temple', 'monastery', 'sacred artifact', 'assassins’ headquarters', 'ritual initiation', 'warning outpost', 'messenger way station', 'invasion gathering point', 'prison', 'secret laboratory complex', 'punishment duty', 'insane royal relative', 'exiled warlord', 'forgotten in transit', 'deserters', 'brigands’ haven', 'sacred burial grounds']
landInhabitants = [['viking', 6, 10], ['merchant', 6, 50], ['pirate', 6, 50], ['lizard man', 4, 10], ['fisherman', 6, 10], ['elf', 10, 20], ['were shark', 6, 10], ['were dolphin', 6, 10], ['buccaneer', 6, 50], ['halfling', 10, 30], ['caveman', 10, 10], ['tribesman', 10, 10], ['gnoll', 10, 20], ['goblin', 10, 40], ['gnome', 10, 40], ['cannibal', 10, 10], ['garrison', habitantGarrisons], ['recluse', habitantRecluses], ['castaway', habitantCastaways], ['castaway', 6, 1, habitantCastaways]]
seabedInhabitants = [['nixie', 4, 20], ['lizard man', 4, 10], ['merman', 10, 20], ['triton', 6, 10], ['sahuagin', 10, 2], ['kopoacinth gargoyle', 8, 2], ['koalinth hobgoblin', 6, 10], ['lacedon ghoul', 3, 10], ['locathah', 10, 20], ['ixitxachitl', 10, 10], ['aquatic elf', 10, 20], ['were slug', 6, 4], ['were turtle', 6, 4], ['were dolphin', 6, 3], ['were octopus', 6, 2], ['were frogs', 6, 1], ['were squid', 6, 1], ['were crab', 6, 1], ['were lamprey', 6, 1], ['were sea horse', 6, 1]]
islandMysteries = ['skeletons', 'a broken sword', 'a split shield', 'an arrowhead', 'a map fragment (30% chance it leads to hidden treasure)', 'a broken keg', 'an oar', 'an empty chest', 'an empty wine skin', 'giant tracks', 'a burnt clearing', 'a pit', 'a crumbled wall', 'a rusty knife', 'leather thongs', 'sail scraps', 'a starving castaway', 'an axe', 'buried provisions', 'a passing ship']
islandApproaches = ['hidden rocks', 'a fringing reef', 'a barrier reef', 'a whirlpool', 'shear cliffs', 'sand bars', 'a shallow shelf', 'continual fog', 'water spouts', 'calm current', 'light current', 'strong current', 'a tidal range', 'calm winds', 'light winds', 'strong winds', 'a gale', 'a sheltered cove', 'a channel', 'a lagoon']
shoreOutcomes = ['boat sinks', 'boat overturns', 'boat swept away', 'mysterious find', 'a passing ship', 'lost', 'lured into trap*', 'attacked by flyers*', 'attacked by animals*', 'separated', 'find castaway*', 'find shore party', 'own ship is gone', 'find recluse*', 'find inhabitants*', 'find garrison*', 'attacked by creatures*', 'find habitations*', 'find hidden treasure*', 'find provisions*']
passingShips = ['cannibal canoes', 'longship', 'raft', 'pirate ship', 'fishing boat', 'slave galley', 'sailed warships', 'small galley', 'large galley', 'small merchant', 'large merchant', 'river boat', 'buccaneer ship', 'longship, damaged', 'ghost ship', 'tribal outrigger', 'dolphin sled', 'sea horse carriage', 'mage’s sloop', 'merchant galley']
coastalEncounters = [('sea lion', 1, 8, 4), ('giant sea horse', 20), ('shark', 3, 4), ('giant shark', 3), ('water weird', 3), ('giant sea turtle', 3), 'giant squid', ('giant sea snake', 89), ('sea hag', 4), 'manta ray']

#d100 tables
islandElevations = [
	(5, 0, -500, 'foot'),
	(40, 1, 500, 'foot'),
	(60, 501, 1000, 'foot'),
	(70, 1001, 2000, 'foot'),
	(80, 2001, 5000, 'foot'),
	(90, 5001, 10000, 'foot'),
	(99, 10001, 20000, 'foot'),
	(100, 'over 20,000 feet'),
]
islandPrecipitation = [
	(10, 0, 10, 'inch'),
	(30, 11, 20, 'inch'),
	(45, 21, 30, 'inch'),
	(60, 31, 40, 'inch'),
	(70, 41, 50, 'inch'),
	(80, 51, 60, 'inch'),
	(99, 61, 70, 'inch'),
	(100, 71, 1780, 'inch'),
]
islandGrowingSeason = [
	(15, 1, 100, 'day'),
	(25, 101, 120, 'day'),
	(40, 121, 140, 'day'),
	(60, 141, 180, 'day'),
	(70, 181, 200, 'day'),
	(80, 201, 240, 'day'),
	(90, 241, 260, 'day'),
	(100, 261, 360, 'day'),
]
islandTemperatures = [
	(10, '1 to 20° F'),
	(24, '21 to 40° F'),
	(48, '41 to 60° F'),
	(64, '61 to 80° F'),
	(80, '81 to 100° F'),
	(90, '101 to 120° F'),
	(99, '121 to 140° F'),
	(100, '141 to 160° F'),
]

numbers = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen', 'twenty']

indentationLevel = ''

def rollDie(dieCount, dieSize=None):
	result = 0
	if dieSize == None:
		dieSize = dieCount
		dieCount = 1

	while dieCount > 0:
		result += random.randrange(dieSize)+1
		dieCount -= 1
	return result

def die100(table, needNumber = False):
	roll = rollDie(100)
	index = 0
	dieResult = 0
	result = 'not a d100 table'
	while index < len(table):
		if table[index][0] < roll:
			index += 1
		else:
			row = table[index][1:]
			if type(row[0]) == int:
				lowEnd, highEnd, term = row
				sign = int(abs(highEnd)/highEnd)
				highEnd = abs(highEnd)
				dieResult = rollDie(highEnd-lowEnd) + lowEnd
				result = numberedThing(dieResult, term)
				if sign == -1:
					result += ' underwater'
					dieResult = -dieResult
			else:
				result = row[0]
			break

	if needNumber:
		return result, dieResult
	else:
		return result

def numberedThing(number, noun, addNumber=True):
	if number != 1:
		if noun.endswith('oot'):
			noun = noun[:-3] + 'eet'
		elif noun.endswith('ch'):
			noun = noun + 'es'
		elif noun.endswith('man'):
			noun = noun[:-2] + 'en'
		elif noun.endswith('f'):
			noun = noun[:-1] + 'ves'
		else:
			noun += 's'

	if addNumber:
		if number > 0 and number <= len(numbers):
			numberText = numbers[number-1]
		else:
			numberText = "{:,}".format(number)
		noun = numberText + ' ' + noun

	return noun

def title(text):
	text = indentationLevel + text.title()
	print(text)

def sentence(*phrases):
	sentence = ' '.join(phrases) + '.'
	sentence = sentence[0].upper() + sentence[1:]
	sentence = indentationLevel + sentence
	print(sentence)

def paragraph():
	print()

def increaseIndentation():
	global indentationLevel
	indentationLevel += "\t"
def decreaseIndentation():
	global indentationLevel
	indentationLevel = indentationLevel[:-1]

def generateFeature():
	featureIndex = random.choice(range(len(islandFeatures)))
	feature = islandFeatures[featureIndex]
	featureIndex += 1
	featureSize = rollDie(100) * featureIndex

	counted = numberedThing(featureSize, 'foot')
	return feature + ", " + counted + " in size"
generateFeature.title = "island feature"

def generateVolcano():
	volcano = random.choice(islandVolcanoes)
	if volcano.startswith('erupting'):
		force = random.choice(range(len(volcanoEruptions)))
		eruption = volcanoEruptions[force]
		force += 1
		damage = numberedThing(force, 'point')
		volcano += ' (' + eruption + '; save every turn or take ' + damage + ' damage)'
	return volcano
generateVolcano.title = 'volcano'
generateVolcano.message = 'there is a 10% chance per day of volcanoes moving to the next most active category when the island is visited'

def generateTrap():
	return random.choice(islandTraps)
generateTrap.title = 'obstacle'

def generateCreature():
	return random.choice(islandCreatures)
generateCreature.title = 'dominant creature'

def generateInhabitants(inhabitants):
	inhabitant = random.choice(inhabitants)
	if type(inhabitant) != str:
		#necessary to keep from modifying the list, screwing up multiple hits on this inhabitant
		inhabitant = inhabitant.copy()
		dieSize = 0
		inhabitantOption = ''
		if type(inhabitant[-1]) == list:
			inhabitantOptions = inhabitant.pop()
			inhabitantOption = random.choice(inhabitantOptions)
			if len(inhabitant) == 1:
				inhabitant = inhabitant[0]
			else:
				inhabitant, dieSize, dieMultiplier = inhabitant
		else:
			inhabitant, dieSize, dieMultiplier = inhabitant
		if dieSize > 0:
			inhabitantCount = rollDie(dieSize) * dieMultiplier
			inhabitant = numberedThing(inhabitantCount, inhabitant)
			if inhabitantOption:
				inhabitantOption = numberedThing(inhabitantCount, inhabitantOption, addNumber=False)
		if inhabitantOption:
			if inhabitant == 'garrison':
				garrisonContingent = rollDie(4, 6)
				inhabitantOption = str(garrisonContingent) + ' men, ' + inhabitantOption
			inhabitant += ' (' + inhabitantOption + ')'
	return inhabitant

def generateProvision():
	provision = random.choice(islandProvisions)
	if provision == 'game':
		provision = random.choice(provisionsGame) + ' (' + provision + ')'
	elif provision == 'abandoned habitation':
		provision = 'abandoned ' + random.choice(provisionsHabitation)
	elif provision == 'inhabited':
		if islandElevation > 0:
			provision = generateInhabitants(landInhabitants)
		else:
			provision = generateInhabitants(seabedInhabitants)
	elif type(provision) == tuple:
		nonpotability = provision[1]
		provision = provision[0]
		if rollDie(100) <= nonpotability:
			poison = random.choice(waterPoisons)
			provision += '; provides non-potable water, ' + poison
		else:
			provision += ' (potable water)'

	return provision
generateProvision.title = 'available provision'

featureKeys = {
	'c': generateCreature,
	'f': generateFeature,
	'h': 'is hilly',
	'm': 'is mountainous',
	'p': generateProvision,
	'r': 'contains a mineable resource',
	's': 'contains a stream',
	't': generateTrap,
	'v': generateVolcano,
}

def randomIsland():
	index = random.choice(range(len(types)))
	islandInfo = types[index]
	index += 1
	island = islandInfo.pop(0)

	return index, island, islandInfo

typeNumber, islandType, features = randomIsland()

title(islandType)
increaseIndentation()

islandSize = rollDie(10) * typeNumber * 100
islandElevationString, islandElevation = die100(islandElevations, needNumber=True)

sentence('The island is', numberedThing(islandSize, 'foot'), 'wide')
sentence('It has a general elevation of', islandElevationString)
sentence('It receives', die100(islandPrecipitation), 'of precipitation per year')
sentence('It has a growing season of', die100(islandGrowingSeason))
sentence('Temperatures on the island range from', die100(islandTemperatures))
sentence('It is recognizable by a unique', random.choice(islandLandmarks))
sentence('The weather is currently', random.choice(islandWeathers))

#shore party or marooned
paragraph()
approach = random.choice(islandApproaches)
sentence('The island is approachable through', approach)
sentence('Noise:', random.choice(islandSounds))
shore = random.choice(shoreOutcomes)
if shore.endswith('*'):
	shore = shore[:-1]
	if shore == 'find provisions':
		shore += ', unless unavailable'
	else:
		shore += '; if none on island, find provisions, unless also not available'
elif shore == 'a passing ship':
	shore += ' (' + random.choice(passingShips) + ')'

sentence('Outcome of shore party:', shore)

encounter = random.choice(coastalEncounters)
if type(encounter) == str:
	encounter = numberedThing(1, encounter)
else:
	dieCount = 1
	dieAdd = 0
	if len(encounter) == 2:
		dieSize = encounter[1]
	elif len(encounter) >= 3:
		dieCount = encounter[1]
		dieSize = encounter[2]
		if len(encounter) == 4:
			dieAdd = encounter[3]
	encounter = encounter[0]
	count = rollDie(dieCount, dieSize) + dieAdd
	encounter = numberedThing(count, encounter)
sentence('If an encounter is needed near the coast, consider', encounter)

if rollDie(100) <= 20:
	mystery =  random.choice(islandMysteries)
	if 'passing ship' in mystery:
		mystery += ' (' + random.choice(passingShips) + ')'
	elif 'castaway' in mystery:
		mystery += ' (' + random.choice(habitantCastaways) + ')'
	sentence('If marooned, on the first day they see', mystery)
paragraph()

if features and type(features[0]) == int:
	dieSize = features.pop(0)
	if features and type(features[0]) == int:
		dieCount = dieSize
		dieSize = features.pop(0)
	else:
		dieCount = 1
else:
	dieSize = len(features) or 1
	dieCount = 1

additionalMessages = []
featureList = {}
featureCount = rollDie(dieCount, dieSize)
while features and featureCount > 0:
	newFeature = random.choice(features)
	if newFeature not in featureList:
		featureList[newFeature] = 0
	featureList[newFeature] += 1
	featureCount -= 1

for feature in featureList:
	generator = featureKeys[feature]
	if type(generator) == str:
		sentence('The island', generator)
	else:
		featureCount = featureList[feature]
		if featureCount > 1:
			title(numberedThing(featureCount, generator.title))
			increaseIndentation()
			for index in range(1, featureCount+1):
				thing = generator()
				sentence(str(index) + '.', thing)
			decreaseIndentation()
			paragraph()
		else:
			thing = generator()
			sentence(generator.title + ':', thing)

		if hasattr(generator, 'message') and generator.message not in additionalMessages:
			additionalMessages.append(generator.message)

decreaseIndentation()
paragraph()
title('Notes')
increaseIndentation()
sentence('multiply average precipitation by three if within 150 miles of equator')
sentence('reduce temperatures by 10° for every 200 miles north of the equator, and by 5° for every 1,500 feet above sea level; in the winter subtract 30%, in the spring subtract 20%, in the summer add 10%, and in the fall subtract 25%')
for message in additionalMessages:
	sentence(message)

