# coding: utf-8
"""Implements a class to help with randomized text generation"""
import random
import re
import fnmatch
import operator
[docs]class NameNotFound(Exception): """An expansion for the name was not found"""
[docs]class NoOccurrence(Exception): """There was no occurrence of a fact"""
[docs]class InvalidTime(Exception): """The time specified was invalid"""
[docs]class TextGenerator(object):
"""Generate text from forms
A form gives the possible values for something, for example the
form for colour might give [red, green, blue].
You can then convert sentences like 'the @colour@ book'
Conversion can be hierarchical like:
objects are [book, table, @{colour}@ cat]
Sentence 'the @{object}@' would give a "book" or a "red cat".
When looking up examples from text or files the form should be:
type: example1
type: example2
Or
type {
example1
example2
}
"""
def __init__(self):
"""Initialise the generator"""
self.forms = {}
[docs] def addExample(self, name, conversion):
"""Add a new form"""
try:
the_form = self.forms[name]
except KeyError:
the_form = set()
self.forms[name] = the_form
#
the_form.add(conversion)
[docs] def addExampleFromText(self, text):
"""Add an example from text - the name is ':' separated from the conversion"""
parts = [i.strip() for i in text.strip().split(':', 1)]
if len(parts) != 2:
raise ValueError('Need ":" separated name and then conversion. Got [%s]' % (parts,))
self.addExample(*parts)
[docs] def addExamplesFromText(self, text):
"""Add a number of examples from text"""
in_multiline = None
for idx, full_line in enumerate(text.splitlines()):
line = full_line.strip()
if line and not line.startswith('#'):
if line.endswith('{'):
# Multiple lines follow - store the name
parts = line.split(' ')
if len(parts) != 2:
raise ValueError('Need "<name> {" for multi-line (line %d:"%s")' % (idx, full_line))
in_multiline = parts[0].strip()
elif line.endswith('}'):
# Multiple lines end
in_multiline = None
else:
if not in_multiline:
self.addExampleFromText(line)
else:
self.addExample(in_multiline, line)
[docs] def addExamplesFromFile(self, filename):
"""Add multiple examples from a file"""
text = file(filename, 'r').read()
self.addExamplesFromText(text)
[docs] def getRandomFormCompletion(self, name, properties=None):
"""Return the comletion of a form randomly"""
properties = {} if properties is None else properties
try:
results = self.forms[name]
except KeyError:
# No name found
raise NameNotFound('The expansion @%s@ was not defined' % name)
else:
result = random.choice(list(results))
if name not in properties:
properties[name] = result
return result
[docs] def getRandomSentence(self, text, properties=None):
"""Return a random sentence from the text"""
if properties is None:
properties = {}
match = re.match('(.*)@{(.+?)}@(.*)', text, re.DOTALL+re.M)
if match:
name = match.groups()[1]
try:
notfound = False
replacement = properties[name]
except KeyError:
replacement = self.getRandomFormCompletion(name, properties)
#
result = self.getRandomSentence(
match.groups()[0] +
replacement +
match.groups()[2], properties)
return result
else:
return text
import random
# from http://www.geocities.com/anvrill/names/cc_goth.html
PLACES = ['Adara', 'Adena', 'Adrianne', 'Alarice', 'Alvita', 'Amara', 'Ambika', 'Antonia', 'Araceli', 'Balandria', 'Basha',
'Beryl', 'Bryn', 'Callia', 'Caryssa', 'Cassandra', 'Casondrah', 'Chatha', 'Ciara', 'Cynara', 'Cytheria', 'Dabria', 'Darcei',
'Deandra', 'Deirdre', 'Delores', 'Desdomna', 'Devi', 'Dominique', 'Drucilla', 'Duvessa', 'Ebony', 'Fantine', 'Fuscienne',
'Gabi', 'Gallia', 'Hanna', 'Hedda', 'Jerica', 'Jetta', 'Joby', 'Kacila', 'Kagami', 'Kala', 'Kallie', 'Keelia', 'Kerry',
'Kerry-Ann', 'Kimberly', 'Killian', 'Kory', 'Lilith', 'Lucretia', 'Lysha', 'Mercedes', 'Mia', 'Maura', 'Perdita', 'Quella',
'Riona', 'Safiya', 'Salina', 'Severin', 'Sidonia', 'Sirena', 'Solita', 'Tempest', 'Thea', 'Treva', 'Trista', 'Vala', 'Winta']
###############################################################################
# Markov Name model
# A random name generator, by Peter Corbett
# http://www.pick.ucam.org/~ptc24/mchain.html
# This script is hereby entered into the public domain
###############################################################################
[docs]class Mdict:
def __init__(self):
self.d = {}
def __getitem__(self, key):
if key in self.d:
return self.d[key]
else:
raise KeyError(key)
[docs] def add_key(self, prefix, suffix):
if prefix in self.d:
self.d[prefix].append(suffix)
else:
self.d[prefix] = [suffix]
[docs] def get_suffix(self,prefix):
l = self[prefix]
return random.choice(l)
[docs]class MarkovNameGenerator(object):
"""
A name from a Markov chain
"""
def __init__(self, names, chainlen=2):
"""
Building the dictionary
"""
self.mcd = Mdict()
oldnames = []
self.chainlen = chainlen
for l in names:
l = l.strip()
oldnames.append(l)
s = " " * chainlen + l
for n in range(0,len(l)):
self.mcd.add_key(s[n:n+chainlen], s[n+chainlen])
self.mcd.add_key(s[len(l):len(l)+chainlen], "\n")
[docs] def getName(self, max_length=9):
"""
New name from the Markov chain
"""
prefix = " " * self.chainlen
name = ""
suffix = ""
while True:
suffix = self.mcd.get_suffix(prefix)
if suffix == "\n" or len(name) > max_length:
break
else:
name = name + suffix
prefix = prefix[1:] + suffix
return name.capitalize()
EUROPE = [
"Moscow",
"LONDON",
"St Petersburg",
"BERLIN",
"MADRID",
"ROMA",
"KIEV",
"PARIS",
"Bucharest",
"BUDAPEST",
"Hamburg",
"MINSK",
"Warsaw",
"Belgrade",
"Vienna",
"Kharkov",
"Barcelona",
"Novosibirsk",
"Nizhny Novgorod",
"Milan",
"Ekaterinoburg",
"Munich",
"Prague",
"Samara",
"Omsk",
"SOFIA",
"Dnepropetrovsk",
"Kazan",
"Ufa",
"Chelyabinsk",
"Donetsk",
"Naples",
"Birmingham",
"Perm",
"Rostov-na-Donu",
"Odessa",
"Volgograd",
"Cologne",
"Turin",
"Voronezh",
"Krasnoyarsk",
"Saratov",
"ZAGREB",
"Zaporozhye",
"Lódz",
"Marseille",
"RIGA",
"Lvov",
"Athens",
"Salonika",
"STOCKHOLM",
"Kraków",
"Valencia",
"AMSTERDAM",
"Leeds",
"Tolyatti",
"Kryvy Rig",
"Sevilla",
"Palermo",
"Ulyanovsk",
"KISHINEV",
"Genova",
"Izhevsk",
"Frankfurt am Main",
"Krasnodar",
"Breslau",
"Glasgow",
"Yaroslave",
"Khabarovsk",
"Vladivostok",
"Zaragoza",
"Essen",
"Rotterdam",
"Irkutsk",
"Dortmund",
"Stuttgart",
"Barnaul",
"VILNIUS",
"Poznan",
"Düsseldorf",
"Novokuznetsk",
"Lisbon",
"HELSINKI",
"Málaga",
"Bremen",
"Sheffield",
"SARAJEVO",
"Penza",
"Ryazan",
"Orenburg",
"Naberezhnye Tchelny",
"Duisburg",
"Lipetsk",
"Hannover",
"Mykolaiv",
"Tula",
"OSLO",
"Tyumen",
"Copenhagen",
"Kemerovo",
"Moscow",
"LONDON",
"St Petersburg",
"BERLIN",
"MADRID",
"ROMA",
"KIEV",
"PARIS",
"Bucharest",
"BUDAPEST",
"Hamburg",
"MINSK",
"Warsaw",
"Belgrade",
"Vienna",
"Kharkov",
"Barcelona",
"Novosibirsk",
"Nizhny Novgorod",
"Milan",
"Ekaterinoburg",
"Munich",
"Prague",
"Samara",
"Omsk",
"SOFIA",
"Dnepropetrovsk",
"Kazan",
"Ufa",
"Chelyabinsk",
"Donetsk",
"Naples",
"Birmingham",
"Perm",
"Rostov-na-Donu",
"Odessa",
"Volgograd",
"Cologne",
"Turin",
"Voronezh",
"Krasnoyarsk",
"Saratov",
"ZAGREB",
"Zaporozhye",
"Lódz",
"Marseille",
"RIGA",
"Lvov",
"Athens",
"Salonika",
"STOCKHOLM",
"Kraków",
"Valencia",
"AMSTERDAM",
"Leeds",
"Tolyatti",
"Kryvy Rig",
"Sevilla",
"Palermo",
"Ulyanovsk",
"KISHINEV",
"Genova",
"Izhevsk",
"Frankfurt am Main",
"Krasnodar",
"Breslau",
"Glasgow",
"Yaroslave",
"Khabarovsk",
"Vladivostok",
"Zaragoza",
"Essen",
"Rotterdam",
"Irkutsk",
"Dortmund",
"Stuttgart",
"Barnaul",
"VILNIUS",
"Poznan",
"Düsseldorf",
"Novokuznetsk",
"Lisbon",
"HELSINKI",
"Málaga",
"Bremen",
"Sheffield",
"SARAJEVO",
"Penza",
"Ryazan",
"Orenburg",
"Naberezhnye Tchelny",
"Duisburg",
"Lipetsk",
"Hannover",
"Mykolaiv",
"Tula",
"OSLO",
"Tyumen",
"Copenhagen",
"Kemerovo",
]
[docs]class FactDatabase(object):
"""Stores facts about the world"""
def __init__(self):
"""Initialise the database"""
self.db = {}
self.current_time = 0
[docs] def addFact(self, fact_id, *params):
"""Add a fact to the database"""
self.db.setdefault(fact_id, []).append((self.current_time, ) + params)
[docs] def getTime(self):
"""Return the current time"""
return self.current_time
[docs] def setTime(self, time):
"""Set the current time"""
if time < self.current_time:
raise InvalidTime('Tried to set time (%s) before current time (%s)' % (time, self.current_time))
self.current_time = time
def _getMatchingFacts(self, fact_id):
"""Return a list of the facts matching the id"""
if '*' not in fact_id:
return [fact_id]
else:
return fnmatch.filter(self.db.keys(), fact_id)
[docs] def hasOccurred(self, fact_id):
"""Return True if the fact has occurred"""
ids = self._getMatchingFacts(fact_id)
if not ids:
return False
#
for this_id in ids:
if this_id in self.db:
return True
else:
return False
[docs] def getOccurrenceFrequency(self, fact_id):
"""Return the frequency with which a fact as occurred"""
if self.current_time == 0:
return 0
return float(self.getNumberOfOccurrences(fact_id)) / self.current_time
[docs] def getNumberOfOccurrences(self, fact_id):
"""Return the number of times a fact has occurred"""
total = 0
for this_id in self._getMatchingFacts(fact_id):
try:
total += len(self.db[this_id])
except KeyError:
pass
return total
[docs] def getLastOccurrenceTime(self, fact_id):
"""Return the last time a fact occurred"""
return self.getLastOccurrence(fact_id)[0]
[docs] def getLastOccurrence(self, fact_id):
"""Return the last instance of an event"""
results = []
for this_id in self._getMatchingFacts(fact_id):
try:
results.append(self.db[this_id][-1])
except KeyError:
pass
if not results:
raise NoOccurrence('No occurrences of fact %s' % fact_id)
else:
results.sort()
return results[-1]
[docs] def getLastFactParameters(self, fact_id):
"""Return the parameters for the last time the fact occurred"""
return self.getLastOccurrence(fact_id)[1:]
[docs] def getFactInstances(self, fact_id):
"""Return all instances of a certain fact"""
result = []
for this_id in self._getMatchingFacts(fact_id):
result.extend(self.db.get(this_id, []))
result.sort()
return result
[docs]class RuleSystem(object):
"""Implements a rule checking system"""
def __init__(self, db):
"""Initialise the rule system"""
self.db = db
self.rules = []
[docs] def addRule(self, rule, priority=None):
"""Add a rule to the system"""
if priority is None:
priority = -len(self.rules)
self.rules.append((priority, rule))
self.rules.sort()
[docs] def getBestMatchingRule(self):
"""Return the best rule matching the current world"""
for _, rule in reversed(self.rules):
if rule.isMatched(self.db):
return rule
else:
return None
[docs] def FactOccurred(self, fact_id):
"""Return a checker for a fact"""
return lambda db: db.hasOccurred(fact_id)
[docs] def FactNotOccurred(self, fact_id):
"""Return a checker for a fact not occurring"""
return lambda db: not db.hasOccurred(fact_id)
[docs] def FactOccurredNumberOfTimes(self, fact_id, number):
"""Return a checker for a fact occurring a number of times"""
return lambda db: db.getNumberOfOccurrences(fact_id) >= number
[docs] def FactNotOccurredNumberOfTimes(self, fact_id, number):
"""Return a checker for a fact not occurring a number of times"""
return lambda db: db.getNumberOfOccurrences(fact_id) < number
[docs] def FactOccurredExactNumberOfTimes(self, fact_id, number):
"""Return a checker for a fact occurring a number of times"""
return lambda db: db.getNumberOfOccurrences(fact_id) == number
[docs] def FactNotOccurredExactNumberOfTimes(self, fact_id, number):
"""Return a checker for a fact not occurring a number of times"""
return lambda db: db.getNumberOfOccurrences(fact_id) != number
[docs] def FactOccurredFrequency(self, fact_id, frequency):
"""Return a checker for a fact occurring at least the given frequency"""
return lambda db: db.getOccurrenceFrequency(fact_id) >= frequency
[docs] def FactNotOccurredFrequency(self, fact_id, frequency):
"""Return a checker for a fact occurring at most the given frequency"""
return lambda db: db.getOccurrenceFrequency(fact_id) < frequency
[docs] def FactRecentlyOccurred(self, fact_id, interval):
"""Return a checker for a fact occurring in the last time interval"""
def checker(db):
try:
return db.getTime() - db.getLastOccurrenceTime(fact_id) <= interval
except NoOccurrence:
return False
return checker
[docs] def FactNotRecentlyOccurred(self, fact_id, interval):
"""Return a checker for a fact not occurring in the last time interval"""
def checker(db):
try:
return db.getTime() - db.getLastOccurrenceTime(fact_id) > interval
except NoOccurrence:
return False
return checker
[docs]class Rule(object):
"""Implements a rule"""
def __init__(self, rule_id, clauses):
"""Initialise the rule"""
self.rule_id = rule_id
self.clauses = clauses
[docs] def isMatched(self, db):
"""Return True if the rule is matched"""
for clause in self.clauses:
if not clause(db):
return False
else:
return True
[docs] def fireRule(self, db):
"""Call this when the rule should fire
Override this method to give particular rule behaviours
"""
[docs]class RechargingRule(Rule):
"""A rule that must recharge once it fires"""
def __init__(self, rule_id, clauses, recharge_time):
"""Initialise the rule"""
super(RechargingRule, self).__init__(rule_id, clauses)
#
self.recharge_time = recharge_time
self._last_fire_time = None
[docs] def fireRule(self, db):
"""Fire the rule"""
self._last_fire_time = db.getTime()
[docs] def isMatched(self, db):
"""Return True if we are matched - must match and not be charging"""
if self._last_fire_time is not None and db.getTime() - self._last_fire_time < self.recharge_time:
return False
else:
return super(RechargingRule, self).isMatched(db)
if __name__ == '__main__':
t = TextGenerator()
t.addExample('colour', 'red')
t.addExample('colour', 'green')
t.addExample('colour', 'blue')
t.addExample('object', '@thing@')
t.addExample('object', 'a @colour@ @thing@')
t.addExample('object', 'a @colour@ @thing@')
t.addExample('thing', 'cat')
t.addExample('thing', 'dog')
t.addExample('thing', 'book')
t.addExample('size', 'small')
t.addExample('size', 'tiny')
t.addExample('size', 'large')
t.addExample('thing', '@size@ @thing@')
for i in range(10):
print t.getRandomSentence('@object@')
n = TextGenerator()
n.addExamplesFromText(
"""
colour: red
colour: blue
colour: green
colour: yellow
colour: purple
colour: black
colour: fuscia
#
jewel: diamond
jewel: ruby
jewel: emerald
jewel: saphire
#
size: small
size: tiny
size: large
size: giant
#
time-span: everlasting
time-span: temporary
time-span: nighttime
time-span: daytime
time-span: lifelong
time-span: eternal
#
effect: wellness
effect: charm
effect: charisma
effect: intellect
#
jewel-item: @colour@ @jewel@
jewel-item: @jewel@
#
jewel-description: The @size@ @jewel-item@ of @property@
jewel-description: The @jewel-item@ of @property@
short-jewel-description: @jewel-item@
#
property: @time-span@ @effect@
property: @effect@
#
reason: was @verb@ by @name@ in @time@
verb: lost
verb: placed
verb: discarded
verb: mislaid
verb: recorded
#
name: @first-name@ @last-name@
name: @first-name@ @last-name@ @post-name@
first-name: Bob
first-name: Fred
first-name: Jim
first-name: Bill
first-name: Marvin
first-name: Jill
first-name: Alice
first-name: Sheila
first-name: Lemon
last-name: Smith
last-name: Jones
last-name: Crimson
last-name: Little
last-name: Jenson
last-name: Williams
post-name: Junior
post-name: Senior
post-name: I
post-name: II
#
time: 1900's
time: 1800's
time: 1950's
time: 1960's
time: 1970's
time: 1980's
time: 1990's
#
description: @jewel-description@ @reason@
#
"""
)
for i in range(20):
print n.getRandomSentence('@description@')