One school brief, delivered the way a real software team would ship it: a requirements spec, a RACI, tested Python, a turtle that draws your pizza, and this website.
Mario's Pizzeria needs an automated ordering kiosk. It asks the customer's name, whether they want a Small ($10) or Large ($15) pizza, then three dietary questions: Gluten-Free base (+$3), Dairy-Free cheese (+$2) and Vegetarian toppings (no charge, different toppings). It finishes with a personalised summary and the total.
Fast finishers add a Garlic Bread side (+$4) and a delivery fee (+$5). We built both, plus input checking so typing "medium" asks again instead of crashing.
====================================
MARIO'S PIZZERIA KIOSK
====================================
What is your name?
Sarah
Would you like a Small ($10) or
Large ($15) pizza?
small
--- DIETARY REQUIREMENTS ---
Do you need a Gluten-Free base? (yes/no)
yes
Added $3 for Gluten-Free base.
Do you need Dairy-Free cheese? (yes/no)
yes
Added $2 for Dairy-Free cheese.
Would you like Vegetarian toppings? (yes/no)
yes
Vegetarian toppings selected
(Mushrooms, Peppers, Olives).
------------------------------------
Thank you, Sarah!
Your total for your custom pizza
comes to: $15
====================================
The numbers mean something here: each step depends on the one before it. You can't test code you haven't written, and you shouldn't write code you haven't agreed the rules for.
Pulled every price and every question out of Mario's brief and wrote them down.
Turned the brief into 13 numbered requirements with acceptance criteria and priorities.
Decided who is Responsible, Accountable, Consulted and Informed for each task.
pizza_bot.py for the kiosk, pizza_turtle.py so Shelly can draw the order.
Fifteen test cases from $10 to $34, including typing rubbish — and hostile code — to see if it breaks.
This site and the browser kiosk, hosted as static files on pizza.locbee.ai.
Two practices borrowed from real teams. Both changed what got built, which is the only reason they are worth naming.
Every requirement got one of four labels before any code was written. Must means the brief fails without it. Should means it was offered as an extension. Could means we added it because it makes the project better, not because anyone asked. And Won't — the one people forget — means agreed as out of scope for now.
Writing down what you are deliberately not doing is what stops it quietly reappearing later. If time had run short, the order to abandon work in was already agreed, in writing, by the person who marks it.
Before a requirement was accepted it was looked at from three directions at once — not three job titles, but three questions:
Business — Mario, who owns the brief: what problem is this solving, and how will we know it's solved? That question produced the $15 worked example everything is measured against.
Development — the student, who has to build it: is this buildable with plain if/else, and can the person writing it explain every line?
Testing — the family, whose job is to break it: what happens when someone types something silly? Nobody asked for input validation. It exists because the testing voice was in the room before the code was written.
Ask the testing question afterwards and you get a bug report. Ask it first and you get a requirement — FR-10.
R = does the work · A = signs it off (exactly one per row) · C = asked for input · I = told when it's done. The student is A/R for everything that gets marked.
| Task | Student Developer | Sponsor (Dad) | Mario (Teacher) | AI Pair-Programmer | QA (Family) |
|---|---|---|---|---|---|
| Read and understand the brief | R | C | A | C | I |
| Write the Business Requirements Spec | C | R | A | R | I |
| Define the RACI | I | A/R | I | C | I |
| Design the ordering flow | A/R | C | C | C | I |
| Code pizza_bot.py (core) | A/R | C | I | C | I |
| Code the extensions | A/R | I | I | C | I |
| Code pizza_turtle.py (Shelly) | R | C | I | R | I |
| Test against the $15 example | R | C | I | C | A/R |
| Build the website and web kiosk | C | A | I | R | I |
| Host on pizza.locbee.ai | I | A/R | I | C | I |
| Present and submit | A/R | C | I | I | I |
| v1.1 — Build enhanced.html | C | A | I | R | C |
| v1.1 — Code pizza_bot_enhanced.py | C | A | I | R | I |
| v1.1 — BRS, RTM, Test Summary, Commissioning docs | I | A | I | R | C |
| v1.1 — Run pizza_turtle.py on a real screen (TC-6) | R | A | I | I | C |
| Go-live commissioning and production sign-off | C | A/R | C | I | C |
From the Business Requirements Specification. Must comes straight from the brief, Should are the extensions, Could are our extras. Optional rows are the v1.1 enhancements in enhanced.html and the v1.2 animated build in deluxe.html — never part of the school submission. FR-01 to FR-13 come from Mario's brief; FR-14 to FR-22 were asked for by the project sponsor after v1.0 was accepted, which is why they are tracked separately. The Quality rows are different in kind: NFR-01 to NFR-06 are non-functional requirements, meaning they do not add a feature but say how the whole thing has to behave — readable, portable, forgiving. They have no MoSCoW priority because you cannot ship half of them. All twenty-eight rows below are traced in the Requirements Traceability Matrix.
| ID | Requirement | Acceptance criteria | Priority |
|---|---|---|---|
| FR-01 | Friendly welcome header | Header lines print first | Must |
| FR-02 | Ask for and store the customer's name | Name appears in the summary | Must |
| FR-03 | Small ($10) or Large ($15) | "small" → 10, else 15 | Must |
| FR-04 | Gluten-Free base question | "yes" adds $3 and prints the message | Must |
| FR-05 | Dairy-Free cheese question | "yes" adds $2 and prints the message | Must |
| FR-06 | Vegetarian toppings question | Veg list or Classic Meat Supreme | Must |
| FR-07 | Personalised order summary | Sarah example totals $15 | Must |
| FR-08 | Garlic Bread side | "yes" adds $4 | Should |
| FR-09 | Delivery or pickup | "delivery" adds $5 | Should |
| FR-10 | Re-ask on invalid answers | "medium" re-prompts, no crash | Should |
| FR-11 | Save the order to last_order.json | File contains the whole order | Could |
| FR-12 | Shelly draws the pizza with turtle | Size, crust, cheese and toppings match | Could |
| FR-13 | Browser kiosk with identical pricing | Same inputs, same total as Python | Could |
| FR-14 | Shelly reacts to every change; toppings animate in | Visible reaction per click; reduced-motion respected | Optional |
| FR-15 | Email the receipt | Opens mail app with receipt filled in | Optional |
| FR-16 | Text the receipt | Opens messages app with receipt filled in | Optional |
| FR-17 | Order log | Browser log + Download orders.log; Python appends orders.log | Optional |
| FR-18 | Supreme topping set | +$3 | Optional |
| FR-19 | Chicken topping set | +$3 | Optional |
| FR-20 | BBQ sauce | +$1; sauce drawn dark brown | Optional |
| FR-21 | Extra Crispy crust | +$1; crust darkens with char spots | Optional |
| FR-22 | Deluxe animated kiosk — sponsor asked for the site to be brought to life | Six-stage animated pizza build, animated Shelly, receipt printer; pricing unchanged from v1.1; reduced-motion respected | Optional |
| NFR-01 | Readable code | Plain if/else and descriptive names; the student can explain every line | Quality |
| NFR-02 | Portable | Python 3.10+ and nothing to install — turtle and json ship with Python | Quality |
| NFR-03 | Brief's question order | Questions asked in exactly the order Mario's brief lists them | Quality |
| NFR-04 | Forgiving input | Capitals and stray spaces in answers are accepted, not rejected | Quality |
| NFR-05 | Static hosting | HTML/CSS/JS only, so it serves from Cloudflare Pages with no server code | Quality |
| NFR-06 | Security posture | What a customer types is treated as text, never code; security headers shipped; no secrets in the files | Quality |
pizza_bot.py is the school submission: plain if/else, one variable called total_cost, questions in the brief's order. pizza_turtle.py reads the saved order and draws it. Run them with python pizza_bot.py then python pizza_turtle.py. There is a third, pizza_bot_enhanced.py, which adds the v1.1 menu and the order log; it is in the downloads rather than here, because the two above are the ones being marked.
"""
pizza_bot.py - Mario's Pizzeria Automated Ordering Kiosk
===========================================================
Asks the customer a few questions, works out the bill, and prints a summary.
Steps (in the order the brief asks for them):
1. Welcome message
2. Customer name + pizza size (Small $10 / Large $15)
3. Dietary checks: Gluten-Free base (+$3), Dairy-Free cheese (+$2), Vegetarian toppings
4. Extensions: Garlic Bread (+$4), Delivery (+$5)
5. Final order summary
The finished order is also saved to last_order.json so pizza_turtle.py
can draw it. (Shelly the turtle is waiting!)
"""
import json
# ---------- helper functions ----------
def ask_yes_no(question):
"""Keep asking until the customer types yes or no. Returns True for yes."""
while True:
answer = input(question + " (yes/no)\n").strip().lower()
if answer in ("yes", "y"):
return True
if answer in ("no", "n"):
return False
print("Please type yes or no.")
def ask_choice(question, options):
"""Keep asking until the answer is one of the options (e.g. small/large)."""
while True:
answer = input(question + "\n").strip().lower()
if answer in options:
return answer
print("Please type one of: " + " or ".join(options))
# ---------- 1. Welcome message ----------
print("====================================")
print(" MARIO'S PIZZERIA KIOSK ")
print("====================================")
# ---------- 2. Customer details & base pizza ----------
name = input("What is your name?\n").strip().title()
size = ask_choice("Would you like a Small ($10) or Large ($15) pizza?", ["small", "large"])
# Base price setup (if/else)
if size == "small":
total_cost = 10
else:
total_cost = 15
# ---------- 3. Dietary & allergy checks ----------
print(" --- DIETARY REQUIREMENTS --- ")
gluten_free = ask_yes_no("Do you need a Gluten-Free base?")
if gluten_free:
total_cost = total_cost + 3
print("Added $3 for Gluten-Free base.")
dairy_free = ask_yes_no("Do you need Dairy-Free cheese?")
if dairy_free:
total_cost = total_cost + 2
print("Added $2 for Dairy-Free cheese.")
vegetarian = ask_yes_no("Would you like Vegetarian toppings?")
if vegetarian:
toppings = "Vegetarian toppings (Mushrooms, Peppers, Olives)"
print("Vegetarian toppings selected (Mushrooms, Peppers, Olives).")
else:
toppings = "Classic Meat Supreme toppings"
print("Classic Meat Supreme toppings selected.")
# ---------- 4. Extensions (fast finishers) ----------
print(" --- EXTRAS --- ")
garlic_bread = ask_yes_no("Would you like a side of Garlic Bread ($4)?")
if garlic_bread:
total_cost = total_cost + 4
print("Added $4 for Garlic Bread.")
delivery = ask_choice("Is this for delivery or pickup?", ["delivery", "pickup"]) == "delivery"
if delivery:
total_cost = total_cost + 5
print("Added $5 delivery fee.")
# ---------- 5. Final order summary ----------
print(" ------------------------------------ ")
print("Thank you, " + name + "!")
print("Pizza size: " + size.title())
dietary_notes = []
if gluten_free:
dietary_notes.append("Gluten-Free base")
if dairy_free:
dietary_notes.append("Dairy-Free cheese")
if dietary_notes:
print("Dietary notes: " + ", ".join(dietary_notes))
else:
print("Dietary notes: none")
print("Toppings: " + toppings)
if garlic_bread:
print("Side: Garlic Bread")
print("Delivery or pickup: " + ("Delivery" if delivery else "Pickup"))
print("Your total for your custom pizza comes to: $" + str(total_cost))
print("====================================")
# ---------- save the order for Shelly the turtle ----------
order = {
"name": name,
"size": size,
"gluten_free": gluten_free,
"dairy_free": dairy_free,
"vegetarian": vegetarian,
"garlic_bread": garlic_bread,
"delivery": delivery,
"total_cost": total_cost,
}
with open("last_order.json", "w") as f:
json.dump(order, f, indent=2)
print("Order saved! Run python pizza_turtle.py to watch Shelly draw your pizza.")
"""
pizza_turtle.py - Shelly the turtle draws your pizza!
========================================================
Reads last_order.json (written by pizza_bot.py) and draws the pizza that was
ordered. If there is no saved order, it asks a couple of quick questions
instead so it always has something to draw.
Run: python pizza_turtle.py
"""
import json
import math
import random
import turtle
# ---------- colours ----------
CRUST_NORMAL = "#d9a066" # golden crust
CRUST_GF = "#c98a4b" # gluten-free base is a little darker
SAUCE = "#c0392b"
CHEESE_NORMAL = "#f6d365"
CHEESE_VEGAN = "#fff3b0" # dairy-free cheese is paler
MUSHROOM = "#c9b79c"
PEPPER = "#27ae60"
OLIVE = "#2c3e50"
PEPPERONI = "#8e2f1f"
HAM = "#e8a0a0"
BACON = "#a0522d"
BOARD = "#8b5a2b"
# ---------- get the order ----------
def load_order():
"""Load last_order.json, or ask a few questions if it isn't there."""
try:
with open("last_order.json") as f:
return json.load(f)
except FileNotFoundError:
print("No saved order found - let's make a quick one for Shelly to draw.")
order = {
"name": input("Name? ").strip().title() or "Friend",
"size": "large" if input("Small or Large? ").strip().lower().startswith("l") else "small",
"gluten_free": input("Gluten-Free base? (yes/no) ").strip().lower().startswith("y"),
"dairy_free": input("Dairy-Free cheese? (yes/no) ").strip().lower().startswith("y"),
"vegetarian": input("Vegetarian toppings? (yes/no) ").strip().lower().startswith("y"),
"garlic_bread": False,
"delivery": False,
}
order["total_cost"] = (10 if order["size"] == "small" else 15) \
+ (3 if order["gluten_free"] else 0) + (2 if order["dairy_free"] else 0)
return order
# ---------- drawing helpers ----------
def filled_circle(t, x, y, radius, colour):
"""Draw a filled circle centred on (x, y)."""
t.penup()
t.goto(x, y - radius)
t.pendown()
t.color(colour)
t.begin_fill()
t.circle(radius)
t.end_fill()
t.penup()
def scatter(t, count, max_radius, draw_one):
"""Call draw_one(x, y) at 'count' random spots inside the cheese circle."""
for _ in range(count):
angle = random.uniform(0, 2 * math.pi)
dist = random.uniform(0, max_radius)
draw_one(dist * math.cos(angle), dist * math.sin(angle))
def draw_pizza(t, order):
radius = 180 if order["size"] == "large" else 120
# chopping board
filled_circle(t, 0, 0, radius + 40, BOARD)
# crust, sauce, cheese
filled_circle(t, 0, 0, radius, CRUST_GF if order["gluten_free"] else CRUST_NORMAL)
filled_circle(t, 0, 0, radius - 18, SAUCE)
filled_circle(t, 0, 0, radius - 26, CHEESE_VEGAN if order["dairy_free"] else CHEESE_NORMAL)
topping_area = radius - 50
if order["vegetarian"]:
# mushrooms
scatter(t, 6, topping_area, lambda x, y: filled_circle(t, x, y, 12, MUSHROOM))
# green pepper strips (little arcs)
def pepper(x, y):
t.penup(); t.goto(x, y); t.setheading(random.randint(0, 360))
t.pendown(); t.color(PEPPER); t.pensize(6); t.circle(18, 120); t.pensize(1)
scatter(t, 6, topping_area, pepper)
# olives (ring = circle with a hole)
def olive(x, y):
filled_circle(t, x, y, 8, OLIVE)
filled_circle(t, x, y, 3, CHEESE_VEGAN if order["dairy_free"] else CHEESE_NORMAL)
scatter(t, 8, topping_area, olive)
else:
# meat supreme: pepperoni, ham, bacon
scatter(t, 8, topping_area, lambda x, y: filled_circle(t, x, y, 14, PEPPERONI))
scatter(t, 5, topping_area, lambda x, y: filled_circle(t, x, y, 9, HAM))
def bacon(x, y):
t.penup(); t.goto(x, y); t.setheading(random.randint(0, 360))
t.pendown(); t.color(BACON); t.pensize(5); t.forward(28); t.pensize(1)
scatter(t, 5, topping_area, bacon)
# slice lines
t.color("#7a3b1e")
t.pensize(2)
for i in range(4):
angle = math.radians(i * 45)
t.penup()
t.goto(-radius * math.cos(angle), -radius * math.sin(angle))
t.pendown()
t.goto(radius * math.cos(angle), radius * math.sin(angle))
t.pensize(1)
t.penup()
def draw_garlic_bread(t, x, y):
t.penup(); t.goto(x, y); t.setheading(0); t.pendown()
t.color("#e0b070"); t.begin_fill()
for _ in range(2):
t.forward(90); t.circle(20, 180)
t.end_fill()
t.penup(); t.goto(x + 10, y + 12); t.color("#fff5cc"); t.pendown(); t.pensize(4)
t.forward(70); t.pensize(1); t.penup()
def write_label(t, order):
t.penup()
t.goto(0, -260 if order["size"] == "large" else -200)
t.color("white")
notes = []
if order["gluten_free"]:
notes.append("Gluten-Free")
if order["dairy_free"]:
notes.append("Dairy-Free")
notes.append("Vegetarian" if order["vegetarian"] else "Meat Supreme")
line1 = order["name"] + "'s " + order["size"].title() + " Pizza"
line2 = " | ".join(notes) + " Total: $" + str(order["total_cost"])
t.write(line1, align="center", font=("Arial", 22, "bold"))
t.goto(0, t.ycor() - 30)
t.write(line2, align="center", font=("Arial", 14, "normal"))
# ---------- main ----------
def main():
order = load_order()
screen = turtle.Screen()
screen.title("Mario's Pizzeria - Shelly draws " + order["name"] + "'s pizza")
screen.bgcolor("#1e2a38")
screen.setup(width=760, height=680)
shelly = turtle.Turtle()
shelly.shape("turtle")
shelly.color("#2ecc71")
shelly.shapesize(2, 2)
shelly.speed(0) # fastest drawing; change to 6 to watch her work
shelly.hideturtle()
draw_pizza(shelly, order)
if order.get("garlic_bread"):
draw_garlic_bread(shelly, 190, 220)
write_label(shelly, order)
# Shelly takes a bow
shelly.goto(0, 0)
shelly.showturtle()
shelly.penup()
for _ in range(36):
shelly.right(10)
print("Done! Close the window to finish.")
screen.mainloop()
if __name__ == "__main__":
main()