Add files via upload
This commit is contained in:
1 parent
94dca98c00
commit
ddad3bbaa7
1 file changed
+378
-297
+378
-297
@@ -8,30 +8,20 @@
|
||||
"\n",
|
||||
"# `xkcd` Name Dominoes\n",
|
||||
"\n",
|
||||
"The March 21, 2018 `xkcd` comic (#1970) was [Name Dominoes](https://xkcd.com/1970/): domino tiles laid out as if in a game, but the tiles contain names of famous people rather than numbers. \n",
|
||||
"In [dominoes](https://en.wikipedia.org/wiki/Dominoes) each tile has two halves, and a tile can be legally placed only if one half is adjacent to another tile half with the same number/name, and neither half is adjacent to a tile half with a different number/name. (Exception: the very first tile can be placed anywhere.) \n",
|
||||
"\n",
|
||||
"\n",
|
||||
"I will write a function to lay out tiles in a random, legal array. First, the key data structures:\n",
|
||||
"The March 21, 2018 `xkcd` comic (#1970) was **[Name Dominoes](https://xkcd.com/1970/)**: tiles laid out as if in a game of dominoes, but the tiles contain names of famous people. \n",
|
||||
"In regular [dominoes](https://en.wikipedia.org/wiki/Dominoes) a tile consists of two squares, each with a number of spots from 0 to 6. In xkcd name dominoes, the tiles can be one, two, or three squares in length, and each square contains a name (first, middle, or last). In both versions, a tile can be placed if it is adjacent to another tile on the board, and if no two adjacent squares contain different numbers/names.\n",
|
||||
"I will write a function, `place_tiles`, to lay out name dominoes according to these rules. \n",
|
||||
"\n",
|
||||
"- **`Tile`**: a tile is a 2-element tuple of names, like `('TIM', 'COOK')`.\n",
|
||||
"The tile `('FRANK LLOYD', 'WRIGHT')` has a space in the first name.\n",
|
||||
"- **`Name`**: a name (first name or last name) is a string.\n",
|
||||
"- **`Board(width, height)`**: a `Board` represents\n",
|
||||
"a width × height array of squares, initially `empty`, but when we put a tile on the board,\n",
|
||||
"the first name covers one location and the last name covers an adjacent location, e.g. `board[0, 0], board[0, 1] = ('TIM', 'COOK')`. Implemented as a subtype of `dict` that keeps track of `width` and `height`.\n",
|
||||
"- **`Location`**: a location is an `(x, y)` pair of coordinates for a square on the board.\n",
|
||||
"# Key Data Structures:\n",
|
||||
"\n",
|
||||
"Now I need a strategy to fill the board with tiles. I will randomly place tiles one at a time, and to make things easier I will *not* consider removing a tile from the board and backtracking. Some more concepts and functions:\n",
|
||||
"\n",
|
||||
"- **`frontier`**: I'll maintain a *frontier*, a set of locations that are adjacent to tiles on the board, and thus are candidates for placing new tiles.\n",
|
||||
"- **`dominoes(tiles)`**: makes a board and places tiles on it, one at a time, until no more can be placed. Chooses a random tile for the first tile, then repeatedly calls `try_one` to legally place an additional tile, stopping when either there is no `frontier` left (meaning no place to legally place a tile) or no `tiles` left to place.\n",
|
||||
"- **`try_one(tiles, board, frontier)`**: pop a location off the frontier, and try to find some tile that can legally put one of its halves there; when found, `put` the tile there, and remove it from `tiles`.\n",
|
||||
"- **`legal(name, loc, board)`**: a name can be placed if the location is empty, and there are no conflicts with any neighboring location.\n",
|
||||
"- **`neighbors(loc, board)`**: returns the (up to 4) neighbors of a location that are on the board.\n",
|
||||
"- **`put_tile(board, loc0, loc1, tile, frontier)`**: places a tile on the board across `loc0` and `loc1`; update the `frontier` to say that the just-covered locations are no longer in the frontier, but the empty neighbors of the tile are.\n",
|
||||
"- **`shuffle(items)`**: used to randomize lists; calls `random.shuffle` and returns the result.\n",
|
||||
"\n",
|
||||
"# The Code"
|
||||
"- **`Tile`**: a tile is a tuple of names, like `('ISSAC', 'NEWTON')`\n",
|
||||
"or `('FRANK', 'LLOYD', 'WRIGHT')` or `('DRAKE',)`.\n",
|
||||
"- **`Name`**: a name (first, middle, or last) is a string, like `'FRANK'`.\n",
|
||||
"- **`Box`**: a structure with a tile, the square that the tile's first name goes on, and the direction in which to move to place subsequent names.\n",
|
||||
"- **`Point`**: an `(x, y)` pair denoting the coordinates of a square on the board. Can also be used for **directions**: `(0, 1)` is the direction that goes 0 in the x axis and 1 in the y axis; also known as \"up\" or \"90 degrees\".\n",
|
||||
"- **`Board`**: a [width × height] grid of squares, where each square can be filled with a name. `Board` is implemented as a subclass of `dict` of the form `{(0, 0): 'ISSAC', (0, 1): 'NEWTON', ...}`, but with additional attributes to keep track of the width and height, and the boxes placed so far. If an `(x, y)` square is not in the dict, it is `empty` by default, unless the `(x, y)` coordinates are out of bounds; then it is `off` the board."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -40,102 +30,63 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%matplotlib inline\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"import random\n",
|
||||
"\n",
|
||||
"empty = None # An empty square\n",
|
||||
"\n",
|
||||
"class Board(dict):\n",
|
||||
" \"A mapping from location to name.\"\n",
|
||||
" def __init__(self, width, height): self.width, self.height = width, height\n",
|
||||
" def __missing__(self, loc): return empty\n",
|
||||
"\n",
|
||||
"def dominoes(tiles, width=16, height=24):\n",
|
||||
" \"Place as many tiles on board as possible (legally and randomly).\"\n",
|
||||
" tiles = shuffle(list(tiles))\n",
|
||||
" board = Board(width, height)\n",
|
||||
" frontier = set()\n",
|
||||
" m = min(width, height) // 2\n",
|
||||
" put_tile(board, (m, m), (m, m + 1), tiles.pop(), frontier) # Place first tile\n",
|
||||
" while tiles and frontier:\n",
|
||||
" try_one(tiles, board, frontier)\n",
|
||||
" return board\n",
|
||||
" \n",
|
||||
"def try_one(tiles, board, frontier):\n",
|
||||
" \"Pop a frontier location, and try to place a tile on that location.\"\n",
|
||||
" loc0 = frontier.pop()\n",
|
||||
" for tile in shuffle(tiles):\n",
|
||||
" for (name0, name1) in [tile, tile[::-1]]:\n",
|
||||
" if legal(name0, loc0, board):\n",
|
||||
" for loc1 in shuffle(neighbors(loc0, board)):\n",
|
||||
" if legal(name1, loc1, board):\n",
|
||||
" put_tile(board, loc0, loc1, (name0, name1), frontier)\n",
|
||||
" tiles.remove(tile)\n",
|
||||
" return tile\n",
|
||||
" \n",
|
||||
"def legal(name, loc, board):\n",
|
||||
" \"Is it legal to place this name on this location on board?\"\n",
|
||||
" return (board[loc] is empty and\n",
|
||||
" all(board[nbr] is empty or board[nbr] == name\n",
|
||||
" for nbr in neighbors(loc, board)))\n",
|
||||
"\n",
|
||||
"def neighbors(loc, board):\n",
|
||||
" \"Neighbors of this location on the board.\"\n",
|
||||
" x, y = loc\n",
|
||||
" return [(x+dx, y+dy) for (dx, dy) in ((0, 1), (1, 0), (0, -1), (-1, 0))\n",
|
||||
" if 0 <= x+dx < board.width and 0 <= y+dy < board.height]\n",
|
||||
"\n",
|
||||
"def put_tile(board, loc0, loc1, tile, frontier): \n",
|
||||
" \"Place the tile across the two locations, and update frontier.\"\n",
|
||||
" board[loc0], board[loc1] = tile\n",
|
||||
" frontier -= {loc0, loc1}\n",
|
||||
" frontier |= {loc for loc in neighbors(loc0, board) + neighbors(loc1, board)\n",
|
||||
" if board[loc] is empty}\n",
|
||||
" \n",
|
||||
"def shuffle(items): random.shuffle(items); return items"
|
||||
"from collections import namedtuple"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{(0, 0): 'RY',\n",
|
||||
" (1, 0): 'KE',\n",
|
||||
" (1, 1): 'KE',\n",
|
||||
" (2, 1): 'JO',\n",
|
||||
" (2, 2): 'JO',\n",
|
||||
" (3, 2): 'GR',\n",
|
||||
" (3, 3): 'GR',\n",
|
||||
" (3, 4): 'KE',\n",
|
||||
" (4, 4): 'KE',\n",
|
||||
" (4, 5): 'JA'}"
|
||||
]
|
||||
},
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"tiles8 = [('BO', 'JA'), ('JA', 'PO'), ('JA', 'RY'), ('RY', 'KE'), \n",
|
||||
" ('GR', 'KE'), ('GR', 'JO'), ('JA', 'KE'), ('KE', 'JO')]\n",
|
||||
"Tile = tuple # e.g., ('ISSAC', 'NEWTON')\n",
|
||||
"Name = str # e.g., 'ISSAC'\n",
|
||||
"Box = namedtuple('Box', 'start, dir, tile')\n",
|
||||
"\n",
|
||||
"dominoes(tiles8, 6, 6)"
|
||||
"Point = tuple\n",
|
||||
"def X(point) -> int: \"The x coordinate of a point\"; return point[0]\n",
|
||||
"def Y(point) -> int: \"The x coordinate of a point\"; return point[1]\n",
|
||||
"\n",
|
||||
"directions = {(1, 0): 0, (0, 1): 90, (-1, 0): 180, (0, -1): 270}\n",
|
||||
"\n",
|
||||
"off = '.' # A square that is off the edge of the board\n",
|
||||
"empty = '' # An empty square (does not have a tile covering it)\n",
|
||||
"\n",
|
||||
"class Board(dict):\n",
|
||||
" \"\"\"A mapping from (x, y) squares to names; for a width x height board.\"\"\"\n",
|
||||
" def __init__(self, width, height): \n",
|
||||
" \"\"\"Keep track of the width and height, and of boxes that have been placed.\"\"\"\n",
|
||||
" self.width = width \n",
|
||||
" self.height = height\n",
|
||||
" self.boxes = []\n",
|
||||
" \n",
|
||||
" def __missing__(self, square: Point) -> str: \n",
|
||||
" \"\"\"The contents of a filled square is a name; a missing square is empty or off.\"\"\"\n",
|
||||
" return empty if (0 <= X(square) < self.width and 0 <= Y(square) < self.height) else off"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Pretty Output\n",
|
||||
"# Algorithm to Fill the Board\n",
|
||||
"\n",
|
||||
"Technically, this is a legal solution, but there are two problems with it: One, it is not visual. Two, it doesn't say where each tile is: when three names come together, which of the outside names goes with the middle name? To fix those two problems I will:\n",
|
||||
"Now I need a strategy to fill the board with tiles. I will place tiles one at a time, and to make things easier I will *never* consider removing a tile from the board and backtracking. The major functions are:\n",
|
||||
"\n",
|
||||
"- Define `plot_board(board)` to use `matplotlib` to plot the board, the names, and the tiles.\n",
|
||||
"- Modify the `Board` class and the `put` function so that the board maintains a list of `boxes` that surround each two-location rectangle that a tile occupies: `[loc0, loc1]`. The constant ϵ is the distance between two adjacent tiles; we want them to not-quite touch (as in the xkcd comic)."
|
||||
"\n",
|
||||
"- **`place_tiles(tiles)`**: The top-level strategy. Makes a board and places tiles on it, one at a time, until no more can be placed. Chooses a random tile for the first tile, then repeatedly calls `try_one_square` to legally place an additional tile on a square from the frontier, stopping when either there is no `frontier` left or no `tiles` left.\n",
|
||||
"- **`frontier`**: I'll maintain a *frontier*, a set of `(x, y)` squares that are adjacent to tiles on the board, and thus are candidates for placing new tiles.\n",
|
||||
"- **`try_one_square(tiles, board, frontier)`**: pop a square off the frontier, and try to find some tile that can legally put one of its names there; when found, `put_tile` the tile there, and remove it from `tiles`.\n",
|
||||
"- **`can_put_tile(tile, board, start, dir)`**: Can we put this tile on the board at this start square, going in this direction?\n",
|
||||
"- **`can_put_name(name, board, square)`**: Can this one name (first, middle, or last) be placed on this square?\n",
|
||||
"- **`allowed(name, nbr_contents)`**: Is this name allowed to be placed next to this neighboring square's contents?\n",
|
||||
"- **`neighbors(square, board)`**: returns the four neighbors of a square.\n",
|
||||
"- **`put_tile(tile, board, start, dir, tiles, frontier)`**: places a tile on the board starting at `start` and going in direction `dir`; update the `frontier` to say that the just-covered squares are not in the frontier, but the empty neighbors of new tiles are. Remove the tile from `tiles`.\n",
|
||||
"- **`move(start, steps, dir)`**: Returm the square that results from starting at `start` and going `steps` in \n",
|
||||
"direction `dir`."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -144,65 +95,220 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%matplotlib inline\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"def place_tiles(tiles, width=16, height=35) -> Board:\n",
|
||||
" \"\"\"Place as many tiles on board as possible.\"\"\"\n",
|
||||
" tiles = list(tiles); random.shuffle(tiles)\n",
|
||||
" board = Board(width, height)\n",
|
||||
" frontier = set()\n",
|
||||
" put_tile(tiles[0], board, (width//2, height//2), (0, 1), tiles, frontier)\n",
|
||||
" while tiles and frontier:\n",
|
||||
" try_one_square(tiles, board, frontier) \n",
|
||||
" return board\n",
|
||||
"\n",
|
||||
"ϵ = 0.06 # A small amount; the space between adjacent lines\n",
|
||||
"def try_one_square(tiles, board, frontier):\n",
|
||||
" \"\"\"Pop a frontier square, and try to place a tile on that square.\"\"\"\n",
|
||||
" square = frontier.pop()\n",
|
||||
" for tile in tiles:\n",
|
||||
" # The offset is used to shift the tile along `direction`\n",
|
||||
" for offset in range(len(tile)):\n",
|
||||
" for dir in directions:\n",
|
||||
" start = move(square, -offset, dir)\n",
|
||||
" if can_put_tile(tile, board, start, dir):\n",
|
||||
" return put_tile(tile, board, start, dir, tiles, frontier)\n",
|
||||
" \n",
|
||||
"def can_put_tile(tile, board, start, dir) -> bool:\n",
|
||||
" \"\"\"Is it legal to put this tile on this square/direction on board?\"\"\"\n",
|
||||
" return all(can_put_name(name, board, move(start, i, dir))\n",
|
||||
" for i, name in enumerate(tile))\n",
|
||||
"\n",
|
||||
"class Board(dict):\n",
|
||||
" \"A mapping from location to name.\"\n",
|
||||
" def __init__(self, width=16, height=24): \n",
|
||||
" self.width, self.height, self.boxes = width, height, []\n",
|
||||
" def __missing__(self, loc): return empty\n",
|
||||
" \n",
|
||||
"def plot_board(board):\n",
|
||||
" \"Plot the box and name for every tile, plus a big box around the board.\"\n",
|
||||
" plt.figure(figsize=(board.width, board.height))\n",
|
||||
" plt.subplots_adjust(left=ϵ, right=1-ϵ, top=1-ϵ, bottom=ϵ)\n",
|
||||
" plt.axis('off') \n",
|
||||
" plt.axis('equal')\n",
|
||||
" for box in board.boxes:\n",
|
||||
" plot_box(box)\n",
|
||||
" plot_box([(-2*ϵ, -2*ϵ), (board.width - 1 + 2*ϵ, board.height - 1 + 2*ϵ)])\n",
|
||||
" for (x, y) in board:\n",
|
||||
" plt.text(x + 0.5, y + 0.5, board[x, y], \n",
|
||||
" va='center', ha='center', fontsize=8)\n",
|
||||
"def can_put_name(name, board, square) -> bool:\n",
|
||||
" return (board[square] is empty \n",
|
||||
" and all(allowed(name, board[nbr])\n",
|
||||
" for nbr in neighbors(square, board)))\n",
|
||||
"\n",
|
||||
"def plot_box(box):\n",
|
||||
" \"Plot a box, which is a [loc0, loc1] pair.\"\n",
|
||||
" Xs, Ys = {loc[0] for loc in box}, {loc[1] for loc in box}\n",
|
||||
" x0, x1 = min(Xs), max(Xs) + 1 - ϵ\n",
|
||||
" y0, y1 = min(Ys), max(Ys) + 1 - ϵ\n",
|
||||
" plt.plot([x0, x1, x1, x0, x0], \n",
|
||||
" [y0, y0, y1, y1, y0], 'k-')\n",
|
||||
"def allowed(name, nbr_contents) -> bool: return nbr_contents in (name, empty, off)\n",
|
||||
"\n",
|
||||
"def put_tile(board, loc0, loc1, tile, frontier): \n",
|
||||
" \"Place the tile across the two locations, and update frontier and boxes.\"\n",
|
||||
" board[loc0], board[loc1] = tile\n",
|
||||
" frontier -= {loc0, loc1}\n",
|
||||
" frontier |= {loc for loc in neighbors(loc0, board) + neighbors(loc1, board)\n",
|
||||
" if board[loc] is empty}\n",
|
||||
" board.boxes.append([loc0, loc1]) "
|
||||
"def neighbors(square, board) -> list:\n",
|
||||
" \"\"\"Neighbors of this square on the board.\"\"\"\n",
|
||||
" x, y = square\n",
|
||||
" return [(x+dx, y+dy) for (dx, dy) in directions]\n",
|
||||
"\n",
|
||||
"def put_tile(tile, board, start, dir, tiles, frontier): \n",
|
||||
" \"\"\"Place the tile on the board at the start square, going in the given direction. \n",
|
||||
" Update tiles and frontier.\"\"\"\n",
|
||||
" board.boxes.append(Box(start, dir, tile))\n",
|
||||
" tiles.remove(tile)\n",
|
||||
" for i, name in enumerate(tile):\n",
|
||||
" square = move(start, i, dir)\n",
|
||||
" board[square] = name\n",
|
||||
" frontier -= {square}\n",
|
||||
" frontier |= {L for L in neighbors(square, board) if board[L] is empty}\n",
|
||||
" \n",
|
||||
"def move(start: Point, steps: int, dir: Point) -> Point: \n",
|
||||
" \"\"\"Starting at `start` square, move a number of `steps` in direction `dir`.\"\"\"\n",
|
||||
" return (X(start) + steps * X(dir),\n",
|
||||
" Y(start) + steps * Y(dir))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Small Example\n",
|
||||
"\n",
|
||||
"Here I create a small example with 8 tiles, and try to fill a board. You can see the boxes that were placed, and the names that are in each square:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"tiles8 = [('JAMES', 'EARL', 'JONES'), ('QUINCY', 'JONES'), ('QUINCY', 'ADAMS'), ('JOHN', 'ADAMS'), \n",
|
||||
" ('JOHN', 'QUINCY', 'ADAMS'), ('HENRY', 'JAMES'), ('JOHN', 'HENRY'), ('EARL',)]\n",
|
||||
"\n",
|
||||
"board = place_tiles(tiles8, 7, 7)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAaAAAAGdCAYAAABU0qcqAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAEFdJREFUeJzt3X/I7nV9x/HXW06bISjnZLa0tQ0nk6g/GkjtCDqUapkQ5bJlev4Yts1ibP9sI4SonX8CSdpgk5BFM0a0qS0o4eT0aDtr0f4xGTKChWNrMhce2xDW+vHeH/d17O4sPfdt59zv+3vdjwfceJ/rluPrvu7L63l/P/cNV3V3AGCnnTU9AIC9SYAAGCFAAIwQIABGCBAAIwQIgBECBMAIAQJghAABMEKAABghQACMECAARggQACMECIARAgTACAECYIQAATBCgAAYIUAAjBAgAEYIEAAjBAiAEQIEwAgBAmCEAAEwQoAAGCFAAIwQIABGCBAAIwQIgBECBMAIAQJghAABMEKAABghQACMECAARggQACMECIARAgTACAECYIQAATBCgAAYIUAAjBAgAEYIEAAjBAiAEQIEwAgBAmCEAAEwQoAAGCFAAIwQIABGCBAAIwQIgBECBMCIfdMDdpuqeirJ/ukdACvHu/vA9Igzobp7esOuUlXd3TW9AyBZ7+ckR3AAjBAgAEYIEAAjBAiAEQIEwAgBAmCEAAEwQoAAGCFAAIwQIABGCBAAIwQIgBECBMAIAQJghAABMEKAABghQACMECAARggQACMECIARAgTACAECYIQAATBCgAAYIUAAjBAgAEYIEAAjBAiAEQIEwAgBAmCEAAEwQoAAGCFAAIwQIABGCBAAIwQIgBECBMAIAQJghAABMEKAABghQACMECAARggQACMECIARAgTACAECYIQAATBCgAAYIUAAjBAgAEYIEAAjBAiAEfumByxBVT2VZP/0jj3meHcfmB7x41roY+fZ+36h+5dsLR73W1XdPb1hV6mq7u461W2cWetyny/x89i8eYn7l2yvPf84ggNghAABMEKAABghQACMECAARggQACMECIARAgTACAECYIQAATBCgAAYIUAAjBAgAEYIEAAjBAiAEQI0pKqurKrDq/fvrKo7pjdtxWr341X1YFX9bVX9QlW9uKo+XlVHq+qBqvrF6Z3r6sTjpqp+rqruW30d/qqqXjK9bSuWvP+k/2evq6q7q+rJ1efwYFXdPL1xabwi6qyuqrOSvDzL+mbgru7+QFW9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 truncated
|
||||
"text/plain": [
|
||||
"<matplotlib.figure.Figure at 0x110568940>"
|
||||
"[Box(start=(3, 3), dir=(0, 1), tile=('QUINCY', 'ADAMS')),\n",
|
||||
" Box(start=(3, 2), dir=(1, 0), tile=('QUINCY', 'JONES')),\n",
|
||||
" Box(start=(5, 4), dir=(-1, 0), tile=('JOHN', 'ADAMS')),\n",
|
||||
" Box(start=(5, 0), dir=(0, 1), tile=('JAMES', 'EARL', 'JONES')),\n",
|
||||
" Box(start=(0, 4), dir=(1, 0), tile=('JOHN', 'QUINCY', 'ADAMS')),\n",
|
||||
" Box(start=(3, 0), dir=(1, 0), tile=('HENRY', 'JAMES')),\n",
|
||||
" Box(start=(6, 4), dir=(0, 1), tile=('JOHN', 'HENRY')),\n",
|
||||
" Box(start=(6, 1), dir=(1, 0), tile=('EARL',))]"
|
||||
]
|
||||
},
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"board.boxes"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{(3, 3): 'QUINCY',\n",
|
||||
" (3, 4): 'ADAMS',\n",
|
||||
" (3, 2): 'QUINCY',\n",
|
||||
" (4, 2): 'JONES',\n",
|
||||
" (5, 4): 'JOHN',\n",
|
||||
" (4, 4): 'ADAMS',\n",
|
||||
" (5, 0): 'JAMES',\n",
|
||||
" (5, 1): 'EARL',\n",
|
||||
" (5, 2): 'JONES',\n",
|
||||
" (0, 4): 'JOHN',\n",
|
||||
" (1, 4): 'QUINCY',\n",
|
||||
" (2, 4): 'ADAMS',\n",
|
||||
" (3, 0): 'HENRY',\n",
|
||||
" (4, 0): 'JAMES',\n",
|
||||
" (6, 4): 'JOHN',\n",
|
||||
" (6, 5): 'HENRY',\n",
|
||||
" (6, 1): 'EARL'}"
|
||||
]
|
||||
},
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"board"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Plotting Pretty Output\n",
|
||||
"\n",
|
||||
"Technically, this is a solution. But it is not pretty to look at. \n",
|
||||
"\n",
|
||||
"I'll define `plot_board(board)` to plot each name in each tile and draw rectangles around them. I'll take care to make the font size appropriate for the length of each name, and I'll rotate the names according to the direction the tile is going in. The `fantasy` font is something like standard cartoon lettering.\n",
|
||||
"\n",
|
||||
"The `(x, y)` coordinates are taken to be the **center** of a square; names are centered on `(x, y)`. But the box is drawn a half-unit to each side of the center. Actually, we want it to be a biot less than a half-unit, becuase we want the outlines of the tiles to not-quite touch. In `plot_rectangle_around` we use 0.47 instead of 0.5 for this reason.\n",
|
||||
"\n",
|
||||
"One more trick: Randall has `\"Billy D. WIlliams\"` as a two-square name, not three, so I will write it as `\"Billy_D. Williams\"`, split that into two names, and only convert the `\"_\"` to a `\" \"` when plotting the text.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def plot_board(board):\n",
|
||||
" \"\"\"Plot each tile box, plus a big box around the whole board.\"\"\"\n",
|
||||
" plt.figure(figsize=(board.width, board.height))\n",
|
||||
" plt.axis('off'); plt.axis('equal')\n",
|
||||
" print(f'{len(board.boxes)} tiles placed on {len(board)} squares')\n",
|
||||
" for box in board.boxes:\n",
|
||||
" squares = [move(box.start, i, box.dir) for i in range(len(box.tile))]\n",
|
||||
" plot_rectangle_around(squares)\n",
|
||||
" for (x, y), name in zip(squares, box.tile):\n",
|
||||
" plt.text(x, y, name.replace('_', ' '), va='center', ha='center', \n",
|
||||
" fontsize=fontsizes[len(name)], fontdict={'family': 'fantasy'}, \n",
|
||||
" rotation=directions[box.dir])\n",
|
||||
"\n",
|
||||
"def plot_rectangle_around(squares, half=0.47):\n",
|
||||
" \"\"\"Plot a rectangle around this list of squares.\"\"\"\n",
|
||||
" Xs, Ys = transpose(squares)\n",
|
||||
" x0, y0 = min(Xs) - half, min(Ys) - half\n",
|
||||
" x1, y1 = max(Xs) + half, max(Ys) + half\n",
|
||||
" plt.plot([x0, x1, x1, x0, x0], \n",
|
||||
" [y0, y0, y1, y1, y0], \n",
|
||||
" 'k-')\n",
|
||||
" \n",
|
||||
"def transpose(matrix) -> list: return list(zip(*matrix))\n",
|
||||
"\n",
|
||||
"fontsizes = (0, 15, 15, 15, 13, 11, 10, 9, 7, 7, 6, 6, 6, 6, 6) # Indexed by len(name)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"8 tiles placed on 17 squares\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 504x504 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"plot_board(dominoes(tiles8, 6, 6))"
|
||||
"plot_board(board)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -212,46 +318,56 @@
|
||||
"# All the Names\n",
|
||||
"\n",
|
||||
"Now let's try all the names from the comic, courtesy of \n",
|
||||
" [explainxkcd](http://www.explainxkcd.com/wiki/index.php/1970:_Name_Dominoes), with a few typos corrected:"
|
||||
" [explainxkcd](http://www.explainxkcd.com/wiki/index.php/1970:_Name_Dominoes), with a few typos corrected. "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"271"
|
||||
]
|
||||
},
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def name_tiles(text):\n",
|
||||
" \"For each line of text, create a tile of ('First Name(s)', 'Lastname').\"\n",
|
||||
" return [name.strip().rpartition(' ')[0::2]\n",
|
||||
" for name in text.upper().split(',')]\n",
|
||||
"def define_tiles(text) -> list:\n",
|
||||
" \"Create a list of tiles from the text containing names.\"\n",
|
||||
" return [Tile(name.strip().split()) for name in text.upper().split(',')]\n",
|
||||
" \n",
|
||||
"xkcdtiles = name_tiles(\"\"\"Christian Campbell, Neve Campbell, Joe McCarthy, Eugene McCarthy, \n",
|
||||
" Gene Vincent, Gene Kelly, Kate Hudson, Rock Hudson, Gordon Brown, James Brown, Jon Brown, \n",
|
||||
"xkcdtiles = define_tiles(\"\"\"Christian Campbell, Neve Campbell, Joe McCarthy, Eugene McCarthy, \n",
|
||||
" Gene Vincent, Gene Kelly, Kate Hudson, Rock Hudson, Gordon Brown, James Brown, John Brown, \n",
|
||||
" John Howard, Columbo, Chris Columbus, Christopher Columbus, Naomi Campbell, Joseph Campbell, \n",
|
||||
" Joseph Smith, Frank Vincent, John Kelly, Katherine Johnson, The Rock, Chris Rock, Chris Isaac, \n",
|
||||
" James Newton Howard, John Wayne, Howard Stern, Howard Hunt, Chris Hughes, Naomi Watts, \n",
|
||||
" Naomi Klein, Kevin Kline, Francis Bacon, Francis Drake, Lyndon Johnson, Oscar The Grouch, \n",
|
||||
" Oscar Isaac, Isaac Hayes, Isaac Newton, Wayne Newton, Wayne Knight, Helen Hunt, Helen Hughes, \n",
|
||||
" James Watt, James Watt, Kevin Costner, Kevin Bacon, Kevin Love, Lisa Frank, Frank Drake, Drake, \n",
|
||||
" Oscar de la Renta, Oscar de la Hoya, Sean Hayes, Wallace Shawn, Wayne Howard, Wayne Brady, \n",
|
||||
" Oscar de_la Renta, Oscar de_la Hoya, Sean Hayes, Wallace Shawn, Wayne Howard, Wayne Brady, \n",
|
||||
" James Brady, Tom Brady, Helen Thomas, Tom Hanks, Hank Aaron, Aaron Carter, Stephen James, \n",
|
||||
" Will Smith, Kevin Smith, Kevin James, Garfield, James Garfield, Warren Buffett, Jimmy Buffett, \n",
|
||||
" Warren Beatty, Elizabeth Warren, Earl Warren, Elizabeth Kolbert, Stephen Colbert, \n",
|
||||
" Charles Wallace, James Monroe, Marilyn Monroe, Hank Williams, William C. Williams, Steve Harvey,\n",
|
||||
" Warren Beatty, Elizabeth Warren, Earl Warren, Elizabeth Kolbert, Stephen Colbert, George Wallace,\n",
|
||||
" Charles Wallace, James Monroe, Marilyn Monroe, Hank Williams, William_C. Williams, Steve Harvey,\n",
|
||||
" Domino Harvey, Harvey Milk, James Saint James, Etta James, Jim Jones, James Earl Jones, \n",
|
||||
" Charlie Parker, Ray Parker, Ray Charles, Charles Manson, Marilyn Manson, Robin Williams, \n",
|
||||
" Billy D. Williams, Will Wright, Fats Domino, Bill Clinton, Jimmy John, Tom Jones, Tommy John, \n",
|
||||
" Charlie Parker, Ray Parker_Jr., Ray Charles, Charles Manson, Marilyn Manson, Robin Williams, \n",
|
||||
" Billy_D. Williams, Will Wright, Fats Domino, Bill Clinton, Jimmy John, Tom Jones, Tommy John, \n",
|
||||
" Quincy Jones, James Earl Ray, Man Ray, Rachel Ray, Ray Allen, Tim Allen, Tim Cook, Tim Howard, \n",
|
||||
" Robin Wright, Wilbur Wright, Fatty Arbuckle, Fat Joe, George Clinton, John Kerry, \n",
|
||||
" Kerry Washington, John Irving, John Quincy Adams, John Adams, Amy Adams, Aimee Mann, Super Man, \n",
|
||||
" Bat Man, Ayn Rand, Lily Allen, Paul Allen, Ron Howard, Howard Hughes, Joe Kennedy, George Bush, \n",
|
||||
" George Washington, Washington Irving, Martha Washington, Ma Rainey, Jack Ma, Super Grover, \n",
|
||||
" Jack Black, Rand Paul, Paul Ryan, Paul Simon, Ron Paul, John Hughes, Langston Hughes, \n",
|
||||
" John F. Kennedy, Little Richard, Rich Little, Martha Stewart, Yo Yo Ma, Ma Bell, \n",
|
||||
" John_F. Kennedy, Little Richard, Rich Little, Martha Stewart, Yo-Yo Ma, Ma Bell, \n",
|
||||
" Grover Cleveland Alexander, Grover Cleveland, Jack White, Jack Ryan, Debby Ryan, Carly Simon, \n",
|
||||
" Carly Hughes, Charles Evans Hughes, John Williams, Little John, Stuart Little, Potter Stewart, \n",
|
||||
" Kristen Stewart, Kristen Bell, Kristen Hooks, Alexander Graham Bell, Franklin Graham, \n",
|
||||
" Kristen Stewart, Kristen Bell, Bell Hooks, Alexander Graham Bell, Franklin Graham, \n",
|
||||
" Lloyd Alexander, Meg White, Meg Ryan, Debbie Reynolds, John Reynolds, Carly Fiorina, \n",
|
||||
" Grace Lee Boggs, Wade Boggs, William Safire, Prince William, Little Prince, Harry Potter, \n",
|
||||
" James Potter, James Hook, James Dean, Aretha Franklin, Frank Lloyd Wright, Barry White, \n",
|
||||
@@ -265,138 +381,14 @@
|
||||
" Chris Hemsworth, Chris Evans, Topher Grace, Van Morrison, Sheryl Crow, Sheryl Sandberg, \n",
|
||||
" Cameron Crowe, Long John Silver, Olivia Newton John, Huey Long, John Edwards, Candy Crowley, \n",
|
||||
" Aleister Crowley, James Fenimore Cooper, James Cook, Robert Frost, Bob Evans, Evan Taylor Jones,\n",
|
||||
" James Cameron, Cam Newton, Cameron Diaz, Huey Newton, Huey Lewis, John Lewis, Jenny Lewis, \n",
|
||||
" Ryan Lewis, Burt Reynolds, Alistair Cooke, Alistair Cookie, Cokie Roberts, John Roberts, \n",
|
||||
" Robert Johnson, Robert E. Lee, Tommy Lee, Tommy Lee Jones, Etta James, John Oliver, \n",
|
||||
" Ryan Reynolds, Alastair Reynolds, George Wallace\"\"\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"270"
|
||||
]
|
||||
},
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
" Van Jones, James Cameron, Cam Newton, Cameron Diaz, Huey Newton, Huey Lewis, John Lewis, \n",
|
||||
" Jenny Lewis, Ryan Lewis, Burt Reynolds, Alistair Cooke, Alistair Cookie, Cokie Roberts, \n",
|
||||
" John Roberts, Robert Johnson, Robert E._Lee, Tommy Lee, Tommy Lee Jones, Etta James,\n",
|
||||
" John Oliver, Ryan Reynolds, Alastair Reynolds\"\"\") \n",
|
||||
"\n",
|
||||
"len(xkcdtiles)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[('JAMES EARL', 'RAY'),\n",
|
||||
" ('BURT', 'REYNOLDS'),\n",
|
||||
" ('CHRISTOPHER', 'COLUMBUS'),\n",
|
||||
" ('KRISTEN', 'STEWART'),\n",
|
||||
" ('FAT', 'JOE'),\n",
|
||||
" ('HAROLD', 'LLOYD'),\n",
|
||||
" ('ISAAC', 'HAYES'),\n",
|
||||
" ('PAUL', 'RYAN'),\n",
|
||||
" ('GENE', 'KELLY'),\n",
|
||||
" ('MEGYN', 'KELLY')]"
|
||||
]
|
||||
},
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"random.sample(xkcdtiles, 10)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Approximate and Partial Matches\n",
|
||||
"\n",
|
||||
"Two tile halves match if they are an exact match, like \"ADAMS\" and \"ADAMS\", or if they are an **approximate match**, like \"AMY\" and \"AIMEE\". To accomodate this, you can manually define allowable approximate matches by making the global variable `synsets` (synonym sets) be a mapping from a name to the set of approximate matches it should match, which can be done like this:\n",
|
||||
"\n",
|
||||
" synsets = synonyms(\"AMY=AIMEE, COOK=COOKE=COOKIE=COKIE, ...\")\n",
|
||||
"\n",
|
||||
"Another issue is a **partial match**: in the comic, some tiles, like \"FRANK LLOYD WRIGHT\" are 3 squares wide, and some, like \"PRINCE\" are only one square wide. For simplicity, I chose to have all my tiles be 2 squares wide, but I still want `'LLOYD'` to match the first name of `('FRANK LLOYD', 'WRIGHT')`. To accomplish this, the second argument to `synonyms` is a list of tiles; the function will go through these and add synset entries for parts of first names,\n",
|
||||
"so that both `'FRANK'` and `'LLOYD'` will match `'FRANK LLOYD'`. As for \"PRINCE\", he gets represented as the tile `('', 'PRINCE')`. "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import collections\n",
|
||||
"\n",
|
||||
"def synonyms(text='', tiles=()): \n",
|
||||
" \"synonyms('AMY=AIMEE') => dict(AMY={'AMY', 'AIMEE'}, AIMEE={'AMY', 'AIMEE'})\"\n",
|
||||
" synsets = collections.defaultdict(set)\n",
|
||||
" # Process `text`\n",
|
||||
" for text1 in text.upper().split(','):\n",
|
||||
" synset = set(text1.strip().split('='))\n",
|
||||
" for s in synset:\n",
|
||||
" synsets[s] |= synset\n",
|
||||
" # Process `tiles`\n",
|
||||
" for (first, last) in tiles:\n",
|
||||
" for part in first.split():\n",
|
||||
" synsets[part].add(first)\n",
|
||||
" synsets[first].add(part)\n",
|
||||
" return synsets\n",
|
||||
"\n",
|
||||
"synsets = synonyms(\"\"\"AMY=AIMEE, COOK=COOKE=COOKIE=COKIE, ALASTAIR=ALISTAIR, \n",
|
||||
" COLUMBO=COLUMBUS, SAFIRE=SAPPHIRE=GARNET, GARNET=RUBY, CHARLIE=CHARLES, SEAN=SHAWN,\n",
|
||||
" JIMMY=JAMES, MAN=MANN, JACK=JOHN, TOM=TOMMY, WILL=WILLIAM=WILLIAMS, ROBERT=ROBERTS=BOB, \n",
|
||||
" CAM=CAMERON, OLIVER=OLIVIA, EDWARD=EDWARDS, RICH=RICHARD, CHRIS=CHRISTOPHER=TOPHER, \n",
|
||||
" FAT=FATS=FATTY, WALT=WALTER, HANK=HANKS, CROW=CROWE, COLBERT=KOLBERT\"\"\", xkcdtiles)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"collapsed": true
|
||||
},
|
||||
"source": [
|
||||
"To make this work, I update `legal` to consult the `synsets` global variable for an approximate or partial matches, and while I'm changing `legal`, I'll also disallow a match between the empty first name of \"PRINCE\" with the empty first name of \"DRAKE\"."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def legal(name, loc, board):\n",
|
||||
" \"Is it legal to place this value on this location on board?\"\n",
|
||||
" return (board[loc] is empty and \n",
|
||||
" all(board[nbr] is empty \n",
|
||||
" or board[nbr] == name != '' \n",
|
||||
" or board[nbr] in synsets[name]\n",
|
||||
" for nbr in neighbors(loc, board)))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Final Result (with Random Restart)\n",
|
||||
"\n",
|
||||
"The program sometimes gets stuck after placing relatively few tiles. I could modify the program to *back up* in this case, but it will be easier to just *give up* and restart with an empty board. I can restart multiple times, and accept the best board (the one on which the most tiles were placed):"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
@@ -404,37 +396,126 @@
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<matplotlib.figure.Figure at 0x11058c668>"
|
||||
"[('EVAN', 'TAYLOR', 'JONES'),\n",
|
||||
" ('JOHN', 'LEWIS'),\n",
|
||||
" ('RYAN', 'REYNOLDS'),\n",
|
||||
" ('RAY', 'PARKER_JR.'),\n",
|
||||
" ('FATS', 'DOMINO'),\n",
|
||||
" ('ARETHA', 'FRANKLIN'),\n",
|
||||
" ('CHRIS', 'PRATT'),\n",
|
||||
" ('HENRY', 'JAMES'),\n",
|
||||
" ('FAT', 'JOE'),\n",
|
||||
" ('TOMMY', 'JOHN'),\n",
|
||||
" ('JAMES', 'NEWTON', 'HOWARD'),\n",
|
||||
" ('BARRY', 'WHITE')]"
|
||||
]
|
||||
},
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def best(boards): return max(boards, key=lambda board: len(board.boxes))\n",
|
||||
"\n",
|
||||
"plot_board(best(dominoes(xkcdtiles) for _ in range(100)))"
|
||||
"random.sample(xkcdtiles, 12)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"*Note:* I used a 16×24 square board while the xkcd comic used 27×35, but if I tried to fit 27 squares across then each tile would be smaller, and many names would overflow the sides of the dominoes too much. Here is the original xkcd comic:\n",
|
||||
"# Approximate Matches\n",
|
||||
"\n",
|
||||
"[](https://xkcd.com/1970/)\n",
|
||||
"Two names in adjacent squares match if they are an exact match, like `'ADAMS'` and `'ADAMS'`, or if they are an **approximate match**, like `'AMY'` and `'AIMEE'`. To accomodate this, I will manually define a set of approximate matches (again courtesy of \n",
|
||||
" [explainxkcd](http://www.explainxkcd.com/wiki/index.php/1970:_Name_Dominoes)) by making the global variable `synonyms` be a set of pairs of names that should match: `{('AMY', 'AIMEE'), ('COOK', 'COOKE'),...}`. I will also redefine the function `allowed` to say that synonyms are allowed.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def define_synonyms(text='', tiles=()) -> set: \n",
|
||||
" \"\"\"Build a set of {(A, B),...} pairs, meaning that A and B are synonyms.\"\"\"\n",
|
||||
" synsets = [synset.strip().split('=') for synset in text.split(',')]\n",
|
||||
" return {(a, b) for syn in synsets for a in syn for b in syn}\n",
|
||||
"\n",
|
||||
"synonyms = define_synonyms(\"\"\"AMY=AIMEE, COOK=COOKE=COOKIE=COKIE, ALASTAIR=ALISTAIR=ALESTIER, \n",
|
||||
" COLUMBO=COLUMBUS, SAFIRE=SAPPHIRE=GARNET, GARNET=RUBY, CHARLIE=CHARLES, SEAN=SHAWN,\n",
|
||||
" JIMMY=JIM=JAMES, MAN=MANN, JACK=JOHN=JON=JOHN_F., TOM=TOMMY=THOMAS, HOOK=HOOKS, \n",
|
||||
" ROBERT=ROBERTS=BOB, CAM=CAMERON, OLIVER=OLIVIA, EDWARD=EDWARDS, RICH=RICHARD, MEG=MEGYN, \n",
|
||||
" CHRIS=CHRISTOPHER=CHRISTINE=TOPHER, FAT=FATS=FATTY, WALT=WALTER, HANK=HANKS, CROW=CROWE, \n",
|
||||
" COLBERT=KOLBERT, LEE=E._LEE, KLINE=KLEIN, JOE=JOSEPH, GENE=EUGENE, KATE=KATHERINE, \n",
|
||||
" FRANK=FRANCIS=FRANKLIN, WATT=WATTS, STEVE=STEPHEN, DEBBY=DEBBIE, STUART=STEWART, \n",
|
||||
" WILL=WILLIAM=WILLIAMS=BILLY=BILLY_D.=WILLIAM_C., RUSS=RUSSELL, EVAN=EVANS\"\"\")\n",
|
||||
"\n",
|
||||
"def allowed(name, nbr_contents) -> bool: \n",
|
||||
" \"\"\"A name is allowed next to an empty square, a square off the board, an exact match, or a synonym.\"\"\"\n",
|
||||
" return nbr_contents in (name, empty, off) or (name, nbr_contents) in synonyms"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Final Result (after Multiple Tries)\n",
|
||||
"\n",
|
||||
"The program sometimes gets stuck after placing relatively few tiles. I could modify the program to *back up* in this case, but it will be easier to just *give up* and restart with an empty board. I can restart multiple times, and accept the best board (the one on which the most names were placed):"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"metadata": {
|
||||
"scrolled": false
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"152 tiles placed on 312 squares\n",
|
||||
"CPU times: user 3min 56s, sys: 940 ms, total: 3min 57s\n",
|
||||
"Wall time: 3min 59s\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/png": "iVBORw0KGgoAAAANSUhEUgAAA4sAAAd9CAYAAADOl8h0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjMsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+AADFEAAAgAElEQVR4nOzdd5hdZbXH8e8vgUAgCb13pEsLIBcEKQJSFEERRRS5IL0IiAWxYJCiUqWDgKIQqgrSQaqUSy/Sm7SQBEJPAiGEdf9Y7yF7zpkJJcnsk5nf53nmmTl7n7LOnLL3est6FRGYmZmZmZmZVfWpOwAzMzMzMzNrP04WzczMzMzMrIWTRTMzMzMzM2vhZNHMzMzMzMxaOFk0MzMzMzOzFk4WzczMzMzMrIWTRTMzMzMzM2vhZNHMzMzMzMxaOFk0MzMzMzOzFk4WzczMzMzMrIWTRTMzMzMzM2vhZNHMzMzMzMxaOFk0MzMzMzOzFk4WzczMzMzMrIWTRTMzMzMzM2vhZNHMzMzMzMxaOFk0MzMzMzOzFk4WzczMzMzMrIWTRTMzMzMzM2vhZNHMzMzMzMxaOFk0MzMzMzOzFk4WzczMzMzMrIWTRTMzMzMzM2vhZNHMzMzMzMxaOFk0MzMzMzOzFk4WzczMzMzMrIWTRTMzMzMzM2vhZNHMzMzMzMxaOFk0MzMzMzOzFk4WzczMzMzMrIWTRTMzMzMzM2vhZNHMzMzMzMxaOFk0MzMzMzOzFk4WzczMzMzMrIWTRTMzMzMzM2vhZNHMzMzMzMxaOFk0MzMzMzOzFk4WzczMzMzMrIWTRTMzMzMzM2vhZNHMzMzMzMxaOFk0MzMzMzOzFk4WzczMzMzMrIWTRTMzMzMzM2vhZNHMzMzMzMxaOFk0MzMzMzOzFk4WzczMzMzMrIWTRTMzMzMzM2vhZNHMzMzMzMxaOFk0MzMzMzOzFk4WzczMzMzMrIWTRTMzMzMzM2vhZNHMzMzMzMxaOFk0MzMzMzOzFk4WzczMzMzMrIWTRTMzMzMzM2vhZNHMzMzMzMxaOFk0MzMzMzOzFk4WzczMzMzMrIWTRTMzMzMzM2vhZNHMzMzMzMxaOFk0MzMzMzOzFk4WzczMzMzMrIWTRTMzMzMzM2vhZNHMzMzMzMxaOFk0MzMzMzOzFk4WzczMzMzMrIWTRTMzMzMzM2sxXd0BTA5JuwDb1h3HZBoaEaf1kOfSUwyNiNOgx7zHeooPX5eewu+vtuP3WHvqca+LtSd/XtpTD3hdpunXZFrvWdwWWLnuICbDykx880/rz6WnqL4m4NelXTS/Lj2F31/tw++x9tRTXxdrT/68tKdp+XWZ5l+Tabpnsbg/ItarO4hPQ9KNTZum2efSU3TymoBfl9p18br0FH5/tQG/x9pTD39drD3589KepsnXpSe8JtN6z6KZmZmZmZlNBU4WzczMzMzMrEVPGIZqZmZmNZC0BjAQeK9pV19gNmBURNzU7YGZmdkU4WTRzMzMPq0dgaVoHam0ILAocD+wSjfHZFYrSV8F/hMR/5UkYPaIeLWyv19ENDewWA0kzQusBVwZEWPrjqcdeRhqhaQFy++BkvaWtIMkJ9RmZmadiIhdImK9iFin8QP8kexVPBFYu94IzWpxBDC4/P1l4NzGDknzA0/XEZR1JGkw8AhwPNnoZZ1wstjRUeX3mcAo4N3yt5mZmU2CpP6SzgIOBbaIiL3dUm+91MLAM+Xvt4B5Kvv6N122+pwB3AgsEBH3N++UtISkE7o9qjbjZLGj2ST1AxaOiHMj4lxgkbqDMjMza2eSFgX+jzwJXjEibq41ILN6zQB8UP4eAczdtL9v94ZjzSQtTa6B+MuIiC6utiKwR/dF1Z48xLKjX5MtDPcCSFoc6OoNZGZm1utJWh34J3AZsEtEfPARNzHr6UYBn5X0n/L3HJV9A8jeRqvXGHIE4eGSbgBeIF+rMWTBrgnkvOtez8liRUTcJmntyoFuUWCnGkMyMzNrW5LWAy4FjoyIIU37ZgbmiIjn64jNrEbvAueUHwAkvUCO6JsNOK2muKyIiBclrQJsB2wGfAaYl+wVtgonixWSfgvMK+ldYBhwS0Q8VXNYZmZm7epscgTOBpK+BPQjT7bmBOYiR+r8T33hmdViMYCImAAgaR5gRnJo6msRMabG2KyIiMeAn1e3leloMwLTA18l5zX2ak4WKyLigFLieCCwELCppO0iYseaQzMzM2tHJwK3Nm2bALwDDI+I4d0fklntBgMbSZqPLGhTnaM4QdLDEXFsPaHZpJQlTd4DkDQCGFlvRPVzstikTHJ9C3gYeFjSxTWHZGZm1q7mIOcrPgdcDfys0Zti1ov9ChhLVkR9FhhHJiADgCFkj7uTxTYhaQvgi2Re9BRwaRlZeBUeGeFksaoME5i5XJwZ2AB4qb6IzMzM2ldE/EjSQcAB5HCurSQNAf46iQqDZj1aRHy1eVtZX/EK4AFyeKPVTFIf4CJgU+Bm4DVgV+BISWcDe3vOtZfOaPYt4LvAd4ANgfsjoteXzDUzM+tKmX91Ijl38c9kZfF7Ja1fY1hmbUPScuTSMo8BG0WEq6G2h32AjYF1I2LjiPh2RCwDbAR8Afi3pAG1RtgGnCx2dArwEFnZ7Q5gJ0kL1huSmZlZe4uIEeRwu5eBLch5i9dJOrnOuMzqJukbwL+BUyNim4h4t+6Y7EM7AqdHxJ3VjRFxPbAGOcx+SGc37E2cLHb0Z3I8+Z5k9aNDgSPqDMjMzKxdSZpF0raSjiWPnycCt5DztU4BbqgzPrO6SBoo6QzgOOBbEXFo3TFZiyWAezrbEREvA8cD3+zWiNqQk8WOFgAuBo4BHoqIR4G56w3J2l05IJwjaaykNyX9XNK5nVxvV0m7Seov6TpJ10g6qMxjaFxnP0njJX0gaYykZyXtW9m/qKSLJb0s6U5JG3TX8zSbEiRdKun18j5/U9KRk3l/90kaLek9Sa9K2m9KxWofyx3AT8jkcE9g6YiYJSLWiYg9IuKCesMz635lGZlHgFWBrwPPSFq8/CwqqX+9EVrxGjDPJPa/gPMAJ4tNLgMOJw98SPo7LnBjkyCpL3AJMAuwJlk5621gvk6uvho5p2ce4HPlup8DHpK0TrnOScCywJXAHsBawB/LY80P3E6enG0KHA28MTWel9mUIOnG8nsmSf8uRcRmJeeE9wMWAY6StJGkq0uxgcZt15J0rKRlJN0jaabKvoUl/a1cnAlYNiL6AcsAf5U0m6QXJB0taZHK7daXdFrl8vySrp56/4FeYbmIWDkiDoyIiyLi6boDMmsDR5HfdbMDQ4HrKj+34h73dnEBsHc5NnVmIXJofa/mZLEiIo6IiD0j4n8jYmuyupuXzrBJ+QF5QNgqIh4AtiVbEzvTD3ifXLNnxog4OiK+AuwFnC+pf0SMK+WaPwDGRMSwyuK9uwPXR8ThEXFPRJwXEZ0On+iKpG9J+nzl8jGll/J+SXdJ+krZfpOkfpKWkvTXyvU3l3SDpL9KGiTpLyURkKQbGyf7ks4rv7csPT+3ShpS7vPvJQlYuFxnz/J4oyTdIuk0SX+oPObMZFLRq0jaWtLbkt4vvcxDyvaZJJ0q6TVJIyWdWXqrB0q6qOk+Npe0k6QdJZ3ZtG/b5l44SV8vjzmuvB43SZqcqtmLShJwFnBLRIwk57I9F+mNsg5fP7L69I8rt32RLDDQF1iJPPlqGA403sfjI+IFgIh4JSJGkY03TwJPAJdLOqy8N28CViit/gD7k42E9ilFxAfKoaj7lfflkZK+oVzY2qxXiogVImJgRCwcEYtHxGKVnwUiYo26YzQAfgm8Atwh6XulAwAASQsBO5NLAvVqThYrJO1STlQvLL2KewLj647L2lMZRnIgsFtEjAP4iPXFpiMXqp4BaCSARMRQ4E1gucp1ZwSaJ8H3B+aSNHAywu5P9sQ0zAVcUXoGPhcRl5UWtnWAzwLrkZO8G0vL7EFWctuuVHN7gFyb6H/IXtAFJC0DjJQ0L5ngrhERa5FDOeYj15u6HDhH0lwRcWJErAtcFxFrR8Qu5KTyaoyajOc8TYqICyNiIHAN8PmIOKjsOpV8vyxAJtGvkknPDMCcTXezNNlA0R/4nqRvVfYNA9Zuuv4g4NiImCEi5gQ2i4j3J/Op/I78Hj2wXJ4e+ElJLC4rDRRjgLOBbSU11rQaXuIZA1wLLC3p6wARMZ6yaDIwk6SDJZ0h6QpJq5XtYyPiFDLR7A8cGhEfALsAh5eeyq2AP03m8+vVJM0F3Ad8mxzpMDs51/+pSlJu1qtIuk3SUXKRxLYWEaPJRsmLyTnWb5QG7gfJxsZ3ySWBejUnix2NJ9dXnI3sHbqQrIxq1pkNgCebq2gVs0jaXtLxyuF3I8ilWd4iE6YXJU0vaTFJh5HJ0ZOV209HJlVVJwNLAW8q53pdLWmFyXwO7wGLS9pL0qGlJW0l4C5yiOwmwLOSpicTlNHA7eXEfDoy6VsH+DK5MPcS5GTwoeQyNAc3EmngYWBRYHREXEsmPetWYvmgixinm8S+3kBkbxwlEf9MRPwkIt4pVfVOBFbv4rZ9yNd4DFlkYYikxcu+l8hkrNlOkl6RNBz43mTGvhD5Ptq+sube3GSCe2uJ6ZayfTSwDXCmpIUi4j1y2Dbk8/828BtJK5Vt40qv5QDyvXkd2fv4cDWA0oDzQ+CrkqaLiP8AjwL/AC4vJwv26f0OuDsiVo+In0bEjsBngEOAv0vaqN7wzGqxCzAv8ISkq8pIkb4fdSPrfhExNiL2Jc/NvgecA/wF+AawUhkR06tNzvCiHici/gT8qZyQ7UOefNxFa+u71UzSmhFxe81hLEfOH2zWj+zR+S5ZZetk4KnyeyyZGC7PxJ6RO4FNmtZdEhNPlAGIiKclfYZMuBYhe+2ulLRw6TH5OPoA35D0PfKLsQ85wfu/5HzI4eRB7kRyXtnAsn3ZiHgQ2BpA0s+Line truncated
|
||||
"text/plain": [
|
||||
"<Figure size 1152x2520 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"%time plot_board(max((place_tiles(xkcdtiles) for _ in range(100)), key=len))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"*Note:* I used a 16×35 square board while the xkcd comic used 27×35, because if I tried to fit 27 squares across then each tile would be smaller, and many names would overflow the sides of the dominoes too much. Here is the [original xkcd comic](https://xkcd.com/1970/):\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# What's Next?\n",
|
||||
"\n",
|
||||
"I'm happy with the results, but here are some ideas for improvements, if you want something to work on:\n",
|
||||
"- Allow tiles that are 1 or 3 squares wide, like `('PRINCE',)` or `('FRANK', 'LLOYD', 'WRIGHT')`.\n",
|
||||
"- Print names vertically in tiles that are placed vertically, and upside down for tiles that are placed horizontally, but with the first name on the right.\n",
|
||||
"- Print shorter names with a bigger font and longer names with a smaller font.\n",
|
||||
"- Download a bunch of names from Wikipedia and fill a 200 × 300 board.\n",
|
||||
"- If you like xkcd try [regex golf](https://github.com/norvig/pytudes/blob/master/ipynb/xkcd1313.ipynb), and [part 2](https://github.com/norvig/pytudes/blob/master/ipynb/xkcd1313-part2.ipynb).\n"
|
||||
"- See if a different strategy can fit more tiles on the board. \n",
|
||||
" - Backtracking? \n",
|
||||
" - Being smarter about which frontier square to try next? \n",
|
||||
" - Being smarter about which tile to place on that frontier square?\n",
|
||||
"- Make the dominoes black with white letters? Ever-so-slightly round the corners?\n",
|
||||
"- Download a bunch of names from Wikipedia and fill a 200 × 300 board, then laser cut tham and hang them on your wall?\n",
|
||||
"- If you like xkcd try [regex golf](https://github.com/norvig/pytudes/blob/master/ipynb/xkcd1313.ipynb), and [part 2](https://github.com/norvig/pytudes/blob/master/ipynb/xkcd1313-part2.ipynb).\n",
|
||||
"- What do you want to do?\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -454,7 +535,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.5.3"
|
||||
"version": "3.7.7"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
Reference in new issue
Block a user