Saturday, December 20, 2008

Zombie noises

So... the important things in life include zombies, obviously. Therefore, a program that mangles words into something a zombie might have said must be pretty damn important as well.

That's what it does. It takes a bunch of text and applies some basic transformations to it at random. The transformation multiplies some letters, removes some letters or replaces some letters with entire bits of (short) mangled text. And when there's an empty line somewhere, a generic answer might appear. And sometimes nothing happens... that's zombies for you.

It's actually a re-write of an earlier program I did in the Ocaml programming language; however I could not stop myself from writing it. Comes with the added benefit that it's a script now and does not need recompilation every time you want to add a noise.

It has several modes, so you get to put files and pipes through it or converse with it. The former is good to produce, i.e., a zombie translation of De Bello Gallico by Mr Dog.

The latter is far more interesting, and perhaps dangerous. Some people spend hours on end conversing with the zombie... and I swear, that thing gets intelligent at you, when you're not expecting it!

Right. And it comes with a simple way for people to tinker with it. To add a letter that gets multiplied, include it at lines 37 - 39; and the number of times it might get multiplied is defined in line 43. The removed letters are defined at lines 58 - 61. Generic sounds are defined at lines 29 - 31.

Right, and there are two other number to adjust - the time the zombie thinks of a response in conversation mode (line ) and the odds defining when a the zombie mangles text. It has two numbers there (in which we would call a tuple... if only we knew how to pronounce that) - 3,4 at the time, which means that the odds are 3 in 4 of the zombie doing some mangling.

The substitutions are defined at lines 49 - 52. Defining these is more tricky, since you have to put in two things... it's a bit taxing, I know, but I believe in you.

Oh, and it's GPL'd, so zombie noises is free software. That's free as in '...-range chicken'. Yeah, so feel free to modify it and stuff, if you want to. It's even nicely commented for that purpose.

Here's how to run it in conversation mode (two separate ways):

./zombie_noises.py -i
python zombie_noises.py -i

and here's how to run it with a pipe (two ways as well):

cat de_bello_gallico.txt | ./zombie_noises.py
cat de_bello_gallico.txt | python zombie_noises.py

and here's how to run it with a file (two ways):

./zombie_noises.py -f de_bello_gallico.txt
python zombie_noises.py -f de_bello_gallico.txt

and here's the Spanish Inquisition hiding behind a shed.

Right, enough banter, here's the code. You need Python to run it.
 
