#!/usr/bin/python

# shoutout = Copyright (c) 2002, 2003 John Morton.
#
# Permission to use, copy, modify, distribute, and sell this software and
# its documentation for any purpose is hereby granted without fee, 
# provided that the above copyright notice appear in all copies and that 
# both that copyright notice and this permission notice appear in supporting
# documentation.  No representations are made about the suitability of this
# software for any purpose.  It is provided "as is" without express or 
# implied warranty.

"""
Shoutout - an automatic reality preserving credit generator

It appears that one of the most important coding duties that must be performed
by a demo scene hacker is the display ('shout out') of a long list of credits
('mad props') to family, friends, their crew and various other assorted
homies, 133t haXX0rs,  people they met at the pub, and so on, all in the aid
of 'keeping it real' (evidently taking the square root of a negative number is
considered a clear and present danger in such circles).

Unfortunately, one of the consequences of generating such a list of salutations
is that the time required comes straight out of the time budget allocated to the
actual coding of the demo itself, leading to the demo not actually being very
good.

So. In order to address this problem, 'shoutout' implements a general algorithm
for generating the salutations list, leaving the demo hacker free to implement
the method of display. It is hoped that this will lead to a greater level of
meritocracy in the demo scene - the longer a demo is left running, the more
names are shouted out to, and the more likely it is that some of those names
are those of actual homies known to the hacker (or, indeed, anyone). One's
ability to 'keep it real' becomes directly correlated to the attractiveness of
the demo itself.

Shoutout is written in python, and is primarily intended to serve as a
demonstration of the algorithm, hence it's coded for readability.
Optimized implementations for Z80 machine code are left as an exercise
for the reader.

"""

debug = 1 # Not that there's any debuging code, mind you

# Name forms are strings containing various kinds of honourifics and word form tags
# that we stuff words and numbers into. The weight indicates how relatively common
# that form should be.

name_forms = [{'string': "%(number_word)s",
          'weight': 25, },
         {'string': "MC %(long_word)s",
          'weight': 1, },
         {'string': "DJ %(long_word)s",
          'weight': 1, },
         {'string': "Mr %(short_word)s",
          'weight': 1, },
         {'string': "Ms %(short_word)s",
          'weight': 1, },
         {'string': "The %(short_word)s",
          'weight': 1, },         
         {'string': "The %(long_word)s crew",
          'weight': 2, },
         {'string': "%(long_word)s posse",
          'weight': 1, },
         {'string': "all@%(number_word)s",
          'weight': 2, },
         {'string': "Grand Master %(short_word)s",
          'weight': 1, }, 
         {'string': "Mix Master %(short_word)s",
          'weight': 1, },
         ]


# Word forms embody various amounts of words and numbers that go to make up a name.
# We stuff these full of first and second words and numbers, optionally capitalize
# and apply transforms to the whole result, then stuff them into our selected
# name form.

word_forms = {'number_word': "%(first_word)s%(second_word)s%(number)s",
              'long_word': "%(first_word)s%(second_word)s",
              'short_word': "%(second_word)s",
              }

# First words to replace instances of first_word in the various name_forms
# that want them. Weight indicates relative frequency, as for the forms.

# TODO: trawl through slashdot postings at -2 threshold and the Mind Candy dvd
# looking for more words to add to these lists.

first_words = [{'string' : "acid", 'weight' : 3,},
               {'string' : "cyber", 'weight' : 3,},
               {'string' : "dark", 'weight' : 3,},
               {'string' : "night", 'weight' : 3,},
               {'string' : "bat", 'weight' : 2,},               
               {'string' : "eldritch", 'weight' : 1,},               
               {'string' : "chaos", 'weight' : 1,},
               {'string' : "spank", 'weight' : 1,},               
               {'string' : "smack", 'weight' : 2,},
               {'string' : "crack", 'weight' : 2,},
               {'string' : "super", 'weight' : 2,},
               {'string' : "hyper", 'weight' : 2,},
               {'string' : "uber", 'weight' : 2,},
               {'string' : "elite", 'weight' : 1,},
               {'string' : "over", 'weight' : 1,},
               {'string' : "arse", 'weight' : 1,},
               {'string' : "wank", 'weight' : 1,},
               {'string' : "wibble", 'weight' : 1,},
               {'string' : "bubble", 'weight' : 1,},
               {'string' : "monkey", 'weight' : 2,},
               {'string' : "sausageseggsand", 'weight' : 1,},
               {'string' : "bacon", 'weight' : 1,},
               {'string' : "alien", 'weight' : 2,},               
               {'string' : "sex", 'weight' : 1,},
               {'string' : "dream", 'weight' : 1,},               
               ]

# Second words follow the same pattern as first words.

