Amy's simple Markov chain text and randomized menu choices

A place for Ren'Py tutorials and reusable Ren'Py code.
Forum rules
Do not post questions here!

This forum is for example code you want to show other people. Ren'Py questions should be asked in the Ren'Py Questions and Announcements forum.
Post Reply
Message
Author
User avatar
PatchyDoll
Regular
Posts: 33
Joined: Fri Apr 06, 2012 11:03 pm
Completed: Rock Bottom, Your Swimsuit Jumped Over Its Own Weathercock You Liar!
Projects: In progress: Trigger
Location: Bay Area, CA
Contact:

Amy's simple Markov chain text and randomized menu choices

#1 Post by PatchyDoll »

The following is code that runs my eroge spoof "Your Swimsuit Jumped Over Its Own Weathercock, You Liar!" (http://www.amydentata.com/yourswimsuit) It uses a slightly modified Markov chain generator that I found online from Python example code. My original non-Python code is really simple and should be accessible to beginners.

My setup has the following features:
1. Takes input from three separate sources: One for an NPC character (dialogM.txt), one for your character/narration (dialogY.txt), and one for menu options (dialogC.txt). These text files go in the /game folder.
2. Generates either a two-option or three-option menu choice, with random text.
3. Modifies a "like" variable at random when the player chooses a menu option. (In my original game, this is used to track your progress toward wooing Miko. The code doesn't reveal choice outcomes to the player.)

I also have code that displays random emotes from the NPC, but it's highly customized to my image set so I won't include it here. You can check out the full source by downloading the game itself; my content is CC BY licensed.

There is one major caveat: Your input files must be plain ASCII. The Markov generator can create output that breaks Ren'Py. I assume it sometimes passes escape characters that cause Ren'Py to think it's now receiving arguments instead of dialog text. There's a really simple bugfix added to the Markovgen script that sort of handles this problem. However, it is essential that you use grep or some other text editing tool to remove non-ASCII characters from your source text. All quotes must be straight, and special punctuation such as em dashes must be removed. I used TextWrangler to get my source files in shape.

Here is the modified version of markovgen.py — Place it in your /game folder.

Code: Select all

import renpy.store as store
import renpy.exports as renpy
import random

class Markov(object):
    
    def __init__(self, open_file):
        self.cache = {}
        self.open_file = open_file
        self.words = self.file_to_words()
        self.word_size = len(self.words)
        self.database()
        
    
    def file_to_words(self):
        self.open_file.seek(0)
        data = self.open_file.read()
        words = data.split()
        return words
        
    
    def triples(self):
        """ Generates triples from the given data string. So if our string were
                "What a lovely day", we'd generate (What, a, lovely) and then
                (a, lovely, day).
        """
        
        if len(self.words) < 3:
            return
        
        for i in range(len(self.words) - 2):
            yield (self.words[i], self.words[i+1], self.words[i+2])
            
    def database(self):
        for w1, w2, w3 in self.triples():
            key = (w1, w2)
            if key in self.cache:
                self.cache[key].append(w3)
            else:
                self.cache[key] = [w3]
                
    def generate_markov_text(self, size=25):
        seed = random.randint(0, self.word_size-3)
        seed_word, next_word = self.words[seed], self.words[seed+1]
        w1, w2 = seed_word, next_word
        gen_words = []
        for i in xrange(size): # Cheap fix for Ren'Py errors caused by EOF
            gen_words.append(w1)
            try:
                w1, w2 = w2, random.choice(self.cache[(w1, w2)])
            except KeyError:
                break
        gen_words.append(w2)
        return ' '.join(gen_words)
This is the init code placed at the top of script.rpy:

Code: Select all

init python:
    import markovgen

    filenameM = "dialogM.txt" # NPC
    filenameY = "dialogY.txt" # You
    filenameC = "dialogC.txt" # Menu choice

    fileM_ = open(renpy.loader.transfn(filenameM)) # Open the files so markovgen.py can access them
    fileY_ = open(renpy.loader.transfn(filenameY))
    fileC_ = open(renpy.loader.transfn(filenameC))
    
    markovM = markovgen.Markov(fileM_) # Generate the Markov chain data
    markovY = markovgen.Markov(fileY_)
    markovC = markovgen.Markov(fileC_)

    choice = 0  # records the menu choice the player selected

    effectGen = 0 # 
    effectA = 0 # Use to randomize modding of the "like" variable
    effectB = 0 # in menu choices.
    effectC = 0 # (Different "win" path every time)

    like = 0    # track how well you're doing (like > 0 to "win")
The following are function calls that you can access in the script by using a "call [label]" command. Place them before the start label:

Code: Select all

label Msay: # Make NPC say something.
    $ mString = markovM.generate_markov_text()
    m "%(mString)s"
    return

label Nsay: # Make narrator think something.
    $ mString = markovY.generate_markov_text()
    "%(mString)s"
    return

label Ysay: # Make you say something.
    $ mString = markovY.generate_markov_text()
    y "%(mString)s"
    return

label Gen: #generates a series of three strings for menu options, don't call directly
    $ mStringA = markovC.generate_markov_text()
    $ mStringB = markovC.generate_markov_text()
    $ mStringC = markovC.generate_markov_text()
    return

label GenEffect: # generates randomized effects to "like" variable in menu choices, using terrible programming. Don't call directly.
    python:
        effectGen = renpy.random.randint(0, 2)
        
        if (effectGen == 0):
            effectA = -1
        
            effectGen = renpy.random.randint(0,1)

            if (effectGen == 0):
                effectB = 0
                effectC = 1
            else:
                effectC = 0
                effectB = 1
        
        elif (effectGen == 1):
            effectB = -1

            effectGen = renpy.random.randint(0,1)

            if (effectGen == 0):
                effectA = 0
                effectC = 1
            else:
                effectC = 0
                effectA = 1
        
        elif (effectGen == 2):
            effectC = -1

            effectGen = renpy.random.randint(0,1)

            if (effectGen == 0):
                effectA = 0
                effectB = 1
            else:
                effectB = 0
                effectA = 1

    #"like=%(like)s\n\n%(effectA)s, %(effectB)s, %(effectC)s" -- CHEAT CODE! Uncomment to see menu effects and your "score"
    return

label MenuTwo: #two menu choices
    call GenEffect
    call Gen
    menu:
        "%(mStringA)s":
            $ choice = 1
            $ like += effectA
            return
        "%(mStringB)s":
            $ choice = 2
            $ like += effectB
            return

label MenuThree: #three menu choices
    call GenEffect
    call Gen
    menu:
        "%(mStringA)s":
            $ choice = 1
            $ like += effectA
            return
        "%(mStringB)s":
            $ choice = 2
            $ like += effectB
            return
        "%(mStringC)s":
            $ choice = 3
            $ like += effectC
            return
The labels you actually call in the script are Ysay, Nsay, Msay, MenuTwo, and MenuThree. For example (this won't work out of the box, it's just to show you how the calls are used):

Code: Select all

label start:
    play music "music/music_beach_house.ogg" fadeout 0.25 fadein 0

    scene bg classroom day
    with dissolve

    call Nsay
    call Nsay

    show m
    with dissolve

    call Msay
    call Msay
    call Ysay

    m "..."

    call Nsay
    call Nsay
    call Msay

    call MenuTwo

    # the menu modified the "like" variable, so let's change the story branch based on its new value
    if like > 0: 
        jump branch1
    else:
        jump branch2
There you go! Random text, random menu options, random outcomes. It's really basic but the outcome can be a lot of fun. Enjoy and modify to your heart's content!

~ Amy

User avatar
OokamiKasumi
Eileen-Class Veteran
Posts: 1779
Joined: Thu Oct 14, 2010 3:53 am
Completed: 14 games released -- and Counting.
Organization: DarkErotica Games
Deviantart: OokamiKasumi
Location: NC, USA
Contact:

Re: Amy's simple Markov chain text and randomized menu choic

#2 Post by OokamiKasumi »

What an awesome code!
-- Out of curiosity, is it possible to adjust the Markov generator to select whole sentences (the whole line from capitol to period,) instead of bits and pieces? I have this vision of one character quoting lines from The Princess Bride while the other quotes from Star Wars.

m "Do you think they'll make it to the castle?"
y "Use the force, Luke."
m "You killed my father. Prepare to die!"
y "Obi Wan Kenobi, you're my only hope!"
Ookami Kasumi ~ Purveyor of fine Smut.
Most recent Games Completed: For ALL my completed games visit: DarkErotica Games

"No amount of great animation will save a bad story." -- John Lasseter of Pixar

User avatar
PatchyDoll
Regular
Posts: 33
Joined: Fri Apr 06, 2012 11:03 pm
Completed: Rock Bottom, Your Swimsuit Jumped Over Its Own Weathercock You Liar!
Projects: In progress: Trigger
Location: Bay Area, CA
Contact:

Re: Amy's simple Markov chain text and randomized menu choic

#3 Post by PatchyDoll »

It would be pretty simple, I think, to add code so that it truncates the string to a complete sentence, removing the fragments at the beginning and end. However, that's currently beyond my coding ability :D I'd have to do a lot of Googling to make it happen.

If you wanted to quote entire lines without modifying them, then that would be a different bit of code altogether. Yes, also very doable. Anyone got tips on that? I'm extremely new to Python.

User avatar
OokamiKasumi
Eileen-Class Veteran
Posts: 1779
Joined: Thu Oct 14, 2010 3:53 am
Completed: 14 games released -- and Counting.
Organization: DarkErotica Games
Deviantart: OokamiKasumi
Location: NC, USA
Contact:

Re: Amy's simple Markov chain text and randomized menu choic

#4 Post by OokamiKasumi »

PatchyDoll wrote:It would be pretty simple, I think, to add code so that it truncates the string to a complete sentence, removing the fragments at the beginning and end. However, that's currently beyond my coding ability :D I'd have to do a lot of Googling to make it happen.
Ah... I see.
-- It was just an idea.
PatchyDoll wrote:If you wanted to quote entire lines without modifying them, then that would be a different bit of code altogether. Yes, also very doable. Anyone got tips on that? I'm extremely new to Python.
I did find a way to post whole lines at random. I just thought you might have a new (and hopefully more efficient) way to do it.

I used renpy.random.choice.

Code: Select all

label location:
    $ loc1 = renpy.random.choice([
        "at a haunted house to witness an exorcism",
        "at a haunted house, during a ghost-busting operation",
        "at a party in a penthouse suite",
        "at a rock concert",
        "at a sporting event",
        "at a sports car rally",
        "at a strip club",
        ])


Then inserted the results into a sentence to create a story made up entirely of randomized characters, locations, and actions.

Code: Select all

    "The Story...\n
    \nWhile %(loc1)s, %(adj1)s %(ch_f)s met %(adj2)s %(ch_m)s.\n
    \n%(plot1)s the %(ch_f)s %(smut1)s.\n
    \nWhile %(loc2)s, they met again.\n
    \n%(plot2)s the %(ch_m)s %(smut2)s.\n
    \n-- Which resulted in %(results)s."
Using the randomized results from 15 different lists, I was able to generate 3 types of erotic stories for my Erotic Story Generator. <-- Adult link! I'm currently working on a broader (and larger) version of this divided into selectable Genres.
Ookami Kasumi ~ Purveyor of fine Smut.
Most recent Games Completed: For ALL my completed games visit: DarkErotica Games

"No amount of great animation will save a bad story." -- John Lasseter of Pixar

User avatar
PatchyDoll
Regular
Posts: 33
Joined: Fri Apr 06, 2012 11:03 pm
Completed: Rock Bottom, Your Swimsuit Jumped Over Its Own Weathercock You Liar!
Projects: In progress: Trigger
Location: Bay Area, CA
Contact:

Re: Amy's simple Markov chain text and randomized menu choic

#5 Post by PatchyDoll »

I love your erotic fiction generator! That was a lot of fun. A quick Google search brought up this page, which might be of use: http://stackoverflow.com/questions/3540 ... -in-python

User avatar
SusanTheCat
Miko-Class Veteran
Posts: 952
Joined: Mon Dec 13, 2010 9:30 am
Location: New Brunswick, Canada
Contact:

Re: Amy's simple Markov chain text and randomized menu choic

#6 Post by SusanTheCat »

I'll have to take a look at this later.

How could a sentence-at-a-time generator work. Hmmmmm....

You could have the lines linked by subject, then it would hop from subject to subject.

Susan
" It's not at all important to get it right the first time. It's vitally important to get it right the last time. "
— Andrew Hunt and David Thomas

Post Reply

Who is online

Users browsing this forum: No registered users