1 #!/usr/bin/python
2
3 # Copyright 2009 Konrad Siek.
4 #
5 # This program is free software: you can redistribute it and/
6 # or modify it under the terms of the GNU General Public
7 # License as published by the Free Software Foundation, either
8 # version 3 of the License, or (at your option) any later
9 # version.
10 #
11 # This program is distributed in the hope that it will be
12 # useful, but WITHOUT ANY WARRANTY; without even the implied
13 # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
14 # PURPOSE. See the GNU General Public License for more
15 # details.
16 #
17 # You should have received a copy of the GNU General Public
18 # License along with this program. If not, see
19 # <http://www.gnu.org/licenses/>.
20
21 import sys
22 import time
23 import getopt
24 import random
25
26 # A list of generic noises for the zombie to use when nothing
27 # else is available, i.e., when there was no input.
28 GENERIC_NOISES = [
29 'braaaains', 'coooffeee',
30 ':grunt:', ':groan:',
31 ':gurgle:', ':burp:', ':moan:'
32 ]
33
34 # Characters that are easy for the zombie to elongate when
35 # speaking, as 'a' in 'braaaains'.
36 ELONGATABLE = [
37 'a', 'e', 'i', 'o', 'u', 'y', 'r', 's',
38 'A', 'E', 'I', 'O', 'U', 'Y', 'R', 'S',
39 '!'
40 ]
41
42 # Maximum number of allowed repeated characters.
43 MAX_ELONGATION = 6
44
45 # Characters which zombies make an effort to reproduce but
46 # sometimes fail. Or maybe they do it for the slurrin effect.
47 # It's sometimes hard to tell with zombies.
48 SUBSTITUTABLE = {
49 'n': 'ngh', 'N': 'NGH',
50 'k': 'kh', 'K': 'KH',
51 'g': 'gh', 'G': 'GH',
52 '?': '!'
53 }
54
55 # Characters which zombies have trouble pronuning, so they
56 # tend to drop them altogether. This is mostly punctuation.
57 REMOVABLE = [
58 '.', ',', ';', '-', '@', '#', '$', '%',
59 '^', '&', '*', '(', ')', '_', '+', '=',
60 '{', '}', '[', ']', ':', '"', '\', ''',
61 '|', '<', '>', '/', '`', '~'
62 ]
63
64 # The odds deciding the chance of a character being mangled
65 # by a zombie. The higher the value of the second part, the
66 # *less* likely the zombie is to touch a given character. The
67 # higher the value of the first part, the *more* likely the
68 # zombie is to touch a given character. The value of one to
69 # one will cause all opportunities to mangle to be utilized.
70 # The suggestion here, is to use values generally close to
71 # one, especially in conversation mode.
72 ODD_FACTOR = (3, 4)
73
74 # Prompts used for interactive mode to represent the user's
75 # input to the conversation and the zombie's reponses.
76 USER_PROMPT = 'You: '
77 ZOMBIE_PROMPT = 'Zombie: '
78
79 # Message used for interactive mode to indicate the time when
80 # the zombie is cogitating.
81 ZOMBIE_THINKS = "(zombie thinks)"
82
83 # The maximum amount of time for the zombie to conjure a
84 # response, mesaured in seconds. Should be greater or equal to
85 # one.
86 THINK_TIME = 6
87
88 # A dictionary to all the characters with manglers appropriate
89 # for their class. Intentionally left empty. This will fill up
90 # later automatically.
91 MANGLERS = {}
92
93 def zombify(string):
94 """ Throughputs a string through a zombie producing a
95 mangled and more interesting alternative to the
96 original input.
97
98 A certain class of characters can be removed, some
99 characters can be multiplied and some can be
100 substituted for other characters or phrases. And when
101 the input is empty, a random noise can be made.
102
103 @param string before a zombie mangles it.
104
105 @return string after a zombie mangles it.
106 """
107 # If there's nothing in the line, then there's an
108 # opportunity to insert a generic word here. This is a
109 # high-probability event, because we like zombies to say
110 # 'braaaains' and ':grunt:' a lot.
111 if len(string.strip()) == 0:
112 if lottery():
113 return random.choice(GENERIC_NOISES) + "\n"
114 else:
115 return string
116
117 # Check out each character now, and try to apply some
118 # gruesome mangling to them, if odds allow. This is a
119 # typical probability event and uses odds. Also, if
120 # there is no mangler available for use, just ignore the
121 # character - opportunity wasted.
122 result = ""
123 for character in string:
124 if lottery() and character in MANGLERS:
125 # Select the appropriate class for the character
126 # and use the function appropriate for that class.
127 mangler = MANGLERS[character]
128 result += mangler(character)
129 else:
130 result += character
131 return result
132
133 def elongate(character):
134 """ Elongate a character or string a random number of
135 times.
136
137 @param character to elongate.
138
139 @return elongated string of characters.
140 """
141 return random.randint(2, MAX_ELONGATION) * character
142
143 def remove(character):
144 """ Ignore a character.
145
146 @param character to ignore.
147
148 @return empty string.
149 """
150 return ''
151
152 def substitute(character):
153 """ Replace a character with the defined substitution.
154
155 @throw in case the substitution is not present throw
156 a KeyError exception.
157
158 @param character to replace with a string.
159
160 @return replacement string.
161 """
162 return SUBSTITUTABLE[character]
163
164 def lottery():
165 """ A simple binary lottery, with odds of success equal
166 to ODD_FACTORY[0] in ODD_FACTOR[1].
167
168 @return either True or False, in a random fashion.
169 """
170 return random.randint(ODD_FACTOR[0], ODD_FACTOR[1]) \
171 in range(1, ODD_FACTOR[0] + 1)
172
173 def handle_pipe():
174 """ Zombify individual lines, one by one, so a file or
175 on-the-fly input can be processed. A method to work
176 with large input.
177
178 @return Generally, True if the processing should be
179 stopped afterwards and False if it should go on. Here,
180 always True.
181 """
182 for line in sys.stdin:
183 line = zombify(line)
184 sys.stdout.write(line)
185 sys.stdout.write("\n")
186
187 return True
188
189 def handle_interactive():
190 """ Zombify input in a form of conversation with the
191 user, with an additional effect of simulating a zombie
192 in the process of conceptualizing. That process could
193 take a bit of time, so there is a delay introduced.
194
195 @return Generally, True if the processing should be
196 stopped afterwards and False if it should go on. Here,
197 always True.
198 """
199 # Secure in case amodifying user does not understand the
200 # concept of 'greater or equal'.
201 if THINK_TIME < 1:
202 sys.stderr.write("THINK_TIME, must be at least 1")
203 return True
204
205 # Continue conversation endlessly!
206 while True:
207 try:
208 line = raw_input(USER_PROMPT)
209 print(ZOMBIE_THINKS)
210 think_time = random.randint(1, THINK_TIME)
211 time.sleep(think_time)
212 response = zombify(line)
213 print(ZOMBIE_PROMPT + response)
214 except:
215 print
216 break
217
218 return True
219
220 def handle_file(path):
221 """ Zombify input taken directly from a named file. The
222 file is processed one line after another and the
223 output is directed to stdout. It's a sort of batch-
224 -processed zombie then.
225
226 @return Generally, True if the processing should be
227 stopped afterwards and False if it should go on. Here,
228 always False, unless an error appears, then True.
229 """
230 try:
231 input_file = open(path)
232 for line in input_file:
233 line = zombify(line)
234 sys.stdout.write(line)
235 sys.stdout.write("\n")
236 except:
237 sys.stderr.write("Cannot open file: " + path + "\n");
238 return True
239
240 return False
241
242 def handle_word(word):
243 """ Zombify a single word and continue processing. This
244 can be used in conjunction with other command,
245 although I can't imagine what for.
246
247 @return Generally, True if the processing should be
248 stopped afterwards and False if it should go on. Here,
249 always False.
250 """
251 word = zombify(word)
252 sys.stdout.write(word)
253 sys.stdout.write("\n")
254
255 return False
256
257 def handle_params(params):
258 """ Zombify all the parameters as individual lines. It's
259 the zombie's way of making fun of you typing in
260 incorrect parameters. This is also a way to work with
261 the program arguments vector as input, if you want to
262 do it that way.
263
264 @return Generally, True if the processing should be
265 stopped afterwards and False if it should go on. Here,
266 always True.
267 """
268 for param in params:
269 param = zombify(param)
270 sys.stdout.write(param)
271 sys.stdout.write("\n")
272
273 return True
274
275 def print_usage():
276 """ Prints usage information for the program.
277
278 @return Generally, True if the processing should be
279 stopped afterwards and False if it should go on. Here,
280 always False.
281 """
282 print(\
283 """Usage:
284 zombie_noises.py [parameters]
285
286 Parameters:
287 -p process standard input (default for no parameters)
288 -i converse interactively
289 -f {filename} process file (and continue)
290 -w {word} process words (and continue)
291 -h show usage screen (and continue)
292
293 Author:
294 Konrad Siek <konrad.siek@gmail.com>
295
296 License:
297 GNU General Public License
298 """\
299 )
300
301 return False
302
303 def resolve_opts(opt_handlers, def_handler, arg_handler):
304 """ Handles all the options and parameters for the script
305 with the provided functions.
306
307 @param opt_handlers: a dictionary, translating an
308 option string to a function. Depending on whether the
309 function is parameterless or has one parameter the
310 option string will be just a letter or a letter ending
311 in a colon.
312
313 @param def_handler: a function used to handle the
314 program when no parameters or arguments are present.
315 This is a parameterless function.
316
317 @param arg_handler: a function used to handle all
318 the arguments - it takes one parameter.
319 """
320 string = "".join(["%s" % (i) \
321 for i in opt_handlers.keys()])
322 options, arguments = getopt.getopt(sys.argv[1:], string)
323
324 # Handle options.
325 for key, value in options:
326 if value != '':
327 stop = opt_handlers[key[1:]+":"](value)
328 else:
329 stop = opt_handlers[key[1:]]()
330 if stop:
331 return
332
333 # Handle arguments.
334 if len(arguments) > 0:
335 arg_handler(arguments)
336
337 # Handle when no arguments or params are present.
338 elif len(options) == 0:
339 def_handler()
340
341 # Execution starts here. Try to make out the arguments and
342 # run the appropriate function.
343 if __name__ == "__main__":
344 # Join all the classes into one giant dictionary. This
345 # eliminates inconsistencies and makes manging functions
346 # easy to grab in the code for individual characters.
347 for character in ELONGATABLE:
348 MANGLERS[character] = elongate
349 for character in REMOVABLE:
350 MANGLERS[character] = remove
351 for character in SUBSTITUTABLE.keys():
352 MANGLERS[character] = substitute
353
354 # Define how to react to the various arguments.
355 options = {
356 "p" : handle_pipe,
357 "i" : handle_interactive,
358 "w:" : handle_word,
359 "f:" : handle_file,
360 "h" : print_usage
361 }
362
363 # Run the functions appropriate to the options.
364 resolve_opts(options, options["p"], handle_params)


The code is also available at GitHub as python/zombie_noises.py.

No comments: