[SUMMARY] Story Generator (#96)

R

Ruby Quiz

The solutions to this quiz where wildly creative, both in form and output. I'm
still having nightmares about poor Little Red-Cap and Princess Lily.

The chief element of all the solutions was randomness. Jim Menard and Morton
Goldberg took game engines (already a bit random) and added an autoplay mode to
drain stories out of them. Myself and Jordan randomized sentence construction
to varying degrees and slung together a mess of invented lines. The other
solutions narrowed their tale telling focus to a specific genre and randomized
the story elements themselves. The end results were quite varied and certainly
entertaining!

I really wish we could go through all of those solutions and talk about the
unique approach of each one. This week, even more than usual, each idea was
quite unique. Unfortunately time and space demand that I pick one. Do look the
others over though, so you can learn from the clever people who played with the
quiz.

Below I'm going to discuss the code from Boris Prinz. Boris took the randomized
story elements approach and ended up with a pretty flexible system. On top of
that, the code is just plain cool and worth a look. Before we dive in though,
here's a story produced by a sample run of Boris's code:

Once upon a time little red-cap met the huntsman although the huntsman lived
in the village. Then the wolf was delighted. Then he took a piece of cake.
Grandmother snored very loud, but little red-cap swallowed up grandmother,
and the huntsman saw a bottle of wine. Later he snored very loud. The wolf
was not afraid of little red-cap although he got deeper and deeper into
grandmother's house. Little red-cap opened the stomach of mother. She ran
straight to grandmother's house. Little red-cap saw a bottle of wine. The
huntsman ran straight to grandmother's house. Soon the wolf was delighted.
The huntsman walked for a short time by the side of grandmother.
The End.

Alright, let's see how the misadventures of Little Red-Cap get constructed.
Here's the beginning of the code:

class Base
def self.constructor *args
attr_accessor(*args)
define_method :initialize do |*values|
(0...args.size).each do |i|
self.instance_variable_set("@#{args}", values)
end
end
end
def to_s
@name
end
end

Boris decided to take a metaprogramming approach to the large amount of classes
that would need to be built. The first step was to simplify the building of
constructors for the classes, with a class method that rolls an initialize()
plus some accessors for you. This is pretty close to redefining Ruby's Struct
class and it may be possible to use that instead, though the addition of the
to_s() to pull the name attribute complicates this slightly.

Let's see how this class method gets used:

class Character < Base
constructor :name, :gender
end

class Action < Base
constructor :name, :eek:bjects_or_types
end

class Item < Base
constructor :name
end

class Place < Base
constructor :name
end

class Bridge < Base
constructor :name
end

Here we see classes for the story elements being assembled. All of these
examples have the name element used by to_s() and Character and Action define
additional elements for those types.

Remember all the work being done by this simple call. In the case of Character
for example, a constructor is built to accept and set the two parameters and
name(), name=(), gender(), and gender=() are defined as instance methods of the
class.

The next class forms a new base type for some sentence construction elements:

class PronounBase < Base
constructor :gender
class << self
attr_accessor :cases
end
def to_s
cases = self.class.cases
@gender == :female ? cases[0] : cases[1]
end
end

With pronouns the code needs to worry about the gender of the referred to
Character. To support this a class level attribute is defined that can hold
both cases (female and male) and to_s() is modified to make the right choice
based on the assigned gender.

Now we can examine the extended family of this class:

class PossessiveAdjective < PronounBase
self.cases = ['her', 'his']
end

class Pronoun < PronounBase
self.cases = ['she', 'he']
end

class ReflexivePronoun < PronounBase
self.cases = ['herself', 'himself']
end

Boris takes another metaprogramming step to help with the construction, storage,
and selection of these entities:

class Entities
def initialize klass
@entities = []
@klass = klass
yield(self)
end
def create *args
@entities << @klass.new(*args)
end
def pick
@entities[rand(@entities.size)]
end
end

This class is an object container. You tell it the kind of objects you will
store in it at construction time. After that, you can use create() to construct
an object of the indicated type and add it to the collection. When needed,
pick() returns a random object from the collection.

Here's the code that populates the containers for the Little Red-Cap story
world:

CAST = Entities.new(Character) do |c|
c.create 'little red-cap', :female
c.create 'mother', :female
c.create 'grandmother', :female
c.create 'the wolf', :male
c.create 'the huntsman', :male
end

ACTIONS = Entities.new(Action) do |a|
a.create 'met', [Character]
a.create 'gave', [Item, 'to', Character]
a.create 'took', [Item]
a.create 'ate', [Item]
a.create 'saw', [Item]
a.create 'told', [Character, 'to be careful']
a.create 'lived in', [Place]
a.create 'lied in', [Place]
a.create 'went into', [Place]
a.create 'ran straight to', [Place]
a.create 'raised', [PossessiveAdjective, 'eyes']
a.create 'was on', [PossessiveAdjective, 'guard']
a.create 'thought to', [ ReflexivePronoun,
'"what a tender young creature"' ]
a.create 'swallowed up', [Character]
a.create 'opened the stomach of', [Character]
a.create 'looked very strange', []
a.create 'was delighted', []
a.create 'fell asleep', []
a.create 'snored very loud', []
a.create 'said: "oh,', [Character, ', what big ears you have"']
a.create 'was not afraid of', [Character]
a.create 'walked for a short time by the side of', [Character]
a.create 'got deeper and deeper into', [Place]
end

ITEMS = Entities.new(Item) do |i|
i.create 'a piece of cake'
i.create 'a bottle of wine'
i.create 'pretty flowers'
i.create 'a pair of scissors'
end

PLACES = Entities.new(Place) do |p|
p.create 'the wood'
p.create 'the village'
p.create 'bed'
p.create "grandmother's house"
p.create 'the room'
end

BRIDGES = Entities.new(Bridge) do |b|
5.times{b.create '.'}
b.create ', because'
b.create ', while'
b.create '. Later'
b.create '. Then'
b.create '. The next day'
b.create '. And so'
b.create ', but'
b.create '. Soon'
b.create ', and'
b.create ' until'
b.create ' although'
end

ALL = { Character => CAST, Action => ACTIONS,
Place => PLACES, Item => ITEMS }

I know that's a lot of code, but you can see that it's really just object
construction in groups. There are two points of interest in the above code,
however.

First note that we begin to get hints of sentence structure where the ACTIONS
are constructed. The second parameter for those seems to be a mapping of the
elements and joining phrases that come after the action. We will see how that
comes together shortly now.

Also of interest is the simple but effective first line of BRIDGES construction.
By adding the ordinary period five times, the scale is tilted so that it will
randomly be selected more often. This gives the final text a more natural flow.

Here's the class that turns all of those simple lists into sentences:

class Sentence
attr_accessor :subject
def initialize
@subject = CAST.pick
@verb = ACTIONS.pick
@objects = []
@verb.objects_or_types.each do |obj_or_type|
if String === obj_or_type
@objects << obj_or_type
else
if obj_or_type == PossessiveAdjective or
obj_or_type == ReflexivePronoun
@objects << obj_or_type.new(@subject.gender)
else
thingy = ALL[obj_or_type].pick
if thingy == @subject
thingy = ReflexivePronoun.new(thingy.gender)
end
@objects << thingy
end
end
end
end

def to_s
[@subject, @verb, @objects].flatten.map{|e| e.to_s}.join(' ')
end
end

The constructor does the heavy lifting here. First a subject and verb are
selected from the appropriate lists. The rest of the method turns the sentence
patterns we examined earlier into an actual list of objects. Strings are just
added to the list, PossessiveAdjectives and ReflexivePronouns are constructed
based on the gender of the subject, and everything else is a random pick from
the indicated list but swapped with a pronoun if the subject comes up again.
With the sentence pieces tucked away in instance variables, to_s() can just
flatten() and join() the list to produce a final output. (The call to map() is
not needed since join() automatically stringifies the elements.)

We only need one more class to turn those sentences into a complete story.
Here's the code:

require 'enumerator'

class Story
def initialize
@sentences = []
1.upto(rand(10)+10) do
@sentences << Sentence.new
end
combine_subjects
end

# When the last sentence had the same subject, replace subject with
# 'he' or 'she':
def combine_subjects
@sentences.each_cons(2) do |s1, s2|
if s1.subject == s2.subject
s2.subject = Pronoun.new(s1.subject.gender)
end
end
end

# Combine sentences to a story:
def to_s
text = 'Once upon a time ' + @sentences[0].to_s
@sentences[1..-1].each do |sentence|
bridge = BRIDGES.pick.to_s
text += bridge + ' ' +
( bridge[-1,1] == '.' ? sentence.to_s.capitalize :
sentence.to_s )
end
text.gsub!(/ ,/, ',') # a little clean-up
text.gsub!(/(.{70,80}) /, "\\1\n")
text + ".\nThe End.\n"
end
end

puts Story.new.to_s

Here the constructor creates a random number of sentences (between 10 and 19)
and modifies them if needed with a call to combine_subjects(). The comment
gives you the scoop on that one, which just replaces the second of two
consecutive subjects with a pronoun.

Finally, to_s() produces the end result. The first sentence is combined with
the immortal opening "Once upon a time" to get us started. After that, each
sentence is joined to the story using one of the BRIDGES we saw in the entity
construction, careful to maintain proper capitalization for the added sentence.
A few Regexps are used to tidy up and and the story closed with "The End."

You can see in the final line of the program is all it takes to produce a
solution. A Story is constructed, stringified, and printed to the screen.
(Again the call to to_s() is not required, since puts() will handle this for
you.)

Once upon a time a band of seven Rubyists discovered the mysterious Ruby Quiz.
Together they unlocked its mysterious and enlightened the entire realm of
programmers. The quizmaster was eternally grateful for their efforts and shared
knowledge. The end.

Tomorrow we will continue our word games, but turn our focus to Posix commands
instead of using a normal dictionary...
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,888
Messages
2,569,964
Members
46,294
Latest member
HollieYork

Latest Threads

Top