{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "17d68bd7",
   "metadata": {},
   "source": [
    "# Introduction to AI · What are we actually talking about?\n",
    "\n",
    "The lesson claims that of the four ways people define AI, only one can be\n",
    "*scored*. This notebook is that claim, made checkable: four agents in one\n",
    "room, and then the same four under a different scorecard.\n",
    "\n",
    "The room is the vacuum world — the standard first agent in the field, because\n",
    "it has the three parts of the definition and nothing else: something it can\n",
    "**perceive**, something it can **do**, and a **performance measure** someone\n",
    "had to choose."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "a6400e14",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-18T11:03:04.513498Z",
     "iopub.status.busy": "2026-08-18T11:03:04.513048Z",
     "iopub.status.idle": "2026-08-18T11:03:04.863437Z",
     "shell.execute_reply": "2026-08-18T11:03:04.863171Z"
    }
   },
   "outputs": [],
   "source": [
    "# hide — plumbing so this file runs both in the repo and in Colab.\n",
    "try:\n",
    "    from _figkit import save_fig, record\n",
    "except ImportError:  # Colab — no repo, no problem\n",
    "    def save_fig(name, plot, **kw):\n",
    "        import matplotlib.pyplot as plt\n",
    "        fig, ax = plt.subplots(figsize=kw.get('figsize', (7, 4.2))); plot(ax); plt.show()\n",
    "    def record(key, value):\n",
    "        print(f'{key} = {value}'); return value"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "56e52e49",
   "metadata": {},
   "source": [
    "## 1. The room\n",
    "\n",
    "A grid with dirt in some squares. The agent starts in a corner. Each step it\n",
    "gets one percept — *where am I, and is this square dirty* — and takes one\n",
    "action: suck, or move.\n",
    "\n",
    "That is the whole environment, and it is deliberately tiny. Everything\n",
    "interesting here is about the **scorecard**, not the room."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "91f1b470",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-18T11:03:04.864847Z",
     "iopub.status.busy": "2026-08-18T11:03:04.864728Z",
     "iopub.status.idle": "2026-08-18T11:03:04.872335Z",
     "shell.execute_reply": "2026-08-18T11:03:04.872079Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "room_size = 5x5\n",
      "steps = 60\n",
      "trials = 400\n",
      "avg_dirt = 9.5\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "9.5"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "#| caption: The room, the percept and the two actions\n",
    "import random\n",
    "\n",
    "SIZE, STEPS, TRIALS = 5, 60, 400\n",
    "MOVES = {'N': (-1, 0), 'S': (1, 0), 'E': (0, 1), 'W': (0, -1)}\n",
    "\n",
    "def new_room(seed):\n",
    "    rng = random.Random(seed)\n",
    "    dirt = {(r, c) for r in range(SIZE) for c in range(SIZE) if rng.random() < 0.4}\n",
    "    dirt.discard((0, 0))\n",
    "    return dirt, rng\n",
    "\n",
    "def percept(pos, dirt):\n",
    "    \"\"\"Everything the agent is allowed to know: where it is, and whether\n",
    "    THIS square is dirty. It cannot see the rest of the room.\"\"\"\n",
    "    return pos, pos in dirt\n",
    "\n",
    "record('room_size', f'{SIZE}x{SIZE}')\n",
    "record('steps', STEPS)\n",
    "record('trials', TRIALS)\n",
    "record('avg_dirt', round(sum(len(new_room(s)[0]) for s in range(TRIALS)) / TRIALS, 1))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3572e023",
   "metadata": {},
   "source": [
    "## 2. Four agents\n",
    "\n",
    "Same room, same percept, same actions. They differ only in what they do with\n",
    "what they see.\n",
    "\n",
    "The last one is the interesting one: it is written to look *human* — it\n",
    "second-guesses itself, goes back to check squares it has already done, and\n",
    "sometimes just pauses. Nothing about it is stupid. It is simply not optimising\n",
    "anything."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "c82c9fc8",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-18T11:03:04.873529Z",
     "iopub.status.busy": "2026-08-18T11:03:04.873457Z",
     "iopub.status.idle": "2026-08-18T11:03:04.877111Z",
     "shell.execute_reply": "2026-08-18T11:03:04.876868Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "n_agents = 4\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "4"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "#| caption: Four agents — the only difference is what each does with the percept\n",
    "def random_agent(pos, dirty, memory, rng):\n",
    "    \"\"\"Ignores the percept completely. It is an agent — it acts — but it does\n",
    "    not perceive, so it sucks at empty squares and drives past dirty ones.\"\"\"\n",
    "    return rng.choice(['suck'] + list(MOVES))\n",
    "\n",
    "def reflex_agent(pos, dirty, memory, rng):\n",
    "    \"\"\"The whole difference from Random is one `if`: it LOOKS at the square it\n",
    "    is standing on. No memory, though — it cannot tell a square it has already\n",
    "    done from one it has not.\"\"\"\n",
    "    if dirty: return 'suck'\n",
    "    return rng.choice(list(MOVES))\n",
    "\n",
    "def systematic_agent(pos, dirty, memory, rng):\n",
    "    \"\"\"Model-based: it remembers where it has been, and walks the room in\n",
    "    boustrophedon order — along a row, down, back along the next.\"\"\"\n",
    "    if dirty: return 'suck'\n",
    "    r, c = pos\n",
    "    memory.add(pos)\n",
    "    going_east = (r % 2 == 0)\n",
    "    ahead = (r, c + 1) if going_east else (r, c - 1)\n",
    "    if 0 <= ahead[1] < SIZE: return 'E' if going_east else 'W'\n",
    "    return 'S' if r + 1 < SIZE else ('W' if c > 0 else 'N')\n",
    "\n",
    "def humanish_agent(pos, dirty, memory, rng):\n",
    "    \"\"\"Written to look like a person doing the job: doubles back to check its\n",
    "    own work, and now and then does nothing at all.\"\"\"\n",
    "    if dirty: return 'suck'\n",
    "    if rng.random() < 0.25: return 'wait'          # a pause\n",
    "    if memory and rng.random() < 0.35:             # go back and check\n",
    "        return rng.choice(list(MOVES))\n",
    "    memory.add(pos)\n",
    "    return rng.choice(list(MOVES))\n",
    "\n",
    "AGENTS = [\n",
    "    ('Random', random_agent),\n",
    "    ('Reflex', reflex_agent),\n",
    "    ('Remembers', systematic_agent),\n",
    "    ('Human-like', humanish_agent),\n",
    "]\n",
    "record('n_agents', len(AGENTS))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c97a52ad",
   "metadata": {},
   "source": [
    "## 3. The scorecard\n",
    "\n",
    "Two of them, and this is the whole point of the notebook.\n",
    "\n",
    "* **Squares cleaned** — the obvious measure. How much dirt is gone.\n",
    "* **Cleaned, minus the electricity** — every move costs something. A machine\n",
    "  that cleans one more square by driving across the room twice has not\n",
    "  necessarily done better.\n",
    "\n",
    "Nothing about any agent changes between the two. Only the definition of\n",
    "*good* does."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "48e318f6",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-18T11:03:04.878313Z",
     "iopub.status.busy": "2026-08-18T11:03:04.878169Z",
     "iopub.status.idle": "2026-08-18T11:03:04.929276Z",
     "shell.execute_reply": "2026-08-18T11:03:04.928765Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Random       cleaned   2.4   moves  48.0   net  -26.3\n",
      "Reflex       cleaned   6.1   moves  53.9   net  -26.2\n",
      "Remembers    cleaned   9.5   moves  50.5   net  -20.7\n",
      "Human-like   cleaned   5.1   moves  41.2   net  -19.6\n",
      "move_cost = 0.6\n",
      "random_cleaned = 2.4\n",
      "random_moves = 48.0\n",
      "random_net = -26.3\n",
      "reflex_cleaned = 6.1\n",
      "reflex_moves = 53.9\n",
      "reflex_net = -26.2\n",
      "remembers_cleaned = 9.5\n",
      "remembers_moves = 50.5\n",
      "remembers_net = -20.7\n",
      "human_like_cleaned = 5.1\n",
      "human_like_moves = 41.2\n",
      "human_like_net = -19.6\n"
     ]
    }
   ],
   "source": [
    "#| caption: One run, and the two scores it produces\n",
    "MOVE_COST = 0.6\n",
    "\n",
    "def run(agent, seed):\n",
    "    dirt, rng = new_room(seed)\n",
    "    started = len(dirt)\n",
    "    pos, memory, moves = (0, 0), set(), 0\n",
    "    for _ in range(STEPS):\n",
    "        where, dirty = percept(pos, dirt)\n",
    "        action = agent(where, dirty, memory, rng)\n",
    "        if action == 'suck':\n",
    "            dirt.discard(pos)\n",
    "        elif action in MOVES:\n",
    "            dr, dc = MOVES[action]\n",
    "            nxt = (pos[0] + dr, pos[1] + dc)\n",
    "            if 0 <= nxt[0] < SIZE and 0 <= nxt[1] < SIZE:\n",
    "                pos = nxt\n",
    "            moves += 1\n",
    "    cleaned = started - len(dirt)\n",
    "    return cleaned, moves\n",
    "\n",
    "scores = {}\n",
    "for name, agent in AGENTS:\n",
    "    runs = [run(agent, s) for s in range(TRIALS)]\n",
    "    cleaned = sum(c for c, _ in runs) / TRIALS\n",
    "    moves = sum(m for _, m in runs) / TRIALS\n",
    "    scores[name] = {\n",
    "        'cleaned': round(cleaned, 1),\n",
    "        'moves': round(moves, 1),\n",
    "        'net': round(cleaned - MOVE_COST * moves, 1),\n",
    "    }\n",
    "\n",
    "for name, s in scores.items():\n",
    "    print(f\"{name:12} cleaned {s['cleaned']:5}   moves {s['moves']:5}   net {s['net']:6}\")\n",
    "\n",
    "record('move_cost', MOVE_COST)\n",
    "for name, s in scores.items():\n",
    "    key = name.lower().replace('-', '_')\n",
    "    record(f'{key}_cleaned', s['cleaned'])\n",
    "    record(f'{key}_moves', s['moves'])\n",
    "    record(f'{key}_net', s['net'])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a4986316",
   "metadata": {},
   "source": [
    "## 4. Who won?\n",
    "\n",
    "Ask the question the way most people ask it — *which one cleaned the most* —\n",
    "and there is a clear answer."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "f54709ea",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-18T11:03:04.930887Z",
     "iopub.status.busy": "2026-08-18T11:03:04.930774Z",
     "iopub.status.idle": "2026-08-18T11:03:05.032740Z",
     "shell.execute_reply": "2026-08-18T11:03:05.032412Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "winner_cleaned = Remembers\n",
      "winner_net = Human-like\n",
      "scorecard_changes_winner = yes\n",
      "figure who-cleaned-most -> what-are-we-actually-talking-about.who-cleaned-most.{light,dark}.svg\n"
     ]
    }
   ],
   "source": [
    "#| caption: Squares cleaned in sixty steps\n",
    "names = [n for n, _ in AGENTS]\n",
    "cleaned = [scores[n]['cleaned'] for n in names]\n",
    "net = [scores[n]['net'] for n in names]\n",
    "\n",
    "winner_clean = max(names, key=lambda n: scores[n]['cleaned'])\n",
    "winner_net = max(names, key=lambda n: scores[n]['net'])\n",
    "record('winner_cleaned', winner_clean)\n",
    "record('winner_net', winner_net)\n",
    "record('scorecard_changes_winner', 'yes' if winner_clean != winner_net else 'no')\n",
    "\n",
    "def plot_cleaned(ax):\n",
    "    bars = ax.bar(names, cleaned, color=['#9aa1ab', '#9aa1ab', '#ee785b', '#9aa1ab'])\n",
    "    for b, v in zip(bars, cleaned):\n",
    "        ax.text(b.get_x() + b.get_width() / 2, v + 0.15, f'{v}', ha='center', fontsize=11)\n",
    "    ax.set_ylabel('squares cleaned')\n",
    "    ax.set_ylim(0, max(cleaned) * 1.25)\n",
    "    ax.spines[['top', 'right']].set_visible(False)\n",
    "\n",
    "save_fig('who-cleaned-most', plot_cleaned, figsize=(7, 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0c6dd40b",
   "metadata": {},
   "source": [
    "## 5. Now change the scorecard\n",
    "\n",
    "Same agents. Same rooms. Same runs — these numbers come from the identical\n",
    "trials as the chart above. The only thing that changes is what we decided to\n",
    "count."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "6d1d4161",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-18T11:03:05.033956Z",
     "iopub.status.busy": "2026-08-18T11:03:05.033865Z",
     "iopub.status.idle": "2026-08-18T11:03:05.104391Z",
     "shell.execute_reply": "2026-08-18T11:03:05.104071Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "figure two-scorecards -> what-are-we-actually-talking-about.two-scorecards.{light,dark}.svg\n",
      "best at cleaning: Remembers\n",
      "best once movement costs something: Human-like\n"
     ]
    }
   ],
   "source": [
    "#| caption: The same four, under two different definitions of \"good\"\n",
    "def plot_two_scorecards(ax):\n",
    "    x = range(len(names))\n",
    "    w = 0.38\n",
    "    ax.bar([i - w / 2 for i in x], cleaned, w, label='squares cleaned', color='#9aa1ab')\n",
    "    ax.bar([i + w / 2 for i in x], net, w, label='cleaned minus electricity', color='#ee785b')\n",
    "    ax.axhline(0, color='#6b7280', linewidth=0.8)\n",
    "    for i, (c, n) in enumerate(zip(cleaned, net)):\n",
    "        ax.text(i - w / 2, c + 0.2, f'{c}', ha='center', fontsize=10)\n",
    "        ax.text(i + w / 2, n + (0.2 if n >= 0 else -0.9), f'{n}', ha='center', fontsize=10)\n",
    "    ax.set_xticks(list(x)); ax.set_xticklabels(names)\n",
    "    ax.set_ylabel('score')\n",
    "    ax.legend(frameon=False, fontsize=10)\n",
    "    ax.spines[['top', 'right']].set_visible(False)\n",
    "\n",
    "save_fig('two-scorecards', plot_two_scorecards, figsize=(7.6, 4.2))\n",
    "print('best at cleaning:', winner_clean)\n",
    "print('best once movement costs something:', winner_net)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cdc02849",
   "metadata": {},
   "source": [
    "## 6. And the three definitions that produce no number\n",
    "\n",
    "Every number above answers one question: *did it get the result it was after?*\n",
    "That is the bottom-right cell of the lesson's table — acting rationally — and\n",
    "it is the only one this notebook can compute.\n",
    "\n",
    "There is no cell here for *did it think like a person*, and its absence is not\n",
    "an omission. Try to write it: you would need a way to check what happened\n",
    "inside the agent against what happens inside a human being, and neither side\n",
    "of that comparison is available.\n",
    "\n",
    "Which is why the field settled where it did — not because the other three\n",
    "questions are uninteresting, but because they do not produce a column."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "596acfb9",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-18T11:03:05.105936Z",
     "iopub.status.busy": "2026-08-18T11:03:05.105807Z",
     "iopub.status.idle": "2026-08-18T11:03:05.141918Z",
     "shell.execute_reply": "2026-08-18T11:03:05.141707Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "definitions = 4\n",
      "definitions_scored = 1\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "figure what-can-be-scored -> what-are-we-actually-talking-about.what-can-be-scored.{light,dark}.svg\n"
     ]
    }
   ],
   "source": [
    "#| caption: How many of the four definitions produced a number here\n",
    "definitions = ['Thinking\\nlike a human', 'Thinking\\nrationally',\n",
    "               'Acting\\nlike a human', 'Acting\\nrationally']\n",
    "scoreable = [0, 0, 0, 1]\n",
    "record('definitions', len(definitions))\n",
    "record('definitions_scored', sum(scoreable))\n",
    "\n",
    "def plot_scoreable(ax):\n",
    "    ax.bar(definitions, [1, 1, 1, 1], color='#eceef1')\n",
    "    ax.bar(definitions, scoreable, color='#ee785b')\n",
    "    for i, s in enumerate(scoreable):\n",
    "        ax.text(i, 0.5, 'a number' if s else 'no test', ha='center', va='center',\n",
    "                fontsize=10, color='#ffffff' if s else '#6b7280')\n",
    "    ax.set_yticks([])\n",
    "    ax.spines[['top', 'right', 'left']].set_visible(False)\n",
    "\n",
    "save_fig('what-can-be-scored', plot_scoreable, figsize=(7.6, 3.4))"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
