From cc9c671d471a6016caf1a78e112757e70390b237 Mon Sep 17 00:00:00 2001 From: Peter Norvig Date: Sun, 14 Feb 2021 11:48:22 -0800 Subject: [PATCH] Add files via upload --- ipynb/Advent-2020.ipynb | 1658 ++++++++++++++++++++++++-------------- ipynb/CrossProduct.ipynb | 407 ++++++++++ ipynb/Life.ipynb | 733 ++++++++--------- 3 files changed, 1832 insertions(+), 966 deletions(-) create mode 100644 ipynb/CrossProduct.ipynb diff --git a/ipynb/Advent-2020.ipynb b/ipynb/Advent-2020.ipynb index 50abe91..30f0766 100644 --- a/ipynb/Advent-2020.ipynb +++ b/ipynb/Advent-2020.ipynb @@ -8,13 +8,13 @@ "\n", "# Advent of Code 2020\n", "\n", - "This year I return to [Advent of Code](https://adventofcode.com), as I did in [2016](Advent+of+Code), [17](Advent+2017), and [18](Advent-2018.ipynb). Thank you, [**Eric Wastl**](http://was.tl/)! You'll have to look at the [Advent of Code website](https://adventofcode.com/2020) if you want the full description of each puzzle; this notebook gives only a brief summary. \n", + "This year I return to [Advent of Code](https://adventofcode.com), as I did in [2016](Advent+of+Code), [17](Advent+2017), and [18](Advent-2018.ipynb). Thank you, [Eric Wastl](http://was.tl/)! This notebook describes each day's puzzle only briefly; you'll have to look at the [Advent of Code website](https://adventofcode.com/2020) if you want the full details. Each puzzle has a part 1 and a part 2.\n", "\n", - "For each day from 1 to 25, I'll write **four pieces of code**, each in a separate notebook cell. For example, on day 3:\n", - "- `in3: List[str] = data(3)`: the day's input data, parsed into an appropriate form (here, a list of string lines). Most days the data comes from a file, read via the function `data` (which has optional arguments to say how to split the data into sections/records and how to parse each one), but some days the data is so small I just copy and paste it.\n", - "- `def day3_1(nums): ... `: a function that takes the day's data as input and returns the answer for Part 1.\n", - "- `def day3_2(nums): ... `: a function that takes the day's data as input and returns the answer for Part 2.\n", - "- `do(3)`: executes the call `day3_1(in3)`. I'll then use the result to hopefully unlock part 2 and define `day3_2`, which also gets run when I call `do(3)` again. Once I verify both answers, I'll change `do(3)` to `do(3, 167, 736527114)` to serve as a unit test.\n", + "For each day from 1 to 25, I'll write **four pieces of code** with the following format (and perhaps some auxiliary code). For example, on day 3:\n", + "- `in3: List[str] = data(3)`: the day's input data, parsed into an appropriate form (here, a list of string lines). Some days the data is so small I just copy and paste it. But most days the data comes from a file, read via the function `data(day, parser, sep)`, which breaks the file into sections/records separated by `sep` (newline by default), and applies a `parser` to each section (default is to leave the section as a `str`).\n", + "- `def day3_1(nums): ... `: a function that takes the day's data as input and returns the answer for part 1.\n", + "- `def day3_2(nums): ... `: a function that takes the day's data as input and returns the answer for part 2.\n", + "- `do(3)`: runs `day3_1(in3)`. I'll then use the result to hopefully unlock part 2 and define `day3_2`, which also gets run when I call `do(3)` again. Once I verify both answers, I'll change `do(3)` to `do(3, 167, 736527114)` to serve as a unit test.\n", "\n", "# Day 0: Imports and Utility Functions\n", "\n", @@ -33,9 +33,8 @@ "from __future__ import annotations\n", "from collections import Counter, defaultdict, namedtuple, deque\n", "from itertools import permutations, combinations, product, chain\n", - "from functools import lru_cache, reduce\n", - "from typing import Dict, Tuple, Set, List, Iterator, Optional, Union, Sequence\n", - "from contextlib import contextmanager\n", + "from functools import lru_cache\n", + "from typing import Dict, Tuple, Set, List, Iterator, Optional, Union\n", "\n", "import operator\n", "import math\n", @@ -52,12 +51,11 @@ "source": [ "def data(day: int, parser=str, sep='\\n') -> list:\n", " \"Split the day's input file into sections separated by `sep`, and apply `parser` to each.\"\n", - " with open(f'data/advent2020/input{day}.txt') as f:\n", - " sections = f.read().rstrip().split(sep)\n", - " return list(map(parser, sections))\n", + " sections = open(f'data/advent2020/input{day}.txt').read().rstrip().split(sep)\n", + " return [parser(section) for section in sections]\n", " \n", - "def do(day, *answers) -> List[int]:\n", - " \"E.g., do(3) returns [day3_1(in3), day3_2(in3)]. Verifies `answers` if given.\"\n", + "def do(day, *answers) -> Dict[int, int]:\n", + " \"E.g., do(3) returns {1: day3_1(in3), 2: day3_2(in3)}. Verifies `answers` if given.\"\n", " g = globals()\n", " got = []\n", " for part in (1, 2):\n", @@ -67,8 +65,6 @@ " if len(answers) >= part: \n", " assert got[-1] == answers[part - 1], (\n", " f'{fname}(in{day}) got {got[-1]}; expected {answers[part - 1]}')\n", - " else:\n", - " got.append(None)\n", " return got" ] }, @@ -78,13 +74,6 @@ "metadata": {}, "outputs": [], "source": [ - "Number = Union[float, int]\n", - "Atom = Union[Number, str]\n", - "Char = str # Type used to indicate a single character\n", - "\n", - "cat = ''.join\n", - "flatten = chain.from_iterable\n", - "\n", "def quantify(iterable, pred=bool) -> int:\n", " \"Count the number of items in iterable for which pred is true.\"\n", " return sum(1 for item in iterable if pred(item))\n", @@ -93,48 +82,49 @@ " \"Return first item in iterable, or default.\"\n", " return next(iter(iterable), default)\n", "\n", - "def prod(numbers) -> Number:\n", - " \"The product of an iterable of numbers.\" \n", - " return reduce(operator.mul, numbers, 1)\n", + "def rest(sequence) -> object: return sequence[1:]\n", "\n", - "def dot(A, B) -> Number: \n", - " \"The dot product of two vectors of numbers.\"\n", - " return sum(a * b for a, b in zip(A, B))\n", + "def multimap(items: Iterable[Tuple]) -> dict:\n", + " \"Given (key, val) pairs, return {key: [val, ....], ...}.\"\n", + " result = defaultdict(list)\n", + " for (key, val) in items:\n", + " result[key].append(val)\n", + " return result\n", + "\n", + "def prod(numbers) -> float: # Will be math.prod in Python 3.8, but I'm in 3.7\n", + " \"The product of an iterable of numbers.\" \n", + " result = 1\n", + " for n in numbers:\n", + " result *= n\n", + " return result\n", "\n", "def ints(text: str) -> Tuple[int]:\n", " \"Return a tuple of all the integers in text.\"\n", - " return mapt(int, re.findall('-?[0-9]+', text))\n", - "\n", - "def lines(text: str) -> List[str]:\n", - " \"Split the text into a list of lines.\"\n", - " return text.strip().splitlines()\n", - "\n", - "def mapt(fn, *args): \n", - " \"Do map(fn, *args) and make the result a tuple.\"\n", - " return tuple(map(fn, *args))\n", + " return tuple(map(int, re.findall('-?[0-9]+', text)))\n", "\n", "def atoms(text: str, ignore=r'', sep=None) -> Tuple[Union[int, str]]:\n", - " \"Parse text into atoms separated by sep, with regex ignored.\"\n", - " text = re.sub(ignore, '', text)\n", - " return mapt(atom, text.split(sep))\n", + " \"Parse text into atoms (numbers or strs), possibly ignoring a regex.\"\n", + " if ignore:\n", + " text = re.sub(ignore, '', text)\n", + " return tuple(map(atom, text.split(sep)))\n", "\n", - "def atom(text: str, types=(int, str)):\n", - " \"Parse text into one of the given types.\"\n", - " for typ in types:\n", - " try:\n", - " return typ(text)\n", - " except ValueError:\n", - " pass\n", - "\n", - "@contextmanager\n", - "def binding(**kwds):\n", - " \"Bind global variables within a context; revert to old values on exit.\"\n", - " old_values = {k: globals()[k] for k in kwds}\n", + "def atom(text: str) -> Union[float, int, str]:\n", + " \"Parse text into a single float or int or str.\"\n", " try:\n", - " globals().update(kwds)\n", - " yield # Stuff within the context gets run here.\n", - " finally:\n", - " globals().update(old_values)" + " val = float(text)\n", + " return round(val) if round(val) == val else val\n", + " except ValueError:\n", + " return text\n", + " \n", + "def dotproduct(A, B) -> float: return sum(a * b for a, b in zip(A, B))\n", + "\n", + "def mapt(fn, *args):\n", + " \"map(fn, *args) and return the result as a tuple.\"\n", + " return tuple(map(fn, *args))\n", + "\n", + "cat = ''.join\n", + "flatten = chain.from_iterable\n", + "Char = str # Type used to indicate a single character" ] }, { @@ -142,12 +132,12 @@ "metadata": {}, "source": [ "Notes:\n", - "- Since I'm not even attempting to compete to be among the first 100 people to find a solution, I'll take the time to write docstrings; to use reasonable variable names (not single-letter names); and to give type annotations for most functions (but not the `day` functions, which all return `int`, except `day21_2`).\n", + "- Since I'm not even attempting to compete for speed, I'll take the time to use reasonable variable names (not single-letter names), and to give type annotations for most of the functions I define (but not the `day` functions, which all return `int`).\n", "- Traditionally, a lot of AoC problems are solved by one of the following two forms:\n", " - `quantify(inputs, P)`: How many of your input items have property P?\n", " - `sum(map(F, inputs))`: What is the sum of the result of applying F to each input item?\n", - "- Some days I will re-use code that was defined on a previous day.\n", - "- I will give a few tests using `assert`, but far fewer test cases than if I was programming seriously." + "- I will feel free to re-use a definition that I define one day in a subsequent day's puzzle.\n", + "- I will define a few test cases with `assert`, but far fewer test cases than if I was programming seriously." ] }, { @@ -377,6 +367,8 @@ " ecl:gry pid:860033327 eyr:2020 hcl:#fffffd\n", " byr:1937 iyr:2017 cid:147 hgt:183cm\n", "\n", + " iyr:2013 ecl:amb cid:350 eyr:2023 pid:028048884 hcl:#cfa07d byr:1929\n", + "\n", " hcl:#ae17e1 iyr:2013\n", " eyr:2024 ecl:brn pid:760753108 byr:1931 hgt:179cm\n", " \n", @@ -409,8 +401,9 @@ "outputs": [], "source": [ "required_fields = {'byr', 'ecl', 'eyr', 'hcl', 'hgt', 'iyr', 'pid'}\n", + "valid_passport = required_fields.issubset\n", "\n", - "def day4_1(passports): return quantify(passports, required_fields.issubset) " + "def day4_1(passports): return quantify(passports, valid_passport) " ] }, { @@ -433,8 +426,9 @@ " ecl (Eye Color) - exactly one of: amb blu brn gry grn hzl oth.\n", " pid (Passport ID) - a nine-digit number, including leading zeroes.\n", " cid (Country ID) - ignored, missing or not.'''\n", - " return all(field in passport and field_validator[field](passport[field])\n", - " for field in required_fields)\n", + " return (valid_passport(passport)\n", + " and all(field_validator[field](passport[field])\n", + " for field in required_fields))\n", "\n", "field_validator = dict(\n", " byr=lambda v: 1920 <= int(v) <= 2002,\n", @@ -547,6 +541,10 @@ "\n", " abc\n", "\n", + " a\n", + " b\n", + " c\n", + "\n", " ab\n", " ac\n", " \n", @@ -560,9 +558,9 @@ "metadata": {}, "outputs": [], "source": [ - "Group = List[str]\n", + "in6: List[List[str]] = data(6, str.splitlines, sep='\\n\\n')\n", "\n", - "in6: List[Group] = data(6, lines, sep='\\n\\n')" + "assert in6[1] == ['arke', 'qzr', 'plmgnr', 'uriq'] # A group is a list of strs" ] }, { @@ -583,7 +581,7 @@ "metadata": {}, "outputs": [], "source": [ - "def day6_2(groups): \n", + "def day6_2(groups: List[List[str]]): \n", " \"For each group, compute the number of letters that EVERYONE got. Sum them.\"\n", " return sum(len(set.intersection(*map(set, group)))\n", " for group in groups)" @@ -619,6 +617,7 @@ "\n", " light red bags contain 1 bright white bag, 2 muted yellow bags.\n", " dark orange bags contain 3 bright white bags, 4 muted yellow bags.\n", + " bright white bags contain 1 shiny gold bag.\n", " \n", "1. How many bag colors must eventually contain at least one shiny gold bag?\n", "2. How many individual bags must be inside your single shiny gold bag?\n", @@ -677,12 +676,13 @@ "metadata": {}, "outputs": [], "source": [ - "def num_contained_in(rules, target='shiny gold') -> int:\n", - " \"How many bags are contained (recursively) in the target bag?\"\n", - " return sum(n + n * num_contained_in(rules, inner) \n", - " for (inner, n) in rules[target].items() if n > 0)\n", + "def day7_2(rules, target='shiny gold'): \n", + " return num_contained_in(target, rules)\n", "\n", - "day7_2 = num_contained_in" + "def num_contained_in(target, rules) -> int:\n", + " \"How many bags are contained (recursively) in the target bag?\"\n", + " return sum(n + n * num_contained_in(bag, rules) \n", + " for (bag, n) in rules[target].items() if n > 0)" ] }, { @@ -755,7 +755,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "I had to think about what to do for Part 2. Do I need to make a flow graph of where the loops are? That sounds hard. But I soon realized that I can just use brute force—try every alteration of an instruction (there are only 638 instructions and at most one way to alter each instruction), and run each altered program to see if it terminates (that too takes no more than 638 steps each)." + "I had to think about what to do for Part 2. Do I need to make a flow graph of where the loops are? That sounds hard. But I soon realized that I can just use brute force—try every alteration of an instruction (there are only $O(n)$ of them), and run each altered program to see if it terminates (that too takes only $O(n)$ time)." ] }, { @@ -946,7 +946,6 @@ "\n", "@lru_cache(None)\n", "def arrangements(jolts, prev) -> int:\n", - " \"The number of arrangements that go from prev to the end of `jolts`.\"\n", " first, rest = jolts[0], jolts[1:]\n", " if first - prev > 3:\n", " return 0\n", @@ -1000,7 +999,7 @@ "1. Simulate your seating area by applying the seating rules repeatedly until no seats change state. How many seats end up occupied?\n", "2. Same problem, but with two rule changes:\n", " - When considering adjacency, if there is a *floor* cell in some direction, skip over that to the next visible seat in that direction. \n", - " - A seat becomes empty only when there are 5 occupied neighbors, not 4. " + " - Empty a seat only when there are 5 occupied neighbors, not 4. " ] }, { @@ -1017,6 +1016,83 @@ "execution_count": 45, "metadata": {}, "outputs": [], + "source": [ + "Seats = List[str]\n", + "\n", + "floor, empty, occupied, off = \".L#?\"\n", + "\n", + "Contents = Char # The contents of each location is one of the above 4 characters\n", + "\n", + "crowded = 4\n", + " \n", + "deltas = ((-1, -1), (0, -1), (1, -1),\n", + " (-1, 0), (1, 0),\n", + " (-1, +1), (0, +1), (1, +1))\n", + " \n", + "def next_generation(seats) -> Seats:\n", + " \"The next generation, according to the rules.\"\n", + " return [cat(self.next_generation_at(x, y) for x in range(len(self[y])))\n", + " for y in range(len(self))]\n", + "\n", + "def next_generation_at(seats, x, y) -> Contents:\n", + " \"The contents of location (x, y) in the next generation.\"\n", + " old = seats[y][x]\n", + " N = self.neighbors(x, y).count(occupied)\n", + " return (occupied if old is empty and N == 0 else\n", + " empty if old is occupied and N >= crowded else\n", + " old)\n", + "\n", + "def neighbors(seats, x, y) -> List[Contents]: \n", + " \"The contents of the 8 neighboring locations.\"\n", + " return [seats.at(x + dx, y + dy) for dx, dy in self.deltas]\n", + "\n", + " def count(self, kind: Contents) -> int: return cat(self).count(kind)\n", + " \n", + " def at(self, x, y) -> Contents:\n", + " \"The contents of location (x, y): empty, occupied, floor, or off?\"\n", + " if 0 <= y < len(self) and 0 <= x < len(self[y]):\n", + " return self[y][x]\n", + " else:\n", + " return off\n", + " \n", + " def run(self) -> Layout:\n", + " \"Run until equilibrium.\"\n", + " new = self\n", + " while True:\n", + " new, old = new.next_generation(), new\n", + " if new == old:\n", + " return new\n", + "\n", + "def day11_1(seats): return Layout(seats).run().count(occupied)" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'Layout' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n", + "\u001b[0;32m\u001b[0m in \u001b[0;36mday11_1\u001b[0;34m(seats)\u001b[0m\n\u001b[1;32m 45\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mnew\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 46\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 47\u001b[0;31m \u001b[0;32mdef\u001b[0m \u001b[0mday11_1\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mseats\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mLayout\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mseats\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrun\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcount\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0moccupied\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mNameError\u001b[0m: name 'Layout' is not defined" + ] + } + ], + "source": [ + "%time day11_1(in11)" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [], "source": [ "floor, empty, occupied, off = \".L#?\"\n", "\n", @@ -1071,7 +1147,7 @@ }, { "cell_type": "code", - "execution_count": 46, + "execution_count": 48, "metadata": {}, "outputs": [], "source": [ @@ -1098,22 +1174,33 @@ }, { "cell_type": "code", - "execution_count": 47, + "execution_count": 49, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "[2299, 2047]" - ] - }, - "execution_count": 47, - "metadata": {}, - "output_type": "execute_result" + "ename": "KeyboardInterrupt", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n", + "\u001b[0;32m\u001b[0m in \u001b[0;36mdo\u001b[0;34m(day, *answers)\u001b[0m\n\u001b[1;32m 11\u001b[0m \u001b[0mfname\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34mf'day{day}_{part}'\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 12\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mfname\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mg\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 13\u001b[0;31m \u001b[0mgot\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mg\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mfname\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mg\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34mf'in{day}'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 14\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0manswers\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m>=\u001b[0m \u001b[0mpart\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 15\u001b[0m assert got[-1] == answers[part - 1], (\n", + "\u001b[0;32m\u001b[0m in \u001b[0;36mday11_2\u001b[0;34m(seats)\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0;32mdef\u001b[0m \u001b[0mday11_2\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mseats\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mLayout2\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mseats\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrun\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcount\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0moccupied\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0;32mclass\u001b[0m \u001b[0mLayout2\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mLayout\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0;34m\"A layout of seats (occupied or not) and floor space, with new rules.\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m\u001b[0m in \u001b[0;36mrun\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 43\u001b[0m \u001b[0mnew\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 44\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 45\u001b[0;31m \u001b[0mnew\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mold\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnew\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnext_generation\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnew\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 46\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mnew\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mold\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 47\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mnew\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m\u001b[0m in \u001b[0;36mnext_generation\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 16\u001b[0m seats = (cat(self.next_generation_at(x, y) for x in range(len(self[y])))\n\u001b[1;32m 17\u001b[0m for y in range(len(self)))\n\u001b[0;32m---> 18\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mtype\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mseats\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 19\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 20\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mnext_generation_at\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0my\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m->\u001b[0m \u001b[0mContents\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m(.0)\u001b[0m\n\u001b[1;32m 15\u001b[0m \u001b[0;34m\"The next generation, according to the rules.\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 16\u001b[0m seats = (cat(self.next_generation_at(x, y) for x in range(len(self[y])))\n\u001b[0;32m---> 17\u001b[0;31m for y in range(len(self)))\n\u001b[0m\u001b[1;32m 18\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mtype\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mseats\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 19\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m(.0)\u001b[0m\n\u001b[1;32m 14\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mnext_generation\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m->\u001b[0m \u001b[0mLayout\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 15\u001b[0m \u001b[0;34m\"The next generation, according to the rules.\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 16\u001b[0;31m seats = (cat(self.next_generation_at(x, y) for x in range(len(self[y])))\n\u001b[0m\u001b[1;32m 17\u001b[0m for y in range(len(self)))\n\u001b[1;32m 18\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mtype\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mseats\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m\u001b[0m in \u001b[0;36mnext_generation_at\u001b[0;34m(self, x, y)\u001b[0m\n\u001b[1;32m 21\u001b[0m \u001b[0;34m\"The contents of location (x, y) in the next generation.\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 22\u001b[0m \u001b[0mold\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0my\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 23\u001b[0;31m \u001b[0mN\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mneighbors\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0my\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcount\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0moccupied\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 24\u001b[0m return (occupied if old is empty and N == 0 else\n\u001b[1;32m 25\u001b[0m \u001b[0mempty\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mold\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0moccupied\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0mN\u001b[0m \u001b[0;34m>=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcrowded\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m\u001b[0m in \u001b[0;36mneighbors\u001b[0;34m(self, x, y)\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mneighbors\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0my\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m->\u001b[0m \u001b[0mList\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mContents\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 9\u001b[0m \u001b[0;34m\"The contents of the nearest visible seat in each of the 8 directions.\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 10\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvisible\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0my\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdy\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mdx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdy\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdeltas\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 11\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 12\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mvisible\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0my\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdy\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m->\u001b[0m \u001b[0mContents\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m(.0)\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mneighbors\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0my\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m->\u001b[0m \u001b[0mList\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mContents\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 9\u001b[0m \u001b[0;34m\"The contents of the nearest visible seat in each of the 8 directions.\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 10\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvisible\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0my\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdy\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mdx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdy\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdeltas\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 11\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 12\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mvisible\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0my\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdy\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m->\u001b[0m \u001b[0mContents\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m\u001b[0m in \u001b[0;36mvisible\u001b[0;34m(self, x, dx, y, dy)\u001b[0m\n\u001b[1;32m 12\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mvisible\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0my\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdy\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m->\u001b[0m \u001b[0mContents\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 13\u001b[0m \u001b[0;34m\"The contents of the first visible seat in direction (dx, dy).\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 14\u001b[0;31m \u001b[0;32mfor\u001b[0m \u001b[0mi\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msys\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmaxsize\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 15\u001b[0m \u001b[0mx\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0mdx\u001b[0m\u001b[0;34m;\u001b[0m \u001b[0my\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0mdy\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 16\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m0\u001b[0m \u001b[0;34m<=\u001b[0m \u001b[0my\u001b[0m \u001b[0;34m<\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0;36m0\u001b[0m \u001b[0;34m<=\u001b[0m \u001b[0mx\u001b[0m \u001b[0;34m<\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0my\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mKeyboardInterrupt\u001b[0m: " + ] } ], "source": [ - "do(11, 2299, 2047)" + "%time do(11, 2299, 2047)" ] }, { @@ -1121,7 +1208,7 @@ "metadata": {}, "source": [ "I have to confess that I \"cheated\" here: after seeing the problem description for Part 2, I went back and refactored the code for Part 1 in two places:\n", - "- `Layout`: Introduced the `crowded` attribute; it had been an inline literal `4`. Also made `deltas` an attribute, not a global constant.\n", + "- `Layout`: Introduced the `crowded` attribute; it had been an inline literal `4`. Also made `deltas` an attribute.\n", "- `next_generation`: Changed `Layout(seats)` to `type(self)(seats)`.\n", "\n", "There was more refactoring and less reuse in Part 2 than I would have liked, but I don't feel like I made bad choices in Part 1." @@ -1143,7 +1230,7 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 50, "metadata": {}, "outputs": [], "source": [ @@ -1152,7 +1239,7 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": 51, "metadata": {}, "outputs": [], "source": [ @@ -1184,7 +1271,7 @@ }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 52, "metadata": {}, "outputs": [], "source": [ @@ -1202,7 +1289,7 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 53, "metadata": {}, "outputs": [ { @@ -1211,7 +1298,7 @@ "[439, 12385]" ] }, - "execution_count": 51, + "execution_count": 53, "metadata": {}, "output_type": "execute_result" } @@ -1233,19 +1320,19 @@ }, { "cell_type": "code", - "execution_count": 52, + "execution_count": 54, "metadata": {}, "outputs": [], "source": [ "x = 0\n", "in13: Tuple[ID] = (29,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,41,x,x,x,x,x,x,x,x,x,577,\n", - " x,x,x,x,x,x,x,x,x,x,x,x,13,17,x,x,x,x,19,x,x,x,23,x,x,x,x,x,x,x,601,x,x,x,x,x,\n", - " x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,37)" + " x,x,x,x,x,x,x,x,x,x,x,x,13,17,x,x,x,x,19,x,x,x,23,x,x,x,x,x,x,x,601,x,x,x,x,x,x,\n", + " x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,37)" ] }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 55, "metadata": {}, "outputs": [], "source": [ @@ -1269,7 +1356,7 @@ }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 56, "metadata": {}, "outputs": [], "source": [ @@ -1293,9 +1380,20 @@ }, { "cell_type": "code", - "execution_count": 55, + "execution_count": 57, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "[174, 780601154795940]" + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "def day13_2(ids):\n", " \"Find the time where all the buses arrive at the right offsets.\"\n", @@ -1306,26 +1404,8 @@ " while wait(schedule[t], time + t):\n", " time += step\n", " step *= schedule[t]\n", - " return time" - ] - }, - { - "cell_type": "code", - "execution_count": 56, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[174, 780601154795940]" - ] - }, - "execution_count": 56, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ + " return time\n", + "\n", "do(13, 174, 780601154795940)" ] }, @@ -1340,12 +1420,12 @@ "1. Execute the initialization program. What is the sum of all values left in memory after it completes?\n", "2. Execute the initialization program using an emulator for a version 2 decoder chip. What is the sum of all values left in memory after it completes?\n", "\n", - "A *mask* is a bit string but with three possible values at each position, `01X`. I could make it into two bitstrings, but I choose to leave it as a `str`." + "A *mask* is a bit string but with three possible values at each position, 01X. I could make it into two bitstrings, but I choose to leave it as a `str`." ] }, { "cell_type": "code", - "execution_count": 57, + "execution_count": 58, "metadata": {}, "outputs": [], "source": [ @@ -1361,7 +1441,7 @@ }, { "cell_type": "code", - "execution_count": 58, + "execution_count": 59, "metadata": {}, "outputs": [], "source": [ @@ -1382,14 +1462,14 @@ "\n", "def bin36(i) -> str: return f'{i:036b}'\n", "\n", - "assert bin36(255 + 2 ** 20) == '000000000000000100000000000011111111'\n", + "assert bin36(255) == '000000000000000000000000000011111111'\n", "\n", "def day14_1(program): return sum(run_docking(program).values())" ] }, { "cell_type": "code", - "execution_count": 59, + "execution_count": 60, "metadata": {}, "outputs": [], "source": [ @@ -1412,7 +1492,7 @@ }, { "cell_type": "code", - "execution_count": 60, + "execution_count": 61, "metadata": {}, "outputs": [ { @@ -1421,7 +1501,7 @@ "[11884151942312, 2625449018811]" ] }, - "execution_count": 60, + "execution_count": 61, "metadata": {}, "output_type": "execute_result" } @@ -1444,7 +1524,7 @@ }, { "cell_type": "code", - "execution_count": 61, + "execution_count": 62, "metadata": {}, "outputs": [], "source": [ @@ -1453,7 +1533,7 @@ }, { "cell_type": "code", - "execution_count": 62, + "execution_count": 63, "metadata": {}, "outputs": [], "source": [ @@ -1475,12 +1555,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Part 2 involves no changes, but asks for the 30 millionth number. If it had been 3 million, I'd think \"no problem!\" If it had been 30 billion, I'd think \"I need a more efficient solution!\" As it is, I'll run it and see how long it takes:" + "Part 2 involves no changes, but looks for the 30 millionth number. If it had been 3 million, I'd think \"no problem!\" If it had been 30 billion, I'd think \"I need a more efficient solution!\" As it is, I'll run it and see how long it takes:" ] }, { "cell_type": "code", - "execution_count": 63, + "execution_count": 64, "metadata": {}, "outputs": [], "source": [ @@ -1489,7 +1569,7 @@ }, { "cell_type": "code", - "execution_count": 64, + "execution_count": 65, "metadata": { "scrolled": true }, @@ -1498,8 +1578,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 7.31 s, sys: 114 ms, total: 7.42 s\n", - "Wall time: 7.42 s\n" + "CPU times: user 11.7 s, sys: 330 ms, total: 12 s\n", + "Wall time: 12.2 s\n" ] }, { @@ -1508,7 +1588,7 @@ "[412, 243]" ] }, - "execution_count": 64, + "execution_count": 65, "metadata": {}, "output_type": "execute_result" } @@ -1530,19 +1610,6 @@ "source": [ "# Day 16: Ticket Translation\n", "\n", - "The input to this puzzle has three parts: some rules for valid fields; your ticket; and a set of nearby tickets. The puzzle is to figure out what field number corresponds to what field name (class, row, seat, etc.). The input looks like:\n", - "\n", - " class: 1-3 or 5-7\n", - " row: 6-11 or 33-44\n", - "\n", - " your ticket:\n", - " 7,1,14\n", - "\n", - " nearby tickets:\n", - " 7,3,47\n", - " 40,4,50\n", - "\n", - "\n", "1. Consider the validity of the nearby tickets you scanned. What is the sum of the values that are are not valid for any field?\n", "2. Discard invalid tickets. Use the remaining valid tickets to determine which field is which. Look for the six fields on your ticket that start with the word departure. What do you get if you multiply those six values together?\n", "\n", @@ -1551,7 +1618,7 @@ }, { "cell_type": "code", - "execution_count": 65, + "execution_count": 66, "metadata": {}, "outputs": [], "source": [ @@ -1564,7 +1631,8 @@ " def __contains__(self, item): return any(item in s for s in self)\n", " \n", "def parse_ticket_sections(fieldstr: str, your: str, nearby: str) -> TicketData:\n", - " return TicketData(fields=dict(map(parse_ticket_line, fieldstr)), \n", + " fields = dict(map(parse_ticket_line, fieldstr))\n", + " return TicketData(fields=fields, \n", " your=Ticket(your[1]), \n", " nearby=[Ticket(line) for line in nearby[1:]])\n", "\n", @@ -1574,12 +1642,12 @@ " a, b, c, d = ints(line.replace('-', ' '))\n", " return field, Sets((range(a, b + 1), range(c, d + 1)))\n", "\n", - "in16 = parse_ticket_sections(*data(16, lines, sep='\\n\\n'))" + "in16 = parse_ticket_sections(*data(16, str.splitlines, sep='\\n\\n'))" ] }, { "cell_type": "code", - "execution_count": 66, + "execution_count": 67, "metadata": {}, "outputs": [], "source": [ @@ -1604,7 +1672,7 @@ }, { "cell_type": "code", - "execution_count": 67, + "execution_count": 68, "metadata": {}, "outputs": [], "source": [ @@ -1615,13 +1683,13 @@ " fields, your, nearby = ticket_data\n", " ranges = Sets(ticket_data.fields.values())\n", " valid = [t for t in nearby + [your] if valid_ticket(t, ranges)]\n", - " possible = {i: set(fields) for i in range(len(your))}\n", - " while any(len(possible[i]) > 1 for i in possible):\n", + " possible = [set(fields) for _ in range(len(your))]\n", + " while any(len(p) > 1 for p in possible):\n", " for field_name, i in invalid_fields(valid, fields):\n", " possible[i] -= {field_name}\n", " if len(possible[i]) == 1:\n", " eliminate_others(possible, i)\n", - " return {field: i for i, [field] in possible.items()}\n", + " return {field: i for i, [field] in enumerate(possible)}\n", "\n", "def invalid_fields(valid, fields) -> Iterable[Tuple[str, int]]:\n", " \"Yield (field_name, field_number) for all invalid fields.\"\n", @@ -1631,7 +1699,7 @@ "\n", "def eliminate_others(possible, i):\n", " \"Eliminate possible[i] from all other possible[j].\"\n", - " for j in possible:\n", + " for j in range(len(possible)):\n", " if j != i:\n", " possible[j] -= possible[i]\n", "\n", @@ -1644,7 +1712,7 @@ }, { "cell_type": "code", - "execution_count": 68, + "execution_count": 69, "metadata": {}, "outputs": [ { @@ -1653,13 +1721,13 @@ "[21071, 3429967441937]" ] }, - "execution_count": 68, + "execution_count": 69, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "do(16, 21071, 3429967441937)" + "do(16)" ] }, { @@ -1668,28 +1736,7 @@ "source": [ "# Day 17: Conway Cubes\n", "\n", - "Now we are explicitly playing *Life*, but in three dimensions, not two. I've coded this before; I'll adapt my [old version](Life.ipynb) to three dimensions (actually, make it `d` dimensions in general). My implementation represents a generation as the set of active cell coordinates.\n", - "\n", - "1. Starting with your given initial configuration, simulate six cycles. How many cubes are left in the active state after the sixth cycle?\n", - "2. Simulate six cycles in a 4-dimensional space. How many cubes are left in the active state after the sixth cycle?" - ] - }, - { - "cell_type": "code", - "execution_count": 69, - "metadata": {}, - "outputs": [], - "source": [ - "in17: Picture = lines('''\n", - "##.#....\n", - "...#...#\n", - ".#.#.##.\n", - "..#.#...\n", - ".###....\n", - ".##.#...\n", - "#.##..##\n", - "#.####..\n", - "''')" + "Now we are explicitly playing *Life*, but in three dimensions not two. I've coded this before; I'll adapt my [old version](Life.ipynb) to three dimensions. My implementation represents a generation as the set of active cell coordinates." ] }, { @@ -1698,9 +1745,39 @@ "metadata": {}, "outputs": [], "source": [ + "in17: Picture = '''\n", + "##.#....\n", + "...#...#\n", + ".#.#.##.\n", + "..#.#...\n", + ".###....\n", + ".##.#...\n", + "#.##..##\n", + "#.####..'''.strip().splitlines()" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[291, 1524]" + ] + }, + "execution_count": 71, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "### New - Thu\n", + "\n", "Cell = Tuple[int,...]\n", "\n", - "def day17_1(picture, d=3, n=6):\n", + "def day17_1(picture, n=6, d=3):\n", " \"How many cells are active in the nth generation?\"\n", " return len(life(parse_cells(picture, d), n))\n", "\n", @@ -1722,7 +1799,7 @@ " if count == 3 or (count == 2 and cell in cells)}\n", "\n", "@lru_cache()\n", - "def cell_deltas(d: int):\n", + "def cell_deltas(d: int): \n", " return set(filter(any, product((-1, 0, +1), repeat=d)))\n", "\n", "def neighbor_counts(cells) -> Dict[Cell, int]:\n", @@ -1731,17 +1808,19 @@ "\n", "def neighbors(cell) -> List[cell]:\n", " \"All adjacent neighbors of cell in three dimensions.\"\n", - " return [tuple(map(operator.add, cell, delta))\n", - " for delta in cell_deltas(len(cell))]" + " return [tuple(map(operator.add, cell, delta)) \n", + " for delta in cell_deltas(len(cell))]\n", + "\n", + "def day17_2(picture): return day17_1(picture, d=4)\n", + "\n", + "do(17, 291, 1524)" ] }, { - "cell_type": "code", - "execution_count": 71, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "def day17_2(picture): return day17_1(picture, d=4)" + "Part 2 asks us to move to 4 dimensions. I'll generalize the previous code to work in 3 or 4 dimensions:" ] }, { @@ -1750,27 +1829,53 @@ "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "[291, 1524]" - ] - }, - "execution_count": 72, - "metadata": {}, - "output_type": "execute_result" + "ename": "IndentationError", + "evalue": "unexpected indent (, line 10)", + "output_type": "error", + "traceback": [ + "\u001b[0;36m File \u001b[0;32m\"\"\u001b[0;36m, line \u001b[0;32m10\u001b[0m\n\u001b[0;31m \"How many cells are active in the nth generation in a d-dimensional world?\"\u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mIndentationError\u001b[0m\u001b[0;31m:\u001b[0m unexpected indent\n" + ] } ], "source": [ - "do(17, 291, 1524)" + "def parse_cells(picture, d=3, active='#') -> Set[Cell]:\n", + " \"Convert a 2-d picture into a set of d-dimensional active cells.\"\n", + " return {(x, y, *(d - 2) * (0,) )\n", + " for (y, row) in enumerate(picture)\n", + " for x, cell in enumerate(row) if cell is active}\n", + "\n", + "def day17_1(picture): return day17_2(picture, n=6, d=3)\n", + "\n", + "\n", + " \"How many cells are active in the nth generation in a d-dimensional world?\"\n", + " cells = parse_cells(picture, d=d)\n", + " for g in range(n):\n", + " cells = next_generation(cells)\n", + " return len(cells)\n", + "\n", + "deltas = [set(product((-1, 0, +1), repeat=d)) - {(0,) * d}\n", + " for d in range(5)]\n", + "\n", + "def neighbors(cell) -> List[cell]:\n", + " \"All adjacent neighbors of cell in all dimensions.\"\n", + " return [tuple(map(operator.add, cell, delta)) \n", + " for delta in deltas[len(cell)]]" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Day 18: Operation Order\n", "\n", - "At first I thought I could just apply `eval` to each line, but alas, the operation order is non-standard.\n", + "At first I thought I could just apply `eval` to each line, but alas, the operation order is non-standard. I could have used a parsing framework, but I decided to do it all from scratch.\n", "\n", "1. All operations are done left-to-right. Evaluate the expression on each line of the homework; what is the sum of the resulting values?\n", "2. Addition is done before multiplication. What do you get if you add up the results of evaluating the homework problems using these new rules?" @@ -1778,7 +1883,7 @@ }, { "cell_type": "code", - "execution_count": 73, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1791,7 +1896,7 @@ }, { "cell_type": "code", - "execution_count": 74, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1811,7 +1916,7 @@ }, { "cell_type": "code", - "execution_count": 75, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1830,20 +1935,9 @@ }, { "cell_type": "code", - "execution_count": 76, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[3885386961962, 112899558798666]" - ] - }, - "execution_count": 76, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "do(18, 3885386961962, 112899558798666)" ] @@ -1864,7 +1958,7 @@ }, { "cell_type": "code", - "execution_count": 77, + "execution_count": null, "metadata": { "scrolled": true }, @@ -1880,18 +1974,18 @@ "\n", "def parse_rule(line):\n", " \"Parse '1: 2 3' => (1, [2, 3]); '4: 5, 6 | 7' => (4, Choice(([5, 6], [7]))).\"\n", - " n, *rhs = atoms(line.replace(':', ' ').replace('\"', ' '))\n", + " n, *rhs = atoms(line, ignore='[:\"]')\n", " if '|' in rhs:\n", " i = rhs.index('|')\n", " rhs = [Choice((rhs[:i], rhs[i + 1:]))]\n", " return n, rhs\n", " \n", - "in19 = parse_messages(*data(19, lines, sep='\\n\\n'))" + "in19 = parse_messages(*data(19, str.splitlines, sep='\\n\\n'))" ] }, { "cell_type": "code", - "execution_count": 78, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1928,7 +2022,7 @@ }, { "cell_type": "code", - "execution_count": 79, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1943,20 +2037,9 @@ }, { "cell_type": "code", - "execution_count": 80, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[190, 311]" - ] - }, - "execution_count": 80, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "do(19, 190, 311)" ] @@ -1975,7 +2058,7 @@ }, { "cell_type": "code", - "execution_count": 81, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1984,19 +2067,19 @@ " return {first(ints(header)): tile\n", " for (header, *tile) in sections}\n", " \n", - "in20 = jigsaw_tiles(data(20, lines, sep='\\n\\n'))" + "in20 = jigsaw_tiles(data(20, str.splitlines, sep='\\n\\n'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "For Part 1, I can find the corners without knowing where all the other tiles go. It is guaranteed that \"the outermost edges won't line up with any other tiles,\" but all the inside edges will. We'll define `edge_count` to count how many times an edge appears on any tile (using a `canonical` orientation, because tiles might be flipped). Then the corner tiles are ones that have two edges that have an edge count of 1." + "For Part 1, it is guaranteed that \"the outermost edges won't line up with any other tiles,\" but all the inside edges will. We'll define `edge_count` to count how many times an edge appears on any tile (using a `canonical` orientation, because tiles might be flipped). Then the corner tiles are ones that have two edges that have an edge count of 1." ] }, { "cell_type": "code", - "execution_count": 82, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -2021,20 +2104,9 @@ }, { "cell_type": "code", - "execution_count": 83, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[15670959891893, None]" - ] - }, - "execution_count": 83, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "do(20, 15670959891893) " ] @@ -2043,24 +2115,44 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Family holiday preparations kept me from doing **Part 2** on the night it was released, and unfortunately I didn't feel like coming back to it later: it seemed too tedious for too little reward. I thought it was inelegant that a solid block of `#` pixels would be considered a sea monster with waves. " + "Holiday preparations kept me from doing Part 2 on the night of the 19th, and unfortunately I didn't feel like coming back to it later: it seemed too tedious for too little reward. And I thought it was inelegant that a solid block of `#` pixels would be considered a sea monster with waves. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# Day 21: Allergen Assessment\n", + "# Timing\n", "\n", - "This is another Sudoku-like problem, similar to Day 16, involving ingredients and allergens. Crucially, each allergen is found in exactly one ingredient, but a food may not list every allergen. I can reuse the function `eliminate_others`, but the rest I need to define here. `day21_2` is the only `day` function that does not return an `int`.\n", - "\n", - "1. Determine which ingredients cannot possibly contain any of the allergens in your list. How many times do any of those ingredients appear?\n", - "2. What is your canonical dangerous ingredient list?" + "Advent of Code [states that each day's puzzle should run in 15-seconds or less](https://adventofcode.com/2020/about)).\n", + "I met that goal, with only days 11 and 15 taking more than a second. Here's a report, with stars in the first column indicating run times on a logarithmic base-10 scale: zero stars for under 1/100 seconds up to 4 stars for over 10 seconds:" ] }, { "cell_type": "code", - "execution_count": 84, + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "\n", + "def timing(days=range(1, 26)):\n", + " \"Report on timing of `do(day)` for all days.\"\n", + " results = []\n", + " for day in days:\n", + " t0 = time.time()\n", + " answers = do(day)\n", + " t = time.time() - t0\n", + " if answers:\n", + " stars = '*' * int(3 + math.log(t, 10))\n", + " print(f'{stars:>4} {day:2}: {t:6.3f} sec ⇒ {answers}')\n", + "\n", + "%time timing() " + ] + }, + { + "cell_type": "code", + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -2073,23 +2165,16 @@ " ingredients, allergens = line.split('(contains')\n", " return Food(set(atoms(ingredients)), set(atoms(allergens, ignore='[,)]')))\n", "\n", - "in21 = data(21, parse_food)" - ] - }, - { - "cell_type": "code", - "execution_count": 85, - "metadata": {}, - "outputs": [], - "source": [ + "in21 = data(21, parse_food)\n", + "\n", "def day21_1(foods):\n", - " \"How many times does an ingredient with an allergen appear?\"\n", " bad = bad_ingredients(foods)\n", " allergens = set(flatten(bad.values()))\n", " return sum(len(food.I - allergens) for food in foods)\n", "\n", "def bad_ingredients(foods) -> Dict[Allergen, Set[Ingredient]]:\n", - " \"A dict of {allergen: {set_of_ingredients_it_could_be}}; each set should have len 1.\"\n", + " \"A dict of {allergen: {set_of_ingredients_it_could_be}}\"\n", + " # Each allergen is found in exactly one ingredient.\n", " all_I = set(flatten(food.I for food in foods))\n", " all_A = set(flatten(food.A for food in foods))\n", " possible = {a: set(all_I) for a in all_A}\n", @@ -2098,39 +2183,19 @@ " for a in food.A:\n", " possible[a] &= food.I\n", " if len(possible[a]) == 1:\n", - " eliminate_others(possible, a)\n", - " return possible" - ] - }, - { - "cell_type": "code", - "execution_count": 86, - "metadata": {}, - "outputs": [], - "source": [ + " eliminate_others21(possible, a)\n", + " return possible\n", + "\n", + "def eliminate_others21(possible, a):\n", + " \"Eliminate possible[a] from all other allergens.\"\n", + " for a2 in possible:\n", + " if a2 != a:\n", + " possible[a2] -= possible[a]\n", + " \n", "def day21_2(foods) -> str:\n", - " \"What are the bad ingredients, sorted by the allergen name that contains them?\"\n", " bad = bad_ingredients(in21)\n", - " return ','.join(first(bad[a]) for a in sorted(bad))" - ] - }, - { - "cell_type": "code", - "execution_count": 87, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[2282, 'vrzkz,zjsh,hphcb,mbdksj,vzzxl,ctmzsr,rkzqs,zmhnj']" - ] - }, - "execution_count": 87, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ + " return ','.join(first(g[x]) for x in sorted(g))\n", + "\n", "do(21, 2282, 'vrzkz,zjsh,hphcb,mbdksj,vzzxl,ctmzsr,rkzqs,zmhnj')" ] }, @@ -2138,139 +2203,121 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Day 22: Crab Combat\n", + "# Day 22: Crab Combat \n", "\n", - "Part 1 is the card game *War* (here called *Combat*) with no ties. Part 2 is a more complex recursive version of the game.\n", + "The card game *War*.\n", "\n", - "1. Play a game of Combat using the two decks you just dealt. What is the winning player's score?\n", - "2. Play a game of Recursive Combat. What is the winning player's score?\n", + "1. Play the small crab in a game of Combat using the two decks you just dealt. What is the winning player's score?\n", "\n", - "Each player holds a *deal* of cards, which I will represent as a `deque` so I can deal from the top and add cards to the bottom." - ] - }, - { - "cell_type": "code", - "execution_count": 88, - "metadata": {}, - "outputs": [], - "source": [ - "Deal = Union[tuple, deque] # Cards are a tuple on input; a deque internally \n", - "Deals = Tuple[Deal, Deal] # Cards are split into two piles for the two players.\n", + "Card = int\n", + "Player = int\n", + "Deal = Tuple[Card]\n", "\n", - "in22: Deals = (\n", - " (12,40,50,4,24,15,22,43,18,21,2,42,27,36,6,31,35,20,32,1,41,14,9,44,8),\n", - " (30,10,47,29,13,11,49,7,25,37,33,48,16,5,45,19,17,26,46,23,34,39,28,3,38))" - ] - }, - { - "cell_type": "code", - "execution_count": 89, - "metadata": {}, - "outputs": [], - "source": [ + "in22 = ((12, 40, 50, 4, 24, 15, 22, 43, 18, 21, 2, 42, 27, 36, 6, 31, 35, 20, 32, 1, 41, 14, 9, 44, 8), \n", + " (30, 10, 47, 29, 13, 11, 49, 7, 25, 37, 33, 48, 16, 5, 45, 19, 17, 26, 46, 23, 34, 39, 28, 3, 38))\n", + " \n", "def day22_1(deals): return combat_score(combat(deals))\n", "\n", - "def combat(deals: Deals) -> Deals:\n", - " \"Given two deals, play Combat and return the final deals (one of which will be empty).\"\n", + "def combat_score(deals) -> int:\n", + " deal = deals[0] or deals[1]\n", + " return dotproduct(deal, reversed(range(1, len(deal) + 1)))\n", + " \n", + "def combat(deals: Tuple[Deal]) -> Tuple[Player, Deal]:\n", " deals = mapt(deque, deals)\n", - " while all(deals):\n", - " topcards = mapt(deque.popleft, deals)\n", - " winner = 0 if topcards[0] > topcards[1] else 1\n", - " deals[winner].extend(sorted(topcards, reverse=True))\n", + " while deals[0] and deals[1]:\n", + " tops = mapt(deque.popleft, deals)\n", + " winner = 0 if tops[0] > tops[1] else 1\n", + " deals[winner].extend(sorted(tops)[::-1])\n", + " return deals\n", + " \n", + "def day22_2(deals): return combat_score(recursive_combat(deals))\n", + " \n", + "def recursive_combat(deals) -> Tuple[Deal, Deal]:\n", + " \"A game of Recursive Combat\"\n", + " printv('recursive game', mapt(len, deals))\n", + " assert sum(map(len, deals)) <= 50\n", + " previous = set()\n", + " P = (0, 1)\n", + " while deals[0] and deals[1]:\n", + " if sum(mapt(len, deals)) <= 11:\n", + " printv(' deals', deals)\n", + " if deals in previous:\n", + " printv('recursive game ends in repeat')\n", + " return (deals[0], ())\n", + " previous.add(deals)\n", + " tops = mapt(first, deals)\n", + " deals = mapt(rest, deals)\n", + " if all(len(deals[p]) >= tops[p] for p in P):\n", + " rec = recursive_combat(tuple(deals[p][:tops[p]] for p in P))\n", + " winner = 0 if rec[0] else 1\n", + " else:\n", + " winner = 0 if tops[0] > tops[1] else 1\n", + " def bounty(p): return (tops[winner], tops[1 - winner]) if p == winner else ()\n", + " deals = tuple(deals[p] + bounty(p) for p in P)\n", + " printv('game ends')\n", " return deals\n", "\n", - "def combat_score(deals: Deals) -> int:\n", - " \"The winner's cards, each multiplied by their reverse index number, and summed.\"\n", - " winning = deals[0] or deals[1]\n", - " return dot(winning, range(len(winning), 0, -1))" + "verbose = False\n", + "n = [0]\n", + "def printv(*args): \n", + " n[0] += 1\n", + " #if n[0] > 100: 1/0\n", + " verbose and print(*args)\n", + "\n", + "#do(22) \n", + "\n", + "assert (recursive_combat(((9, 2, 6, 3, 1), (5, 8, 4, 7, 10)))\n", + " == ((), (7, 5, 6, 2, 4, 1, 10, 8, 9, 3)))" ] }, { "cell_type": "code", - "execution_count": 90, + "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "def day22_2(deals): return combat_score(recursive_combat(deals))\n", - " \n", - "def recursive_combat(deals: Deals) -> Deals:\n", - " \"A game of Recursive Combat.\"\n", - " deals = mapt(deque, deals)\n", - " previously = set()\n", - " while all(deals):\n", - " if seen(deals, previously):\n", - " return (deals[0], ())\n", - " topcards = mapt(deque.popleft, deals)\n", - " if all(len(deals[p]) >= topcards[p] for p in (0, 1)):\n", - " deals2 = [tuple(deals[p])[:topcards[p]] for p in (0, 1)]\n", - " result = recursive_combat(deals2)\n", - " winner = 0 if result[0] else 1\n", - " else:\n", - " winner = 0 if topcards[0] > topcards[1] else 1\n", - " deals[winner].extend([topcards[winner], topcards[1 - winner]])\n", - " return deals\n", - "\n", - "def seen(deals, previously) -> bool:\n", - " \"Return True if we have seen this pair of deals previously; else just remember it.\"\n", - " hasht = mapt(tuple, deals)\n", - " if hasht in previously:\n", - " return True\n", - " else:\n", - " previously.add(hasht)\n", - " return False" - ] + "source": [] }, { "cell_type": "code", - "execution_count": 91, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[31809, 32835]" - ] - }, - "execution_count": 91, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "do(22, 31809, 32835)" - ] + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] }, { "cell_type": "markdown", "metadata": {}, + "source": [ + "# Day 23: Crab Cups" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "# Day 23: Crab Cups\n", "\n", - "A game involving moving around some cups that are labelled with positive integers.\n", + "in23 = '872495136'\n", "\n", - "1. Using your labeling, simulate 100 moves. What are the labels on the cups after cup 1?\n", - "2. With 1 million cups and 10 million moves, determine which two cups will end up immediately clockwise of cup 1. What do you get if you multiply their labels together?" - ] - }, - { - "cell_type": "code", - "execution_count": 92, - "metadata": {}, - "outputs": [], - "source": [ - "in23 = '872495136'" - ] - }, - { - "cell_type": "code", - "execution_count": 93, - "metadata": {}, - "outputs": [], - "source": [ - "Cup = int # The label on a cup (not the index of the cup in the list of cups)\n", + "Cup = int\n", "\n", "def day23_1(cupstr: str, n=100):\n", - " \"Return the int representing the cups, in order, after cup 1; resulting from n moves.\"\n", " cups = list(map(int, cupstr))\n", " current = cups[0]\n", " for i in range(n):\n", @@ -2289,286 +2336,695 @@ " picked += cups[:extra]\n", " cups[:extra] = []\n", " return picked\n", - " \n", - "def destination(cups, current) -> Cup:\n", + " \n", + "def destination(cups, current) -> Cup: \n", " \"The cup with label one less than current, or max(cups).\"\n", " return max((c for c in cups if c < current), default=max(cups))\n", "\n", "def clockwise(cups, current) -> Cup:\n", " \"The cup one clockwise of current.\"\n", - " return cups[(cups.index(current) + 1) % len(cups)]\n", + " i = cups.index(current)\n", + " return cups[(i + 1) % len(cups)]\n", "\n", "def place(cups, picked, dest):\n", " \"Put `picked` after `dest`\"\n", " i = cups.index(dest) + 1\n", " cups[i:i] = picked\n", - "\n", + " \n", "def after(cup, cups) -> int:\n", " \"All the cups after `cup`, in order.\"\n", " i = cups.index(cup) + 1\n", " string = cat(map(str, cups + cups))\n", - " return int(string[i:i+len(cups)])" + " return int(string[i:i+len(cups)])\n", + " \n", + "do(23)\n", + "\n" ] }, { "cell_type": "code", - "execution_count": 94, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[278659341, None]" - ] - }, - "execution_count": 94, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "do(23, 278659341)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For Part 1, I aimed to be very explicit in my code, and the solution works fine for 9 cups and 100 moves (although it did end up being more lines of code than I wanted/expected). However, my approach would not be efficient enough to do Part 2; thus it was a poor choice. For now I'll skip Part 2; maybe I'll come back to it later. I had an idea for a representation where each entry in `cups` is either a `list` or a `range` of cups; that way we are only breaking up and/or shifting small lists, not million-element lists." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Day 24: Lobby Layout\n", - "\n", - "This puzzle involves a hexagonal grid. The input is a list of directions to follow to identify a destination hex tile that should be flipped from white to black. I recall that Amit Patel of Red Blob Games has [covered hex grids topic thoroughly](https://www.redblobgames.com/grids/hexagons/); I followed his recommendation to use a (pointy) **axial coordinate** system. \n", - "\n", - "1. Go through the renovation crew's list and determine which tiles they need to flip. After all of the instructions have been followed, how many tiles are left with the black side up?\n", - "2. Another version of *Life*, but on a hex grid, where a tile is live (black) if it was white and has two black neighbors, or it was black and has 1 or 2 black neighbors. How many tiles will be black after 100 days (generations)?" - ] - }, - { - "cell_type": "code", - "execution_count": 95, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "in24 = data(24)" + "foo = [0,1,2,3,4]\n", + "foo[0:0] = ['hello', 'world']\n", + "foo" ] }, { "cell_type": "code", - "execution_count": 96, + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, "metadata": {}, "outputs": [], "source": [ + "in24 = data(24)\n", + "\n", "def day24_1(lines: List[str]):\n", " \"How many tiles are flipped an odd number of times?\"\n", - " counts = Counter(map(follow_hex, lines))\n", - " return quantify(counts[tile] % 2 for tile in counts)\n", + " counts = Counter(map(follow_hex, lines)).values()\n", + " return quantify(v % 2 for v in counts)\n", "\n", "hexdirs = dict(e=(1, 0), w=(-1, 0), ne=(1, -1), sw=(-1, 1), se=(0, 1), nw=(0, -1))\n", "\n", - "def parse_hex(line) -> List[str]: return re.findall('e|w|ne|sw|se|nw', line)\n", + "def parse_hex(line) -> List[str]: return re.findall('|'.join(hexdirs), line)\n", "\n", - "def follow_hex(directions: str):\n", - " \"What (x, y) location do you end up at after following directions?\"\n", + "def follow_hex(line):\n", " x, y = 0, 0\n", - " for dir in parse_hex(directions):\n", - " dx, dy = hexdirs[dir]\n", - " x += dx\n", + " for d in parse_hex(line):\n", + " dx, dy = hexdirs[d]\n", + " x += dx \n", " y += dy\n", " return (x, y)\n", "\n", - "assert parse_hex('wsweesene') == ['w', 'sw', 'e', 'e', 'se', 'ne']\n", - "assert follow_hex('eeew') == (2, 0)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "I'll use `binding` to temporarily redefine `next_generation` and `cell_deltas` to work with this problem; then call `life`." - ] - }, - { - "cell_type": "code", - "execution_count": 97, - "metadata": {}, - "outputs": [], - "source": [ + "#####################\n", + "\n", "def day24_2(lines: List[str], days=100):\n", " \"How many tiles are black after 100 days of Life?\"\n", " counts = Counter(map(follow_hex, lines))\n", - " blacks = {tile for tile in counts if counts[tile] % 2}\n", - " with binding(next_generation=next_generation24, \n", - " cell_deltas=cell_deltas24):\n", + " blacks = {c for c in counts if counts[c] % 2}\n", + " with binding(next_generation=next_generation24, cell_deltas=cell_deltas24):\n", " return len(life(blacks, 100))\n", "\n", "def next_generation24(cells) -> Set[Cell]:\n", - " \"The set of live cells in the next generation.\"\n", + " \"\"\"The set of live cells in the next generation.\"\"\"\n", " counts = neighbor_counts(cells)\n", " return ({c for c in cells if counts[c] in (1, 2)} |\n", " {c for c in counts if c not in cells and counts[c] == 2})\n", "\n", "@lru_cache()\n", - "def cell_deltas24(d) -> Iterable[Cell]:\n", - " \"The neighbors are the 6 surrounding hex squares.\"\n", - " return hexdirs.values()" + "def cell_deltas24(d: int): \n", + " return set(hexdirs.values())\n", + " return set(filter(any, product((-1, 0, +1), repeat=d)))\n", + "\n", + "do(24)" ] }, { "cell_type": "code", - "execution_count": 98, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[420, 4206]" - ] - }, - "execution_count": 98, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "do(24, 420, 4206) " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Day 25: Combo Breaker\n", - "\n", - "This puzzle involves breaking a cryptographic protocol (whose details I won't describe here).\n", - "\n", - "1. What encryption key is the handshake trying to establish?\n", - "2. The last rule of AoC: there is no Part 2 for Day 25." - ] - }, - { - "cell_type": "code", - "execution_count": 99, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "in25 = 1965712, 19072108" + "cell_deltas(2)" ] }, { "cell_type": "code", - "execution_count": 100, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "def transform(subj) -> Iterator[int]:\n", - " \"A stream of transformed values, according to the protocol.\"\n", + "test = Counter(mapt(follow_hex, '''sesenwnenenewseeswwswswwnenewsewsw\n", + "neeenesenwnwwswnenewnwwsewnenwseswesw\n", + "seswneswswsenwwnwse\n", + "nwnwneseeswswnenewneswwnewseswneseene\n", + "swweswneswnenwsewnwneneseenw\n", + "eesenwseswswnenwswnwnwsewwnwsene\n", + "sewnenenenesenwsewnenwwwse\n", + "wenwwweseeeweswwwnwwe\n", + "wsweesenenewnwwnwsenewsenwwsesesenwne\n", + "neeswseenwwswnwswswnw\n", + "nenwswwsewswnenenewsenwsenwnesesenew\n", + "enewnwewneswsewnwswenweswnenwsenwsw\n", + "sweneswneswneneenwnewenewwneswswnese\n", + "swwesenesewenwneswnwwneseswwne\n", + "enesenwswwswneneswsenwnewswseenwsese\n", + "wnwnesenesenenwwnenwsewesewsesesew\n", + "nenewswnwewswnenesenwnesewesw\n", + "eneswnwswnwsenenwnwnwwseeswneewsenese\n", + "neswnwewnwnwseenwseesewsenwsweewe\n", + "wseweeenwnesenwwwswnew'''.splitlines()))\n", + "test2 = {c for c in test if test[c] % 2}\n", + "len(test2)\n", + "test2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "{i: len(life(test2, i)) for i in range(7)}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from contextlib import contextmanager\n", + "\n", + "@contextmanager\n", + "def binding(**kwds):\n", + " \"Bind global variables in a context; revert to old values on exit.\"\n", + " temp = {k: globals()[k] for k in kwds}\n", + " try:\n", + " globals().update(kwds)\n", + " yield\n", + " finally:\n", + " globals().update(temp)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "foo = 42\n", + "print(foo)\n", + "with bind(foo=1):\n", + " print(foo)\n", + " 1/0\n", + "print(foo)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "foo" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "in25 = 1965712, 19072108\n", + "\n", + "def transform(subj) -> Iterator[int, int]: \n", " val = 1\n", - " while True:\n", + " for i in range(1, sys.maxsize):\n", " val = (val * subj) % 20201227\n", - " yield val\n", + " yield i, val\n", + " \n", + "def nth_transform(subj, n): return first(val for i, val in transform(subj) if i == n)\n", + "def transform_to(subj, val): return first(i for i, val in transform(subj) if val == final)\n", "\n", "def day25_1(keys):\n", - " \"Find the loopsize for the first key; transform the other key that number of times.\"\n", - " loopsize = first(i for i, val in enumerate(transform(7)) if val == keys[0]) \n", - " return first(val for i, val in enumerate(transform(keys[1])) if i == loopsize)" - ] - }, - { - "cell_type": "code", - "execution_count": 101, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[16881444, None]" - ] - }, - "execution_count": 101, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ + " loopsize = transform_to(7, keys[0])\n", + " return nth_transform(keys[1], loopsize)\n", + "\n", "do(25, 16881444)" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "# Postmortem: Timing\n", - "\n", - "Advent of Code suggests that each day's puzzle should run in [15 seconds or less](https://adventofcode.com/2020/about).\n", - "I met that goal, with days 11 and 15 taking about 8 seconds each, days 22 and 25 about 2 seconds, and the average under a second per day. (However, I skipped Part 2 on days 20 and 23.)\n", - "\n", - "Here's a report, with stars in the first column indicating run times on a log scale: 0 stars for under 1/100 seconds up to 4 stars for over 10 seconds:" + "last(transform(17807724, 8)), last(transform(5764801, 11))" ] }, { "cell_type": "code", - "execution_count": 102, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " Day Secs. Answers\n", - " === ===== =======\n", - " 1 0.000 [787776, 262738554]\n", - " 2 0.000 [383, 272]\n", - " 3 0.000 [167, 736527114]\n", - " 4 0.001 [237, 172]\n", - " 5 0.000 [906, 519]\n", - " 6 0.001 [6530, 3323]\n", - " 7 0.001 [103, 1469]\n", - " 8 0.008 [1521, 1016]\n", - " 9 0.003 [776203571, 104800569]\n", - " 10 0.000 [2346, 6044831973376]\n", - " *** 11 8.271 [2299, 2047]\n", - " 12 0.001 [439, 12385]\n", - " 13 0.000 [174, 780601154795940]\n", - " * 14 0.063 [11884151942312, 2625449018811]\n", - " *** 15 7.480 [412, 243]\n", - " ** 16 0.145 [21071, 3429967441937]\n", - " ** 17 0.273 [291, 1524]\n", - " 18 0.005 [3885386961962, 112899558798666]\n", - " ** 19 0.262 [190, 311]\n", - " 20 0.001 [15670959891893, None]\n", - " 21 0.001 [2282, 'vrzkz,zjsh,hphcb,mbdksj,vzzxl,ctmzsr,rkzqs,zmhnj']\n", - " *** 22 2.246 [31809, 32835]\n", - " 23 0.000 [278659341, None]\n", - " ** 24 0.724 [420, 4206]\n", - " *** 25 1.827 [16881444, None]\n", - "CPU times: user 21.3 s, sys: 62.1 ms, total: 21.3 s\n", - "Wall time: 21.3 s\n" - ] - } - ], + "outputs": [], "source": [ - "import time\n", + "from collections import Counter\n", + "import re\n", "\n", - "def timing(days=range(1, 26)):\n", - " \"Report on timing of `do(day)` for all days.\"\n", - " print(' Day Secs. Answers')\n", - " print(' === ===== =======') \n", - " for day in days:\n", - " t0 = time.time()\n", - " answers = do(day)\n", - " t = time.time() - t0\n", - " if answers != [None, None]:\n", - " stars = '*' * int(3 + math.log(t, 10))\n", - " print(f'{stars:>4} {day:2} {t:6.3f} {answers}')\n", + "def words(text): return re.findall(\"[a-z']+\", text.lower())\n", "\n", - "%time timing()" + "def top(lyrics: str, n=10):\n", + " \"Top n most common words in lyrics.\"\n", + " return Counter(words(lyrics)).most_common(n)" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "top('''Na na na na, na na na na, hey hey, goodbye\n", + "He'll never love you, the way that I love you\n", + "'Cause if he did, no no, he wouldn't make you cry\n", + "He might be thrillin' baby but a-my love\n", + "(My love, my love)\n", + "So dog-gone willin', so kiss him\n", + "(I wanna see you kiss him, wanna see you kiss him)\n", + "Go on and kiss him goodbye, now\n", + "Na na na na, hey hey, goodbye\n", + "Na na na na, na na na na, hey hey, goodbye\n", + "Listen to me now\n", + "He's never near you to comfort and cheer you\n", + "When all those sad tears are fallin' baby from your eyes\n", + "He might be thrillin' baby but a-my love\n", + "(My love, my love)\n", + "So dog-gone willin', so kiss him\n", + "(I wanna see you kiss him, I wanna see you kiss him)\n", + "Go on and kiss him goodbye, na na na na, na na na\n", + "Na na na na, hey hey, goodbye\n", + "Hey hey, goodbye\n", + "Na na na na, na na na na, hey hey, goodbye\n", + "Na na na na, na na na na, hey hey, goodbye\n", + "Na na na na, na na na na, hey hey, goodbye\n", + "Hey hey, goodbye\n", + "Na na na na, na na na na, hey hey, goodbye\n", + "Na na na na, na na na na, hey hey, goodbye\n", + "Na na na na, na na na na, hey hey, goodbye\n", + "Hey hey, goodbye\n", + "Na na na na, na na na na, hey hey, goodbye\n", + "Na na na na, na na na na, hey hey, goodbye\n", + "Na na na na, na na na na, hey hey, goodbye''')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "top('''Ain't no sunshine when she's gone\n", + "It's not warm when she's away\n", + "Ain't no sunshine when she's gone\n", + "And she's always gone too long\n", + "Anytime she's goes away\n", + "Wonder this time where she's gone\n", + "Wonder if she's gone to stay\n", + "Ain't no sunshine when she's gone\n", + "And this house just ain't no home\n", + "Anytime she goes away\n", + "And I know, I know, I know, I know\n", + "I know, I know, I know, I know, I know\n", + "I know, I know, I know, I know, I know\n", + "I know, I know, I know, I know, I know\n", + "I know, I know, I know, I know, I know\n", + "I know, I know\n", + "Hey I oughta leave young thing alone\n", + "But ain't no sunshine when she's gone, woh woh\n", + "Ain't no sunshine when she's gone\n", + "Only darkness every day\n", + "Ain't no sunshine when she's gone\n", + "And this house just ain't no home\n", + "Anytime she goes away\n", + "Anytime she goes away\n", + "Anytime she goes away\n", + "Anytime she goes away''')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "top('''Duke, Duke, Duke, Duke of Earl\n", + "Duke, Duke, Duke of Earl\n", + "Duke, Duke, Duke of Earl\n", + "Duke, Duke, Duke of Earl\n", + "Duke, Duke, Duke of Earl\n", + "Duke, Duke, Duke of Earl\n", + "Duke, Duke, Duke of Earl\n", + "Duke, Duke, Duke of Earl\n", + "As I walk through this world\n", + "Nothing can stop the Duke of Earl\n", + "And-a you, you are my girl\n", + "And no one can hurt you, oh no\n", + "Yes-a, I, oh I'm gonna love you, oh oh\n", + "Come on let me hold you darlin'\n", + "'Cause I'm the Duke of Earl\n", + "So hey yea yea yeah\n", + "And when I hold you\n", + "You'll be my Duchess, Duchess of Earl\n", + "We'll walk through my dukedom\n", + "And a paradise we will share\n", + "Yes-a, I, oh I'm gonna love you, oh oh\n", + "Nothing can stop me now\n", + "'Cause I'm the Duke of Earl\n", + "So hey yeah yeah yeah\n", + "Well, I, oh I'm gonna love you, oh oh\n", + "Nothing can stop me now\n", + "'Cause I'm the Duke of Earl\n", + "So hey yeah yeah yeah''')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def cut(times=15):\n", + " nums = {1}\n", + " for _ in range(times):\n", + " nums |= {n + 3 for n in nums}\n", + " return nums\n", + " \n", + "cut()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "'''Her true love, Marian, has issued a challenge. Robin must fire as many arrows as she can, such that each arrow is closer to the center of the target than the previous arrow. For example, if Robin fires three arrows, each closer to the center than the previous, but the fourth arrow is farther than the third, then she is done with the challenge and her score is four.'''" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "arrow()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "from typing import Iterable\n", + "from statistics import mean\n", + "Point = complex\n", + "\n", + "def sample_circle() -> Point:\n", + " \"\"\"Uniform sampling of a point within a circle of radius 1, via rejection.\"\"\"\n", + " point = Point(random.random(), random.random())\n", + " return point if abs(point) <= 1 else sample_circle()\n", + " \n", + "def sample_arrows() -> Iterable[Point]:\n", + " \"\"\"Uniform rejection sampling of a point within a circle of radius 1.\"\"\"\n", + " arrows = []\n", + " while True:\n", + " arrows.append(abs(sample_arrow()))\n", + " if arrows != sorted(arrows, reverse=True):\n", + " return arrows \n", + " \n", + "%time mean(len(sample_arrows()) for _ in range(1_000_000))\n", + "\n", + "That answer is approximately $e$ (2.718281828...). Could $e$ be the exact answer? The Taylor series for $e^x$ is as follows:\n", + "\n", + " $$e^x = \\sum_{i=0}^{\\infty} x^n / n! $$\n", + " \n", + "and thus\n", + "\n", + " $$e = e^1 = \\sum_{i=0}^{\\infty} 1 / n! $$\n", + " \n", + "That makes so much sense now! I worked hard to make sure that we were sampling points uniformly across all the area of the circle\n", + "\n", + "def sample_arrows2() -> Iterable[Point]:\n", + " \"\"\"Uniform rejection sampling of a point within a circle of radius 1.\"\"\"\n", + " arrows = []\n", + " while True:\n", + " arrows.append(abs(int(10 * abs(sample_arrow()))))\n", + " if not monotonic(arrows):\n", + " return arrows \n", + " \n", + "def monotonic(items): \n", + " pairs = (items[i:i + 2] for i in range(len(items) - 1))\n", + " return all(a > b for a, b in pairs) \n", + "\n", + "%time mean(len(sample_arrows2()) for _ in range(1_000_000))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "That answer is approximately $e$ (2.718281828...). Could $e$ be the exact answer? The Taylor series for $e^x$ is as follows:\n", + "\n", + " $$e^x = \\sum_{i=0}^{\\infty} x^n / n! $$\n", + " \n", + "and thus\n", + "\n", + " $$e = e^1 = \\sum_{i=0}^{\\infty} 1 / n! $$\n", + " \n", + "That makes so much sense now! I worked hard to make sure that we were sampling points uniformly across all the area of the circle" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from statistics import mean\n", + "import random" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "N = 100\n", + "\n", + "def candidates(N): return [random.randrange(1000) for _ in range(N)]\n", + "\n", + "def hiring(candidates, test_amount=math.exp(-1), ratio=1.0):\n", + " i = round(test_amount * len(candidates))\n", + " bar = max(candidates[:i])\n", + " return next((c for c in candidates[i:] if c > bar), candidates[-1])\n", + "\n", + "def score(test_amount=math.exp(-1), ratio=1.0, trials=10_000):\n", + " return mean(hiring(candidates(N), test_amount, ratio) for _ in range(trials))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "X = [i / 1000 for i in range(10, 500, 10)]\n", + "Y = [score(test_amount=x, trials=3000) for x in X]\n", + "plt.plot(X, Y, 'o-')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def ants(n, trials=10_000): \n", + " return mean(max(random.random() for a in range(n)) \n", + " for t in range(trials))\n", + "\n", + "X = range(1, 100)\n", + "Y = [ants(x) for x in X]\n", + "plt.plot(X, Y, '.:')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "[f'{n:3d}: {abs(ants(n, 100_000) - n/(n+1)):.4f} ' for n in range(1, 20)]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Lingo" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "from statistics import mean\n", + "from collections import defaultdict\n", + "\n", + "def read_dict(text):\n", + " W = re.findall(r'^[A-Z]{5}$', text, re.M)\n", + " D = defaultdict(list)\n", + " for w in W:\n", + " D[w[0]].append(w)\n", + " return W, D\n", + "\n", + "alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'\n", + "\n", + "W, D = read_dict(open('enable1.txt').read().upper())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "len(W), {L: len(D[L]) for L in alphabet}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def unique_guesses(n=4):\n", + " random.shuffle(W)\n", + " letters, words = set(), []\n", + " for word in W:\n", + " S = set(word)\n", + " if len(S) == 5 and letters.isdisjoint(S):\n", + " words.append(word)\n", + " letters |= S\n", + " if len(words) == n:\n", + " return words\n", + " return unique_guesses(n, W)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "[unique_guesses() for _ in range(10)]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def possibles(guesses, secret):\n", + " L = secret[0]\n", + " return [w for w in W[L] if feasible(w, guesses)\n", + " \n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def reply(guess, secret):\n", + " regex, ins, outs = '', set(), set()\n", + " for g, s in list(zip(guess, secret)):\n", + " if g == s or g in secret:\n", + " regex += (g if g == s else '.')\n", + " ins.add(g)\n", + " secret = secret.replace(g, '', 1)\n", + " else:\n", + " regex += '.'\n", + " outs.add(g)\n", + " return regex, ins, outs\n", + "\n", + "def replies(guesses, secret):\n", + " return [reply(guess, secret) for guess in guesses]\n", + "\n", + "def consolidate(replies):\n", + " exacts = ''.join(max(reg[i] for reg, _, _ in replies) for i in range(5))\n", + " if '.' not in exacts:\n", + " return exacts, set(exacts)\n", + " letters = set().union(*(L for _, L, _ in replies))\n", + " return exacts, letters\n", + "\n", + "def average_score(guesses):\n", + " return mean(score(guesses, L) for L in alphabet)\n", + "\n", + "def startswith(L, W=W):\n", + " return [w for w in W if w.startswith(L)]\n", + "\n", + "def matches(exacts, letters, L, W):\n", + " return [w for w in startswith(L, W) and match(w, exacts, letters)]\n", + "\n", + "def match(word, exacts, letters):\n", + " \n", + " \n", + "\n", + "p = pick_unique()\n", + "n = len(set(''.join(p)))\n", + "r = replies(p, 'ALOHA') \n", + "c = consolidate(r)\n", + "m = matches(*c, 'A', W)\n", + "p, n, r, c" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "re.findall('.Z...', Wtext)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "len(text)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Counter(''.join(W)).most_common(20)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "'fuss'.replace('s', '', 1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pick()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -2587,7 +3043,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.6" + "version": "3.7.7" } }, "nbformat": 4, diff --git a/ipynb/CrossProduct.ipynb b/ipynb/CrossProduct.ipynb new file mode 100644 index 0000000..b70aed7 --- /dev/null +++ b/ipynb/CrossProduct.ipynb @@ -0,0 +1,407 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
Peter Norvig, Feb 2020
\n", + "\n", + "# CrossProduct Puzzle\n", + "\n", + "The 538 Riddler [poses a puzzle](https://fivethirtyeight.com/features/can-you-cross-like-a-boss/) called ***CrossProduct***, which works like this:\n", + "> *Fill in each empty cell of the table with a single digit, 1—9, so that the product of the digits in each row equals the number to the right of the row, and the product of the digits in each column equals the number below the column.*\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
210
144
54
135
4
49
6,61515,552420
\n", + "\n", + " \n", + "# Data Type definitions\n", + " \n", + "Here are the data types we will use in trying to solve CrossProduct puzzles: \n", + "- `Row`: a sequence of digits that forms a row in the table, e.g. `(7, 6, 5)`.\n", + "- `Table`: a filled-in table (but not the row and column products): a list of rows, e.g. `[(7, 6, 5), (9, 8, 2), ...]`.\n", + "- `Products`: a list of the numbers that corresponding digits must multiply to, e.g. in the puzzle above, `[210, 144, 54, 135, 4, 49]` for the row products, and `[6615, 15552, 420]` for the column products." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Tuple, List, Set, Iterable, Optional\n", + "\n", + "Row = Tuple[int, ...] \n", + "Table = List[Row] \n", + "Products = List[int] " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Filling in one row\n", + "\n", + "A first step in solving the puzzle is filling in a single row of the table.\n", + "
`fill_row(n, k)` will return the set of all `k`-digit tuples whose product is `n`.\n", + "
By default there are 3 digits in a row, but any number of digits is allowed." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "def fill_row(n, k=3) -> Set[Row]:\n", + " \"All permutations of k digits that multiply to n.\"\n", + " if k == 0:\n", + " return {()} if n == 1 else set()\n", + " else:\n", + " return {(d, *rest) for d in range(1, 10)\n", + " if (n / d).is_integer()\n", + " for rest in fill_row(n // d, k - 1)}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For example:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "fill_row(210)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "fill_row(729)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{(1, 9, 9, 9),\n", + " (3, 3, 9, 9),\n", + " (3, 9, 3, 9),\n", + " (3, 9, 9, 3),\n", + " (9, 1, 9, 9),\n", + " (9, 3, 3, 9),\n", + " (9, 3, 9, 3),\n", + " (9, 9, 1, 9),\n", + " (9, 9, 3, 3),\n", + " (9, 9, 9, 1)}" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fill_row(729, 4)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{(1, 5, 5, 5, 5, 5),\n", + " (5, 1, 5, 5, 5, 5),\n", + " (5, 5, 1, 5, 5, 5),\n", + " (5, 5, 5, 1, 5, 5),\n", + " (5, 5, 5, 5, 1, 5),\n", + " (5, 5, 5, 5, 5, 1)}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fill_row(5**5, 6)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Solving the whole puzzle\n", + "\n", + "We can now solve the whole puzzle with a simple brute-force strategy: for every possible way of filling the first row, try every way of recursively solving the rest of the puzzle, and yield a table for each way that works. We'll define the function `solutions` to yield all possible solutions, and `solve` to find just the first solution. (A well-formed puzzle will have exactly one solution, but some puzzles might have no solution, or multiple solutions.)\n", + "\n", + "The function `solutions` has three main cases to consider:\n", + "- Any puzzle with a non-integer column product is unsolvable. Return without yielding anything.\n", + "- The empty puzzle (with no rows) has the empty table, `[]`, as a solution, as long as the column products are all 1. \n", + "- In the general case, call `fill_row` to get all possible ways to fill the first row, and recursively call `solutions` to get all the possible ways of filling the rest of the rows (making sure to pass in an altered `col_prods` that accounts for the first row). " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "def solutions(row_prods: Products, col_prods: Products) -> Iterable[Table]:\n", + " \"\"\"Yield table(s) that solve the puzzle.\n", + " The product of the digits in row r must equal row_prods[r], for all r.\n", + " The product of the digits in column c must equal col_prods[c], for all c.\"\"\"\n", + " if not all(c == int(c) for c in col_prods):\n", + " return\n", + " if not row_prods and all(c == 1 for c in col_prods):\n", + " yield []\n", + " elif row_prods:\n", + " for row1 in fill_row(row_prods[0], len(col_prods)):\n", + " for rows in solutions(row_prods[1:], divide(col_prods, row1)):\n", + " yield [row1, *rows]\n", + " \n", + "def solve(r, c) -> Optional[Table]: return next(solutions(r, c), None)\n", + " \n", + "def divide(A, B) -> List[float]: return [a / b for a, b in zip(A, B)]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Solutions\n", + "\n", + "Here are solutions to the puzzles posed by *The Riddler*:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[(3, 9, 5), (5, 9, 1), (8, 1, 8), (5, 7, 8), (5, 7, 2)]" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "solve([135, 45, 64, 280, 70], [3000, 3969, 640])" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[(7, 6, 5), (9, 8, 2), (3, 9, 2), (5, 9, 3), (1, 4, 1), (7, 1, 7)]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# The puzzle that appears at the top of this notebook\n", + "solve([210, 144, 54, 135, 4, 49], [6615, 15552, 420])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Tests\n", + "\n", + "A suite of unit tests:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def test():\n", + " assert fill_row(1, 0) == {()}\n", + " assert fill_row(2, 0) == set()\n", + " assert fill_row(9, 1) == {(9,)}\n", + " assert fill_row(10, 1) == set()\n", + " assert fill_row(73, 3) == set()\n", + " \n", + " assert solve([], []) == []\n", + " assert solve([], [1]) == []\n", + " assert solve([], [2]) == None\n", + " assert solve([5], [5]) == [(5,)]\n", + " assert solve([0], [0]) == None # Maybe should allow zero as a digit?\n", + " \n", + " assert fill_row(729, 3) == {(9, 9, 9)} # Unique fill\n", + " \n", + " assert fill_row(729, 4) == {\n", + " (1, 9, 9, 9),\n", + " (3, 3, 9, 9),\n", + " (3, 9, 3, 9),\n", + " (3, 9, 9, 3),\n", + " (9, 1, 9, 9),\n", + " (9, 3, 3, 9),\n", + " (9, 3, 9, 3),\n", + " (9, 9, 1, 9),\n", + " (9, 9, 3, 3),\n", + " (9, 9, 9, 1)}\n", + " \n", + " assert max(range(1, 9*9*9 + 1), key=lambda n: len(fill_row(n, 3))) == 72\n", + " assert fill_row(72, 3) == { # 72 has the most ways to fill a 3-digit row\n", + " (1, 8, 9),\n", + " (1, 9, 8),\n", + " (2, 4, 9),\n", + " (2, 6, 6),\n", + " (2, 9, 4),\n", + " (3, 3, 8),\n", + " (3, 4, 6),\n", + " (3, 6, 4),\n", + " (3, 8, 3),\n", + " (4, 2, 9),\n", + " (4, 3, 6),\n", + " (4, 6, 3),\n", + " (4, 9, 2),\n", + " (6, 2, 6),\n", + " (6, 3, 4),\n", + " (6, 4, 3),\n", + " (6, 6, 2),\n", + " (8, 1, 9),\n", + " (8, 3, 3),\n", + " (8, 9, 1),\n", + " (9, 1, 8),\n", + " (9, 2, 4),\n", + " (9, 4, 2),\n", + " (9, 8, 1)}\n", + " \n", + " assert solve([6, 120, 504], [28, 80, 162]) == [\n", + " (1, 2, 3), \n", + " (4, 5, 6), \n", + " (7, 8, 9)]\n", + " \n", + " assert solve([210, 144, 54, 135, 4, 49], [6615, 15552, 420]) == [\n", + " (7, 6, 5), \n", + " (9, 8, 2), \n", + " (3, 9, 2), \n", + " (5, 9, 3), \n", + " (1, 4, 1), \n", + " (7, 1, 7)]\n", + " \n", + " assert sorted(solutions([8, 8, 1], [8, 8, 1])) == [ # Multi-solution puzzle\n", + " [(1, 8, 1), \n", + " (8, 1, 1), \n", + " (1, 1, 1)],\n", + " [(2, 4, 1), \n", + " (4, 2, 1), \n", + " (1, 1, 1)],\n", + " [(4, 2, 1), \n", + " (2, 4, 1), \n", + " (1, 1, 1)],\n", + " [(8, 1, 1), \n", + " (1, 8, 1), \n", + " (1, 1, 1)]]\n", + " \n", + " assert not list(solutions([8, 8, 1], [8, 8, 2])) # Unsolvable puzzle\n", + " \n", + " assert solve([1470, 720, 270, 945, 12, 343], \n", + " [6615, 15552, 420, 25725]) == [ # 4 column puzzle\n", + " (7, 6, 5, 7),\n", + " (9, 8, 2, 5),\n", + " (3, 9, 2, 5),\n", + " (5, 9, 3, 7),\n", + " (1, 4, 1, 3),\n", + " (7, 1, 7, 7)]\n", + " \n", + " return True\n", + " \n", + "test()" + ] + } + ], + "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.7.7" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/ipynb/Life.ipynb b/ipynb/Life.ipynb index 060437f..a77aaef 100644 --- a/ipynb/Life.ipynb +++ b/ipynb/Life.ipynb @@ -4,118 +4,121 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# John Conway's Game of Life\n", + "
Peter Norvig
2017 | 2020
\n", "\n", - "The cellular automata game *Life*, invented by the mathematician [John Conway](https://en.wikipedia.org/wiki/John_Horton_Conway), makes a fun programming exercise. Let's review the [rules](http://en.wikipedia.org/wiki/Conway%27s_Game_of_Life):\n", + "# Conway's Game of Life \n", "\n", - "The *world* of the Game of Life is an infinite two-dimensional orthogonal grid of *cells*, each of which is in one of two possible states, *live* or *empty*. Each cell has eight *neighbors*, the cells that are horizontally, vertically, or diagonally adjacent. At each step in time, the following rules are applied to create the next *generation*:\n", + "![](https://upload.wikimedia.org/wikipedia/commons/thumb/4/45/Glider.svg/140px-Glider.svg.png)\n", "\n", - "+ Any live cell with two or three live neighbors lives on to the next generation.\n", - "+ Any empty cell with exactly three live neighbors becomes a live cell in the next generation.\n", - "+ All other cells are empty in the next generation.\n", + "The cellular automata game *Life*, invented by the mathematician [John H. Conway](https://en.wikipedia.org/wiki/John_Horton_Conway), makes a fun programming exercise. Let's review the [rules](http://en.wikipedia.org/wiki/Conway%27s_Game_of_Life):\n", + "\n", + ">The **world** of *Life* is an infinite two-dimensional orthogonal grid of **cells**, each of which is in one of two possible states, **live** or **empty**. Each cell has eight **neighbors**, the cells that are horizontally, vertically, or diagonally adjacent. We go from one generation to the next with these rules:\n", + ">\n", + ">+ Any live cell with two or three live neighbors remains live in the next generation.\n", + ">+ Any empty cell with exactly three live neighbors becomes live in the next generation.\n", + ">+ All other cells are empty in the next generation.\n", "\n", "For example, in the diagram below, \"`@`\" cells are live. In the transition from Generation 0 to 1, the cell marked \"`,`\" becomes empty (dies off) because it has zero live neighbors. In the next transition, a fourth `@` becomes live, because it has 3 live neighbors. All other cells stay the same. \n", "\n", - " . . . . . . . . . . . . . . .\n", - " . . . @ . . . . , . . . . . .\n", - " . @ . . . . @ . . . . @ @ . .\n", - " . @ @ . . . @ @ . . . @ @ . .\n", - " . . . . . . . . . . . . . . .\n", - " Gen 0 Gen 1 Gen 2\n", + " . . . . . . . . . . . . . . .\n", + " . . . @ . . . . , . . . . . .\n", + " . @ . . . . @ . . . . @ @ . .\n", + " . @ @ . . . @ @ . . . @ @ . .\n", + " . . . . . . . . . . . . . . .\n", + " Gen 0 Gen 1 Gen 2\n", " \n", "\n", "\n", - "The world continues to evolve by these rules for as long as you care to observe. \n", + "The world continues to evolve by these rules for as long as you care to observe. *Life* is a zero-player infinite game.\n", "\n", "# Developing a Life Program\n", "\n", "\n", - "To create a program to play Life, start with the vocabulary of concepts:\n", + "To create a program to play *Life*, go through the inventory of concepts and decide how to implement each one:\n", "\n", - "+ **World**\n", - "+ **Cell**\n", - "+ **Live/Empty**\n", - "+ **Neighbors**\n", - "+ **Next Generation**\n", - "+ **Display**\n", - "+ **Live Neighbor Counts**\n", - "\n", - "and consider how to implement them:\n", - "\n", - "+ **World**: The state of the world must represent which cells are empty and which are live. The tricky part is that the number of cells is infinite, and we can't store an infinite array in a finite computer. I can think of three ways to deal with this problem:\n", - " 1. **Change the rules**; make the world finite instead of infinite. (Cells at the edge of the world have fewer neighbors, or perhaps they wrap around to the other side of the world.)\n", - " 2. Use a **finite rectangular window** that covers all the live cells in the infinite grid. As the world\n", - " evolves, this window may have to grow or shift.\n", - "
Example: `world = [[0, 0, 0, 0, 0], [0, 0, 0, 1, 0], [0, 1, 0, 0, 0], [0, 1, 1, 0, 0], [0, 0, 0, 0, 0]]` \n", - "\n", - " 3. Represent a world as a **set of live cells.** This set will grow and shrink in size from one generation to the next, but we don't have to worry about overflowing the edges of an array.\n", - "
Example: `world = {(3, 1), (1, 2), (1, 3), (2, 3)}` \n", - "
I will go with this choice.\n", + "+ **World** and **Cell**: A state of the world must represent which cells are empty and which are live. That looks like a job for a two-dimensional array with values 1 for live and 0 for empty. The tricky part is that the number of cells is infinite, and we can't store an infinite array in a finite computer. I can think of three ways around this issue:\n", + " - Use a **fixed-size two-dimensional array** but change the rules: cells at the edge have fewer neighbors, or they wrap around.\n", + " - Use a **sparse matrix** that can change size, expanding in any direction to contain all the live cells.\n", + " - Use a **set of live cells**, where a **cell** is represented as an (x, y)-coordinate pair. I think this is the easiest choice.\n", + "
Example: `world = {(3, 1), (1, 2), (1, 3), (2, 3)}; cell = (1, 2)` \n", + "+ **Live** and **Empty**: A cell is live if it is a member of a world, i.e. if `cell in world` is true. \n", + "+ **Neighbors**: The cell `(x, y)` has eight neighbors, formed by adding or subtracting 1 from `x` or `y` or both.\n", + "
Example: `neighbors((1, 2))` → `((0, 1), (1, 1), (2, 1), (0, 2), (2, 2), (0, 3), (1, 3), (2, 3))`\n", + "+ **Next Generation**: The function `next_generation(world)` returns a new world with the new set of live cells according to the rules.
Example: `next_generation({(3, 1), (1, 2), (1, 3), (2, 3)})` → `{(1, 2), (1, 3), (2, 3)}`\n", + "+ **Sequence of Generations**: The generator function `life(world, n)` yields `n` generations starting from the given world. \n", + "+ **Display**: We will need some way to display the generations. Let's defer that for now.\n", + "+ **Live Neighbor Counts**: To determine the next generation, we need to know how many live neighbors each cell has. A good way to represent this is a mapping of `{cell: count}`. An easy way to produce this mapping is with a `Counter`, passing it every neighbor of every live cell. This may feel like we're doing the counting \"backwards.\" Instead of asking \"for each cell, how many live neighbors does it have?\" we are saying \"for each live cell, increment the count of each of its neighbors.\" The two amount to the same thing because *neighbor* is symmetric—if P is a neighbor of Q, then Q is a neighbor of P. Below we see the neighbor counts for each of the three generations of the example above; in each generation the top diagram gives the neighbor counts for the empty cells, and the bottom diagram gives the counts for the live cells. This is just to make the diagram easier to read; in the code the counts are all in one `Counter`. \n", "\n", "\n", - "+ **Cell**: Each cell will be represented as an (x, y) pair of integer coordinates.
Example: `cell = (1, 2)`.\n", - "+ **Live/Empty**: \n", - "A cell is live if it is a member of the set of live cells. \n", - "
Example: \"`cell in world`\" is True, given the definition of `cell` and `world` above, so `cell` is live.\n", - "+ **Neighbors**: The cell `(x, y)` has eight neighbors, formed by adding or subtracting 1 from `x` or `y` or both. We can define\n", - "a function `neighbors(cell)` to return this set.\n", - "
Example: `neighbors((8, 8)) == [(7, 7), (8, 7), (9, 7), (7, 8), (9, 8), (7, 9), (8, 9), (9, 9)]`\n", - "\n", - "+ **Display**: We will need some way to display the state of the world. Let's defer that for now.\n", - "\n", - "+ **Next Generation**: We can define a function, `next_generation(world)`, that takes a world as input and returns\n", - "a new world with the new set of live cells according to the rules.\n", - "Example: `next_generation({(3, 1), (1, 2), (1, 3), (2, 3)}) == {(1, 2), (1, 3), (2, 3)}`\n", - "\n", - "+ **Live Neighbor Counts**: I need to know how many live neighbors each cell has. A good way to represent this is a dict of `{(x, y): count}`. But which cells need to be the keys of this dict? We can start with the live cells, and also add any cells neighboring the live cells. An easy way to generate this dict is to create a `Counter` and pass it every neighbor of every live cell. This may feel like we're doing the counting \"backwards.\" Instead of asking \"for each cell, how many live neighbors does it have?\" we are saying \"for each live cell, increment the count of each of its neighbors.\" The two amount to the same thing because *neighbor* is symmetric—if P is a neighbor of Q, then Q is a neighbor of P. Below we see the neighbor counts for each of the three generations; in each generation the top diagram gives the neighbor counts for the empty cells, and the bottom diagram gives the counts for the live cells. This is just to make the diagram easier to read; in the code these are combined into one `Counter`. Here are the counts:\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - " . . 1 1 1 . . . . . . . . . .\n", - " 1 1 2 @ 1 1 1 1 , . 1 2 2 1 .\n", - " 2 @ 4 2 1 2 @ 3 1 . 2 @ @ 2 .\n", - " 2 @ @ 1 . 2 @ @ 1 . 2 @ @ 2 .\n", - " 1 2 2 1 . 1 2 2 1 . 1 2 2 1 .\n", - " Gen 0 Gen 1 Gen 2\n", - " . . . . . . . . . . . . . . .\n", - " . . . 0 . . . . , . . . . . .\n", - " . 2 . . . . 2 . . . . 3 3 . .\n", - " . 2 2 . . . 2 2 . . . 3 3 . .\n", - " . . . . . . . . . . . . . . .\n", + " . . 1 1 1 . . . . . . . . . .\n", + " 1 1 2 @ 1 1 1 1 , . 1 2 2 1 .\n", + " 2 @ 4 2 1 2 @ 3 1 . 2 @ @ 2 .\n", + " 2 @ @ 1 . 2 @ @ 1 . 2 @ @ 2 .\n", + " 1 2 2 1 . 1 2 2 1 . 1 2 2 1 .\n", + " Gen 0 Gen 1 Gen 2\n", + " . . . . . . . . . . . . . . .\n", + " . . . 0 . . . . , . . . . . .\n", + " . 2 . . . . 2 . . . . 3 3 . .\n", + " . 2 2 . . . 2 2 . . . 3 3 . .\n", + " . . . . . . . . . . . . . . .\n", " \n", "\n", - "Here is the implementation. Note that in `next_generation` the `neighbor_counts` is used two ways so I decided to use two different names for clarity: `possible_cells` is used to iterate over all cells that might be live, and `counts` is used to check if a cell has the right number of neighbors." + "Now we're ready to start implementing. First some imports and type declarations:" ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ - "from collections import Counter\n", + "from collections import Counter\n", + "from typing import Set, Tuple, Dict, Iterator\n", + "from itertools import islice\n", + "from IPython.display import clear_output, display_html\n", + "from time import sleep\n", + "import sys\n", "\n", - "def next_generation(world):\n", - " \"The set of live cells in the next generation.\"\n", - " possible_cells = counts = neighbor_counts(world)\n", - " return {cell for cell in possible_cells\n", - " if (counts[cell] == 3) \n", - " or (counts[cell] == 2 and cell in world)}\n", + "Cell = Tuple[int, int]\n", + "World = Set[Cell] " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now the complete implementation, except for display:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "def life(world, n=sys.maxsize) -> Iterator[World]:\n", + " \"\"\"Yield `n` generations, starting from the given world.\"\"\"\n", + " for g in range(n):\n", + " yield world\n", + " world = next_generation(world)\n", "\n", - "def neighbor_counts(world):\n", - " \"A {cell: int} counter of the number of live neighbors for each cell that has neighbors.\"\n", - " return Counter(nb for cell in world \n", - " for nb in neighbors(cell))\n", + "def next_generation(world) -> World:\n", + " \"\"\"The set of live cells in the next generation.\"\"\"\n", + " return {cell for cell, count in neighbor_counts(world).items()\n", + " if count == 3 or (count == 2 and cell in world)}\n", "\n", - "def neighbors(cell):\n", - " \"All 8 adjacent neighbors of cell.\"\n", + "def neighbor_counts(world) -> Dict[Cell, int]:\n", + " \"\"\"A Counter of the number of live neighbors for each cell.\"\"\"\n", + " return Counter(xy for cell in world \n", + " for xy in neighbors(cell))\n", + "\n", + "def neighbors(cell) -> List[Cell]:\n", + " \"\"\"All 8 adjacent neighbors of cell.\"\"\"\n", " (x, y) = cell\n", - " return [(x-1, y-1), (x, y-1), (x+1, y-1), \n", - " (x-1, y), (x+1, y), \n", - " (x-1, y+1), (x, y+1), (x+1, y+1)]" + " return [(x + dx, y + dy) \n", + " for dx in (-1, 0, 1) \n", + " for dy in (-1, 0, 1) \n", + " if not (dx == 0 == dy)]" ] }, { @@ -127,7 +130,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "metadata": {}, "outputs": [ { @@ -136,7 +139,7 @@ "{(1, 2), (1, 3), (2, 3)}" ] }, - "execution_count": 2, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -146,26 +149,6 @@ "next_generation(world)" ] }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{(1, 2), (1, 3), (2, 2), (2, 3)}" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "next_generation(next_generation(world))" - ] - }, { "cell_type": "code", "execution_count": 4, @@ -174,7 +157,10 @@ { "data": { "text/plain": [ - "[(1, 3), (2, 3), (3, 3), (1, 4), (3, 4), (1, 5), (2, 5), (3, 5)]" + "[{(1, 2), (1, 3), (2, 3), (3, 1)},\n", + " {(1, 2), (1, 3), (2, 3)},\n", + " {(1, 2), (1, 3), (2, 2), (2, 3)},\n", + " {(1, 2), (1, 3), (2, 2), (2, 3)}]" ] }, "execution_count": 4, @@ -183,7 +169,7 @@ } ], "source": [ - "neighbors((2, 4))" + "list(life(world, 4))" ] }, { @@ -197,23 +183,23 @@ "Counter({(0, 1): 1,\n", " (0, 2): 2,\n", " (0, 3): 2,\n", - " (0, 4): 1,\n", " (1, 1): 1,\n", - " (1, 2): 2,\n", " (1, 3): 2,\n", - " (1, 4): 2,\n", - " (2, 0): 1,\n", " (2, 1): 2,\n", " (2, 2): 4,\n", " (2, 3): 2,\n", - " (2, 4): 2,\n", + " (2, 0): 1,\n", " (3, 0): 1,\n", " (3, 2): 2,\n", - " (3, 3): 1,\n", - " (3, 4): 1,\n", " (4, 0): 1,\n", " (4, 1): 1,\n", - " (4, 2): 1})" + " (4, 2): 1,\n", + " (1, 2): 2,\n", + " (1, 4): 2,\n", + " (2, 4): 2,\n", + " (3, 3): 1,\n", + " (3, 4): 1,\n", + " (0, 4): 1})" ] }, "execution_count": 5, @@ -225,44 +211,24 @@ "neighbor_counts(world)" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "`run` is a function to play n generations of Life:" - ] - }, { "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "def run(world, n):\n", - " \"Run the world for n generations. No display; just return the nth generation.\"\n", - " for g in range(n):\n", - " world = next_generation(world)\n", - " return world" - ] - }, - { - "cell_type": "code", - "execution_count": 7, + "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{(1, 2), (1, 3), (2, 2), (2, 3)}" + "[(0, 1), (0, 2), (0, 3), (1, 1), (1, 3), (2, 1), (2, 2), (2, 3)]" ] }, - "execution_count": 7, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "run(world, 100)" + "neighbors((1, 2))" ] }, { @@ -271,26 +237,44 @@ "source": [ "# Display\n", "\n", - "Now let's see how to display worlds. We'll consider a rectangular window on the infinite plane, specified as ranges of `Xs` and `Ys` coordinates. The function `picture` turns a world into a string showing what the world looks like:" + "To display a world, we'll specify a rectangular window on the infinite plane with ranges of `Xs` and `Ys` coordinates. The function `picture` returns a string depicting the world within that window:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "LIVE = '@'\n", + "EMPTY = '.'\n", + "PAD = ' '\n", + " \n", + "def picture(world, Xs: range, Ys: range) -> str:\n", + " \"\"\"Return a picture of the world: a grid of characters representing the cells in this window.\"\"\"\n", + " def row(y): return PAD.join(LIVE if (x, y) in world else EMPTY for x in Xs)\n", + " return '\\n'.join(row(y) for y in Ys)" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "({(1, 2), (1, 3), (2, 3), (3, 1)}, {(1, 2), (1, 3), (2, 3)})" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "import time\n", - "from IPython.display import clear_output, display_html\n", - "\n", - "LIVE = '@'\n", - "EMPTY = '.'\n", - "PAD = ' '\n", - " \n", - "def picture(world, Xs, Ys):\n", - " \"Return a picture: a grid of characters representing the cells in this window.\"\n", - " def row(y): return PAD.join(LIVE if (x, y) in world else EMPTY for x in Xs)\n", - " return '\\n'.join(row(y) for y in Ys)" + "g = life(world)\n", + "next(g), next(g)" ] }, { @@ -318,7 +302,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The function `display_run` runs the world for `n` steps, displaying the picture at each step:" + "# Animated Display\n", + "\n", + "The function `animate_life` displays `n` generations: display `world`, pause for `pause` seconds, then clear the screen and display the next generation." ] }, { @@ -327,17 +313,15 @@ "metadata": {}, "outputs": [], "source": [ - "def display_run(world, n=10, Xs=range(10), Ys=range(10), pause=0.2):\n", - " \"Step and display the world for the given number of generations.\"\n", - " for g in range(n + 1):\n", - " html = ('Generation {}, Population {}\\n{}'\n", - " .format(g, len(world), pre(picture(world, Xs, Ys))))\n", - " clear_output()\n", - " display_html(html, raw=True)\n", - " time.sleep(pause)\n", - " world = next_generation(world)\n", + "def animate_life(world, n=10, Xs=range(10), Ys=range(10), pause=1/5):\n", + " \"\"\"Display the evolving world for `n` generations.\"\"\"\n", + " for g, world in enumerate(life(world, n)):\n", + " clear_output(wait=True)\n", + " display_html(pre(f'Generation: {g:2}, Population: {len(world):2}\\n' +\n", + " picture(world, Xs, Ys)), raw=True)\n", + " sleep(pause)\n", " \n", - "def pre(text): return '
' + text + '
'" + "def pre(text) -> str: return '
' + text + '
'" ] }, { @@ -348,8 +332,8 @@ { "data": { "text/html": [ - "Generation 5, Population 4\n", - "
. . . . .\n",
+       "
Generation:  3, Population:  4\n",
+       ". . . . .\n",
        ". . . . .\n",
        ". @ @ . .\n",
        ". @ @ . .\n",
@@ -361,7 +345,7 @@
     }
    ],
    "source": [
-    "display_run(world, 5, range(5), range(5))"
+    "animate_life(world, 4, range(5), range(5), 1)"
    ]
   },
   {
@@ -370,45 +354,44 @@
    "source": [
     "# Interesting Worlds\n",
     "\n",
-    "Now let's take a look at some initial worlds that *Life* enthusiasts have discovered. It would be tedious to enumerate these with an explicit set of `(x, y)` coordinates, so we will define the function `shape` that takes a picture as input and returns a world; `shape` and `picture` are more-or-less inverses. "
+    "Now let's take a look at some configurations of cells that *Life* enthusiasts have discovered. It would be tedious to write out a set of `(x, y)` coordinates, so we will define the function `shape` that takes a picture as input and returns a world; `shape` and `picture` are more-or-less inverses. "
    ]
   },
   {
    "cell_type": "code",
    "execution_count": 12,
-   "metadata": {
-    "collapsed": true
-   },
+   "metadata": {},
    "outputs": [],
    "source": [
-    "def shape(picture, offset=(3, 3)):\n",
-    "    \"Convert a graphical picture (e.g. '@ @ .\\n. @ @') into a world (set of cells).\"\n",
+    "def shape(picture, dx=3, dy=3) -> World:\n",
+    "    \"\"\"Convert a graphical picture (e.g. '@ @ .\\n. @ @') into a world (set of cells).\"\"\"\n",
     "    cells = {(x, y) \n",
     "             for (y, row) in enumerate(picture.splitlines())\n",
     "             for (x, c) in enumerate(row.replace(PAD, ''))\n",
     "             if c == LIVE}\n",
-    "    return move(cells, offset)\n",
+    "    return slide(cells, dx, dy)\n",
     "\n",
-    "def move(cells, offset):\n",
-    "    \"Move/Translate/slide a set of cells by a (dx, dy) displacement/offset.\"\n",
-    "    (dx, dy) = offset\n",
-    "    return {(x+dx, y+dy) for (x, y) in cells}\n",
+    "def slide(cells, dx, dy):\n",
+    "    \"\"\"Translate/slide a set of cells by a (dx, dy) offset.\"\"\"\n",
+    "    return {(x + dx, y + dy) for (x, y) in cells}\n",
     "\n",
     "blinker     = shape(\"@@@\")\n",
     "block       = shape(\"@@\\n@@\")\n",
-    "beacon      = block | move(block, (2, 2))\n",
+    "beacon      = block | slide(block, 2, 2)\n",
     "toad        = shape(\".@@@\\n@@@.\")\n",
     "glider      = shape(\".@.\\n..@\\n@@@\")\n",
-    "rpentomino  = shape(\".@@\\n@@.\\n.@.\", (36, 20))\n",
-    "line        = shape(\".@@@@@@@@.@@@@@...@@@......@@@@@@@.@@@@@\", (10, 10))\n",
-    "growth      = shape(\"@@@.@\\n@\\n...@@\\n.@@.@\\n@.@.@\", (10, 10))"
+    "rpentomino  = shape(\".@@\\n@@.\\n.@.\", 36, 20)\n",
+    "line        = shape(\".@@@@@@@@.@@@@@...@@@......@@@@@@@.@@@@@\", 10, 10)\n",
+    "growth      = shape(\"@@@.@\\n@\\n...@@\\n.@@.@\\n@.@.@\", 15, 20)\n",
+    "zoo         = (slide(blinker, 5, 25) | slide(glider, 8, 13) | slide(blinker, 20, 25) |\n",
+    "               slide(beacon, 24, 25) | slide(toad, 30, 25)  | slide(block, 13, 25) | slide(block, 17, 33))"
    ]
   },
   {
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Here is how `shape` and `move` work:"
+    "Here is how `shape` and `slide` work:"
    ]
   },
   {
@@ -438,18 +421,21 @@
    "metadata": {},
    "outputs": [
     {
-     "data": {
-      "text/plain": [
-       "{(3, 3), (3, 4), (4, 3), (4, 4)}"
-      ]
-     },
-     "execution_count": 14,
-     "metadata": {},
-     "output_type": "execute_result"
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      ". . . . . . .\n",
+      ". . . . . . .\n",
+      ". . . . . . .\n",
+      ". . . @ @ . .\n",
+      ". . . . @ @ .\n",
+      ". . . . . . .\n",
+      ". . . . . . .\n"
+     ]
     }
    ],
    "source": [
-    "block"
+    "print(picture(_, range(7), range(7)))"
    ]
   },
   {
@@ -460,7 +446,7 @@
     {
      "data": {
       "text/plain": [
-       "{(103, 203), (103, 204), (104, 203), (104, 204)}"
+       "{(3, 3), (3, 4), (4, 3), (4, 4)}"
       ]
      },
      "execution_count": 15,
@@ -469,7 +455,27 @@
     }
    ],
    "source": [
-    "move(block, (100, 200))"
+    "block"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 16,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{(103, 203), (103, 204), (104, 203), (104, 204)}"
+      ]
+     },
+     "execution_count": 16,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "slide(block, 100, 200)"
    ]
   },
   {
@@ -479,35 +485,6 @@
     "Let's run some examples. If you are viewing a static notebook, you will only see the last generation; rerun each cell to see all the generations."
    ]
   },
-  {
-   "cell_type": "code",
-   "execution_count": 16,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/html": [
-       "Generation 10, Population 3\n",
-       "
. . . . . . . . . .\n",
-       ". . . . . . . . . .\n",
-       ". . . . . . . . . .\n",
-       ". . . @ @ @ . . . .\n",
-       ". . . . . . . . . .\n",
-       ". . . . . . . . . .\n",
-       ". . . . . . . . . .\n",
-       ". . . . . . . . . .\n",
-       ". . . . . . . . . .\n",
-       ". . . . . . . . . .
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "display_run(blinker)" - ] - }, { "cell_type": "code", "execution_count": 17, @@ -516,14 +493,14 @@ { "data": { "text/html": [ - "Generation 10, Population 8\n", - "
. . . . . . . . . .\n",
+       "
Generation:  9, Population:  3\n",
+       ". . . . . . . . . .\n",
+       ". . . . . . . . . .\n",
+       ". . . . @ . . . . .\n",
+       ". . . . @ . . . . .\n",
+       ". . . . @ . . . . .\n",
        ". . . . . . . . . .\n",
        ". . . . . . . . . .\n",
-       ". . . @ @ . . . . .\n",
-       ". . . @ @ . . . . .\n",
-       ". . . . . @ @ . . .\n",
-       ". . . . . @ @ . . .\n",
        ". . . . . . . . . .\n",
        ". . . . . . . . . .\n",
        ". . . . . . . . . .
" @@ -534,7 +511,7 @@ } ], "source": [ - "display_run(beacon)" + "animate_life(blinker)" ] }, { @@ -545,13 +522,42 @@ { "data": { "text/html": [ - "Generation 10, Population 6\n", - "
. . . . . . . . . .\n",
+       "
Generation:  9, Population:  6\n",
        ". . . . . . . . . .\n",
        ". . . . . . . . . .\n",
-       ". . . . @ @ @ . . .\n",
-       ". . . @ @ @ . . . .\n",
        ". . . . . . . . . .\n",
+       ". . . @ @ . . . . .\n",
+       ". . . @ . . . . . .\n",
+       ". . . . . . @ . . .\n",
+       ". . . . . @ @ . . .\n",
+       ". . . . . . . . . .\n",
+       ". . . . . . . . . .\n",
+       ". . . . . . . . . .
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "animate_life(beacon)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Generation:  9, Population:  6\n",
+       ". . . . . . . . . .\n",
+       ". . . . . . . . . .\n",
+       ". . . . . @ . . . .\n",
+       ". . . @ . . @ . . .\n",
+       ". . . @ . . @ . . .\n",
+       ". . . . @ . . . . .\n",
        ". . . . . . . . . .\n",
        ". . . . . . . . . .\n",
        ". . . . . . . . . .\n",
@@ -563,36 +569,7 @@
     }
    ],
    "source": [
-    "display_run(toad)"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 19,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/html": [
-       "Generation 15, Population 5\n",
-       "
. . . . . . . . . .\n",
-       ". . . . . . . . . .\n",
-       ". . . . . . . . . .\n",
-       ". . . . . . . . . .\n",
-       ". . . . . . . . . .\n",
-       ". . . . . . . . . .\n",
-       ". . . . . . . . . .\n",
-       ". . . . . . . @ . .\n",
-       ". . . . . . . . @ @\n",
-       ". . . . . . . @ @ .
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "display_run(glider, 15)" + "animate_life(toad)" ] }, { @@ -603,47 +580,17 @@ { "data": { "text/html": [ - "Generation 130, Population 178\n", - "
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ @ . . . . . . . . . . . . . @\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ . @ . . . . . . . . . . . . . @\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @\n",
-       ". . . . . . . . . . . @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . @ @ @ @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ @\n",
-       ". . . . . @ . @ @ @ @ . @ @ . . . . . . . . . . . . . . . . . . . . . . @ @ . . . . . . . @ . @\n",
-       ". . . @ @ . . @ @ . . @ @ @ . . . . . . . . . . . . . . . . . . . . . . @ @ . . . . . . . @ . @\n",
-       ". . . . . @ @ @ . . . @ @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ .\n",
-       ". . . . . . . . . . . @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . @ . @ . . . . . . . . . . . . . . . . . . . . . . @ @ . . . . . . . . . . . . .\n",
-       ". . . . . . . . @ @ . . . . . . . . . . . . . . . . . @ . @ . . . @ @ . . . . . . . . . . . . .\n",
-       ". . . . @ @ @ . . @ @ . . . . . . . . . . . . . . . . @ . @ . . . . . . . . . . . . . . . . @ @\n",
-       ". . . . @ @ . . . @ @ . . . . . . . . . . . . . . . . @ . . . . . . . . . . . . . . . . . @ @ @\n",
-       ". . . . @ . . . . . . . . . . . . . . . . . . . . . . . @ . . . . . . . . @ . . . . . . . @ . .\n",
-       ". . . . @ @ . @ . . . . . . . . . . . . . . . . . . . . . . . . . . . @ @ . @ . . . . . . . @ .\n",
-       ". . . . . @ @ @ . . . . . . . . . . . . . . . . . . . . . . . . @ . . . . . . @ . . . . . . . .\n",
-       ". . . . @ . . @ . . . . . . . . . . . . . . . . . . . . . . . @ . . . . . . @ . . . @ @ . . . .\n",
-       ". . . . . @ . . . . . . . . . . . . . . . . . . . . . . . . @ . . . . . @ @ . . . @ . . @ . . .\n",
-       "@ @ . @ @ @ . . . . . . . . . . . . . . . . . . . . . . . . @ . . @ . . . . . . . @ . . @ . . .\n",
-       ". . @ @ . . . . . . . . . . . . . . . . . . . . . . . . . . @ @ . @ @ @ . . . . . . @ @ . . . .\n",
-       ". . . @ @ . . . . . . . . . . . . . . . . . . . . . . . . . . . @ . . . @ . . . . . . . . . . .\n",
-       "@ @ . @ @ . . . . . . . . . . @ @ . . . . . . . . . . . . . . . @ . . . . . . . . . . . . . . .\n",
-       "@ . . @ . . . . . . . . . . . @ @ . . . . . . . . . . . . . . . . . . @ . . . . . . . . . . . .\n",
-       "@ @ @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . @ @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . @ . @ . @ @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . @ . @ . @ @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . @ . @ @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . @ . @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
" + "
Generation: 19, Population:  5\n",
+       ". . . . . . . . . .\n",
+       ". . . . . . . . . .\n",
+       ". . . . . . . . . .\n",
+       ". . . . . . . . . .\n",
+       ". . . . . . . . . .\n",
+       ". . . . . . . . . .\n",
+       ". . . . . . . . . .\n",
+       ". . . . . . . . . .\n",
+       ". . . . . . . . @ .\n",
+       ". . . . . . . . . @
" ] }, "metadata": {}, @@ -651,7 +598,7 @@ } ], "source": [ - "display_run(rpentomino, 130, range(48), range(40))" + "animate_life(glider, 20)" ] }, { @@ -662,23 +609,82 @@ { "data": { "text/html": [ - "Generation 160, Population 111\n", - "
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       "
Generation: 129, Population: 163\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ @ @ . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ . . . . . . . . . . . . @ @\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . @ @ @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ @\n",
+       ". . . . @ . . . @ @ . . @ . . . . . . . . . . . . . . . . . . . . . . . @ @ . . . . . . . @ . @\n",
+       ". . . . @ . @ @ . . . . @ . . . . . . . . . . . . . . . . . . . . . . . @ @ . . . . . . . @ . @\n",
+       ". . . . @ . . . . . . . @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ .\n",
+       ". . . . . . . @ . . . @ . . . . . . . . . . . . . . . . . . . . . @ . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . @ . . . . . . . . . . . . . . . . . . . . . . . @ . . . . . . . . . . . . .\n",
+       ". . . . . . . @ . @ . . . . . . . . . . . . . . . . . . @ @ . . . . @ . . . . . . . . . . . . .\n",
+       ". . . . . @ . . . @ @ . . . . . . . . . . . . . . . . @ @ @ . . . @ . . . . . . . . . . . . . .\n",
+       ". . . . @ @ . . . @ . . . . . . . . . . . . . . . . . @ . . . . . . . . . . . . . . . . . @ @ @\n",
+       ". . . . @ . . . . . . . . . . . . . . . . . . . . . . . @ @ . . . . . . . . . . . . . . . @ . .\n",
+       ". . . . . @ . . @ . . . . . . . . . . . . . . . . . . . . . . . . . . . @ @ @ . . . . . . . @ @\n",
+       ". . . . . @ @ @ . . . . . . . . . . . . . . . . . . . . . . . . . @ @ . @ @ @ . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ @ @ @ @ @ @ . . . @ . . . . .\n",
+       "@ . . . @ @ @ . . . . . . . . . . . . . . . . . . . . . . . . @ @ @ @ . . . . . . @ . @ @ . . .\n",
+       "@ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ @ . . . . . . . . . . @ @ . . . .\n",
+       ". . @ @ . . . . . . . . . . . . . . . . . . . . . . . . . . . @ . . . . . . . . . . @ . . . . .\n",
+       ". . . @ . . @ @ . . . . . . . . . . . . . . . . . . . . . . . . @ @ @ @ @ . . . . . . . . . . .\n",
+       "@ . . @ . . . . . . . . . . . @ @ . . . . . . . . . . . . . . . . @ @ @ . . . . . . . . . . . .\n",
+       "@ @ . @ . . . . . . . . . . . @ @ . . . . . . . . . . . . . . . . . . @ . . . . . . . . . . . .\n",
+       ". @ @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . @ @ @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . @ . . . @ @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . @ . @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . @ @ @ @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "animate_life(rpentomino, 130, range(48), range(40))" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Generation: 159, Population: 105\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
        ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
        ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ . . . . @\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ . . . @ . @ . . @ .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ @ . @ . . @ . @ @ . @ .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ @ . @ @ . . @ @ . . . @\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ . @ . . . @\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ @ @ . . @ . @ . . @ .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ @ . @ . . . @ @ . . . @\n",
        ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ . . . @ . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ @ @ @ . @ @ @ @ @ . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . @ @ @ . . . @ . . . @ @ . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ @ . . @ . @ . @ . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ . . . . . @ . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ @ . @ @ . . @ . . . . . . . @\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ @ . . . . @ . . . . . . @ @ .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . @ . . . . . @ . . . @ . @ @ . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . @ . . . @ @ . @ @ @ . . . @ . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . @ . . . . . . @ @ . . @ @ @ @ . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ . . . . @ . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ @ @ @ @ @ . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ . . . @ @ . . . . . . . @ .\n",
        ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ . . @ . . . . . . . . @ @ .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ @ @ . . . . . . . . @ @ .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ . . . . . . . . . . . @\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @ @ @ . . . . . . . . . @ .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @\n",
        ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
        ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
        ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
@@ -690,9 +696,9 @@
        ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
        ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
        ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . @ @ @ . . . . . @ @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . @ . . @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . @ . . . . . . @ @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . @ . . . . . @ . . @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
        ". . . . . . . . . . . . . . . . @ @ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
        ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
        ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
@@ -710,57 +716,54 @@
     }
    ],
    "source": [
-    "zoo = (move(blinker, (5, 25)) | move(glider, (8, 13)) | move(blinker, (20, 25))  |\n",
-    "       move(beacon, (24, 25)) | move(toad, (30, 25))  | move(block, (13, 25)) | move(block, (17, 33)))\n",
-    "\n",
-    "display_run(zoo, 160, range(48), range(40))"
+    "animate_life(zoo, 160, range(48), range(40))"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 22,
+   "execution_count": 23,
    "metadata": {},
    "outputs": [
     {
      "data": {
       "text/html": [
-       "Generation 100, Population 72\n",
-       "
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . @ . . . @ @ . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . @ . @ . . @ . @ . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . @ . . . @ . . @ . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . @ @ . . . . @ . @ . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . @ @ @ @ . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . @ @ @ . @ @ . @ @ . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . @ @ . . . @ . @ @ @ . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . @ @ @ . . . . . @ . . @ . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . @ @ @ @ @ . @ @ @ @ @ @ @ . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . @ @ @ . @ @ @ @ @ @ @ @ . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . @ @ . @ @ @ @ @ . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
-       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       "
Generation: 199, Population: 100\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . @ @ @ . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . @ @ . . @ @ . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . @ . . @ . @ @ @ . . . @ . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . @ . @ . . . @ @ @ . @ . . . @ @ . . @ @ . . . . . . . . . . . . . .\n",
+       ". . . . . . @ . . . @ . @ @ @ . . . . . . @ @ . @ @ @ . . . . . . . . . . . . .\n",
+       ". . . . . . . @ . . . @ @ @ . . . . . @ . @ . @ . . . @ @ . . . . . . . . . . .\n",
+       ". . . . . . . . @ . . . @ . @ @ . . . . @ . . @ . @ . @ @ . . . . . . . . . . .\n",
+       ". . . . . . . . . . . @ . . . @ . . @ @ . . . @ . @ . @ @ . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . @ @ @ . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . @ @ . . . . . . . . . . . . . . . . .\n",
        ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
        ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
        ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . @ . @ . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . @ . . @ . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . @ . . . . . @ . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . @ . @ @ . @ . . . . . . @ @ @ . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . @ . . . @ @ @ . . . . . @ . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . @ . . . @ . . . . . . @ . . @ . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . @ @ @ @ . . . . . . . @ @ . . . .\n",
        ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
        ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
        ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . @ @ . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . @ @ . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
        ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
        ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
        ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . @ @ . . . . . . . . . . . . . . . . . . . . . .\n",
+       ". . . . . . . . . . . . . . . . @ @ . . . . . . . . . . . . . . . . . . . . . .\n",
        ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
        ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
        ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n",
@@ -772,7 +775,7 @@
     }
    ],
    "source": [
-    "display_run(growth, 100, range(40), range(40))"
+    "animate_life(growth, 200, range(40), range(40))"
    ]
   },
   {
@@ -788,27 +791,27 @@
     "and add these lines:\n",
     "\n",
     "    def clear_output(): print(\"\\033[;H\\033[2J\") # ANSI terminal home and clear\n",
+    "    def display_html(text, raw=False): print(text) \n",
+    "    def pre(text) -> str: return text\n",
     "    \n",
-    "    def display_html(text, raw=False): print(text)\n",
-    "        \n",
-    "    def pre(text): return text\n",
+    "If you want to create a fancier display using some graphics packagee, be my guest. Let me know what you create.\n",
     "    \n",
     "# Coding Kata\n",
     "\n",
-    "I once attended a [code kata](https://en.wikipedia.org/wiki/Kata_(programming%29) in which one of the exercises was to write the Game of Life without using any conditional (e.g. `if`) statements.  I did it by using roughly the program shown here, but changing the lone `if` to a `filter` in `next_generation`:"
+    "I once attended a [code kata](https://en.wikipedia.org/wiki/Kata_%28programming%29) in which one of the exercises was to write *Life* without using any conditional (e.g. `if`) statements.  I did it by using roughly the program shown here, but changing the lone `if` to a `filter` in `next_generation`:"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 23,
+   "execution_count": 24,
    "metadata": {},
    "outputs": [],
    "source": [
     "def next_generation(world):\n",
-    "    \"The set of live cells in the next generation.\"\n",
-    "    possible_cells = counts = neighbor_counts(world)\n",
-    "    def good(cell): return counts[cell] == 3 or (counts[cell] == 2 and cell in world)\n",
-    "    return set(filter(good, possible_cells))"
+    "    \"\"\"The set of live cells in the next generation.\"\"\"\n",
+    "    counts = neighbor_counts(world)\n",
+    "    def live(cell): return counts[cell] == 3 or (counts[cell] == 2 and cell in world)\n",
+    "    return set(filter(live, counts))"
    ]
   }
  ],
@@ -828,7 +831,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.5.3"
+   "version": "3.7.7"
   }
  },
  "nbformat": 4,