Tracery (tracery.io) is a simple text-expansion language made by one of your TAs as a
homework assignment for one of Prof. Mateas’s previous courses. It now has tens of
thousands of users, and runs about 7000 chatbots on Twitter (you never know where a
homework will take you!).
Tracery uses context-free grammars
(https://en.wikipedia.org/wiki/Context-free_grammar) to store information about how to
expand rules. A Tracery grammar is a set of keys, each of which has some set of
expansions that can replace it. In our version, the line
“beverage:tea|coffee|cola|milk” means that the symbol “beverage” can be
replaced with any of those four options. You replace a symbol whenever you see it in
hashtags. This rule “#name# drank a glass of #beverage#” will have the “name”
and “beverage” symbols replaced with expansions associated with those symbols. In
the case of the “beverage” rule above, which has four possible expansions, one will be
picked at random. If the replacement rule also has hashtags in it, we replace those, and
if those replacements have hashtags…. we keep replacing things until all the hashtags
are gone, recursively.
In this assignment, you will be implementing a simplified version of Tracery in Java, and
then using it to generate generative text. You will also be writing your own grammar to
generate new texts (hipster cocktails, emoji stories, or nonsense poems).
● Compile and run a Java program
● Save all the arguments
● Load the Tracery files
● Output all the rules
● Expand rules and print them to the screen
Compile and run a Java program
This program has several files (Rule.java, and TraceryRecursion.java). We can’t
or Python). For Java, we need the computer to put them all together and translate it to
machine code, in a process called “compiling”.
You will compile and run your Java program from the command line. When you see $,
this means that it is a command that you will enter in the command line. Windows and
Unix (such as Linux or the Mac terminal) command lines are a little different, be sure
you know the basics of navigating the one you are using.
Do you have Java installed on your machine? What version? Let’s find out! Type the
following into your command line: $ javac -version $ java -version You should
have at least Javac 1.8 and Java 1.8 (often called “Java 8”) installed. If that’s not the
case, this is a good time to fix that by updating your Java. We will be using some
features of Java 8 in this class.
Compile your java program.
$ javac TraceryRecursion.java
So far, it will compile without errors.
Look in the folder, and you will see that you have a new file TraceryRecursion.class.
This is the compiled version of your file. You can now run this file by typing:
$ java TraceryRecursion
You should get the output Running TraceryRecursion….
Try typing javac TraceryRecursion or java TraceryRecursion.java. Both of these
should give your errors. Compiling requires the .java extension, and running requires
not having it. But it’s easy and common to misremember that, so look at what there
errors look like, so that when it happens later, you know what caused it.
You can compile and run in separate steps. But sometimes you want to compile and run
with one line, and you can combine them like this:
$ javac TraceryRecursion.java && java TraceryRecursion.
Many java programs need outside information provided by the user. This lets users
change the behavior of the program (or tell it which data to use) without having to
modify the source code.
The main method of TraceryRecursion will be the first method called when running
the program. This method is static, meaning that the method is associated directly
with a class and can thus be called without creating an object that is an instance of the
class, and its return type is void, meaning that it won’t return anything. It also has
parameters, an array of Strings called args (you could call this parameter anything, but
the tradition is to call it args or arguments).
Our program will take 0-4 arguments:
● the name of the file containing the grammar (e.g.: “grammar.txt”)
● the starting symbol to begin the grammar expansion (e.g.: “#story#”)
● the number of times we want to perform that expansion (e.g: “5”)
● a seed value for the random number generator (e.g: “123”) (Seeded random
values allow us to make the random generator predictable)
You can see that in main, each variable is given a default value. For example, if no seed
value is given in the arguments, the seed value will be the current time (this means you
will get different random results each time you run the program, unless a specific seed
is specified in the arguments).
For each of these parameters, test whether enough arguments have been specified to
set that parameter. For example, to set the grammarFile parameter there would have
to be at least one argument given. You can test the length of the args array to
determine how many arguments have been passed in. Override each of these default
parameters with its argument override. For the numbers, you will need to use
Integer.parseInt and Long.parseLong to turn the arguments from Strings into
numbers. NOTE: Do not add or remove code outside of the START and END
comments. Keeping all your code between those two comments will help us
grade your work.
Load a Tracery file
Java has many helpful built-in data structures: Lists, Arrays, and Hashtables are only a
few of them. In this assignment, we will be loading and storing a Tracery grammar as a
Hashtable. The Hashtable has a list of keys, and each key has some data associated
with it, much like a dictionary stores words with their definition. Hashtables come with
some helpful methods included:
● table.get(String s) returns the data for that key, but it will throw an error if
that key is not in the table.
● table.put(String s, DATA) creates an entry for that key (or replaces it) and
stores the data there.
In this part of the assignment, we will implement loadGrammar, which takes a String
representing a file path, then loads a file, parses it (breaking it into sections), and stores
it in our grammar object.
In the loadGrammar method, we have already added the line that creates the
Hashtable. You can see that the data type has a weird ending
which tells Java that this Hashtable uses Strings to look up arrays of Rules, so if we try
to use numbers as look up values, or store Strings instead of arrays of Rules, Java
knows to throw an error.
We also included a basic way to load a file. Loading files might cause an IO exception
(for example, if the file does not exist), so for complex reasons, we need to have the
wrapped in a try/catch block. We will talk more about throwing and catching
exceptions later this quarter. For right now, this loads a whole file as a string. By
appending .split(‘\\r?\\n’) on the end, we take the resulting string and splits it into
an array of lines.
For each line in the array of lines, store the data in the grammar hashtable. You can use
either a familiar for (int i = 0…) loop, or use the newer for (String line:
lines) loop, if you don’t need to see the index value.
We used the split String method above to split apart the file into lines. Use split
again, but splitting on the character “:”. Everything before the “:” is the key. Store it in
a variable named key. Everything after it is the rule expansion(s).
First split apart the rules into an array of Strings. For each line in the file, this will give
you a String array of length 2, where the first element of the array is an expandable
symbol (which will be a key in the grammar hashtable), and the second element of the
array is the expansion(s) for this symbol. But there is possibly more than one expansion
for a symbol. If you look at the grammar files, you can see where there is more than one
expansion for a symbol when there’s a comma-delimited list of expansions after a
symbol. So we have to further split the expansions into an array of Strings, one for each
expansion (by splitting on “,”).
Once you have an array of Strings (of expansions) we have to turn it into an array of
Rules. Initialize an array, Rule, of the right size. Use another loop (inside the loop
iterating through lines) to iterate through your array of strings and, for each expansion,
create a Rule (it takes a single string as a parameter), and add it to your array of Rules.
You now have the right kind of data (Rule) to store in the hashtable.
Store your data in grammar. What was that method that stores data in a grammar (hint:
we describe it above)?
In the main() method there’s already a call to outputGrammar; this prints your grammar
to the console, so you can verify that it loaded it correctly (that the correct expansions
are associated with the correct symbols).
Now that you have successfully loaded a file and stored it as a Hashtable
Open up Rule.java, and look at the constructor for this class: Rule(String raw).
This constructor takes the string that you passed to it in TODO #2 and splits it into
sections (using “#”). The even indexed entries of sections are plain text (ie. the zeroth,
second, fourth, etc.), and the odd indexed entries of the variable sections are symbols
for expanding (ie. the first, third, fifth, etc.). This might seem a bit confusing at first. In an
expansion like “once #character# and #character# went to a #place#” it’s
clear that splitting on # will cause the zeroth, second and fourth elements (the even
elements) to be plain text (i.e. “once”, “and” and “went to a”) and the first, third and
fifth elements (the odd elements) to be symbols (i.e. “#character#”, “#character#”,
“#place#”). But consider the expansion “#name# the #adjective# #animal#”.
Won’t this even and odd approach get reversed, since the expansion starts with a
symbol? It turns out it won’t because, if there’s a symbol at the beginning, or two
symbols right next to each other, splitting on “#” produces two strings, the empty string
(which is what is before the “#”) and then the string with the symbol (which is what is
after the “#”, minus the closing “#” since we’re splitting on “#”). So splitting this
expansion on # will produce the following array:
So the even and odd relationship still works out, it’s just that two of our even entries are
the empty string (which is still plain text), which, when we include it in the output of the
expansion, won’t be visible, and so won’t cause us any problems. Phew! Now that we’ve
explained that we can get back to actually writing the code in Rule.java to expand the
The method expand takes a grammar (the type is Hashtable
argument, and returns the expanded String. But right now, it only returns a copy of its
original expansion (with any symbols not expanded). We want it to instead recursively
expand the odd sections.
Create an array of Strings named results that is the same size as the rule sections
(sections.length). This is where we will store the results of expanding each section.
Create a for-loop that iterates down the sections array. Since we need to know whether
each section is odd or even, you will need to use for (int i = 0…), rather than
for (String section: sections) style loops. For each section, if it is even (i%2 ==
0), copy the text from the rule section into the results array.
If it is odd, then this is a symbol that we need to expand. Use the grammar to find the
array of expansions for this symbol. We need to pick a “random” index in the array, but
we will use the seeded random number generator (so we can reproduce our results
using the same random seed). You can get a random int with random.nextInt(int
bound). This will return a random integer between 0 (inclusive) and the bound argument
you pass in (exclusive). Since you’re randomly picking from an array of possible
expansions, what’s the largest random value you will want? Pick out an expansion, and
save it as the variable Rule selectedExpansion.
We want to store that new value in our results array, but since is is an expansion
string we need to process it first because it might contain symbols that need to be
expanded as well. What method should you call on selectedExpansion to do that?
Store the resulting text in the results array.
We now have results values for each section in this rule! Use String.join(“”,
results) to join all of the results together into a single string (with no spaces between
them), and return the results of that operation. This new return statement should replace
the return statement that was in the starter code. That is, your new return statement
should replace the line: return “[” + raw + “]”;
Run your program. It should now output however many outputs of the text you want (as
specified by the count parameter). Try it with and without a seed value. Does it always
output something new? Try out a few other grammars, we included
grammar-recipe.txt which makes bad recipes, and grammar-emojistory.txt which
tells stories with emoji.
Now edit grammar-yourgrammar.txt. Create something interesting (of about equal
complexity to grammar-story.txt). We won’t judge you on what it makes, consider this
a freeplay exercise. Some inspirational twitterbots include @unicode_garden,
@DUNSONnDRAGGAN, @orcish_insults, @GameIdeaGarden , @indulgine,
@CombinationBot, @thinkpiecebot, @infinite_scream, FartingEmoji.
Make sure your grammar generates without causing errors! We didn’t implement a
graceful way to recover from bad formatting or missing symbols, so these grammars are
brittle, they break easily.
Wow, you’re done! Congrats on finishing your first 12B lab.
● Works for no command‑line arguments, a few, or all of them
● If a random seed is specified, the output stays consistent. If not, it changes
● If you switch the grammar file, you get output from a different grammar
● You have edited grammar-yourgrammar.txt and it runs.
To check that your code is right, compare it with the following output. This output
assumes that you’re using Random.nextInt() to generate the random numbers (as
described above). This is running TraceryRecursion with the following arguments:
$ java TraceryRecursion grammar-story.txt “#origin#” 5 10
The output should be:
with grammar:’grammar-story.txt’ startsymbol:’#origin#’
Set seed 10
character:#name# the #adjective# #animal#
place:school,the beach,the zoo,Burning Man
origin:once #character# and #character# went to #place#
place: “school”,”the beach”,”the zoo”,”Burning Man”,
origin: “once #character# and #character# went to
character: “#name# the #adjective# #animal#”,
once anna the green emu and anna the sleepy emu went to the zoo
once emily the green emu and anna the green cat went to school
once charlie the blue okapi and otavio the blue emu went to school
once emily the blue emu and otavio the blue cat went to the beach
once emily the sad okapi and charlie the elated okapi went to the zoo
To turn in your code:
● Create a directory with the following name:
ID is 1234567, then the directory name is 1234567_lab1.
● Put a copy of your edited TraceryRecursion.java, Rule.java and
grammar-yourgrammar.txt files in the directory.
● Compress the folder using zip to create. Zip is a compression utility available on
mac, linux and windows that can compress a directory into a single file. This
should result in a file named
replaced with your real ID of course).
● Upload the zip file through the page for Lab 1 in canvas
● How should I name things in Java?
● Basic Java Syntax https://www.tutorialspoint.com/java/java_basic_syntax.htm
● Need some in‑depth Java tutorials? There are free courses online:
● Command line basics: https://hellowebbooks.com/learn‑command‑line/
● Get Linux/OSX style command line tools on Windows with Cygwin