Renamed wordsearch, added example problem
[ou-summer-of-code-2017.git] / 04-word-search / wordsearch-creation.ipynb
diff --git a/04-word-search/wordsearch-creation.ipynb b/04-word-search/wordsearch-creation.ipynb
new file mode 100644 (file)
index 0000000..5259725
--- /dev/null
@@ -0,0 +1,1650 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import string\n",
+    "import re\n",
+    "import random\n",
+    "import collections\n",
+    "import copy\n",
+    "\n",
+    "from enum import Enum\n",
+    "Direction = Enum('Direction', 'left right up down upleft upright downleft downright')\n",
+    "    \n",
+    "delta = {Direction.left: (0, -1),Direction.right: (0, 1), \n",
+    "         Direction.up: (-1, 0), Direction.down: (1, 0), \n",
+    "         Direction.upleft: (-1, -1), Direction.upright: (-1, 1), \n",
+    "         Direction.downleft: (1, -1), Direction.downright: (1, 1)}\n",
+    "\n",
+    "cat = ''.join\n",
+    "wcat = ' '.join\n",
+    "lcat = '\\n'.join"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# all_words = [w.strip() for w in open('/usr/share/dict/british-english').readlines()\n",
+    "#             if all(c in string.ascii_lowercase for c in w.strip())]\n",
+    "# words = [w for w in all_words\n",
+    "#          if not any(w in w2 for w2 in all_words if w != w2)]\n",
+    "# open('wordsearch-words', 'w').write(lcat(words))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# ws_words = [w.strip() for w in open('wordsearch-words').readlines()\n",
+    "#             if all(c in string.ascii_lowercase for c in w.strip())]\n",
+    "# ws_words[:10]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "ws_words = [w.strip() for w in open('/usr/share/dict/british-english').readlines()\n",
+    "            if all(c in string.ascii_lowercase for c in w.strip())]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "def empty_grid(w, h):\n",
+    "    return [['.' for c in range(w)] for r in range(h)]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "def show_grid(grid):\n",
+    "    return lcat(cat(r) for r in grid)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "..........\n",
+      "..........\n",
+      "..........\n",
+      "..........\n",
+      "..........\n",
+      "..........\n",
+      "..........\n",
+      "..........\n",
+      "..........\n",
+      "..........\n"
+     ]
+    }
+   ],
+   "source": [
+    "grid = empty_grid(10, 10)\n",
+    "print(show_grid(grid))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "def indices(grid, r, c, l, d):\n",
+    "    dr, dc = delta[d]\n",
+    "    w = len(grid[0])\n",
+    "    h = len(grid)\n",
+    "    inds = [(r + i * dr, c + i * dc) for i in range(l)]\n",
+    "    return [(i, j) for i, j in inds\n",
+    "           if i >= 0\n",
+    "           if j >= 0\n",
+    "           if i < h\n",
+    "           if j < w]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "def gslice(grid, r, c, l, d):\n",
+    "    return [grid[i][j] for i, j in indices(grid, r, c, l, d)]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "def set_grid(grid, r, c, d, word):\n",
+    "    for (i, j), l in zip(indices(grid, r, c, len(word), d), word):\n",
+    "        grid[i][j] = l\n",
+    "    return grid"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "..........\n",
+      "..........\n",
+      "...t......\n",
+      "....e.....\n",
+      ".....s....\n",
+      "......t...\n",
+      ".......w..\n",
+      "........o.\n",
+      ".........r\n",
+      "..........\n"
+     ]
+    }
+   ],
+   "source": [
+    "set_grid(grid, 2, 3, Direction.downright, 'testword')\n",
+    "print(show_grid(grid))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'..e.....'"
+      ]
+     },
+     "execution_count": 12,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "cat(gslice(grid, 3, 2, 15, Direction.right))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "<_sre.SRE_Match object; span=(0, 4), match='keen'>"
+      ]
+     },
+     "execution_count": 13,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "re.match(cat(gslice(grid, 3, 2, 4, Direction.right)), 'keen')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 14,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "<_sre.SRE_Match object; span=(0, 3), match='kee'>"
+      ]
+     },
+     "execution_count": 14,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "re.match(cat(gslice(grid, 3, 2, 3, Direction.right)), 'keen')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 15,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "re.fullmatch(cat(gslice(grid, 3, 2, 3, Direction.right)), 'keen')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 16,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "re.match(cat(gslice(grid, 3, 2, 4, Direction.right)), 'kine')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 17,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "def could_add(grid, r, c, d, word):\n",
+    "    s = gslice(grid, r, c, len(word), d)\n",
+    "    return re.fullmatch(cat(s), word)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 18,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "<_sre.SRE_Match object; span=(0, 4), match='keen'>"
+      ]
+     },
+     "execution_count": 18,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "could_add(grid, 3, 2, Direction.right, 'keen')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 19,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "could_add(grid, 3, 2, Direction.right, 'kine')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 20,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "<Direction.right: 2>"
+      ]
+     },
+     "execution_count": 20,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "random.choice(list(Direction))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 58,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "def fill_grid(grid, words, word_count, max_attempts=10000):\n",
+    "    attempts = 0\n",
+    "    added_words = []\n",
+    "    w = len(grid[0])\n",
+    "    h = len(grid)\n",
+    "    while len(added_words) < word_count and attempts < max_attempts:\n",
+    "        attempts += 1\n",
+    "        r = random.randrange(w)\n",
+    "        c = random.randrange(h)\n",
+    "        word = random.choice(words)\n",
+    "        d = random.choice(list(Direction))\n",
+    "        if len(word) >=4 and not any(word in w2 for w2 in added_words) and could_add(grid, r, c, d, word):\n",
+    "            set_grid(grid, r, c, d, word)\n",
+    "            added_words += [word]\n",
+    "            attempts = 0\n",
+    "    return grid, added_words"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 22,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "40"
+      ]
+     },
+     "execution_count": 22,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "g = empty_grid(20, 20)\n",
+    "g, ws = fill_grid(g, ws_words, 40)\n",
+    "len(ws)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 23,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "..pouchexecrate.....\n",
+      ".hazardous....t.wive\n",
+      "...sniradnam.aselcnu\n",
+      "s.weeklies.rnb......\n",
+      "e..lamenessse.o.....\n",
+      "i.tsallab..s.a.i....\n",
+      "tslim.......f.c.l...\n",
+      "iwheelbase...f.tges.\n",
+      "ed....llabhgihinirr.\n",
+      "dw.limbs..nj.bitev.s\n",
+      "te.wiltediu.ructs.e.\n",
+      "elsombretv.oqes..a..\n",
+      "ll..e..te.iela....m.\n",
+      "ie.a..un.lheleiretoc\n",
+      "ord..bi.scsp...kiths\n",
+      "ts.malcentilitren...\n",
+      "..i.e.sexetrov.stolb\n",
+      ".r.s...ruof....htrof\n",
+      "bgnihsirevopmi.....c\n",
+      "....graciousness....\n",
+      "40 words added\n",
+      "lameness impoverishing plasters wilted toilet forth coterie hazardous abutting chequing weeklies sombre execrate mastiffs boilers uncles centilitre mandarins wheelbase graciousness vortexes dwellers ballast limbs four tans highball wive broils beads mils reactive select deities shtik juveniles blots pouch brim coon\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(show_grid(g))\n",
+    "print(len(ws), 'words added')\n",
+    "print(wcat(ws))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 24,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "def present(grid, word):\n",
+    "    w = len(grid[0])\n",
+    "    h = len(grid)\n",
+    "    for r in range(h):\n",
+    "        for c in range(w):\n",
+    "            for d in Direction:\n",
+    "                if cat(gslice(grid, r, c, len(word), d)) == word:\n",
+    "                    return True, r, c, d\n",
+    "    return False, 0, 0, list(Direction)[0]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 25,
+   "metadata": {
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "lameness (True, 4, 3, <Direction.right: 2>)\n",
+      "impoverishing (True, 18, 13, <Direction.left: 1>)\n",
+      "plasters (True, 14, 11, <Direction.upright: 6>)\n",
+      "wilted (True, 10, 3, <Direction.right: 2>)\n",
+      "toilet (True, 15, 0, <Direction.up: 3>)\n",
+      "forth (True, 17, 19, <Direction.left: 1>)\n",
+      "coterie (True, 13, 19, <Direction.left: 1>)\n",
+      "hazardous (True, 1, 1, <Direction.right: 2>)\n",
+      "abutting (True, 15, 4, <Direction.upright: 6>)\n",
+      "chequing (True, 14, 9, <Direction.upright: 6>)\n",
+      "weeklies (True, 3, 2, <Direction.right: 2>)\n",
+      "sombre (True, 11, 2, <Direction.right: 2>)\n",
+      "execrate (True, 0, 7, <Direction.right: 2>)\n",
+      "mastiffs (True, 12, 18, <Direction.upleft: 5>)\n",
+      "boilers (True, 3, 13, <Direction.downright: 8>)\n",
+      "uncles (True, 2, 19, <Direction.left: 1>)\n",
+      "centilitre (True, 15, 6, <Direction.right: 2>)\n",
+      "mandarins (True, 2, 11, <Direction.left: 1>)\n",
+      "wheelbase (True, 7, 1, <Direction.right: 2>)\n",
+      "graciousness (True, 19, 4, <Direction.right: 2>)\n",
+      "vortexes (True, 16, 13, <Direction.left: 1>)\n",
+      "dwellers (True, 8, 1, <Direction.down: 4>)\n",
+      "ballast (True, 5, 8, <Direction.left: 1>)\n",
+      "limbs (True, 9, 3, <Direction.right: 2>)\n",
+      "four (True, 17, 10, <Direction.left: 1>)\n",
+      "tans (True, 1, 14, <Direction.downleft: 7>)\n",
+      "highball (True, 8, 13, <Direction.left: 1>)\n",
+      "wive (True, 1, 16, <Direction.right: 2>)\n",
+      "broils (True, 9, 13, <Direction.downleft: 7>)\n",
+      "beads (True, 11, 5, <Direction.downleft: 7>)\n",
+      "mils (True, 6, 4, <Direction.left: 1>)\n",
+      "reactive (True, 3, 11, <Direction.downright: 8>)\n",
+      "select (True, 14, 10, <Direction.upright: 6>)\n",
+      "deities (True, 9, 0, <Direction.up: 3>)\n",
+      "shtik (True, 14, 19, <Direction.left: 1>)\n",
+      "juveniles (True, 9, 11, <Direction.downleft: 7>)\n",
+      "blots (True, 16, 19, <Direction.left: 1>)\n",
+      "pouch (True, 0, 2, <Direction.right: 2>)\n",
+      "brim (True, 18, 0, <Direction.upright: 6>)\n",
+      "coon (True, 18, 19, <Direction.upleft: 5>)\n"
+     ]
+    }
+   ],
+   "source": [
+    "for w in ws:\n",
+    "    print(w, present(g, w))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 47,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "def interesting(grid, words, words_limit=40, direction_slack=1):\n",
+    "    dirs = set(present(grid, w)[3] for w in words)\n",
+    "    return len(words) > words_limit and len(dirs) + direction_slack >= len(delta)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 48,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "True"
+      ]
+     },
+     "execution_count": 48,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "interesting(g, ws)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 51,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "def interesting_grid(width=20, height=20, words_limit=40, direction_slack=1):\n",
+    "    boring = True\n",
+    "    while boring:\n",
+    "        grid = empty_grid(width, height)\n",
+    "        grid, words = fill_grid(grid, ws_words, 80)\n",
+    "        boring = not interesting(grid, words, words_limit=words_limit, direction_slack=direction_slack)\n",
+    "    return grid, words"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 52,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "dexawwt....ybossm..t\n",
+      "snaprhlukulele..o.er\n",
+      "...erioteb.hrevordla\n",
+      "..sivc..pr.tdekcahkp\n",
+      "atve..h.oiselapcy.cd\n",
+      "nesrehpargohtill.pue\n",
+      "n.leaveyis.smuguhhrt\n",
+      "eyhsifnt.r...aeesetc\n",
+      "xverbseebasedlritl.a\n",
+      ".r..k.wie..mdonaiaeg\n",
+      "..ac.nsof.admeeulsoo\n",
+      "g.opo.cmanaotpqiinmp\n",
+      "nc.us.a.lpnteaerdkiu\n",
+      "ionjoys.leirinioicns\n",
+      "wnkcent.skeracllkase\n",
+      "oab..erusopxeao.antt\n",
+      "dureifidimuhed.nskea\n",
+      "aga...capitols..scrl\n",
+      "h.v.sandblaster..i.i\n",
+      "s.e..sdratoelhitsn.d\n",
+      "61 words added;  8 directions\n",
+      "newscast kittenish cocks lithographers truckle leotards they exposure dehumidifier sandblaster alien paddle shadowing gondola wrest joys minster pales chairs fishy capitols based gums pheromones saki moiety waxed guano thriven dilate moray icons adman ukulele hacked rope rise clue acted nicknack spar verbs boss annex neck repeater befalls drover leave pans brave brigs opus live noun tail riot care hits quilt part\n"
+     ]
+    }
+   ],
+   "source": [
+    "g, ws = interesting_grid()\n",
+    "print(show_grid(g))\n",
+    "print(len(ws), 'words added; ', len(set(present(g, w)[3] for w in ws)), 'directions')\n",
+    "print(wcat(ws))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 30,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "def datafile(name, sep='\\t'):\n",
+    "    \"\"\"Read key,value pairs from file.\n",
+    "    \"\"\"\n",
+    "    with open(name) as f:\n",
+    "        for line in f:\n",
+    "            splits = line.split(sep)\n",
+    "            yield [splits[0], int(splits[1])]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 31,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "def normalise(frequencies):\n",
+    "    \"\"\"Scale a set of frequencies so they sum to one\n",
+    "    \n",
+    "    >>> sorted(normalise({1: 1, 2: 0}).items())\n",
+    "    [(1, 1.0), (2, 0.0)]\n",
+    "    >>> sorted(normalise({1: 1, 2: 1}).items())\n",
+    "    [(1, 0.5), (2, 0.5)]\n",
+    "    >>> sorted(normalise({1: 1, 2: 1, 3: 1}).items()) # doctest: +ELLIPSIS\n",
+    "    [(1, 0.333...), (2, 0.333...), (3, 0.333...)]\n",
+    "    >>> sorted(normalise({1: 1, 2: 2, 3: 1}).items())\n",
+    "    [(1, 0.25), (2, 0.5), (3, 0.25)]\n",
+    "    \"\"\"\n",
+    "    length = sum(f for f in frequencies.values())\n",
+    "    return collections.defaultdict(int, ((k, v / length) \n",
+    "        for (k, v) in frequencies.items()))\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 32,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "english_counts = collections.Counter(dict(datafile('count_1l.txt')))\n",
+    "normalised_english_counts = normalise(english_counts)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 33,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "wordsearch_counts = collections.Counter(cat(ws_words))\n",
+    "normalised_wordsearch_counts = normalise(wordsearch_counts)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 34,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "normalised_wordsearch_counts = normalise(collections.Counter(normalised_wordsearch_counts) + collections.Counter({l: 0.05 for l in string.ascii_lowercase}))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 35,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "def weighted_choice(d):\n",
+    "    \"\"\"Generate random item from a dictionary of item counts\n",
+    "    \"\"\"\n",
+    "    target = random.uniform(0, sum(d.values()))\n",
+    "    cuml = 0.0\n",
+    "    for (l, p) in d.items():\n",
+    "        cuml += p\n",
+    "        if cuml > target:\n",
+    "            return l\n",
+    "    return None\n",
+    "\n",
+    "def random_english_letter():\n",
+    "    \"\"\"Generate a random letter based on English letter counts\n",
+    "    \"\"\"\n",
+    "    return weighted_choice(normalised_english_counts)\n",
+    "\n",
+    "def random_wordsearch_letter():\n",
+    "    \"\"\"Generate a random letter based on wordsearch letter counts\n",
+    "    \"\"\"\n",
+    "    return weighted_choice(normalised_wordsearch_counts)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 36,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'aaaaaabbccddeeeeeeeeeeeffggghhhhhhiiiiiiiiillllmmnnnnnnnooooooooooppppqrrrrrrrssstttttttttuuwyyyyyyz'"
+      ]
+     },
+     "execution_count": 36,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "cat(sorted(random_english_letter() for i in range(100)))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 37,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'aaaaaaaabbbccccddeeeeeeeeggghhhiiiiiiiijjkkllllnnnnoooooooooopppqqrrrrrssssssttttttuuuuvvwxxxxxyzzzz'"
+      ]
+     },
+     "execution_count": 37,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "cat(sorted(random_wordsearch_letter() for i in range(100)))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 38,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'x'"
+      ]
+     },
+     "execution_count": 38,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "random_wordsearch_letter()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 39,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "def pad_grid(g0):\n",
+    "    grid = copy.deepcopy(g0)\n",
+    "    w = len(grid[0])\n",
+    "    h = len(grid)\n",
+    "    for r in range(h):\n",
+    "        for c in range(w):\n",
+    "            if grid[r][c] == '.':\n",
+    "                grid[r][c] = random_wordsearch_letter()\n",
+    "    return grid"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 53,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "dexawwtmxepybossmezt\n",
+      "snaprhlukulelefaoder\n",
+      "ijreriotebahrevordla\n",
+      "jrsivcciprstdekcahkp\n",
+      "atvecshkoiselapcydcd\n",
+      "nesrehpargohtilljpue\n",
+      "nuleaveyisasmuguhhrt\n",
+      "eyhsifntzrmnsaeesetc\n",
+      "xverbseebasedlritlla\n",
+      "prcckzwiefamdonaiaeg\n",
+      "osacqnsofgadmeeulsoo\n",
+      "gdoporcmanaotpqiinmp\n",
+      "ncmusbaslpnteaerdkiu\n",
+      "ionjoyswleirinioicns\n",
+      "wnkcentcskeracllkase\n",
+      "oabsuerusopxeaodantt\n",
+      "dureifidimuhedgnskea\n",
+      "agagvccapitolsgyscrl\n",
+      "hkvgsandblasterdhihi\n",
+      "syenesdratoelhitsnod\n"
+     ]
+    }
+   ],
+   "source": [
+    "padded = pad_grid(g)\n",
+    "print(show_grid(padded))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 54,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "dexawwt....ybossm..t\n",
+      "snaprhlukulele..o.er\n",
+      "...erioteb.hrevordla\n",
+      "..sivc..pr.tdekcahkp\n",
+      "atve..h.oiselapcy.cd\n",
+      "nesrehpargohtill.pue\n",
+      "n.leaveyis.smuguhhrt\n",
+      "eyhsifnt.r...aeesetc\n",
+      "xverbseebasedlritl.a\n",
+      ".r..k.wie..mdonaiaeg\n",
+      "..ac.nsof.admeeulsoo\n",
+      "g.opo.cmanaotpqiinmp\n",
+      "nc.us.a.lpnteaerdkiu\n",
+      "ionjoys.leirinioicns\n",
+      "wnkcent.skeracllkase\n",
+      "oab..erusopxeao.antt\n",
+      "dureifidimuhed.nskea\n",
+      "aga...capitols..scrl\n",
+      "h.v.sandblaster..i.i\n",
+      "s.e..sdratoelhitsn.d\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(show_grid(g))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 55,
+   "metadata": {
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "newscast (True, 7, 6, <Direction.down: 4>)\n",
+      "kittenish (True, 14, 9, <Direction.upright: 6>)\n",
+      "cocks (True, 12, 1, <Direction.upright: 6>)\n",
+      "lithographers (True, 5, 14, <Direction.left: 1>)\n",
+      "truckle (True, 7, 18, <Direction.up: 3>)\n",
+      "leotards (True, 19, 12, <Direction.left: 1>)\n",
+      "they (True, 3, 11, <Direction.up: 3>)\n",
+      "exposure (True, 15, 12, <Direction.left: 1>)\n",
+      "dehumidifier (True, 16, 13, <Direction.left: 1>)\n",
+      "sandblaster (True, 18, 4, <Direction.right: 2>)\n",
+      "alien (True, 9, 17, <Direction.downleft: 7>)\n",
+      "paddle (True, 12, 9, <Direction.upright: 6>)\n",
+      "shadowing (True, 19, 0, <Direction.up: 3>)\n",
+      "gondola (True, 9, 19, <Direction.downleft: 7>)\n",
+      "wrest (True, 0, 5, <Direction.downleft: 7>)\n",
+      "joys (True, 13, 3, <Direction.right: 2>)\n",
+      "minster (True, 11, 18, <Direction.down: 4>)\n",
+      "pales (True, 4, 14, <Direction.left: 1>)\n",
+      "chairs (True, 3, 5, <Direction.downright: 8>)\n",
+      "fishy (True, 7, 5, <Direction.left: 1>)\n",
+      "capitols (True, 17, 6, <Direction.right: 2>)\n",
+      "based (True, 8, 8, <Direction.right: 2>)\n",
+      "gums (True, 6, 14, <Direction.left: 1>)\n",
+      "pheromones (True, 5, 17, <Direction.downleft: 7>)\n",
+      "saki (True, 16, 16, <Direction.up: 3>)\n",
+      "moiety (True, 11, 7, <Direction.up: 3>)\n",
+      "waxed (True, 0, 4, <Direction.left: 1>)\n",
+      "guano (True, 17, 1, <Direction.up: 3>)\n",
+      "thriven (True, 0, 6, <Direction.downleft: 7>)\n",
+      "dilate (True, 19, 19, <Direction.up: 3>)\n",
+      "moray (True, 0, 16, <Direction.down: 4>)\n",
+      "icons (True, 13, 12, <Direction.downright: 8>)\n",
+      "adman (True, 7, 13, <Direction.downleft: 7>)\n",
+      "ukulele (True, 1, 7, <Direction.right: 2>)\n",
+      "hacked (True, 3, 17, <Direction.left: 1>)\n",
+      "rope (True, 5, 8, <Direction.up: 3>)\n",
+      "rise (True, 12, 15, <Direction.upright: 6>)\n",
+      "clue (True, 4, 15, <Direction.down: 4>)\n",
+      "acted (True, 8, 19, <Direction.up: 3>)\n",
+      "nicknack (True, 19, 17, <Direction.up: 3>)\n",
+      "spar (True, 12, 4, <Direction.upleft: 5>)\n",
+      "verbs (True, 8, 1, <Direction.right: 2>)\n",
+      "boss (True, 0, 12, <Direction.right: 2>)\n",
+      "annex (True, 4, 0, <Direction.down: 4>)\n",
+      "neck (True, 14, 5, <Direction.left: 1>)\n",
+      "repeater (True, 13, 11, <Direction.upright: 6>)\n",
+      "befalls (True, 8, 8, <Direction.down: 4>)\n",
+      "drover (True, 2, 17, <Direction.left: 1>)\n",
+      "leave (True, 6, 2, <Direction.right: 2>)\n",
+      "pans (True, 1, 3, <Direction.left: 1>)\n",
+      "brave (True, 15, 2, <Direction.down: 4>)\n",
+      "brigs (True, 2, 9, <Direction.down: 4>)\n",
+      "opus (True, 10, 19, <Direction.down: 4>)\n",
+      "live (True, 1, 6, <Direction.downleft: 7>)\n",
+      "noun (True, 10, 5, <Direction.downleft: 7>)\n",
+      "tail (True, 11, 12, <Direction.downright: 8>)\n",
+      "riot (True, 2, 4, <Direction.right: 2>)\n",
+      "care (True, 14, 13, <Direction.left: 1>)\n",
+      "hits (True, 19, 13, <Direction.right: 2>)\n",
+      "quilt (True, 11, 14, <Direction.upright: 6>)\n",
+      "part (True, 3, 19, <Direction.up: 3>)\n"
+     ]
+    }
+   ],
+   "source": [
+    "for w in ws:\n",
+    "    print(w, present(padded, w))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 43,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def decoys(grid, words, all_words, limit=100):\n",
+    "    decoy_words = []\n",
+    "    dlen_limit = max(len(w) for w in words)\n",
+    "    while len(words) + len(decoy_words) < limit:\n",
+    "        d = random.choice(all_words)\n",
+    "        if d not in words and len(d) >= 4 and len(d) <= dlen_limit and not present(grid, d)[0]:\n",
+    "            decoy_words += [d]\n",
+    "    return decoy_words"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 44,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "['friendship',\n",
+       " 'stepping',\n",
+       " 'featheriest',\n",
+       " 'thriftily',\n",
+       " 'mutilation',\n",
+       " 'nook',\n",
+       " 'clewing',\n",
+       " 'meditated',\n",
+       " 'gooier',\n",
+       " 'cripples',\n",
+       " 'ponderously',\n",
+       " 'roundelay',\n",
+       " 'curtailed',\n",
+       " 'redeemed',\n",
+       " 'perimeters',\n",
+       " 'harelips',\n",
+       " 'overcompensating',\n",
+       " 'rejoicings',\n",
+       " 'adobe',\n",
+       " 'decreasing',\n",
+       " 'interstices',\n",
+       " 'curd',\n",
+       " 'orientate',\n",
+       " 'blueberries',\n",
+       " 'juniors',\n",
+       " 'broadloom',\n",
+       " 'debarring',\n",
+       " 'chandeliers',\n",
+       " 'segues',\n",
+       " 'army',\n",
+       " 'snuck',\n",
+       " 'pugilistic',\n",
+       " 'snugs',\n",
+       " 'dexterity',\n",
+       " 'dallies',\n",
+       " 'curving',\n",
+       " 'newsletter',\n",
+       " 'torn',\n",
+       " 'beaching',\n",
+       " 'limit',\n",
+       " 'blackguards',\n",
+       " 'breezier',\n",
+       " 'reoccur',\n",
+       " 'cabins']"
+      ]
+     },
+     "execution_count": 44,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "ds = decoys(padded, ws, ws_words)\n",
+    "ds"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 45,
+   "metadata": {
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "opted (True, 5, 7, <Direction.downright: 8>)\n",
+      "haywire (True, 8, 9, <Direction.downleft: 7>)\n",
+      "busting (True, 4, 17, <Direction.left: 1>)\n",
+      "succinct (True, 7, 17, <Direction.down: 4>)\n",
+      "institute (True, 16, 18, <Direction.up: 3>)\n",
+      "illicit (True, 13, 14, <Direction.left: 1>)\n",
+      "hypersensitivity (True, 16, 15, <Direction.left: 1>)\n",
+      "placement (True, 3, 11, <Direction.left: 1>)\n",
+      "bathrobe (True, 0, 5, <Direction.right: 2>)\n",
+      "inured (True, 4, 0, <Direction.right: 2>)\n",
+      "caveats (True, 3, 8, <Direction.downleft: 7>)\n",
+      "revisiting (True, 2, 11, <Direction.left: 1>)\n",
+      "pulp (True, 15, 11, <Direction.right: 2>)\n",
+      "teacup (True, 7, 16, <Direction.downleft: 7>)\n",
+      "threading (True, 18, 6, <Direction.right: 2>)\n",
+      "queered (True, 5, 13, <Direction.right: 2>)\n",
+      "parking (True, 6, 9, <Direction.right: 2>)\n",
+      "advent (True, 1, 11, <Direction.left: 1>)\n",
+      "chasuble (True, 19, 19, <Direction.left: 1>)\n",
+      "mosey (True, 7, 5, <Direction.downleft: 7>)\n",
+      "highboys (True, 19, 4, <Direction.right: 2>)\n",
+      "recharging (True, 18, 19, <Direction.up: 3>)\n",
+      "flue (True, 12, 2, <Direction.upright: 6>)\n",
+      "plywood (True, 3, 18, <Direction.left: 1>)\n",
+      "gluing (True, 12, 12, <Direction.upleft: 5>)\n",
+      "worrier (True, 1, 12, <Direction.right: 2>)\n",
+      "karma (True, 17, 9, <Direction.left: 1>)\n",
+      "peepers (True, 8, 7, <Direction.downleft: 7>)\n",
+      "vulnerable (True, 17, 10, <Direction.right: 2>)\n",
+      "boycott (True, 15, 1, <Direction.right: 2>)\n",
+      "rummy (True, 5, 4, <Direction.left: 1>)\n",
+      "tonic (True, 8, 13, <Direction.downleft: 7>)\n",
+      "children (True, 15, 0, <Direction.up: 3>)\n",
+      "reales (True, 6, 1, <Direction.right: 2>)\n",
+      "rectal (True, 7, 15, <Direction.down: 4>)\n",
+      "sledded (True, 14, 16, <Direction.up: 3>)\n",
+      "collocates (True, 14, 5, <Direction.right: 2>)\n",
+      "edict (True, 17, 4, <Direction.left: 1>)\n",
+      "captor (True, 14, 5, <Direction.upright: 6>)\n",
+      "amulet (True, 9, 4, <Direction.upright: 6>)\n",
+      "slipper (True, 2, 13, <Direction.right: 2>)\n",
+      "foot (True, 0, 0, <Direction.right: 2>)\n",
+      "chef (True, 14, 4, <Direction.upright: 6>)\n",
+      "goods (True, 6, 15, <Direction.right: 2>)\n",
+      "salter (True, 1, 5, <Direction.left: 1>)\n",
+      "crows (True, 18, 0, <Direction.right: 2>)\n",
+      "paeans (True, 12, 14, <Direction.up: 3>)\n",
+      "fences (True, 0, 18, <Direction.left: 1>)\n",
+      "iron (True, 5, 9, <Direction.right: 2>)\n",
+      "liras (True, 0, 19, <Direction.down: 4>)\n",
+      "stages (True, 19, 16, <Direction.up: 3>)\n",
+      "defer (True, 14, 2, <Direction.upright: 6>)\n",
+      "racy (True, 5, 4, <Direction.downleft: 7>)\n",
+      "gaps (True, 7, 11, <Direction.right: 2>)\n",
+      "aspen (True, 12, 10, <Direction.upleft: 5>)\n",
+      "rams (True, 6, 0, <Direction.downright: 8>)\n",
+      "friendship (False, 0, 0, <Direction.left: 1>)\n",
+      "stepping (False, 0, 0, <Direction.left: 1>)\n",
+      "featheriest (False, 0, 0, <Direction.left: 1>)\n",
+      "thriftily (False, 0, 0, <Direction.left: 1>)\n",
+      "mutilation (False, 0, 0, <Direction.left: 1>)\n",
+      "nook (False, 0, 0, <Direction.left: 1>)\n",
+      "clewing (False, 0, 0, <Direction.left: 1>)\n",
+      "meditated (False, 0, 0, <Direction.left: 1>)\n",
+      "gooier (False, 0, 0, <Direction.left: 1>)\n",
+      "cripples (False, 0, 0, <Direction.left: 1>)\n",
+      "ponderously (False, 0, 0, <Direction.left: 1>)\n",
+      "roundelay (False, 0, 0, <Direction.left: 1>)\n",
+      "curtailed (False, 0, 0, <Direction.left: 1>)\n",
+      "redeemed (False, 0, 0, <Direction.left: 1>)\n",
+      "perimeters (False, 0, 0, <Direction.left: 1>)\n",
+      "harelips (False, 0, 0, <Direction.left: 1>)\n",
+      "overcompensating (False, 0, 0, <Direction.left: 1>)\n",
+      "rejoicings (False, 0, 0, <Direction.left: 1>)\n",
+      "adobe (False, 0, 0, <Direction.left: 1>)\n",
+      "decreasing (False, 0, 0, <Direction.left: 1>)\n",
+      "interstices (False, 0, 0, <Direction.left: 1>)\n",
+      "curd (False, 0, 0, <Direction.left: 1>)\n",
+      "orientate (False, 0, 0, <Direction.left: 1>)\n",
+      "blueberries (False, 0, 0, <Direction.left: 1>)\n",
+      "juniors (False, 0, 0, <Direction.left: 1>)\n",
+      "broadloom (False, 0, 0, <Direction.left: 1>)\n",
+      "debarring (False, 0, 0, <Direction.left: 1>)\n",
+      "chandeliers (False, 0, 0, <Direction.left: 1>)\n",
+      "segues (False, 0, 0, <Direction.left: 1>)\n",
+      "army (False, 0, 0, <Direction.left: 1>)\n",
+      "snuck (False, 0, 0, <Direction.left: 1>)\n",
+      "pugilistic (False, 0, 0, <Direction.left: 1>)\n",
+      "snugs (False, 0, 0, <Direction.left: 1>)\n",
+      "dexterity (False, 0, 0, <Direction.left: 1>)\n",
+      "dallies (False, 0, 0, <Direction.left: 1>)\n",
+      "curving (False, 0, 0, <Direction.left: 1>)\n",
+      "newsletter (False, 0, 0, <Direction.left: 1>)\n",
+      "torn (False, 0, 0, <Direction.left: 1>)\n",
+      "beaching (False, 0, 0, <Direction.left: 1>)\n",
+      "limit (False, 0, 0, <Direction.left: 1>)\n",
+      "blackguards (False, 0, 0, <Direction.left: 1>)\n",
+      "breezier (False, 0, 0, <Direction.left: 1>)\n",
+      "reoccur (False, 0, 0, <Direction.left: 1>)\n",
+      "cabins (False, 0, 0, <Direction.left: 1>)\n"
+     ]
+    }
+   ],
+   "source": [
+    "for w in ws + ds:\n",
+    "    print(w, present(padded, w))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 46,
+   "metadata": {
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "s.esevresed...dwhims\n",
+      "e.tbk..vapourse.bn.d\n",
+      "h.ificembracesnslo.e\n",
+      "ctr.egukiwic..ddui.r\n",
+      "inor.dwh.rips.rrftsa\n",
+      "reue.eeisrar.tiefiyl\n",
+      "mmli.mgrg.muarth.slc\n",
+      "ieft.un.a.bnbuemdole\n",
+      "nn.nesimrliertseepad\n",
+      "ei.imeloeccdeh.epob.\n",
+      "dfsaprlrio.saf.smri.\n",
+      "cnpdt.ofrn.usu..ap.h\n",
+      "oom.ispeccgntlpew.sa\n",
+      "tcu.e.l.lu.eda.vsgin\n",
+      "e.bdsb.oarrmneloplsg\n",
+      "r.olaetrleromrkr.isa\n",
+      "ibatnhd.nlpoaeic.bir\n",
+      "eesiee.i.luepno.o.e.\n",
+      "snt.d.o.y.pcte.p.mr.\n",
+      "u....jtoquesylduol..\n",
+      "sfesevresedpzcdwhims\n",
+      "eotbkvgvapoursehbnyd\n",
+      "hiificembracesnslone\n",
+      "ctrnegukiwicurdduivr\n",
+      "inorydwhrripscrrftsa\n",
+      "reueleeisrarvtiefiyl\n",
+      "mmlinmgrgzmuarthgslc\n",
+      "ieftuunyanbnbuemdole\n",
+      "nncnesimrliertseepad\n",
+      "eirimeloeccdehzepobm\n",
+      "dfsaprlrioisafesmriq\n",
+      "cnpdtsofrnausuodapxh\n",
+      "oomlispeccgntlpewasa\n",
+      "tcuaehlzluledakvsgin\n",
+      "eibdsbeoarrmneloplsg\n",
+      "rbolaetrleromrkrnisa\n",
+      "ibatnhdtnlpoaeicibir\n",
+      "eesieerimluepnoholey\n",
+      "sntadvoaycpctespsmro\n",
+      "uamapjtoquesylduoldp\n",
+      "56 words added;  8 directions\n",
+      "Present: abreast bigwig bluff bodes bumps clothe concur confinement coteries crier cull daintier declared dendrites denim deserves embraces empties federal fluorite from glib guarded hangar herds iambic joiner kiwi loudly menus mocked panoply pearl poem polling prints proposition pruned resume rices riches rips roped rove seal seem shucks sissier swamped syllabi tine toque truthful unstabler vapours whims\n",
+      "Decoys: abrasions adapters aimlessness alkali awakens blowing burnouses burped cattily coal commences confusion contrivance crudest curies depravity distribute diva emigrate emulsion giveaway hangman house lifeboats maze middy mines mystified obtain organic parsons postulate prefixes pretenders razors scone sloes spuds straight subtleties systematise turncoats unpacked waivers\n"
+     ]
+    }
+   ],
+   "source": [
+    "g, ws = interesting_grid()\n",
+    "p = pad_grid(g)\n",
+    "ds = decoys(p, ws, ws_words)\n",
+    "print(show_grid(g))\n",
+    "print(show_grid(p))\n",
+    "print(len(ws), 'words added; ', len(set(present(g, w)[3] for w in ws)), 'directions')\n",
+    "print('Present:', wcat(sorted(ws)))\n",
+    "print('Decoys:', wcat(sorted(ds)))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 63,
+   "metadata": {
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "...p.mown.\n",
+      ".sdse..ee.\n",
+      ".e.elad.cr\n",
+      "pi.dtir.ah\n",
+      "rzsiwovspu\n",
+      "oawh.kieab\n",
+      "brow.c.rda\n",
+      "ecnotops.r\n",
+      "d.kc.d...b\n",
+      ".staple...\n",
+      "\n",
+      "fhjpamownq\n",
+      "wsdseuqeev\n",
+      "ieaeladhcr\n",
+      "piedtiriah\n",
+      "rzsiwovspu\n",
+      "oawhakieab\n",
+      "browpcfrda\n",
+      "ecnotopssr\n",
+      "dikchdnpnb\n",
+      "bstapleokr\n",
+      "14 words added;  6 directions\n",
+      "Present: apace cowhides crazies dock knows lived mown pears probed rhubarb rioted staple tops wide\n",
+      "Decoys: adapting bombing boor brick cackles carnal casino chaplets chump coaster coccyxes coddle collies creels crumbled cunt curds curled curlier deepen demeanor dicier dowses ensuing faddish fest fickler foaming gambol garoting gliding gristle grunts guts ibex impugns instants kielbasy lanyard loamier lugs market meanly minuend misprint mitts molested moonshot mucking oaks olives orgasmic pastrami perfect proceed puckered quashed refined regards retraces revel ridges ringlet scoff shinier siren solaria sprain sunder sunup tamped tapes thirds throw tiller times trains tranquil transfix typesets uric wariness welts whimsy winced winced\n"
+     ]
+    }
+   ],
+   "source": [
+    "g, ws = interesting_grid(width=10, height=10, words_limit=5, direction_slack=6)\n",
+    "p = pad_grid(g)\n",
+    "ds = decoys(p, ws, ws_words)\n",
+    "print(show_grid(g))\n",
+    "print()\n",
+    "print(show_grid(p))\n",
+    "print(len(ws), 'words added; ', len(set(present(g, w)[3] for w in ws)), 'directions')\n",
+    "print('Present:', wcat(sorted(ws)))\n",
+    "print('Decoys:', wcat(sorted(ds)))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 98,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "['fickler',\n",
+       " 'adapting',\n",
+       " 'chump',\n",
+       " 'foaming',\n",
+       " 'molested',\n",
+       " 'carnal',\n",
+       " 'crumbled',\n",
+       " 'guts',\n",
+       " 'minuend',\n",
+       " 'bombing',\n",
+       " 'winced',\n",
+       " 'coccyxes',\n",
+       " 'solaria',\n",
+       " 'shinier',\n",
+       " 'cackles']"
+      ]
+     },
+     "execution_count": 98,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "ds_original = ['regards', 'perfect', 'instants', 'refined', 'coddle', 'fickler', 'gambol', 'misprint', 'tapes', 'impugns', 'moonshot', 'chump', 'brick', 'siren', 'faddish', 'winced', 'kielbasy', 'market', 'puckered', 'trains', 'welts', 'cackles', 'foaming', 'proceed', 'gliding', 'guts', 'uric', 'oaks', 'molested', 'curled', 'boor', 'solaria', 'gristle', 'bombing', 'loamier', 'ensuing', 'cunt', 'sunder', 'revel', 'coaster', 'grunts', 'mucking', 'typesets', 'carnal', 'whimsy', 'scoff', 'coccyxes', 'meanly', 'sprain', 'minuend', 'ringlet', 'fest', 'winced', 'shinier', 'dicier', 'thirds', 'olives', 'garoting', 'pastrami', 'tranquil', 'tamped', 'sunup', 'crumbled', 'throw', 'ridges', 'chaplets', 'curlier', 'lugs', 'collies', 'adapting', 'demeanor', 'deepen', 'lanyard', 'tiller', 'transfix', 'wariness', 'times', 'mitts', 'dowses', 'creels', 'curds', 'quashed', 'orgasmic', 'ibex', 'retraces', 'casino']\n",
+    "ds = random.sample(ds_original, 15)\n",
+    "ds"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 89,
+   "metadata": {
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "['regards',\n",
+       " 'perfect',\n",
+       " 'instants',\n",
+       " 'refined',\n",
+       " 'coddle',\n",
+       " 'fickler',\n",
+       " 'gambol',\n",
+       " 'misprint',\n",
+       " 'tapes',\n",
+       " 'impugns',\n",
+       " 'moonshot',\n",
+       " 'chump',\n",
+       " 'brick',\n",
+       " 'siren',\n",
+       " 'faddish',\n",
+       " 'winced',\n",
+       " 'kielbasy',\n",
+       " 'market',\n",
+       " 'puckered',\n",
+       " 'trains',\n",
+       " 'welts',\n",
+       " 'cackles',\n",
+       " 'foaming',\n",
+       " 'proceed',\n",
+       " 'gliding',\n",
+       " 'guts',\n",
+       " 'uric',\n",
+       " 'oaks',\n",
+       " 'molested',\n",
+       " 'curled',\n",
+       " 'boor',\n",
+       " 'solaria',\n",
+       " 'gristle',\n",
+       " 'bombing',\n",
+       " 'loamier',\n",
+       " 'ensuing',\n",
+       " 'cunt',\n",
+       " 'sunder',\n",
+       " 'revel',\n",
+       " 'coaster',\n",
+       " 'grunts',\n",
+       " 'mucking',\n",
+       " 'typesets',\n",
+       " 'carnal',\n",
+       " 'whimsy',\n",
+       " 'scoff',\n",
+       " 'coccyxes',\n",
+       " 'meanly',\n",
+       " 'sprain',\n",
+       " 'minuend',\n",
+       " 'ringlet',\n",
+       " 'fest',\n",
+       " 'winced',\n",
+       " 'shinier',\n",
+       " 'dicier',\n",
+       " 'thirds',\n",
+       " 'olives',\n",
+       " 'garoting',\n",
+       " 'pastrami',\n",
+       " 'tranquil',\n",
+       " 'tamped',\n",
+       " 'sunup',\n",
+       " 'crumbled',\n",
+       " 'throw',\n",
+       " 'ridges',\n",
+       " 'chaplets',\n",
+       " 'curlier',\n",
+       " 'lugs',\n",
+       " 'collies',\n",
+       " 'adapting',\n",
+       " 'demeanor',\n",
+       " 'deepen',\n",
+       " 'lanyard',\n",
+       " 'tiller',\n",
+       " 'transfix',\n",
+       " 'wariness',\n",
+       " 'times',\n",
+       " 'mitts',\n",
+       " 'dowses',\n",
+       " 'creels',\n",
+       " 'curds',\n",
+       " 'quashed',\n",
+       " 'orgasmic',\n",
+       " 'ibex',\n",
+       " 'retraces',\n",
+       " 'casino']"
+      ]
+     },
+     "execution_count": 89,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "ds_original"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 99,
+   "metadata": {
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "grid =  [['.', '.', '.', 'p', '.', 'm', 'o', 'w', 'n', '.'], ['.', 's', 'd', 's', 'e', '.', '.', 'e', 'e', '.'], ['.', 'e', '.', 'e', 'l', 'a', 'd', '.', 'c', 'r'], ['p', 'i', '.', 'd', 't', 'i', 'r', '.', 'a', 'h'], ['r', 'z', 's', 'i', 'w', 'o', 'v', 's', 'p', 'u'], ['o', 'a', 'w', 'h', '.', 'k', 'i', 'e', 'a', 'b'], ['b', 'r', 'o', 'w', '.', 'c', '.', 'r', 'd', 'a'], ['e', 'c', 'n', 'o', 't', 'o', 'p', 's', '.', 'r'], ['d', '.', 'k', 'c', '.', 'd', '.', '.', '.', 'b'], ['.', 's', 't', 'a', 'p', 'l', 'e', '.', '.', '.']]\n",
+      "padded_grid =  [['f', 'h', 'j', 'p', 'a', 'm', 'o', 'w', 'n', 'q'], ['w', 's', 'd', 's', 'e', 'u', 'q', 'e', 'e', 'v'], ['i', 'e', 'a', 'e', 'l', 'a', 'd', 'h', 'c', 'r'], ['p', 'i', 'e', 'd', 't', 'i', 'r', 'i', 'a', 'h'], ['r', 'z', 's', 'i', 'w', 'o', 'v', 's', 'p', 'u'], ['o', 'a', 'w', 'h', 'a', 'k', 'i', 'e', 'a', 'b'], ['b', 'r', 'o', 'w', 'p', 'c', 'f', 'r', 'd', 'a'], ['e', 'c', 'n', 'o', 't', 'o', 'p', 's', 's', 'r'], ['d', 'i', 'k', 'c', 'h', 'd', 'n', 'p', 'n', 'b'], ['b', 's', 't', 'a', 'p', 'l', 'e', 'o', 'k', 'r']]\n",
+      "present_words =  ['probed', 'staple', 'rioted', 'cowhides', 'tops', 'knows', 'lived', 'rhubarb', 'crazies', 'dock', 'apace', 'mown', 'pears', 'wide']\n",
+      "decoy_words =  ['fickler', 'adapting', 'chump', 'foaming', 'molested', 'carnal', 'crumbled', 'guts', 'minuend', 'bombing', 'winced', 'coccyxes', 'solaria', 'shinier', 'cackles']\n",
+      "Directions:  [('probed', '`(True, 3, 0, <Direction.down: 4>)`'), ('staple', '`(True, 9, 1, <Direction.right: 2>)`'), ('rioted', '`(True, 6, 7, <Direction.upleft: 5>)`'), ('cowhides', '`(True, 8, 3, <Direction.up: 3>)`'), ('tops', '`(True, 7, 4, <Direction.right: 2>)`'), ('knows', '`(True, 8, 2, <Direction.up: 3>)`'), ('lived', '`(True, 2, 4, <Direction.downright: 8>)`'), ('rhubarb', '`(True, 2, 9, <Direction.down: 4>)`'), ('crazies', '`(True, 7, 1, <Direction.up: 3>)`'), ('dock', '`(True, 8, 5, <Direction.up: 3>)`'), ('apace', '`(True, 5, 8, <Direction.up: 3>)`'), ('mown', '`(True, 0, 5, <Direction.right: 2>)`'), ('pears', '`(True, 0, 3, <Direction.downright: 8>)`'), ('wide', '`(True, 4, 4, <Direction.upright: 6>)`')]\n"
+     ]
+    }
+   ],
+   "source": [
+    "print('grid = ', g)\n",
+    "print('padded_grid = ', p)\n",
+    "print('present_words = ', ws)\n",
+    "print('decoy_words = ', ds)\n",
+    "print('Directions: ', [(w, '`' + str(present(g, w)) + '`') for w in ws])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 100,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "# with open('example-wordsearch.txt', 'w') as f:\n",
+    "#     f.write('10x10\\n')\n",
+    "#     f.write(show_grid(p))\n",
+    "#     f.write('\\n')\n",
+    "#     f.write(lcat(sorted(ws + ds)))\n",
+    "# with open('exmaple-wordsearch-solution.txt', 'w') as f:\n",
+    "#     f.write('10x10\\n')\n",
+    "#     f.write(show_grid(g))\n",
+    "#     f.write('\\n')\n",
+    "#     f.write(lcat(sorted(ws)) + '\\n\\n')\n",
+    "#     f.write(lcat(sorted(ds)))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 77,
+   "metadata": {
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "probed 3 0 6 Direction.down [(3, 0), (4, 0), (5, 0), (6, 0), (7, 0), (8, 0)]\n",
+      "staple 9 1 6 Direction.right [(9, 1), (9, 2), (9, 3), (9, 4), (9, 5), (9, 6)]\n",
+      "rioted 6 7 6 Direction.upleft [(6, 7), (5, 6), (4, 5), (3, 4), (2, 3), (1, 2)]\n",
+      "cowhides 8 3 8 Direction.up [(8, 3), (7, 3), (6, 3), (5, 3), (4, 3), (3, 3), (2, 3), (1, 3)]\n",
+      "tops 7 4 4 Direction.right [(7, 4), (7, 5), (7, 6), (7, 7)]\n",
+      "knows 8 2 5 Direction.up [(8, 2), (7, 2), (6, 2), (5, 2), (4, 2)]\n",
+      "lived 2 4 5 Direction.downright [(2, 4), (3, 5), (4, 6), (5, 7), (6, 8)]\n",
+      "rhubarb 2 9 7 Direction.down [(2, 9), (3, 9), (4, 9), (5, 9), (6, 9), (7, 9), (8, 9)]\n",
+      "crazies 7 1 7 Direction.up [(7, 1), (6, 1), (5, 1), (4, 1), (3, 1), (2, 1), (1, 1)]\n",
+      "dock 8 5 4 Direction.up [(8, 5), (7, 5), (6, 5), (5, 5)]\n",
+      "apace 5 8 5 Direction.up [(5, 8), (4, 8), (3, 8), (2, 8), (1, 8)]\n",
+      "mown 0 5 4 Direction.right [(0, 5), (0, 6), (0, 7), (0, 8)]\n",
+      "pears 0 3 5 Direction.downright [(0, 3), (1, 4), (2, 5), (3, 6), (4, 7)]\n",
+      "wide 4 4 4 Direction.upright [(4, 4), (3, 5), (2, 6), (1, 7)]\n"
+     ]
+    },
+    {
+     "data": {
+      "text/plain": [
+       "[(7, 5), (2, 3), (3, 5)]"
+      ]
+     },
+     "execution_count": 77,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "cts = collections.Counter()\n",
+    "for w in ws:\n",
+    "    _, r, c, d = present(g, w)\n",
+    "    inds = indices(g, r, c, len(w), d)\n",
+    "    for i in inds:\n",
+    "        cts[i] += 1\n",
+    "    print(w, r, c, len(w), d, inds)\n",
+    "[i for i in cts if cts[i] > 1]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 143,
+   "metadata": {
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "0\n",
+      "1\n",
+      "2\n",
+      "3\n",
+      "4\n",
+      "5\n",
+      "6\n",
+      "7\n",
+      "8\n",
+      "9\n",
+      "10\n",
+      "11\n",
+      "12\n",
+      "13\n",
+      "14\n",
+      "15\n",
+      "16\n",
+      "17\n",
+      "18\n",
+      "19\n",
+      "20\n",
+      "21\n",
+      "22\n",
+      "23\n",
+      "24\n",
+      "25\n",
+      "26\n",
+      "27\n",
+      "28\n",
+      "29\n",
+      "30\n",
+      "31\n",
+      "32\n",
+      "33\n",
+      "34\n",
+      "35\n",
+      "36\n",
+      "37\n",
+      "38\n",
+      "39\n",
+      "40\n",
+      "41\n",
+      "42\n",
+      "43\n",
+      "44\n",
+      "45\n",
+      "46\n",
+      "47\n",
+      "48\n",
+      "49\n",
+      "50\n",
+      "51\n",
+      "52\n",
+      "53\n",
+      "54\n",
+      "55\n",
+      "56\n",
+      "57\n",
+      "58\n",
+      "59\n",
+      "60\n",
+      "61\n",
+      "62\n",
+      "63\n",
+      "64\n",
+      "65\n",
+      "66\n",
+      "67\n",
+      "68\n",
+      "69\n",
+      "70\n",
+      "71\n",
+      "72\n",
+      "73\n",
+      "74\n",
+      "75\n",
+      "76\n",
+      "77\n",
+      "78\n",
+      "79\n",
+      "80\n",
+      "81\n",
+      "82\n",
+      "83\n",
+      "84\n",
+      "85\n",
+      "86\n",
+      "87\n",
+      "88\n",
+      "89\n",
+      "90\n",
+      "91\n",
+      "92\n",
+      "93\n",
+      "94\n",
+      "95\n",
+      "96\n",
+      "97\n",
+      "98\n",
+      "99\n"
+     ]
+    }
+   ],
+   "source": [
+    "# for i in range(100):\n",
+    "#     print(i)\n",
+    "#     g, ws = interesting_grid()\n",
+    "#     p = pad_grid(g)\n",
+    "#     ds = decoys(p, ws, ws_words)\n",
+    "#     with open('wordsearch{:02}.txt'.format(i), 'w') as f:\n",
+    "#         f.write('20x20\\n')\n",
+    "#         f.write(show_grid(p))\n",
+    "#         f.write('\\n')\n",
+    "#         f.write(lcat(sorted(ws + ds)))\n",
+    "#     with open('wordsearch-solution{:02}.txt'.format(i), 'w') as f:\n",
+    "#         f.write('20x20\\n')\n",
+    "#         f.write(show_grid(g))\n",
+    "#         f.write('\\n')\n",
+    "#         f.write(lcat(sorted(ws)) + '\\n\\n')\n",
+    "#         f.write(lcat(sorted(ds)))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "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.5.2+"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 1
+}