second_words = [{'string' : "acid", 'weight' : 3,},
                {'string' : "angel", 'weight' : 3,},
                {'string' : "bat", 'weight' : 3,},
                {'string' : "fingers", 'weight' : 2,},
                {'string' : "clown", 'weight' : 1,},
                {'string' : "ferret", 'weight' : 1,},
                {'string' : "chicken", 'weight' : 2,},
                {'string' : "monkey", 'weight' : 2,},
                {'string' : "man", 'weight' : 1,},
                {'string' : "boy", 'weight' : 1,},                
                {'string' : "grl", 'weight' : 1,},                
                {'string' : "skank", 'weight' : 1,},
                {'string' : "bastard", 'weight' : 1,},
                {'string' : "burn", 'weight' : 2,},
                {'string' : "star", 'weight' : 2,},
                {'string' : "master", 'weight' : 2,},
                {'string' : "blaster", 'weight' : 1,},
                {'string' : "butter", 'weight' : 1,},
                {'string' : "noodle", 'weight' : 2,},
                {'string' : "scream", 'weight' : 1,},
                {'string' : "fiend", 'weight' : 1,},
                {'string' : "head", 'weight' : 2,},
                {'string' : "trip", 'weight' : 1,},
                {'string' : "dwarf", 'weight' : 1,},
                {'string' : "fish", 'weight' : 1,},                
                ]

# The bit the comes at the beginning, just before we list all the names.

introductions = [{'string' : "Word up to my homies: ", 'weight' : 9, },
                 {'string' : "Greetz: ",'weight' : 8, },
                 {'string' : "Mad props to: ", 'weight' : 7,},
                 {'string' : "Thanks: ", 'weight' : 6,},
                 {'string' : "I would like to personally thank: ", 'weight' : 5,},
                 {'string' : "I have been obliged to thank: ", 'weight' : 4,},
                 {'string' : "I would like to acknowledge those few" \
                 + " people who have at least attempted to alleviate" \
                 + " my misery: ", 'weight' : 3,},
                 {'string' : "Who the hell are these people? - ", 'weight' : 2,},
                 {'string' : "Stop phoning me: ",  'weight' : 1,},
                 {'string' : "A big 'fuck you' to these loathsome examples of " \
                           + "Slashdot pondlife who just had to email me their " \
                           + "opinions on linux video players. You could have left " \
                           + "a comment, you could have posted something to your " \
                           + "blog, but noooo, you just *had* to confuse me with " \
                           + "someone who gives a shit and tell me *all* about it. " \
                           + "Bastards. -  ",  'weight' : 0,},
                 
                 ]
                 
import sys, string, os, random

# As python uses a single pass parser, I'm defining the translation
# table after the translations.


# Translations are a way to capture the particular manner of
# speech and writing of particular sub-cultures.

# TODO: Add transforms for haXX0r speak and hiphop.

def underscore_translation(str):

    """
    The underscore translation inserts one or more underscores into the
    string in order to simulate the practice, popular on certain heavily
    subscribed web sites, of including arbitary underscores to get a
    particular login name.
    """
    
    if len(str) > 1:
        rand = random.randrange(1,100)
        if rand < 90:
            pos = random.randrange(1,len(str)-1)
            str = str[0:pos] + '_' + str[pos:]            
        if rand < 60:
            pos = random.randrange(1,len(str)-1)
            str = str[0:pos] + '_' + str[pos:]
            
    return str

translations = [{'translation_function' : underscore_translation,
                 'weight': 1,},
               ] 

# Preset mode functions are a good way to tweak the weighting to suit
# a particular audience.

def trance_mode():

    """
    Trance mode greatly increases the likelihood of DJ, MC, Grand Master and
    Mix Master forms being used, and the prevalence of 'acid' in names.
    """
    for form in name_forms:
        if "DJ" == form['string'][0:2]:
            form['weight'] = 8
        elif "MC" == form['string'][0:2]:
            form['weight'] = 5
        elif "Grand Master" == form['string'][0:12]:
            form['weight'] = 3
        elif "Mix Master" == form['string'][0:10]:
            form['weight'] = 6

    for word in first_words:
        if "acid" == word['string']:
            word['weight'] = 20        

    for word in second_words:
        if "acid" == word['string']:
            word['weight'] = 5
        elif "head" == word['string']:
            word['weight'] = 3
        elif "trip" == word['string']:
            word['weight'] = 3
        
def gothic_mode():

    """
    Gothic mode greatly increases the presence of words like 'bat', and
    'dark', and so forth being produced. You get the idea.
    """

    for word in first_words:
        if "night" == word['string']:
            word['weight'] = 5
        elif "dark" == word['string']:
            word['weight'] = 5
        elif "bat" == word['string']:
            word['weight'] = 5
        elif "eldritch" == word['string']:
            word['weight'] = 3
        elif "alien" == word['string']:
            word['weight'] = 5
        elif "sex" == word['string']:
            word['weight'] = 5
        elif "chaos" == word['string']:
            word['weight'] = 5
        elif "dream" == word['string']:
            word['weight'] = 5
        elif "wank" == word['string']:
            word['weight'] = 5


    for word in second_words:
        if "bat" == word['string']:
            word['weight'] = 5
        elif "angel" == word['string']:
            word['weight'] = 5
        elif "fiend" == word['string']:
            word['weight'] = 5
        elif "scream" == word['string']:
            word['weight'] = 5

