home *** CD-ROM | disk | FTP | other *** search
/ MacHack 2000 / MacHack 2000.toast / pc / The Hacks / PizzaHack / pizzahack2.py < prev   
Encoding:
Text File  |  2000-06-23  |  8.9 KB  |  280 lines

  1. # pizza.py -- generate a random pizza order for MacHack
  2. #
  3. # By Eric S. Raymond <esr@thyrsus.com>, June 23 2000, for the MacHack 15 hack show.
  4.  
  5. import random, string, sys, re
  6. from Tkinter import *
  7. from Dialog import *
  8.  
  9. _classes = (
  10.     'plain',
  11.     'vegan',
  12.     'vegetarian',
  13.     'mega-meat',
  14.     'meat-purist', 
  15.     'mixed',
  16.     'mostly-meat',
  17. )
  18.  
  19. _meats = (
  20.     "pepperoni",
  21.     "sausage",
  22.     "ham",
  23.     "hamburger",
  24.     "prosciutto",
  25.     )
  26.  
  27. _veggies = (
  28.     "mushroom",
  29.     "black olives",
  30.     "green peppers",
  31.     "onion",
  32.     "broccoli",
  33.     "cauliflower",
  34.     "pineapple",
  35.     "roasted red peppers",
  36.     )
  37.  
  38. _prefs = (
  39.     "I love it",
  40.     "It's OK",
  41.     "Only a few of these",
  42.     )
  43.  
  44. _cheeses = (
  45.     "cheese",
  46.     "double cheese",
  47.     )
  48.  
  49. _spices = (
  50.     "hot pepper",
  51.     "garlic",
  52.     )
  53.  
  54. # Each entry consists of a pizza class as a key, the value consists of a tuple
  55. # of ingrediant tuples.  Each ingrediabt tuple has as [0] member an ingredients
  56. # array, and as [1] member a range of valid counts for that ingredient.
  57. _rules = {
  58.     'plain': ((_cheeses, [1]),),
  59.     'vegan': ((_veggies, [4]),
  60.               (_spices, [0,1,2])),
  61.     'vegetarian': ((_veggies, [3,4]),
  62.                    (_spices, [0,1,2]),
  63.                    (_cheeses, [1])),
  64.     'mega-meat': ((_meats, [2,3,4]),
  65.                  (_spices, [0,1,2]),
  66.                  (_cheeses, [1])),
  67.     'meat-purist': ((_meats, [1]),
  68.                    (_spices, [0,1]),
  69.                    (_cheeses, [1])),
  70.     'mixed': ((_meats, [1]),
  71.               (_spices, [0,1,2]),
  72.               (_cheeses, [1]),
  73.               (_veggies, [2,3])),
  74.     'mostly-meat': ((_meats, [2,3]),
  75.                   (_spices, [0,1,2]),
  76.                   (_cheeses, [1]),
  77.                   (_veggies, [1])),    
  78.     }
  79.  
  80. #
  81. # Global state
  82. #
  83. classnumbers = {}
  84.  
  85. def compute_order():
  86.     "Fill in missing classes from the order, then generate the order."
  87.     global specified, excess
  88.  
  89.     # Fill in the catch-all classes
  90.     classnumbers['mostly-meat'] = classnumbers['mixed'] = 0
  91.     specified = reduce(lambda x, y: x + y, classnumbers.values())
  92.     excess = total - specified
  93.     classnumbers['mostly-meat'] = excess / 2
  94.     classnumbers['mixed'] = excess - classnumbers['mostly-meat'] 
  95.     # Generate a list of tuples,
  96.     # each consisting of a type, a recipe count, and a recipe:
  97.     order = []
  98.     for key in classnumbers.keys():
  99.         for i in range(classnumbers[key]):
  100.             recipe = generate_pizza(key)
  101.             recipe.sort()
  102.             oldrecipes = list(map(lambda x: x[2], order))
  103.             if recipe in oldrecipes:
  104.                 find = oldrecipes.index(recipe)
  105.                 order[find] = (key, order[find][1]+1, order[find][2])
  106.             else:
  107.                 order.append((key, 1, recipe))
  108.     return order
  109.  
  110. def genreport(order):
  111.     "Format the generated order as a text string."
  112.     result = "Total: %d\n" % (total,)
  113.     oldkey = None
  114.     for (key, count, recipe) in order:
  115.         if key != oldkey:
  116.             result = result + key + " (" + `classnumbers[key]` + "):\n"
  117.             oldkey = key
  118.         result = result + ("    %d with %s\n"%(count,string.join(recipe,", ")))
  119.     return result
  120.  
  121. def multiselect(number, array):
  122.     "Random-select a tuple of exactly number items from a given string tuple."
  123.     length = len(array)
  124.     outlist = []
  125.     while number > 0:
  126.         trial = random.choice(array)
  127.         if trial in outlist:
  128.             continue
  129.         else:
  130.             outlist.append(trial)
  131.             number = number - 1
  132.     return outlist
  133.  
  134. def generate_pizza(pizzaclass):
  135.     "Generate a pizza of a given class using the _rules table."
  136.     recipe = []
  137.     for (ingredient, frequency) in _rules[pizzaclass]:
  138.         recipe = recipe + multiselect(random.choice(frequency), ingredient)
  139.     return recipe
  140.  
  141. def get_number_or_percentage(prompt):
  142.     "Get a number, or a percent scaled to the total if the input includes %."
  143.     raw = string.strip(raw_input(prompt))
  144.     if raw[-1] == "%":
  145.         number = int(total * string.atoi(raw[:-1]) / 100.0)
  146.     else:
  147.         number = string.atoi(raw)
  148.     return number
  149.  
  150. def consolemode():
  151.     global total
  152.     print "Hello, pizza!"
  153.     total = string.atoi(raw_input("How many pizzas ya want? "))
  154.     classnumbers['plain'] = get_number_or_percentage("How many plain pizzas ya want? ")
  155.     classnumbers['vegan'] = get_number_or_percentage("How many vegan pizzas ya want? ")
  156.     classnumbers['vegetarian'] = get_number_or_percentage("How many vegetarian pizzas ya want? ")
  157.     classnumbers['mega-meat'] = get_number_or_percentage("How many mega-meat pizzas ya want? ")
  158.     classnumbers['meat-purist'] = get_number_or_percentage("How many meat purist pizzas ya want? ")
  159.  
  160.     order = compute_order()
  161.  
  162.     if specified > total:
  163.         print "Bogus order, too many pizzas by %d." % (specified - total)
  164.         raise SystemExit
  165.  
  166.     # Generate class numbers as a check
  167.     print genreport(order)
  168.  
  169. # TkInter support
  170.  
  171. class ValidatedField(Frame):
  172.     "Accept a string, decimal or hex value in a labeled field."
  173.     def __init__(self, master, type, prompt, hook):
  174.         Frame.__init__(self, master)
  175.         self.type = type
  176.         self.hook = hook
  177.         self.newval = StringVar(master)
  178.         self.L = Label(self, text=prompt, anchor=W, width=40)
  179.         self.L.pack(side=LEFT)
  180.         self.E = Entry(self, textvar=self.newval)
  181.         self.E.pack({'side':'left', 'expand':YES, 'fill':X})
  182.         self.E.bind('<Return>', self.handlePost)
  183.         self.E.bind('<Enter>', self.handleEnter)
  184.         self.newval.set(0) 
  185.         self.errorwin = None
  186.     def handleEnter(self, event):
  187.         self.E.bind('<Leave>', self.handlePost)
  188.     def handlePost(self, event):
  189.         if self.errorwin:
  190.             return
  191.         self.E.bind('<Leave>', lambda e: None)
  192.         result = string.strip(self.newval.get())
  193.         if self.type == "decimal":
  194.             if not re.compile("[" + string.digits +"]+$").match(result):
  195.                 self.error_popup(title="PizzaHack Error",
  196.                         banner="PizzaHack Error",
  197.                         text="Decimal digits only, please")
  198.                 return
  199.         apply(self.hook, (result,))
  200.     def error_popup(self, title, banner, text):
  201.         self.errorwin = Toplevel()
  202.         self.errorwin.title(title) 
  203.         self.errorwin.iconname(title)
  204.         Label(self.errorwin, text=banner).pack()
  205.         Label(self.errorwin, text=text).pack()
  206.         Button(self.errorwin, text="Done",
  207.                command=lambda x=self.errorwin: Widget.destroy(x), bd=2).pack()
  208.  
  209. def totalhook(number):
  210.     "Callback to accept a pizza total from the GUI panel."
  211.     global total
  212.     total = string.atoi(number)
  213.  
  214. def journalhook(number, ptype):
  215.     "Callback to accept a pizza total from the GUI panel."
  216.     global classnumbers
  217.     raw = string.strip(number)
  218.     if raw[-1] == "%":
  219.         number = int(total * string.atoi(raw[:-1]) / 100.0)
  220.     else:
  221.         number = string.atoi(raw)
  222.     classnumbers[ptype]=number
  223.  
  224. def helpwin(title, banner, text):
  225.     # help message window with a self-destruct button
  226.     helpwin = Toplevel()
  227.     helpwin.title(title) 
  228.     helpwin.iconname(title)
  229.     Label(helpwin, text=banner).pack()
  230.     textframe = Frame(helpwin)
  231.     scroll = Scrollbar(textframe)
  232.     helpwin.textwidget = Text(textframe, width=100, setgrid=TRUE)
  233.     textframe.pack(side=TOP, expand=YES, fill=BOTH)
  234.     helpwin.textwidget.config(yscrollcommand=scroll.set)
  235.     helpwin.textwidget.pack(side=LEFT, expand=YES, fill=BOTH)
  236.     scroll.config(command=helpwin.textwidget.yview)
  237.     scroll.pack(side=RIGHT, fill=BOTH)
  238.     helpwin.textwidget.insert(END, text);
  239.     Button(helpwin, text='Done', 
  240.        command=lambda x=helpwin: Widget.destroy(x), bd=2).pack()
  241.     textframe.pack(side=TOP)
  242.  
  243. def reportwindow():
  244.     "Display the computed order result."
  245.     order = compute_order()
  246.     if specified > total:
  247.         Dialog(self,
  248.                title = "PizzaHack problem",
  249.                text = "Bogus order -- too many pizzas specified.",
  250.                bitmap = 'error',
  251.                default = 0,
  252.                strings = ("Done",))
  253.     else:
  254.         helpwin(title="PizzaHack",
  255.                 banner="Your pizza order:",
  256.                 text=genreport(order))
  257.  
  258. def tkmode():
  259.     global classnumbers
  260.     root = Tk()
  261.     root.title("PizzaHack")
  262.     Label(root, text="PizzaHack").pack(fill=X, expand=YES)
  263.     ValidatedField(root, "decimal", "How many pizzas ya want?", totalhook).pack()
  264.     for pizzatype in ("plain","vegan","vegetarian","mega-meat","meat-purist"):
  265.         ValidatedField(root,
  266.                        "decimals",
  267.                        "How many %s pizzas ya want?" % (pizzatype,),
  268.                        lambda result, ptype=pizzatype, counts=classnumbers: \
  269.                                journalhook(result, ptype)).pack(fill=X, expand=YES)
  270.     botframe = Frame(root).pack()
  271.     Button(botframe,text="Generate order",command=reportwindow).pack(side=LEFT)
  272.     Button(botframe,text="Done",command=root.destroy).pack(side=RIGHT)
  273.     root.mainloop()
  274.  
  275. if __name__ == "__main__":
  276.     if len(sys.argv[1:]):
  277.         consolemode()
  278.     else:
  279.         tkmode()
  280.