{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "8831a7e2",
   "metadata": {},
   "source": [
    "# ZHUDDLE | Functions Quest\n",
    "### Python for Everyone Basic\n",
    "\n",
    "**100 XP available | Easy to medium | Coding first**\n",
    "\n",
    "Your mission: turn small function calls into working Python programs. Collect points, retry any checkpoint, and submit your work with a grade report.\n",
    "\n",
    "All assessed concepts come from [Python for Everybody: Functions](https://www.py4e.com/html3/04-functions) by Charles R. Severance. This is an original practice assignment adapted from that chapter's examples and Exercises 1-7. The conditional structure for Exercises 6-7 is provided. No classes, recursion, file handling, or exception-handling implementation is assessed.\n",
    "\n",
    "| Quest stage | Challenges | Points |\n",
    "| --- | --- | ---: |\n",
    "| Knowledge keys | 10 MCQs, 2 points each | 20 |\n",
    "| Function workshop | 8 coding missions, 10 points each | 80 |\n",
    "| **Total** | **18 scored questions** | **100** |\n",
    "\n",
    "**Badges:** 0-49 Function Starter; 50-74 Function Explorer; 75-99 Function Builder; 100 Function Champion. Badges are milestones, not extra points. There is no time penalty or retry penalty. Your latest executed answer replaces your previous score, even when the new score is lower.\n",
    "\n",
    "## Start here\n",
    "\n",
    "1. Open this notebook in Jupyter Notebook, JupyterLab, VS Code with a Python kernel, or Google Colab. Use Python 3.\n",
    "2. Run the supplied quest engine once, then enter your name and UNT ID in the sign-in cell.\n",
    "3. Read the MCQs and edit their answer letters. Run the answer cell to check them.\n",
    "4. In each coding cell, leave `%%answer Cxx` as the first line. Edit the Python underneath and run the cell. The first line is provided notebook machinery, not a Python concept you need to learn.\n",
    "5. Retry as needed. Each coding cell is independent, so include the imports and definitions it asks for. Starter placeholders deliberately earn little or no credit.\n",
    "6. Before exporting, run every answer cell you changed, plus the MCQ answer cell and reflection cell. Unexecuted edits are not captured. Save your working notebook using the notebook application's Save command.\n",
    "7. Run the final export cell. Submit the generated **PDF grade report** and **student submission `.ipynb`**. The export captures every assignment answer's latest run, its output, and feedback. Extra scratch cells added outside the assignment are retained only in your original working notebook.\n",
    "\n",
    "PDF export uses ReportLab. If it is missing, run the optional installation cell below. The engine, grading, badge, and export code are provided support and are not assessed. Restarting the kernel clears captured answers; rerun your answer cells before exporting.\n",
    "\n",
    "Your name and UNT ID are placed in both exported files. Files are created in the notebook environment; this notebook does not send them to UNT or an external grading service.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a9a59e0a",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Optional: if PDF export says ReportLab is missing, uncomment and run.\n",
    "# %pip install reportlab"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "257ac25f",
   "metadata": {},
   "source": [
    "## Supplied quest engine\n",
    "\n",
    "Run once. You do not need to read or edit this cell. Rerunning it resets all captured answers."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4f3babc5",
   "metadata": {
    "collapsed": true,
    "jupyter": {
     "source_hidden": true
    },
    "tags": [
     "infrastructure"
    ]
   },
   "outputs": [],
   "source": [
    "SOURCE_URL = 'https://www.py4e.com/html3/04-functions'\n",
    "MCQS = [{'id': 'M01', 'title': 'Define now, call later', 'level': 'Easy', 'source': 'Adding new functions; Exercise 4', 'prompt': '### M01 | Define now, call later | Easy | 2 XP\\n\\nWhat does `def` do?\\n\\n- **A.** Immediately runs the function body.\\n- **B.** Defines a function whose body runs when called.\\n- **C.** Displays the return value.\\n- **D.** Imports a module.\\n\\nSource checkpoint: Adding new functions; Exercise 4.', 'answer': 'B'}, {'id': 'M02', 'title': 'Follow the calls', 'level': 'Easy', 'source': 'Exercise 5', 'prompt': '### M02 | Follow the calls | Easy | 2 XP\\n\\nWhat is the output, in order?\\n\\n```python\\ndef fred():\\n    print(\"Zap\")\\n\\ndef jane():\\n    print(\"ABC\")\\n\\njane()\\nfred()\\njane()\\n```\\n\\n- **A.** Zap, ABC, Zap\\n- **B.** ABC, ABC, Zap\\n- **C.** ABC, Zap, ABC\\n- **D.** No output\\n\\nSource checkpoint: Exercise 5.', 'answer': 'C'}, {'id': 'M03', 'title': 'The value supplied', 'level': 'Easy', 'source': 'Function calls', 'prompt': '### M03 | The value supplied | Easy | 2 XP\\n\\nIn `type(32)`, which item is the argument?\\n\\n- **A.** type\\n- **B.** int\\n- **C.** 32\\n- **D.** return\\n\\nSource checkpoint: Function calls.', 'answer': 'C'}, {'id': 'M04', 'title': 'Conversion check', 'level': 'Easy', 'source': 'Type conversion functions', 'prompt': '### M04 | Conversion check | Easy | 2 XP\\n\\nWhat does `int(-2.3)` return?\\n\\n- **A.** -3\\n- **B.** -2\\n- **C.** 2\\n- **D.** -2.3\\n\\nSource checkpoint: Type conversion functions.', 'answer': 'B'}, {'id': 'M05', 'title': 'A module call', 'level': 'Easy', 'source': 'Math functions', 'prompt': '### M05 | A module call | Easy | 2 XP\\n\\nAfter `import math`, which expression calls the square-root function?\\n\\n- **A.** math.sqrt(2)\\n- **B.** math: sqrt(2)\\n- **C.** import.sqrt(2)\\n- **D.** sqrt.math(2)\\n\\nSource checkpoint: Math functions.', 'answer': 'A'}, {'id': 'M06', 'title': 'Random range', 'level': 'Easy', 'source': 'Random numbers; Exercise 1', 'prompt': '### M06 | Random range | Easy | 2 XP\\n\\nWhich interval describes `random.random()`?\\n\\n- **A.** 0.0 through 1.0, both included\\n- **B.** Greater than 0.0 through 1.0, including 1.0\\n- **C.** Only the integers 0 and 1\\n- **D.** At least 0.0 and less than 1.0\\n\\nSource checkpoint: Random numbers; Exercise 1.', 'answer': 'D'}, {'id': 'M07', 'title': 'Two names, one value', 'level': 'Easy', 'source': 'Parameters and arguments', 'prompt': '### M07 | Two names, one value | Easy | 2 XP\\n\\nFor `def print_twice(bruce):`, what is `bruce`?\\n\\n- **A.** A parameter name\\n- **B.** A required name in the calling code\\n- **C.** A module\\n- **D.** The function name\\n\\nSource checkpoint: Parameters and arguments.', 'answer': 'A'}, {'id': 'M08', 'title': 'An action without a result', 'level': 'Easy', 'source': 'Fruitful functions and void functions', 'prompt': '### M08 | An action without a result | Easy | 2 XP\\n\\nA function prints text but has no `return` statement. What is assigned by `result = that_function()`?\\n\\n- **A.** The printed text\\n- **B.** The string \"None\"\\n- **C.** None\\n- **D.** 0\\n\\nSource checkpoint: Fruitful functions and void functions.', 'answer': 'C'}, {'id': 'M09', 'title': 'Definition order', 'level': 'Medium', 'source': 'Definitions and uses; Exercise 3', 'prompt': '### M09 | Definition order | Medium | 2 XP\\n\\nA function is defined first, a helper it calls is defined second, and the first function is called last. What happens?\\n\\n- **A.** It always fails because the helper appears second.\\n- **B.** It can run because both definitions exist before the call.\\n- **C.** Both bodies run during definition.\\n- **D.** Only the helper runs.\\n\\nSource checkpoint: Definitions and uses; Exercise 3.', 'answer': 'B'}, {'id': 'M10', 'title': 'Keep the result', 'level': 'Medium', 'source': 'Fruitful functions and void functions', 'prompt': '### M10 | Keep the result | Medium | 2 XP\\n\\nA script contains only `import math` and `math.sqrt(5)`. What happens to that result?\\n\\n- **A.** It is automatically printed.\\n- **B.** It is stored as x.\\n- **C.** It is returned from the whole script.\\n- **D.** It is computed but is neither stored nor displayed.\\n\\nSource checkpoint: Fruitful functions and void functions.', 'answer': 'D'}]\n",
    "QUESTS = {'C01': {'title': 'Toolbox warm-up', 'level': 'Easy', 'source': 'Built-in functions; Type conversion functions', 'prompt': '## C01 | Toolbox warm-up\\n\\n**Easy | 10 XP | Five checks worth 2 points each**\\n\\nReplace each `None` with one function call. Use `len`, `max`, and `min` on `message`, `int` on `\"32\"`, and `float` on `\"3.14159\"`. Keep the variable names. Each correctly computed variable earns 2 points. Do not type the final values directly.\\n\\n**Hint:** A call has a function name followed by an argument in parentheses.\\n\\n**Source checkpoint:** Built-in functions; Type conversion functions.', 'starter': 'message = \"Hello world\"\\ncharacter_count = None\\nlargest_character = None\\nsmallest_character = None\\nwhole_number = None\\ndecimal_number = None'}, 'C02': {'title': 'Random-number lab', 'level': 'Easy', 'source': 'Random numbers; Exercise 1', 'prompt': '## C02 | Random-number lab\\n\\n**Easy | 10 XP | Five checks worth 2 points each**\\n\\nComplete the three TODOs, keeping the supplied loop. Import `random`, assign a call to `random.random()` to `x`, and print `x` inside the loop. Run your cell twice and compare the outputs. The checks award 2 points each for the import, ten-iteration loop, random call inside the loop, ten numeric output lines, and values in the correct interval. No particular random sequence is required. The loop is supplied; you do not need to design one.\\n\\n**Hint:** Use module.function() notation. Leave the indented lines inside the loop.\\n\\n**Source checkpoint:** Random numbers; Exercise 1.', 'starter': '# TODO: import the module\\n\\nfor i in range(10):\\n    x = None  # TODO: call the function\\n    # TODO: print x'}, 'C03': {'title': 'Repair the order', 'level': 'Easy', 'source': 'Definitions and uses; Exercises 2 and 3', 'prompt': \"## C03 | Repair the order\\n\\n**Easy | 10 XP | Five checks worth 2 points each**\\n\\nThe first line is in the wrong place. Run the starter once to observe the error. Then move `repeat_message()` below both definitions. Keep `repeat_message` defined before `print_message`. The successful program prints `Hello world` twice, on separate lines. This adapts the chapter's two-function example using another string from the chapter. Checks cover both definitions, their requested order, two helper calls, and the final call/output.\\n\\n**Hint:** Defining a function does not run its body. Both definitions must exist when the final call executes.\\n\\n**Source checkpoint:** Definitions and uses; Exercises 2 and 3.\", 'starter': 'repeat_message()\\n\\ndef repeat_message():\\n    print_message()\\n    print_message()\\n\\ndef print_message():\\n    print(\"Hello world\")'}, 'C04': {'title': 'Print twice', 'level': 'Easy', 'source': 'Parameters and arguments; Fruitful functions and void functions', 'prompt': '## C04 | Print twice\\n\\n**Easy | 10 XP | Five checks worth 2 points each**\\n\\nReplace the placeholder body with two print statements using `bruce`. Then replace the final `None` with a call to your function using `\"Bing\"`. Store that call\\'s result in `result`. Do not add a return statement or print `result` in this cell. Checks cover the definition, text input, numeric input, an expression input, and the saved result/sample output.\\n\\n**Hint:** Use the parameter so the function works with different arguments.\\n\\n**Source checkpoint:** Parameters and arguments; Fruitful functions and void functions.', 'starter': 'def print_twice(bruce):\\n    ...  # Replace this line\\n\\nresult = None  # Replace with your function call'}, 'C05': {'title': 'Return a sum', 'level': 'Easy', 'source': 'Fruitful functions and void functions; addtwo example', 'prompt': '## C05 | Return a sum\\n\\n**Easy | 10 XP | Five checks worth 2 points each**\\n\\nComplete `addtwo(a, b)` so it returns the sum of its arguments. Call it with `3` and `5`, store the return value in `x`, and print `x` after the definition. The function itself should not print. Checks cover the definition, three input pairs, and the sample call/output.\\n\\n**Hint:** A fruitful function sends its computed value back with return.\\n\\n**Source checkpoint:** Fruitful functions and void functions; addtwo example.', 'starter': 'def addtwo(a, b):\\n    return None  # Replace None\\n\\nx = None  # Replace with your function call\\n# Print x here'}, 'C06': {'title': 'Compose the math calls', 'level': 'Medium', 'source': 'Math functions; composition example', 'prompt': \"## C06 | Compose the math calls\\n\\n**Medium | 10 XP | Five checks worth 2 points each**\\n\\nImport `math`. Replace `None` values to reproduce the chapter's 45-degree example: compute `radians` with `degrees / 360.0 * 2 * math.pi`, compute `height` by calling `math.sin(radians)`, and compute `comparison` as `math.sqrt(2) / 2.0`. Print `height`, then `comparison`, on separate lines. Each setup/calculation/output check is worth 2 points. The formulas are supplied; no trigonometry knowledge is required.\\n\\n**Hint:** The result of a function call can be part of a larger expression.\\n\\n**Source checkpoint:** Math functions; composition example.\", 'starter': '# Import the module here\\ndegrees = 45\\nradians = None\\nheight = None\\ncomparison = None\\n# Print the two results here'}, 'C07': {'title': 'Overtime checkpoint', 'level': 'Medium', 'source': 'Exercise 6', 'prompt': '## C07 | Overtime checkpoint\\n\\n**Medium | 10 XP | Five checks worth 2 points each**\\n\\nFill only the three `None` placeholders. Return pay at the regular rate for up to 40 hours. For more than 40 hours, pay the first 40 at `rate` and the remaining hours at `1.5 * rate`. Call `computepay(45, 10)` and store the result in `pay`. The supplied print should show `475` or `475.0`. The branching structure is supplied because the chapter refers back to an earlier pay exercise. Checks cover the function, below/exactly/above 40 hours, and a second overtime rate plus the sample output.\\n\\n**Hint:** Overtime pay is 40 * rate plus (hours - 40) * rate * 1.5.\\n\\n**Source checkpoint:** Exercise 6.', 'starter': 'def computepay(hours, rate):\\n    # Supplied condition: complete only the return expressions.\\n    if hours > 40:\\n        return None\\n    else:\\n        return None\\n\\npay = None  # Call your function with 45 and 10\\nprint(pay)'}, 'C08': {'title': 'Grade gate', 'level': 'Medium', 'source': 'Exercise 7', 'prompt': '## C08 | Grade gate\\n\\n**Medium | 10 XP | Five checks worth 2 points each**\\n\\nReplace the six empty strings inside `computegrade` with the correct return strings: `\"Bad score\"` for a score outside 0 through 1, `\"A\"` for at least 0.9, `\"B\"` for at least 0.8, `\"C\"` for at least 0.7, `\"D\"` for at least 0.6, and `\"F\"` below 0.6. Keep the supplied conditions and helper unchanged. `grade_text` handles converting text and invalid text such as `\"perfect\"`; writing exception handling is not part of this assignment. Checks cover the definition, upper boundaries, lower boundaries, invalid numeric scores, and the supplied helper/sample output.\\n\\n**Hint:** Return quoted strings. The conditions are already in order from highest threshold to lowest.\\n\\n**Source checkpoint:** Exercise 7.', 'starter': 'def computegrade(score):\\n    # Supplied conditions: fill only the six return strings.\\n    if score < 0 or score > 1:\\n        return \"\"\\n    if score >= 0.9:\\n        return \"\"\\n    elif score >= 0.8:\\n        return \"\"\\n    elif score >= 0.7:\\n        return \"\"\\n    elif score >= 0.6:\\n        return \"\"\\n    else:\\n        return \"\"\\n\\n\\n# PROVIDED SUPPORT: do not change; exception handling is not assessed.\\ndef grade_text(text):\\n    try:\\n        number = float(text)\\n    except (ValueError, TypeError):\\n        return \"Bad score\"\\n    return computegrade(number)\\n\\nprint(grade_text(\"0.95\"))\\nprint(grade_text(\"perfect\"))'}}\n",
    "\n",
    "import ast\n",
    "import contextlib\n",
    "import hashlib\n",
    "import html\n",
    "import io\n",
    "import json\n",
    "import math\n",
    "import re\n",
    "import sys\n",
    "import uuid\n",
    "from datetime import datetime, timezone\n",
    "from pathlib import Path\n",
    "from IPython.display import HTML, FileLink, display\n",
    "\n",
    "_answers = {}\n",
    "_mcq_answers = {}\n",
    "_student = {}\n",
    "_notes = ''\n",
    "\n",
    "\n",
    "def set_student(name, unt_id):\n",
    "    name, unt_id = str(name).strip(), str(unt_id).strip()\n",
    "    if not name or not unt_id:\n",
    "        raise ValueError('Enter both your name and your UNT ID, then rerun this cell.')\n",
    "    if len(name) > 100 or len(unt_id) > 64 or any(ord(c) < 32 for c in name + unt_id):\n",
    "        raise ValueError('Use a name up to 100 characters and an ID up to 64, without control characters.')\n",
    "    if _student and _student != {'name': name, 'unt_id': unt_id}:\n",
    "        raise ValueError('For a different student, restart the kernel and begin again.')\n",
    "    _student.update(name=name, unt_id=unt_id)\n",
    "    print('Welcome, ' + name + '. Your quest has 100 available points.')\n",
    "\n",
    "\n",
    "@contextlib.contextmanager\n",
    "def _bounded():\n",
    "    # A trace budget catches accidental infinite Python loops, not hostile code.\n",
    "    count = 0\n",
    "    previous = sys.gettrace()\n",
    "    def trace(frame, event, arg):\n",
    "        nonlocal count\n",
    "        if event == 'line':\n",
    "            count += 1\n",
    "            if count > 20000:\n",
    "                raise RuntimeError('This run exceeded the practice limit. Check for an endless loop.')\n",
    "        return trace\n",
    "    sys.settrace(trace)\n",
    "    try:\n",
    "        yield\n",
    "    finally:\n",
    "        sys.settrace(previous)\n",
    "\n",
    "\n",
    "def _called(tree, name):\n",
    "    return any(isinstance(n, ast.Call) and isinstance(n.func, ast.Name) and n.func.id == name\n",
    "               for n in ast.walk(tree))\n",
    "\n",
    "\n",
    "def _attribute_called(tree, module, name):\n",
    "    return any(isinstance(n, ast.Call) and isinstance(n.func, ast.Attribute)\n",
    "               and isinstance(n.func.value, ast.Name) and n.func.value.id == module\n",
    "               and n.func.attr == name for n in ast.walk(tree))\n",
    "\n",
    "\n",
    "def _definition(tree, name, count):\n",
    "    return any(isinstance(n, ast.FunctionDef) and n.name == name\n",
    "               and len(n.args.args) == count for n in tree.body)\n",
    "\n",
    "\n",
    "def _invoke(ns, name, args, expected, printed=None):\n",
    "    sink = io.StringIO()\n",
    "    with _bounded(), contextlib.redirect_stdout(sink):\n",
    "        value = ns[name](*args)\n",
    "    same = (type(value) in (int, float) and math.isclose(value, expected, abs_tol=1e-9)) if type(expected) in (int, float) else value == expected\n",
    "    return same and (printed is None or sink.getvalue() == printed)\n",
    "\n",
    "\n",
    "def _evaluate(qid, source):\n",
    "    ns = {'__name__': '__student__'}\n",
    "    output = io.StringIO()\n",
    "    error = None\n",
    "    tree = None\n",
    "    try:\n",
    "        tree = ast.parse(source)\n",
    "        with _bounded(), contextlib.redirect_stdout(output), contextlib.redirect_stderr(output):\n",
    "            exec(compile(tree, '<' + qid + '>', 'exec'), ns)\n",
    "    except BaseException as exc:\n",
    "        if isinstance(exc, KeyboardInterrupt):\n",
    "            error = 'KeyboardInterrupt: run interrupted; edit and retry.'\n",
    "        else:\n",
    "            error = type(exc).__name__ + ': ' + str(exc)\n",
    "    checks = []\n",
    "    def check(label, predicate):\n",
    "        try:\n",
    "            passed = error is None and bool(predicate())\n",
    "        except BaseException:\n",
    "            passed = False\n",
    "        checks.append({'label': label, 'earned': 2 if passed else 0, 'possible': 2})\n",
    "    if qid == 'C01':\n",
    "        check('Use len for the character count.', lambda: ns.get('character_count') == 11 and _called(tree, 'len'))\n",
    "        check('Use max for the largest character.', lambda: ns.get('largest_character') == 'w' and _called(tree, 'max'))\n",
    "        check('Use min for the smallest character.', lambda: ns.get('smallest_character') == ' ' and _called(tree, 'min'))\n",
    "        check('Use int to convert the text value.', lambda: type(ns.get('whole_number')) is int and ns['whole_number'] == 32 and _called(tree, 'int'))\n",
    "        check('Use float to convert the decimal text.', lambda: type(ns.get('decimal_number')) is float and ns['decimal_number'] == 3.14159 and _called(tree, 'float'))\n",
    "    elif qid == 'C02':\n",
    "        lines = output.getvalue().splitlines()\n",
    "        check('Import the random module.', lambda: any(isinstance(n, ast.Import) and any(a.name == 'random' for a in n.names) for n in tree.body))\n",
    "        check('Use the supplied for loop and range(10).', lambda: any(isinstance(n, ast.For) and isinstance(n.iter, ast.Call) and isinstance(n.iter.func, ast.Name) and n.iter.func.id == 'range' and len(n.iter.args) == 1 and isinstance(n.iter.args[0], ast.Constant) and n.iter.args[0].value == 10 for n in ast.walk(tree)))\n",
    "        check('Call random.random inside the loop.', lambda: any(isinstance(n, ast.For) and _attribute_called(n, 'random', 'random') for n in ast.walk(tree)))\n",
    "        check('Print exactly ten numeric lines.', lambda: len(lines) == 10 and all(math.isfinite(float(v)) for v in lines))\n",
    "        check('All ten values are at least 0 and below 1.', lambda: len(lines) == 10 and all(0 <= float(v) < 1 for v in lines))\n",
    "    elif qid == 'C03':\n",
    "        check('Define repeat_message with no parameters.', lambda: _definition(tree, 'repeat_message', 0))\n",
    "        check('Define print_message with no parameters.', lambda: _definition(tree, 'print_message', 0))\n",
    "        check('Define repeat_message before print_message.', lambda: [n.name for n in tree.body if isinstance(n, ast.FunctionDef)] == ['repeat_message', 'print_message'])\n",
    "        check('Call print_message twice inside repeat_message.', lambda: sum(isinstance(n, ast.Call) and isinstance(n.func, ast.Name) and n.func.id == 'print_message' for f in tree.body if isinstance(f, ast.FunctionDef) and f.name == 'repeat_message' for n in ast.walk(f)) == 2)\n",
    "        check('Call repeat_message after the definitions; print two lines.', lambda: isinstance(tree.body[-1], ast.Expr) and _called(tree.body[-1], 'repeat_message') and output.getvalue() == 'Hello world\\nHello world\\n')\n",
    "    elif qid == 'C04':\n",
    "        check('Define print_twice with one parameter.', lambda: _definition(tree, 'print_twice', 1))\n",
    "        check('Print the text argument twice and return None.', lambda: _invoke(ns, 'print_twice', ('Spam',), None, 'Spam\\nSpam\\n'))\n",
    "        check('Print a numeric argument twice.', lambda: _invoke(ns, 'print_twice', (17,), None, '17\\n17\\n'))\n",
    "        check('Print an expression argument twice.', lambda: _invoke(ns, 'print_twice', ('Spam ' * 4,), None, ('Spam ' * 4 + '\\n') * 2))\n",
    "        check('Call your function with Bing and store its None result.', lambda: 'result' in ns and ns['result'] is None and output.getvalue() == 'Bing\\nBing\\n' and _called(tree, 'print_twice'))\n",
    "    elif qid == 'C05':\n",
    "        check('Define addtwo with two parameters.', lambda: _definition(tree, 'addtwo', 2))\n",
    "        check('Return the sum of 3 and 5.', lambda: _invoke(ns, 'addtwo', (3, 5), 8, ''))\n",
    "        check('Return the sum of 0 and 7.', lambda: _invoke(ns, 'addtwo', (0, 7), 7, ''))\n",
    "        check('Return the sum of -2 and 5.', lambda: _invoke(ns, 'addtwo', (-2, 5), 3, ''))\n",
    "        check('Store addtwo(3, 5) in x and print x.', lambda: ns.get('x') == 8 and output.getvalue() == '8\\n' and _called(tree, 'addtwo'))\n",
    "    elif qid == 'C06':\n",
    "        check('Import math and keep degrees equal to 45.', lambda: ns.get('degrees') == 45 and any(isinstance(n, ast.Import) and any(a.name == 'math' for a in n.names) for n in tree.body))\n",
    "        check('Compute radians from degrees using math.pi.', lambda: math.isclose(ns['radians'], math.pi / 4) and any(isinstance(n, ast.Attribute) and n.attr == 'pi' for n in ast.walk(tree)))\n",
    "        check('Compute height with math.sin(radians).', lambda: math.isclose(ns['height'], math.sin(math.pi / 4)) and _attribute_called(tree, 'math', 'sin'))\n",
    "        check('Compute comparison with math.sqrt(2) divided by 2.', lambda: math.isclose(ns['comparison'], math.sqrt(2) / 2) and _attribute_called(tree, 'math', 'sqrt'))\n",
    "        check('Print height then comparison.', lambda: len(output.getvalue().splitlines()) == 2 and all(math.isclose(float(v), math.sqrt(2) / 2) for v in output.getvalue().splitlines()))\n",
    "    elif qid == 'C07':\n",
    "        check('Define computepay with hours and rate parameters.', lambda: _definition(tree, 'computepay', 2))\n",
    "        check('Return 300 for 30 hours at 10.', lambda: _invoke(ns, 'computepay', (30, 10), 300, ''))\n",
    "        check('Return 400 for exactly 40 hours at 10.', lambda: _invoke(ns, 'computepay', (40, 10), 400, ''))\n",
    "        check('Return 475 for 45 hours at 10.', lambda: _invoke(ns, 'computepay', (45, 10), 475, ''))\n",
    "        check('Handle another overtime rate and print the sample pay.', lambda: _invoke(ns, 'computepay', (42, 20), 860, '') and output.getvalue().strip() in ('475', '475.0'))\n",
    "    elif qid == 'C08':\n",
    "        check('Define computegrade with one parameter.', lambda: _definition(tree, 'computegrade', 1))\n",
    "        check('Return A and B at the correct boundaries.', lambda: all(_invoke(ns, 'computegrade', (v,), grade, '') for v, grade in [(1.0,'A'),(.95,'A'),(.9,'A'),(.899,'B'),(.8,'B')]))\n",
    "        check('Return C, D and F at the correct boundaries.', lambda: all(_invoke(ns, 'computegrade', (v,), grade, '') for v, grade in [(.799,'C'),(.75,'C'),(.7,'C'),(.699,'D'),(.6,'D'),(.599,'F'),(.5,'F'),(0,'F')]))\n",
    "        check('Reject numeric scores outside 0 through 1.', lambda: all(_invoke(ns, 'computegrade', (v,), 'Bad score', '') for v in (-.1, 10.0)))\n",
    "        check('Use the supplied text-input helper for numeric and invalid text.', lambda: all(_invoke(ns, 'grade_text', (v,), grade, '') for v, grade in [('0.95','A'),('0.75','C'),('0.5','F'),('perfect','Bad score'),('10.0','Bad score')]) and output.getvalue() == 'A\\nBad score\\n')\n",
    "    return {'source': source, 'stdout': output.getvalue(), 'error': error,\n",
    "            'checks': checks, 'earned': sum(c['earned'] for c in checks), 'possible': 10}\n",
    "\n",
    "\n",
    "def answer(line, cell):\n",
    "    qid = line.strip()\n",
    "    if qid not in QUESTS:\n",
    "        raise ValueError('Keep the first line of each answer cell unchanged.')\n",
    "    attempts = _answers.get(qid, {}).get('attempts', 0) + 1\n",
    "    result = _evaluate(qid, cell)\n",
    "    result['attempts'] = attempts\n",
    "    _answers[qid] = result\n",
    "    if result['stdout']:\n",
    "        print(result['stdout'], end='')\n",
    "    if result['error']:\n",
    "        print(result['error'])\n",
    "    print('\\n' + qid + ': ' + str(result['earned']) + '/10 points | attempt ' + str(attempts))\n",
    "    for row in result['checks']:\n",
    "        print(('PASS  ' if row['earned'] else 'RETRY ') + row['label'] + ' (' + str(row['earned']) + '/2)')\n",
    "    show_progress()\n",
    "\n",
    "\n",
    "get_ipython().register_magic_function(answer, 'cell', 'answer')\n",
    "\n",
    "\n",
    "def submit_mcqs(*choices):\n",
    "    if len(choices) != len(MCQS):\n",
    "        raise ValueError('Keep all ten answer variables in the supplied call.')\n",
    "    for question, value in zip(MCQS, choices):\n",
    "        choice = str(value).strip().upper()\n",
    "        correct = choice == question['answer']\n",
    "        _mcq_answers[question['id']] = {'choice': choice, 'earned': 2 if correct else 0}\n",
    "        status = 'PASS' if correct else ('UNANSWERED' if not choice else 'RETRY')\n",
    "        print(question['id'] + ': ' + status + ' (' + str(2 if correct else 0) + '/2)')\n",
    "    show_progress()\n",
    "\n",
    "\n",
    "def _total():\n",
    "    return sum(v['earned'] for v in _answers.values()) + sum(v['earned'] for v in _mcq_answers.values())\n",
    "\n",
    "\n",
    "def _badge(score):\n",
    "    if score == 100:\n",
    "        return 'Function Champion'\n",
    "    if score >= 75:\n",
    "        return 'Function Builder'\n",
    "    if score >= 50:\n",
    "        return 'Function Explorer'\n",
    "    return 'Function Starter'\n",
    "\n",
    "\n",
    "def show_progress():\n",
    "    score = _total()\n",
    "    completed = sum(v['earned'] == 10 for v in _answers.values())\n",
    "    display(HTML('<div style=\"border-left:5px solid #008568;padding:12px;background:#f2f7f5;color:#16372c\">'\n",
    "                 '<b>' + str(score) + '/100 XP | ' + _badge(score) + '</b><br>'\n",
    "                 + str(completed) + '/8 coding missions mastered'\n",
    "                 '<div style=\"height:10px;background:#d6dfdc;margin-top:8px\"><div style=\"height:10px;width:'\n",
    "                 + str(score) + '%;background:#008568\"></div></div></div>'))\n",
    "\n",
    "\n",
    "def save_notes(text):\n",
    "    global _notes\n",
    "    _notes = str(text)\n",
    "    print('Reflection saved. This reflection carries no points.')\n",
    "\n",
    "\n",
    "def _rows():\n",
    "    result = []\n",
    "    for q in MCQS:\n",
    "        a = _mcq_answers.get(q['id'], {'choice':'', 'earned':0})\n",
    "        feedback = ('Correct.' if a['earned'] else 'Review: ' + q['source'])\n",
    "        result.append({'id':q['id'], 'title':q['title'], 'level':q['level'],\n",
    "                       'earned':a['earned'], 'possible':2,\n",
    "                       'feedback':'Selected: ' + (a['choice'] or '(unanswered)') + '. ' + feedback})\n",
    "    for qid, q in QUESTS.items():\n",
    "        a = _answers.get(qid)\n",
    "        feedback = 'Not run. No points awarded.' if a is None else '; '.join(\n",
    "            ('PASS: ' if c['earned'] else 'RETRY: ') + c['label'] for c in a['checks'])\n",
    "        if a and a['error']:\n",
    "            feedback = a['error'] + '. ' + feedback\n",
    "        result.append({'id':qid, 'title':q['title'], 'level':q['level'],\n",
    "                       'earned':a['earned'] if a else 0, 'possible':10, 'feedback':feedback})\n",
    "    return result\n",
    "\n",
    "\n",
    "def _md(text):\n",
    "    return {'cell_type':'markdown', 'id':uuid.uuid4().hex[:8], 'metadata':{}, 'source':text}\n",
    "\n",
    "\n",
    "def _code(source, output='', count=None):\n",
    "    return {'cell_type':'code', 'id':uuid.uuid4().hex[:8], 'metadata':{}, 'source':source,\n",
    "            'execution_count':count, 'outputs':([{'output_type':'stream','name':'stdout','text':output}] if output else [])}\n",
    "\n",
    "\n",
    "def _submission(receipt):\n",
    "    cells = [_md('# ZHUDDLE | Functions Quest\\n\\nStudent submission\\n\\n'\n",
    "                 + '**Name:** ' + _student['name'] + '\\n\\n**UNT ID:** ' + _student['unt_id']\n",
    "                 + '\\n\\n**Practice grade:** ' + str(_total()) + '/100\\n\\n**Badge:** ' + _badge(_total())\n",
    "                 + '\\n\\nReceipt: ' + receipt['id'] + '\\n\\nUTC: ' + receipt['created_utc']\n",
    "                 + '\\n\\nAssignment: ' + SOURCE_URL\n",
    "                 + '\\n\\nThis file contains the latest executed answer for every assignment question, captured output, feedback, and reflection. The original answer notebook remains your editable workspace. Scores are local self-check results pending instructor review.')]\n",
    "    for q in MCQS:\n",
    "        cells.append(_md(q['prompt']))\n",
    "        a = _mcq_answers.get(q['id'], {'choice':'','earned':0})\n",
    "        cells.append(_code(q['id'].lower() + ' = ' + repr(a['choice'])))\n",
    "        cells.append(_md('Score: ' + str(a['earned']) + '/2'))\n",
    "    for qid, q in QUESTS.items():\n",
    "        a = _answers.get(qid)\n",
    "        cells.append(_md(q['prompt']))\n",
    "        cells.append(_code(a['source'] if a else q['starter'], a['stdout'] if a else ''))\n",
    "        row = next(r for r in receipt['results'] if r['id'] == qid)\n",
    "        cells.append(_md('Score: ' + str(row['earned']) + '/10\\n\\n' + row['feedback']))\n",
    "    cells.append(_md('## Reflection\\n\\n' + (_notes or '(No reflection entered.)')))\n",
    "    return {'nbformat':4,'nbformat_minor':5, 'metadata':{\n",
    "        'kernelspec':{'display_name':'Python 3','language':'python','name':'python3'},\n",
    "        'language_info':{'name':'python','version':sys.version.split()[0]}, 'zhuddle':receipt}, 'cells':cells}\n",
    "\n",
    "\n",
    "def _pdf(path, receipt):\n",
    "    from reportlab.lib import colors\n",
    "    from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle\n",
    "    from reportlab.lib.enums import TA_LEFT\n",
    "    from reportlab.lib.pagesizes import letter\n",
    "    from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak, KeepTogether\n",
    "    from reportlab.pdfbase import pdfmetrics\n",
    "    from reportlab.pdfbase.ttfonts import TTFont\n",
    "    import reportlab\n",
    "    font_path = Path(reportlab.__file__).parent / 'fonts' / 'Vera.ttf'\n",
    "    pdfmetrics.registerFont(TTFont('QuestFont', str(font_path)))\n",
    "    styles = getSampleStyleSheet()\n",
    "    for style in styles.byName.values():\n",
    "        style.fontName = 'QuestFont'\n",
    "    styles.add(ParagraphStyle(name='QuestSmall',fontName='QuestFont',fontSize=8.5,leading=12,spaceAfter=5,wordWrap='CJK'))\n",
    "    styles['Title'].textColor = colors.HexColor('#00765e')\n",
    "    styles['BodyText'].leading = 14\n",
    "    esc = lambda value: html.escape(str(value))\n",
    "    p = lambda text, style='BodyText': Paragraph(esc(text), styles[style])\n",
    "    story = [p('ZHUDDLE | Functions Quest', 'Title'), p('GRADE REPORT - LOCAL SELF-CHECK', 'Heading2'),\n",
    "             p('Student: ' + _student['name']), p('UNT ID: ' + _student['unt_id']),\n",
    "             Spacer(1,12), p(str(_total()) + ' / 100 points', 'Title'), p(_badge(_total()), 'Heading2'),\n",
    "             p('MCQs: ' + str(sum(v['earned'] for v in _mcq_answers.values())) + '/20 | Coding: ' + str(sum(v['earned'] for v in _answers.values())) + '/80'),\n",
    "             p('Latest executed answers count. Retries do not reduce points. Unanswered items earn zero.'),\n",
    "             p('These results require instructor review; this is not an official UNT transcript.'), Spacer(1,12)]\n",
    "    table = [[p('Question','QuestSmall'),p('Level','QuestSmall'),p('Points','QuestSmall')]]\n",
    "    for row in receipt['results']:\n",
    "        table.append([p(row['id'] + '  ' + row['title'],'QuestSmall'),p(row['level'],'QuestSmall'),p(str(row['earned']) + '/' + str(row['possible']),'QuestSmall')])\n",
    "    t = Table(table, colWidths=[340,80,80], repeatRows=1, hAlign='LEFT')\n",
    "    t.setStyle(TableStyle([('BACKGROUND',(0,0),(-1,0),colors.HexColor('#d9eee6')),\n",
    "                          ('ROWBACKGROUNDS',(0,1),(-1,-1),[colors.white,colors.HexColor('#f5f7f6')]),\n",
    "                          ('VALIGN',(0,0),(-1,-1),'TOP'),('BOTTOMPADDING',(0,0),(-1,-1),4),('TOPPADDING',(0,0),(-1,-1),4)]))\n",
    "    story.extend([t, PageBreak(), p('Question Feedback', 'Heading1')])\n",
    "    for row in receipt['results']:\n",
    "        story.append(KeepTogether([p(row['id'] + ' | ' + row['title'] + ' | ' + str(row['earned']) + '/' + str(row['possible']),'Heading3'),p(row['feedback'],'QuestSmall')]))\n",
    "    story.extend([Spacer(1,12),p('Submission Receipt','Heading2'),p('Receipt: ' + receipt['id'],'QuestSmall'),\n",
    "                  p('Created (UTC): ' + receipt['created_utc'],'QuestSmall'),\n",
    "                  p('Answer SHA-256: ' + receipt['answer_sha256'],'QuestSmall'),\n",
    "                  p('Source: ' + SOURCE_URL,'QuestSmall'),\n",
    "                  p('Adapted from Charles R. Severance, Python for Everybody, Functions. CC BY 4.0. Original question wording and scaffolding added for this assignment.','QuestSmall')])\n",
    "    def footer(canvas, doc):\n",
    "        canvas.setFont('QuestFont',8)\n",
    "        canvas.setFillColor(colors.HexColor('#52645e'))\n",
    "        canvas.drawString(48,28,'ZHUDDLE | Functions Quest | ' + receipt['id'][:8])\n",
    "        canvas.drawRightString(564,28,'Page ' + str(doc.page))\n",
    "    SimpleDocTemplate(str(path),pagesize=letter,rightMargin=48,leftMargin=48,topMargin=40,bottomMargin=48,\n",
    "                      title='ZHUDDLE Functions Quest Grade Report',author='ZHUDDLE').build(story,onFirstPage=footer,onLaterPages=footer)\n",
    "\n",
    "\n",
    "def finalize(output_directory='zhuddle_submissions'):\n",
    "    if not _student:\n",
    "        raise ValueError('Run the student name and UNT ID cell before exporting.')\n",
    "    try:\n",
    "        import reportlab\n",
    "    except ImportError as exc:\n",
    "        raise ImportError('PDF export needs ReportLab. Run %pip install reportlab in a new code cell, then rerun this cell. Your answers are still in memory.') from exc\n",
    "    receipt = {'id':uuid.uuid4().hex,'created_utc':datetime.now(timezone.utc).isoformat(),\n",
    "               'student':dict(_student),'score':_total(),'possible':100,'badge':_badge(_total()),\n",
    "               'results':_rows(),'version':'1.0'}\n",
    "    evidence = {'code':{k:v['source'] for k,v in _answers.items()},'mcqs':_mcq_answers,'reflection':_notes}\n",
    "    receipt['answer_sha256'] = hashlib.sha256(json.dumps(evidence,sort_keys=True,ensure_ascii=False).encode()).hexdigest()\n",
    "    safe_id = re.sub(r'[^A-Za-z0-9_-]', '_', _student['unt_id'])[:64] or 'student'\n",
    "    target = Path(output_directory) / (safe_id + '_' + receipt['id'][:8])\n",
    "    target.mkdir(parents=True,exist_ok=False)\n",
    "    nb_path = target / 'ZHUDDLE_Student_Submission.ipynb'\n",
    "    pdf_path = target / 'ZHUDDLE_Grade_Report.pdf'\n",
    "    nb_path.write_text(json.dumps(_submission(receipt),ensure_ascii=False,indent=1),encoding='utf-8')\n",
    "    _pdf(pdf_path, receipt)\n",
    "    print('Export complete: ' + str(_total()) + '/100 | ' + _badge(_total()))\n",
    "    print('Submit BOTH files. Each export gets a new folder; earlier exports remain available.')\n",
    "    display(FileLink(str(nb_path)))\n",
    "    display(FileLink(str(pdf_path)))\n",
    "    print('In Colab, find the same folder in the Files sidebar and download both files.')\n",
    "    return {'notebook':str(nb_path), 'pdf':str(pdf_path), 'receipt':receipt}\n",
    "\n",
    "\n",
    "print('Quest engine ready. Continue to student sign-in. This supplied cell is not assessed.')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "432a6f69",
   "metadata": {},
   "source": [
    "## Player sign-in\n",
    "\n",
    "Enter your full name and the UNT ID your instructor asks you to use. Keep any leading zeros. Both fields are required."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ec4b5ca6",
   "metadata": {},
   "outputs": [],
   "source": [
    "student_name = input(\"Student full name: \")\n",
    "unt_id = input(\"UNT ID: \")\n",
    "set_student(student_name, unt_id)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1592afc5",
   "metadata": {},
   "source": [
    "## Stage 1 | Knowledge keys\n",
    "\n",
    "Read all ten questions. Enter one letter, A-D, for each in the answer cell below. Each correct answer earns 2 XP."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "02ca86d6",
   "metadata": {},
   "source": [
    "### M01 | Define now, call later | Easy | 2 XP\n",
    "\n",
    "What does `def` do?\n",
    "\n",
    "- **A.** Immediately runs the function body.\n",
    "- **B.** Defines a function whose body runs when called.\n",
    "- **C.** Displays the return value.\n",
    "- **D.** Imports a module.\n",
    "\n",
    "Source checkpoint: Adding new functions; Exercise 4."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "31516818",
   "metadata": {},
   "source": [
    "### M02 | Follow the calls | Easy | 2 XP\n",
    "\n",
    "What is the output, in order?\n",
    "\n",
    "```python\n",
    "def fred():\n",
    "    print(\"Zap\")\n",
    "\n",
    "def jane():\n",
    "    print(\"ABC\")\n",
    "\n",
    "jane()\n",
    "fred()\n",
    "jane()\n",
    "```\n",
    "\n",
    "- **A.** Zap, ABC, Zap\n",
    "- **B.** ABC, ABC, Zap\n",
    "- **C.** ABC, Zap, ABC\n",
    "- **D.** No output\n",
    "\n",
    "Source checkpoint: Exercise 5."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b32bdfd8",
   "metadata": {},
   "source": [
    "### M03 | The value supplied | Easy | 2 XP\n",
    "\n",
    "In `type(32)`, which item is the argument?\n",
    "\n",
    "- **A.** type\n",
    "- **B.** int\n",
    "- **C.** 32\n",
    "- **D.** return\n",
    "\n",
    "Source checkpoint: Function calls."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a3e5ea90",
   "metadata": {},
   "source": [
    "### M04 | Conversion check | Easy | 2 XP\n",
    "\n",
    "What does `int(-2.3)` return?\n",
    "\n",
    "- **A.** -3\n",
    "- **B.** -2\n",
    "- **C.** 2\n",
    "- **D.** -2.3\n",
    "\n",
    "Source checkpoint: Type conversion functions."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0045bccc",
   "metadata": {},
   "source": [
    "### M05 | A module call | Easy | 2 XP\n",
    "\n",
    "After `import math`, which expression calls the square-root function?\n",
    "\n",
    "- **A.** math.sqrt(2)\n",
    "- **B.** math: sqrt(2)\n",
    "- **C.** import.sqrt(2)\n",
    "- **D.** sqrt.math(2)\n",
    "\n",
    "Source checkpoint: Math functions."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7749fad5",
   "metadata": {},
   "source": [
    "### M06 | Random range | Easy | 2 XP\n",
    "\n",
    "Which interval describes `random.random()`?\n",
    "\n",
    "- **A.** 0.0 through 1.0, both included\n",
    "- **B.** Greater than 0.0 through 1.0, including 1.0\n",
    "- **C.** Only the integers 0 and 1\n",
    "- **D.** At least 0.0 and less than 1.0\n",
    "\n",
    "Source checkpoint: Random numbers; Exercise 1."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4049b730",
   "metadata": {},
   "source": [
    "### M07 | Two names, one value | Easy | 2 XP\n",
    "\n",
    "For `def print_twice(bruce):`, what is `bruce`?\n",
    "\n",
    "- **A.** A parameter name\n",
    "- **B.** A required name in the calling code\n",
    "- **C.** A module\n",
    "- **D.** The function name\n",
    "\n",
    "Source checkpoint: Parameters and arguments."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ff345300",
   "metadata": {},
   "source": [
    "### M08 | An action without a result | Easy | 2 XP\n",
    "\n",
    "A function prints text but has no `return` statement. What is assigned by `result = that_function()`?\n",
    "\n",
    "- **A.** The printed text\n",
    "- **B.** The string \"None\"\n",
    "- **C.** None\n",
    "- **D.** 0\n",
    "\n",
    "Source checkpoint: Fruitful functions and void functions."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6caee58c",
   "metadata": {},
   "source": [
    "### M09 | Definition order | Medium | 2 XP\n",
    "\n",
    "A function is defined first, a helper it calls is defined second, and the first function is called last. What happens?\n",
    "\n",
    "- **A.** It always fails because the helper appears second.\n",
    "- **B.** It can run because both definitions exist before the call.\n",
    "- **C.** Both bodies run during definition.\n",
    "- **D.** Only the helper runs.\n",
    "\n",
    "Source checkpoint: Definitions and uses; Exercise 3."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5e142b66",
   "metadata": {},
   "source": [
    "### M10 | Keep the result | Medium | 2 XP\n",
    "\n",
    "A script contains only `import math` and `math.sqrt(5)`. What happens to that result?\n",
    "\n",
    "- **A.** It is automatically printed.\n",
    "- **B.** It is stored as x.\n",
    "- **C.** It is returned from the whole script.\n",
    "- **D.** It is computed but is neither stored nor displayed.\n",
    "\n",
    "Source checkpoint: Fruitful functions and void functions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e3ac3672",
   "metadata": {
    "tags": [
     "mcq-answer"
    ]
   },
   "outputs": [],
   "source": [
    "m01 = \"\"\n",
    "m02 = \"\"\n",
    "m03 = \"\"\n",
    "m04 = \"\"\n",
    "m05 = \"\"\n",
    "m06 = \"\"\n",
    "m07 = \"\"\n",
    "m08 = \"\"\n",
    "m09 = \"\"\n",
    "m10 = \"\"\n",
    "\n",
    "# Keep this supplied call unchanged.\n",
    "submit_mcqs(m01, m02, m03, m04, m05, m06, m07, m08, m09, m10)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d3274773",
   "metadata": {},
   "source": [
    "## Stage 2 | Function workshop\n",
    "\n",
    "Five easy missions build confidence. Three medium missions combine the chapter's examples. Every check is worth 2 XP. A syntax or runtime error makes that mission worth zero until repaired; it does not stop the remaining missions."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b9cf1c9a",
   "metadata": {},
   "source": [
    "## C01 | Toolbox warm-up\n",
    "\n",
    "**Easy | 10 XP | Five checks worth 2 points each**\n",
    "\n",
    "Replace each `None` with one function call. Use `len`, `max`, and `min` on `message`, `int` on `\"32\"`, and `float` on `\"3.14159\"`. Keep the variable names. Each correctly computed variable earns 2 points. Do not type the final values directly.\n",
    "\n",
    "**Hint:** A call has a function name followed by an argument in parentheses.\n",
    "\n",
    "**Source checkpoint:** Built-in functions; Type conversion functions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "38d32267",
   "metadata": {
    "tags": [
     "student-answer",
     "C01"
    ]
   },
   "outputs": [],
   "source": [
    "%%answer C01\n",
    "message = \"Hello world\"\n",
    "character_count = None\n",
    "largest_character = None\n",
    "smallest_character = None\n",
    "whole_number = None\n",
    "decimal_number = None"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "310e25d6",
   "metadata": {},
   "source": [
    "## C02 | Random-number lab\n",
    "\n",
    "**Easy | 10 XP | Five checks worth 2 points each**\n",
    "\n",
    "Complete the three TODOs, keeping the supplied loop. Import `random`, assign a call to `random.random()` to `x`, and print `x` inside the loop. Run your cell twice and compare the outputs. The checks award 2 points each for the import, ten-iteration loop, random call inside the loop, ten numeric output lines, and values in the correct interval. No particular random sequence is required. The loop is supplied; you do not need to design one.\n",
    "\n",
    "**Hint:** Use module.function() notation. Leave the indented lines inside the loop.\n",
    "\n",
    "**Source checkpoint:** Random numbers; Exercise 1."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bb9d45f4",
   "metadata": {
    "tags": [
     "student-answer",
     "C02"
    ]
   },
   "outputs": [],
   "source": [
    "%%answer C02\n",
    "# TODO: import the module\n",
    "\n",
    "for i in range(10):\n",
    "    x = None  # TODO: call the function\n",
    "    # TODO: print x"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cb705621",
   "metadata": {},
   "source": [
    "## C03 | Repair the order\n",
    "\n",
    "**Easy | 10 XP | Five checks worth 2 points each**\n",
    "\n",
    "The first line is in the wrong place. Run the starter once to observe the error. Then move `repeat_message()` below both definitions. Keep `repeat_message` defined before `print_message`. The successful program prints `Hello world` twice, on separate lines. This adapts the chapter's two-function example using another string from the chapter. Checks cover both definitions, their requested order, two helper calls, and the final call/output.\n",
    "\n",
    "**Hint:** Defining a function does not run its body. Both definitions must exist when the final call executes.\n",
    "\n",
    "**Source checkpoint:** Definitions and uses; Exercises 2 and 3."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ce477a63",
   "metadata": {
    "tags": [
     "student-answer",
     "C03"
    ]
   },
   "outputs": [],
   "source": [
    "%%answer C03\n",
    "repeat_message()\n",
    "\n",
    "def repeat_message():\n",
    "    print_message()\n",
    "    print_message()\n",
    "\n",
    "def print_message():\n",
    "    print(\"Hello world\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "37c57f96",
   "metadata": {},
   "source": [
    "## C04 | Print twice\n",
    "\n",
    "**Easy | 10 XP | Five checks worth 2 points each**\n",
    "\n",
    "Replace the placeholder body with two print statements using `bruce`. Then replace the final `None` with a call to your function using `\"Bing\"`. Store that call's result in `result`. Do not add a return statement or print `result` in this cell. Checks cover the definition, text input, numeric input, an expression input, and the saved result/sample output.\n",
    "\n",
    "**Hint:** Use the parameter so the function works with different arguments.\n",
    "\n",
    "**Source checkpoint:** Parameters and arguments; Fruitful functions and void functions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "30f48049",
   "metadata": {
    "tags": [
     "student-answer",
     "C04"
    ]
   },
   "outputs": [],
   "source": [
    "%%answer C04\n",
    "def print_twice(bruce):\n",
    "    ...  # Replace this line\n",
    "\n",
    "result = None  # Replace with your function call"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2be7c6e4",
   "metadata": {},
   "source": [
    "## C05 | Return a sum\n",
    "\n",
    "**Easy | 10 XP | Five checks worth 2 points each**\n",
    "\n",
    "Complete `addtwo(a, b)` so it returns the sum of its arguments. Call it with `3` and `5`, store the return value in `x`, and print `x` after the definition. The function itself should not print. Checks cover the definition, three input pairs, and the sample call/output.\n",
    "\n",
    "**Hint:** A fruitful function sends its computed value back with return.\n",
    "\n",
    "**Source checkpoint:** Fruitful functions and void functions; addtwo example."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "497d08e2",
   "metadata": {
    "tags": [
     "student-answer",
     "C05"
    ]
   },
   "outputs": [],
   "source": [
    "%%answer C05\n",
    "def addtwo(a, b):\n",
    "    return None  # Replace None\n",
    "\n",
    "x = None  # Replace with your function call\n",
    "# Print x here"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1b8ce638",
   "metadata": {},
   "source": [
    "## C06 | Compose the math calls\n",
    "\n",
    "**Medium | 10 XP | Five checks worth 2 points each**\n",
    "\n",
    "Import `math`. Replace `None` values to reproduce the chapter's 45-degree example: compute `radians` with `degrees / 360.0 * 2 * math.pi`, compute `height` by calling `math.sin(radians)`, and compute `comparison` as `math.sqrt(2) / 2.0`. Print `height`, then `comparison`, on separate lines. Each setup/calculation/output check is worth 2 points. The formulas are supplied; no trigonometry knowledge is required.\n",
    "\n",
    "**Hint:** The result of a function call can be part of a larger expression.\n",
    "\n",
    "**Source checkpoint:** Math functions; composition example."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6b9b3f44",
   "metadata": {
    "tags": [
     "student-answer",
     "C06"
    ]
   },
   "outputs": [],
   "source": [
    "%%answer C06\n",
    "# Import the module here\n",
    "degrees = 45\n",
    "radians = None\n",
    "height = None\n",
    "comparison = None\n",
    "# Print the two results here"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b53a4555",
   "metadata": {},
   "source": [
    "## C07 | Overtime checkpoint\n",
    "\n",
    "**Medium | 10 XP | Five checks worth 2 points each**\n",
    "\n",
    "Fill only the three `None` placeholders. Return pay at the regular rate for up to 40 hours. For more than 40 hours, pay the first 40 at `rate` and the remaining hours at `1.5 * rate`. Call `computepay(45, 10)` and store the result in `pay`. The supplied print should show `475` or `475.0`. The branching structure is supplied because the chapter refers back to an earlier pay exercise. Checks cover the function, below/exactly/above 40 hours, and a second overtime rate plus the sample output.\n",
    "\n",
    "**Hint:** Overtime pay is 40 * rate plus (hours - 40) * rate * 1.5.\n",
    "\n",
    "**Source checkpoint:** Exercise 6."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e9cde397",
   "metadata": {
    "tags": [
     "student-answer",
     "C07"
    ]
   },
   "outputs": [],
   "source": [
    "%%answer C07\n",
    "def computepay(hours, rate):\n",
    "    # Supplied condition: complete only the return expressions.\n",
    "    if hours > 40:\n",
    "        return None\n",
    "    else:\n",
    "        return None\n",
    "\n",
    "pay = None  # Call your function with 45 and 10\n",
    "print(pay)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f367b754",
   "metadata": {},
   "source": [
    "## C08 | Grade gate\n",
    "\n",
    "**Medium | 10 XP | Five checks worth 2 points each**\n",
    "\n",
    "Replace the six empty strings inside `computegrade` with the correct return strings: `\"Bad score\"` for a score outside 0 through 1, `\"A\"` for at least 0.9, `\"B\"` for at least 0.8, `\"C\"` for at least 0.7, `\"D\"` for at least 0.6, and `\"F\"` below 0.6. Keep the supplied conditions and helper unchanged. `grade_text` handles converting text and invalid text such as `\"perfect\"`; writing exception handling is not part of this assignment. Checks cover the definition, upper boundaries, lower boundaries, invalid numeric scores, and the supplied helper/sample output.\n",
    "\n",
    "**Hint:** Return quoted strings. The conditions are already in order from highest threshold to lowest.\n",
    "\n",
    "**Source checkpoint:** Exercise 7."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f9167bdf",
   "metadata": {
    "tags": [
     "student-answer",
     "C08"
    ]
   },
   "outputs": [],
   "source": [
    "%%answer C08\n",
    "def computegrade(score):\n",
    "    # Supplied conditions: fill only the six return strings.\n",
    "    if score < 0 or score > 1:\n",
    "        return \"\"\n",
    "    if score >= 0.9:\n",
    "        return \"\"\n",
    "    elif score >= 0.8:\n",
    "        return \"\"\n",
    "    elif score >= 0.7:\n",
    "        return \"\"\n",
    "    elif score >= 0.6:\n",
    "        return \"\"\n",
    "    else:\n",
    "        return \"\"\n",
    "\n",
    "\n",
    "# PROVIDED SUPPORT: do not change; exception handling is not assessed.\n",
    "def grade_text(text):\n",
    "    try:\n",
    "        number = float(text)\n",
    "    except (ValueError, TypeError):\n",
    "        return \"Bad score\"\n",
    "    return computegrade(number)\n",
    "\n",
    "print(grade_text(\"0.95\"))\n",
    "print(grade_text(\"perfect\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "822fb120",
   "metadata": {},
   "source": [
    "## Reflection | No points\n",
    "\n",
    "After running C02 twice, describe what you noticed about the outputs. For C03, describe why moving the final call solved the error. Keep the triple quotes around your response."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0cb8544d",
   "metadata": {},
   "outputs": [],
   "source": [
    "reflection = \"\"\"\n",
    "Random-number observation:\n",
    "\n",
    "Function-order observation:\n",
    "\"\"\"\n",
    "save_notes(reflection)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d6330af9",
   "metadata": {},
   "source": [
    "## Finish line | Export your submission\n",
    "\n",
    "Run all edited answer cells before exporting. This cell exports even an incomplete attempt: missing answers earn zero. You may keep improving and export again. Each export creates a new folder so older submissions are preserved.\n",
    "\n",
    "The PDF includes your identity, score out of 100, badge, every question score, feedback, and a receipt. The companion notebook includes all assignment answers and captured output from their latest runs. The receipt matches the two files. Local scores are self-checks pending instructor review."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "525f5b1e",
   "metadata": {},
   "outputs": [],
   "source": [
    "show_progress()\n",
    "submission_files = finalize()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8ea18b12",
   "metadata": {},
   "source": [
    "## Attribution\n",
    "\n",
    "Source: Charles R. Severance, [Python for Everybody, Functions](https://www.py4e.com/html3/04-functions). The [book](https://www.py4e.com/book) is licensed [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). This adaptation adds original questions, scaffolding, scoring, and export support. It is not an official UNT or PY4E assessment.\n",
    "\n",
    "Assessment version: 1.0."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.11"
  },
  "zhuddle": {
   "points": 100,
   "source": "https://www.py4e.com/html3/04-functions",
   "version": "1.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