def usage():
    print "Usage: %s [-mode trance|gothic] [-count n]" % sys.argv[0]
    return None

def mode_usage():
    # When there are several modes to choose from, this should list them
    # all, rather than in the regular usage.
    return usage()

def print_line(str):

    """
    Prints out characters from the string a line at a time, with
    the line length based on whatever the COLUMNS environment setting
    is set to, or 80 if it's not set. Any excess characters are
    returned as a string.

    This seemed like a really good idea to start out with, but COLUMNS doesn't
    seem to make it back from the environment to the python interpreter :-/
    """
    if os.environ.has_key('COLUMNS'):
        line_length = int(os.environ['COLUMNS'])
    else:
        line_length = 80

    while (len(str) >= line_length):
        line_pos = line_length - 1
        while (str[line_pos] != ' '):
            line_pos -= 1
        print str[0:line_pos]
        str = str[line_pos+1:]

    return str

# Parse the command line args and apply modes. Fugly. Needs replacing.

max_count = 0

if len(sys.argv) > 1:
    n = 1
    while(n < len(sys.argv)):
        if sys.argv[n] == '-mode':
            try:
                if sys.argv[n+1] == 'trance':
                    trance_mode()
                elif sys.argv[n+1] == 'gothic':
                    gothic_mode()
                else:
                    mode_usage()
                    sys.exit(1)
                    
            except IndexError:
                mode_usage()
                sys.exit(1)
            n += 2
        elif sys.argv[n] == '-count':
            try:
                max_count = int(sys.argv[n+1])
            except IndexError:
                mode_usage()
                sys.exit(1)
            n += 2                
        else:
            usage()
            sys.exit(1)


# Generate the arrays we'll use for random generation purposes.

lists = {'first_words' : first_words, 'second_words' : second_words,
         'introductions' : introductions, 'name_forms' : name_forms,}
    
for (name, list) in lists.items():
    choice_list = []
    for str_rec in list:
        for i in xrange(1,str_rec['weight'] + 1):
            choice_list.append(str_rec['string'])
            
    exec "%s_choice = choice_list" % name 

translations_choice = []
for str_rec in translations:
    for i in xrange(1,str_rec['weight'] + 1):
        translations_choice.append(str_rec['translation_function'])


### Righty. Let's get printing.

line = ''

# Print a random intro.

line += random.choice(introductions_choice)
line = print_line(line)

names_produced = 0
while(1):
    pieces = {}
    # pick a form    
    name_form = random.choice(name_forms_choice)
    
    # work out what substitutions we need. Each name form only contains
    # one word form.
    if name_form.find("%(long_word)") != -1:
        word_form = word_forms['long_word']
        word_form_name = 'long_word'
    elif name_form.find("%(short_word)") != -1:
        word_form = word_forms['short_word']
        word_form_name = 'short_word'
    else:
        word_form = word_forms['number_word']
        word_form_name = 'number_word'

    # Stuff words into the word_form.
    if word_form.find("%(first_word)") != -1:
        pieces['first_word'] = ''
        while(1):
            pieces['first_word'] += random.choice(first_words_choice)
            if random.randrange(1,100) < 90:
                break
            
    if word_form.find("%(second_word)") != -1:
        pieces['second_word'] = random.choice(second_words_choice)

    # Silly random number generator. 
    if word_form.find("%(number)") != -1:
        rand = random.randrange(1,100)
        # This threshold should probably be stored in
        # a variable so that mode functions can tweak it.
        if rand > 60:
            rand = random.randrange(1,100)
            if rand > 90:
                pieces['number'] = '666' # \M/
            elif rand > 80:
                pieces['number'] = random.randrange(0,9)
            elif rand > 50:
                pieces['number'] = random.randrange(10,99)
            else:
                # The prospect of someone calling themselves 'wankbat[n]' is only
                # half as funny as the though of 'n' other wankbats who've filled up
                # the succession to that point.
                pieces['number'] = random.randrange(100,999) 
        else:
             pieces['number'] = ''

    # Put the word form together
    words = word_form % pieces
    
    # Use a translation?
    if random.randrange(1,100) > 90:
        translation = random.choice(translations_choice)
        words = translation(words)
        
    # Capitalize?    
    if not (word_form_name == 'number_word' and random.randrange(1,100) > 40):
        words = words.capitalize()

    # Substitute the generated words into the form.
    line += name_form % {word_form_name : words} + ", "
    
    # If our line is getting full, print it, and start a new line buffer.
    line = print_line(line)
    names_produced += 1

    # Time to stop?
    if names_produced == max_count:        
        print line[:-2]
        break

