0

I am making a library for creating text adventure games.

Here is my main.py code:

from txtadvlib import utils

utils.menu("Main Menu", ["play","about","quit"])
utils.getInput(prompt="user>",
               ifs=[
                 "1",
                 "2",
                 "3"
               ],
               thens=[
                 print(1),
                 print(2),
               ],
               catch="print('caught')"
              )

Here is the code I use for the library:

import os
import time
import random
import colorama
from datetime import date
from colorama import Fore

class utils:
  def menu(title,optionsArray,):
    print(title)
    cycles = 0
    for i in optionsArray:
      cycles += 1
      print(f"[{cycles}].{i}")
      
  def getInput(prompt, ifs, thens, catch):
    choice = input(prompt)
    for i in ifs:
      if choice == i:
        eval(thens[ifs.index(i)])
        break
      else:
        eval(catch)

I do not want to be using eval for every function as it requires the library user to format any functions as strings.

Here is the problem: the functions in the thens list run immediately and then the input goes.

Here is the output:

Main Menu
[1].play
[2].about
[3].quit
1    <--- function in the list ran
2    <--- function in the list ran
user>   <--- input prompt

I have tried making the function parameters one line, and I can't think of anything else to try.

mkrieger1
  • 19,194
  • 5
  • 54
  • 65
Rat
  • 5
  • 2

3 Answers3

0

You could expect the library user to pass in a callable, which will be called in case this option is selected:

...
import functools 

def catch():
  print('caught')

utils.getInput(prompt="user>",
               ifs=[
                 "1",
                 "2",
                 "3"
               ],
               thens=[
                 lambda: print(1),
                 functools.partial(print, 2),
               ],
               catch=catch
              )

Then you code should look sth like this:

...
  def getInput(prompt, ifs, thens, catch):
    choice = input(prompt)
    for i in ifs:
      if choice == i:
        thens[ifs.index(i)]()
        break
      else:
        catch()
yjay
  • 942
  • 4
  • 11
0

You need a different way to pass the thens list, because as you found out, calls such as print(1) do execute right away.

One way to do it is to pass a tuple of the function object and its arguments, to be called later:

thens = [
    (print, 1),
    (print, 2)
]

Because the function name print is not followed by parens (), it is not called immediately.

functools.partial is another way to do this.

John Gordon
  • 29,573
  • 7
  • 33
  • 58
0

The basic idea is to use functions as objects, luckily Python can easily do that. E.g. you can use def to make a function, and pass it into the menu.

def on_play():
    print(1)

def on_about():
    print(2)

def on_catch():
    print('caught')

utils.getInput(prompt="user>",
               ifs=["1","2","3"],
               thens=[on_play,on_about,],
               catch=on_catch)

It's important to know that, as soon as you put parentheses beside the function name, the function is called. You want to avoid that, and call it later, so omit parentheses when defining thens. At the point where you're ready to call the function, add parentheses to the function object stored in thens, e.g. thens[i]()

def getInput(prompt, ifs, thens, catch):
    choice = input(prompt)
    try:
        i = ifs.index(choice)
        thens[i]() # now call
    except:
        catch() # now call

Sometimes you would to pass arguments to the functions, to reuse them. I like to use functools.partial for that. It takes one existing function object, and any arguments, and creates a new function object that has some arguments filled in and excluded from the original arguments list. You can achieve this, to make sure that thens[i]() can be called without any arguments, because a newly created function object via functools.partial would already fill in all necessary arguments.

from functools import partial

def on_menu(mode, username=None, game_version=None):
    if mode == 'play'
        print(1, username)
    elif mode == 'about'
        print(2, game_version)

utils.getInput(prompt="user>",
               ifs=["1","2","3"],
               thens=[
                   partial(on_menu, mode='play', username='Bob'),
                   partial(on_menu, mode='about', game_version='v1.0'),],
               catch=on_catch)

You could also use Python lambdas to make simple one-line functions instead of writing long def.

utils.getInput(prompt="user>",
               ifs=["1","2","3"],
               thens=[
                   lambda: print('Whatever'),
                   lambda: print('Game v1.0'),
               catch=on_catch)

Effectively this lambda: print('Whatever') statement also creates a function object, that is NOT yet called, it will be only when you do (lambda: print('Whatever'))(), then the message will be printed. You can also add arguments to lambdas, explore it on your own

TylerH
  • 20,799
  • 66
  • 75
  • 101
Alexey S. Larionov
  • 6,555
  • 1
  • 18
  • 37