{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "
Peter Norvig
2016, 2021
\n", "\n", "# KenKen Puzzles\n", "\n", "[KenKen](https://en.wikipedia.org/wiki/KenKen) is a fill-in-the-grid puzzle game, similar to Sudoku (solved [in another notebook](Sudoku.ipynb)), but with arithmetic as well as logic. \n", "\n", "An example puzzle and solution:\n", "\n", "  \n", "\n", "The rules of KenKen are:\n", "\n", "- Every **square** of the *N*×*N* grid must be filled with a **digit** from 1 to *N*.\n", "- No digit can appear twice in any **row** or **column**.\n", "- Thick lines divide the grid into contiguous groups of squares called **cages**:\n", " - Each cage is annotated with a **target number** and an arithmetic **operator**. For example:\n", " - \"11 +\" means the digits in the cage must add to 11 (e.g. `5 + 6`).\n", " - \"240 ×\" means the digits must form a product of 240 (e.g. `4 × 5 × 3 × 4`).\n", "
(Two `4`s in a cage are ok if they are in different rows and columns.)\n", " - \"2 ÷\" (or \"2 /\") means the digits, in some order, must form a quotient of 2 (e.g. `6 / 3` ).\n", " - \"3 -\" means the digits, in some order, must form a difference of 3 (e.g. `4 - 1`).\n", " - \"5 =\" means the cage must have one square, which is filled with `5`.\n", " - Evaluation is always left to right: `6 - 3 - 2 - 1` = `(((6 - 3) - 2) - 1)` = `0`\n", " \n", " \n", "# Strategy for Solving Puzzles\n", "\n", "The KenKen-solving program uses **constraint propagation** and **search**:\n", " - For each square, keep track of the set of digits that might possibly fill the square.\n", " - Use the constraints on rows, columns, and cages to eliminate some possible digits in some squares.\n", " - **Constraints** on one square can **propagate** to other squares in the row, column, or cage.\n", " - If that doesn't solve the puzzle, then **search**: pick a square and guess a digit to fill the square.\n", " - Recursively search from there.\n", " - If that leads to a solution, great; if it leads to a contradiction, back up and guess a different digit.\n", " \n", "\n", "# Data Types\n", "\n", "Throughout the program the following variable names represent the following data types:\n", "- `r` is a **row** label, e.g. `'A'`\n", "- `c` is a **column** label, e.g. `'3'`\n", "- `s` is a **square** label, e.g. `'A3'`\n", "- `d` is a **digit**, e.g. `9`\n", "- `u` is a **unit** (a row or column), e.g. `('A1', 'A2', 'A3', 'A4', 'A5', 'A6')`\n", "- `cage` is a Cage, consisting of a **target number**, an **operator**, and a list of squares\n", "- `values` is a dict of {square: set_of_possible_digits}, e.g. `{'A1': {5, 6}, 'B1': {6, 5}, ...}`\n", " - A `values` dict is a **solution** when every square has been filled with a single digit, and they satisfy all the constraints.\n", "- `kk` or `kenken` is a KenKen object, which describes the puzzle. It has these attributes:\n", " - `.N`: the number of squares on a side\n", " - `.digits`: the set of digits allowed: `{1, ..., N}`\n", " - `.squares`: a set of all squares in the grid\n", " - `.cages`: a list of all cages in the grid\n", " - `.cagefor`: a dict where `.cagefor[s]` is the cage that square `s` is part of.\n", " - `.unitsfor`: a dict where `.unitsfor[s]` is a tuple of the two units for square `s`.\n", " - `.units`: a set of all units\n", " - `.peers`: a dict where `.peers[s]` is a set of the `2 * (N - 1)` squares in the same column or row as `s`.\n", "- `cage_descriptions` are strings, for example `\"11 + A1 B1\"` means a cage whose target is 11, whose operator is addition, and whose squares are `A1` and `B1`. A semicolon separates cage descriptions." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "from typing import Dict, Set, List, Optional\n", "from itertools import product as crossproduct\n", "from collections import defaultdict\n", "from operator import add, sub, mul, truediv\n", "from dataclasses import dataclass\n", "from functools import reduce" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "Square = str\n", "Unit = tuple\n", "Values = Dict[Square, Set[int]] # The possible values that fill in the squares\n", "\n", "class KenKen:\n", " \"\"\"Describes a KenKen puzzle, but not the values used to fill in the puzzle squares.\"\"\"\n", " def __init__(self, N, cage_descriptions, sep=\";\"):\n", " self.N = N\n", " self.digits = set(range(1, N + 1))\n", " self.cols = '123456789'[:N]\n", " self.rows = 'ABCDEFGHI'[:N]\n", " self.squares = {r + c for r in self.rows for c in self.cols}\n", " self.cages = [make_cage(description) for description in cage_descriptions.split(sep)]\n", " self.cagefor = {s: first(cage for cage in self.cages if s in cage.squares)\n", " for s in self.squares}\n", " self.unitsfor= {s: (Unit(s[0]+c for c in self.cols), Unit(r+s[1] for r in self.rows))\n", " for s in self.squares}\n", " self.units = union(self.unitsfor.values())\n", " self.peers = {s: union(self.unitsfor[s]) - {s}\n", " for s in self.squares}\n", " s1 = sorted(self.squares)\n", " s2 = sorted(s for cage in self.cages for s in cage.squares)\n", " assert s1 == s2, f\"Cage descriptions don't cover all squares:\\n{s1}\\n{s2}\"\n", "\n", "@dataclass\n", "class Cage:\n", " \"\"\"A collection of squares that must evaluate to `target` when `op` is applied to them.\"\"\"\n", " target: int\n", " op: str\n", " squares: List[Square]\n", " \n", "def make_cage(description) -> Cage:\n", " \"\"\"Make a cage from a description such as '3 + A1 A2'.\"\"\"\n", " num, op, *squares = description.upper().split()\n", " assert op in OPS\n", " return Cage(int(num), op, squares)\n", "\n", "OPS = {'+': add, '-': sub, '×': mul, '*': mul, 'X': mul, '/': truediv, '÷': truediv, '=': add}\n", " \n", "def first(iterable) -> Optional[object]: return next(iter(iterable), None)\n", "\n", "def union(sets) -> set: return set().union(*sets)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Example Puzzle \n", "\n", "Let's make a `KenKen` object for the example puzzle (repeated here):\n", "\n", "" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "kk6 = KenKen(6, \"\"\"\n", " 11 + A1 B1; 2 / A2 A3; 20 × A4 B4; 6 × A5 A6 B6 C6; \n", " 3 - B2 B3; 3 / B5 C5;\n", " 240 × C1 C2 D1 D2; 6 × C3 C4; \n", " 6 × D3 E3; 7 + D4 E4 E5; 30 × D5 D6; \n", " 6 × E1 E2; 9 + E6 F6;\n", " 8 + F1 F2 F3; 2 / F4 F5\"\"\")" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{('A1', 'A2', 'A3', 'A4', 'A5', 'A6'),\n", " ('A1', 'B1', 'C1', 'D1', 'E1', 'F1'),\n", " ('A2', 'B2', 'C2', 'D2', 'E2', 'F2'),\n", " ('A3', 'B3', 'C3', 'D3', 'E3', 'F3'),\n", " ('A4', 'B4', 'C4', 'D4', 'E4', 'F4'),\n", " ('A5', 'B5', 'C5', 'D5', 'E5', 'F5'),\n", " ('A6', 'B6', 'C6', 'D6', 'E6', 'F6'),\n", " ('B1', 'B2', 'B3', 'B4', 'B5', 'B6'),\n", " ('C1', 'C2', 'C3', 'C4', 'C5', 'C6'),\n", " ('D1', 'D2', 'D3', 'D4', 'D5', 'D6'),\n", " ('E1', 'E2', 'E3', 'E4', 'E5', 'E6'),\n", " ('F1', 'F2', 'F3', 'F4', 'F5', 'F6')}" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "kk6.units" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[Cage(target=11, op='+', squares=['A1', 'B1']),\n", " Cage(target=2, op='/', squares=['A2', 'A3']),\n", " Cage(target=20, op='×', squares=['A4', 'B4']),\n", " Cage(target=6, op='×', squares=['A5', 'A6', 'B6', 'C6']),\n", " Cage(target=3, op='-', squares=['B2', 'B3']),\n", " Cage(target=3, op='/', squares=['B5', 'C5']),\n", " Cage(target=240, op='×', squares=['C1', 'C2', 'D1', 'D2']),\n", " Cage(target=6, op='×', squares=['C3', 'C4']),\n", " Cage(target=6, op='×', squares=['D3', 'E3']),\n", " Cage(target=7, op='+', squares=['D4', 'E4', 'E5']),\n", " Cage(target=30, op='×', squares=['D5', 'D6']),\n", " Cage(target=6, op='×', squares=['E1', 'E2']),\n", " Cage(target=9, op='+', squares=['E6', 'F6']),\n", " Cage(target=8, op='+', squares=['F1', 'F2', 'F3']),\n", " Cage(target=2, op='/', squares=['F4', 'F5'])]" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "kk6.cages" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'A2', 'A3', 'A4', 'A5', 'A6', 'B1', 'C1', 'D1', 'E1', 'F1'}" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "kk6.peers['A1']" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(('A1', 'A2', 'A3', 'A4', 'A5', 'A6'), ('A1', 'B1', 'C1', 'D1', 'E1', 'F1'))" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "kk6.unitsfor['A1']" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Verifying a Solution\n", "\n", "A solution to a KenKen puzzle is a dict of the form `{square: {value}, ...}` such that:\n", "- Every square has only a single possible value remaining.\n", "- Every unit (row or column) has each of the digits 1 to *N* as values, once each.\n", "- Every cage has a collection of values that, in some order, evaluates to the cage's target under the cage's operator." ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [], "source": [ "def is_solution(values, kenken) -> bool:\n", " \"\"\"Is `values` a solution to `kenken`?\"\"\"\n", " def singleton(s) -> bool: return len(values[s]) == 1\n", " def pandigits(u) -> bool: return union(values[s] for s in u) == kenken.digits\n", " def cage_good(c) -> bool: return cage_solved(c, [first(values[s]) for s in c.squares])\n", " return (all(singleton(s) for s in kenken.squares) and \n", " all(pandigits(u) for u in kenken.units) and \n", " all(cage_good(c) for c in kenken.cages))\n", "\n", "def cage_solved(cage, numbers) -> bool:\n", " \"\"\"Do `numbers`, in some order, solve the equation in this cage?\"\"\"\n", " op = OPS[cage.op]\n", " return any(reduce(op, nums) == cage.target\n", " for nums in orderings(op, numbers))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "What orderings of the numbers in a cage do we have to try? That depends on the operator:\n", "- Addition and multiplication are commutative, so we only need to try one order:\n", " - `1 + 2 + 3 + 4` is the same as `4 + 3 + 2 + 1` (or any other order).\n", "- Subtraction and division are not commutative: \n", " - `4 - 3 - 2 - 1` is different from `1 - 2 - 3 - 4`. \n", "\n", "Remember, the grouping is always left-to-right: `(((4 - 3) - 2) - 1)`.\n", "\n", "With *n* numbers in a subtraction or division cage you might think there are *n*! different possible results, but actually there are only *n*, because only the choice of which number goes first matters; any order of the remaining numbers gives the same result:\n", " - `4 - 3 - 2 - 1` = `4 - (3 + 2 + 1)`, so all 6 permutations of `(3, 2, 1)` give the same result, `-2`.\n", " - `3 - 4 - 2 - 1` = `3 - (4 + 2 + 1)`, so all 6 permutations of `(4, 2, 1)` give the same result, `-4`.\n", " - `2 - 4 - 3 - 1` = `2 - (4 + 3 + 1)`, so all 6 permutations of `(4, 3, 1)` give the same result, `-6`.\n", " - `1 - 4 - 3 - 2` = `1 - (4 + 3 + 2)`, so all 6 permutations of `(4, 3, 2)` give the same result, `-8`." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "def orderings(op, numbers: List[int]) -> List[List[int]]:\n", " \"\"\"What orderings of `numbers` do we try with `op` to try to reach a target?\"\"\"\n", " if op in {add, mul}: # commutative, only need one ordering\n", " return [numbers] \n", " else: # try all rotations, to put each number first in one of the orderings\n", " return [numbers[i:] + numbers[:i] for i in range(len(numbers))]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Constraint Propagation\n", "\n", "Similar to [the Sudoku program](Sudoku.ipynb), we propagate constraints with these two functions:\n", "- `fill` fills in square *s* with digit *d*. It does this by eliminating all the other digits.\n", "- `eliminate` eliminates a digit *d* as a possibility for a square *s*, and checks if there are other possible digits that are consistent with the other squares.\n" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "def fill(kenken, values, s, d) -> Optional[Values]:\n", " \"\"\"Eliminate all the other values (except d) from values[s] and propagate.\n", " Return values, except return None if the eliminations are not possible.\"\"\"\n", " ok = all(eliminate(kenken, values, s, d2) for d2 in values[s] if d2 != d)\n", " return values if ok else None\n", "\n", "def eliminate(kk: KenKen, values, s, d) -> bool:\n", " \"\"\"Eliminate d from values[s]; return True if all consistency checks are True.\"\"\"\n", " if d not in values[s]:\n", " return True ## Already eliminated\n", " values[s] = values[s] - {d}\n", " return (values[s] and\n", " arc_consistent(kk, values, s) and \n", " dual_consistent(kk, values, s, d) and \n", " cage_consistent(kk, values, s))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The function `eliminate` removes the digit `d` from consideration for square `s`, and makes these checks for consistency, propagating constraints from squares to peers along the way:\n", "\n", "1. If a square *s* is reduced to one value *d2*, then eliminate *d2* from the peers.\n", "2. If a unit *u* is reduced to only one place for a value *d*, then put it there.\n", "3. If some value assignments are no longer valid within the cage for `s`, then eliminate them." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "def arc_consistent(kk, values, s) -> bool:\n", " \"\"\"If a square s is reduced to one value d2, then eliminate d2 from the peers.\"\"\"\n", " [d2, *others] = values[s]\n", " return others or all(eliminate(kk, values, s2, d2) for s2 in kk.peers[s])\n", " \n", "def dual_consistent(kk, values, s, d) -> bool:\n", " \"\"\"If a unit u is reduced to only one place for a value d, then put it there.\"\"\"\n", " def place_for_d(u) -> bool:\n", " places = [s for s in u if d in values[s]]\n", " return places and (len(places) > 1 or fill(kk, values, places[0], d))\n", " return all(place_for_d(u) for u in kk.unitsfor[s])\n", "\n", "def cage_consistent(kk, values, s) -> bool:\n", " \"\"\"Make sure that there is some assignment that satisfies the cage for square s,\n", " and eliminate the values that are impossible.\"\"\"\n", " cage = kk.cagefor[s]\n", " possible = possible_cage_values(values, cage)\n", " return (possible and\n", " all(eliminate(kk, values, s1, d)\n", " for s1 in cage.squares\n", " for d in values[s1] if d not in possible[s1]))\n", "\n", "def possible_cage_values(values, cage) -> Values:\n", " \"\"\"Return a dict of {s: possible_digits} for each square s in the cage.\"\"\"\n", " possible = defaultdict(set)\n", " for numbers in crossproduct(*[values[s] for s in cage.squares]):\n", " if cage_solved(cage, numbers):\n", " for (s, v) in zip(cage.squares, numbers):\n", " possible[s].add(v)\n", " return possible" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The initial dict of possible values is created by starting with the assumption that any square could be any digit, and then applying the cage constraints:" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "def initial_values(kenken) -> Values:\n", " \"\"\"Assign initial values to each square, just considering cages, not rows/columns.\"\"\"\n", " values = {s: kenken.digits for s in kenken.squares}\n", " for cage in kenken.cages:\n", " values.update(possible_cage_values(values, cage))\n", " return values" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'B6': {1, 2, 3, 6},\n", " 'B5': {1, 2, 3, 6},\n", " 'C6': {1, 2, 3, 6},\n", " 'D3': {1, 2, 3, 6},\n", " 'A6': {1, 2, 3, 6},\n", " 'D6': {5, 6},\n", " 'E2': {1, 2, 3, 6},\n", " 'D4': {1, 2, 3, 4, 5},\n", " 'E3': {1, 2, 3, 6},\n", " 'A3': {1, 2, 3, 4, 6},\n", " 'A5': {1, 2, 3, 6},\n", " 'F5': {1, 2, 3, 4, 6},\n", " 'F1': {1, 2, 3, 4, 5, 6},\n", " 'A2': {1, 2, 3, 4, 6},\n", " 'E5': {1, 2, 3, 4, 5},\n", " 'F4': {1, 2, 3, 4, 6},\n", " 'B3': {1, 2, 3, 4, 5, 6},\n", " 'D2': {2, 3, 4, 5, 6},\n", " 'B2': {1, 2, 3, 4, 5, 6},\n", " 'F6': {3, 4, 5, 6},\n", " 'C1': {2, 3, 4, 5, 6},\n", " 'F2': {1, 2, 3, 4, 5, 6},\n", " 'B4': {4, 5},\n", " 'C3': {1, 2, 3, 6},\n", " 'E4': {1, 2, 3, 4, 5},\n", " 'C2': {2, 3, 4, 5, 6},\n", " 'A1': {5, 6},\n", " 'F3': {1, 2, 3, 4, 5, 6},\n", " 'E1': {1, 2, 3, 6},\n", " 'A4': {4, 5},\n", " 'D1': {2, 3, 4, 5, 6},\n", " 'D5': {5, 6},\n", " 'B1': {5, 6},\n", " 'E6': {3, 4, 5, 6},\n", " 'C4': {1, 2, 3, 6},\n", " 'C5': {1, 2, 3, 6}}" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "values = initial_values(kk6)\n", "values" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We see, for example, that the two squares in the \"11 +\" cage, `A1` and `B1`, must be either 5 or 6, because no other two digits sum to 11:" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Cage(target=11, op='+', squares=['A1', 'B1'])" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "cage = kk6.cagefor['A1']\n", "cage " ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "assert possible_cage_values(values, cage) == {'A1': {5, 6}, 'B1': {5, 6}}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Search\n", "\n", "We use depth-first-search to find a solution:\n", "- If all squares have exactly one possible value, that's a solution.\n", "- Otherwise, choose an unfilled square *s* with the minimum number of possible values.\n", "- For each possible value *d* for square *s*, try filling *s* with *d* and recursively searching for a solution.\n", " - Recursive calls use `values.copy()`, so that if we have to back up, `values` is unchanged. " ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "def solve(kenken) -> Values:\n", " \"\"\"Solve a KenKen puzzle with search, assert the solution is valid, and return the solution.\"\"\"\n", " values = search(kenken, initial_values(kenken))\n", " assert is_solution(values, kenken)\n", " return values\n", "\n", "def search(kenken, values) -> Optional[Values]:\n", " \"\"\"Using depth-first search and constraint propagation, find values that solve puzzle.\"\"\"\n", " if values is None:\n", " return None ## Failed earlier\n", " if all(len(values[s]) == 1 for s in kenken.squares):\n", " assert is_solution(values, kenken)\n", " return values ## Solved!\n", " ## Chose the unfilled square s with the fewest possibilities and try each possible value for s\n", " s = min((s for s in kenken.squares if len(values[s]) > 1), key=lambda s: len(values[s]))\n", " return first_true(search(kenken, fill(kenken, values.copy(), s, d))\n", " for d in values[s])\n", "\n", "def first_true(iterable): return first(x for x in iterable if x)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can see that search solves the example puzzle, finding a single value for every square:" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'B6': {3},\n", " 'B5': {2},\n", " 'C6': {1},\n", " 'D3': {1},\n", " 'A6': {2},\n", " 'D6': {6},\n", " 'E2': {3},\n", " 'D4': {2},\n", " 'E3': {6},\n", " 'A3': {3},\n", " 'A5': {1},\n", " 'F5': {3},\n", " 'F1': {1},\n", " 'A2': {6},\n", " 'E5': {4},\n", " 'F4': {6},\n", " 'B3': {4},\n", " 'D2': {4},\n", " 'B2': {1},\n", " 'F6': {4},\n", " 'C1': {4},\n", " 'F2': {2},\n", " 'B4': {5},\n", " 'C3': {2},\n", " 'E4': {1},\n", " 'C2': {5},\n", " 'A1': {5},\n", " 'F3': {5},\n", " 'E1': {2},\n", " 'A4': {4},\n", " 'D1': {3},\n", " 'D5': {5},\n", " 'B1': {6},\n", " 'E6': {5},\n", " 'C4': {3},\n", " 'C5': {6}}" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "solve(kk6)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Display\n", "\n", "The `values` dict is not a visually pleasing portrayal of the solution. Let's develop a better one, using the `HTML` and `display` functions from the `IPython.core.display` module:" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "from IPython.core.display import HTML, display\n", "\n", "def show(kenken, values=None):\n", " \"\"\"Display a KenKen puzzle, with the values filled in.\"\"\"\n", " if values is None:\n", " values = solve(kenken)\n", " result = ['\\n']\n", " for r in kenken.rows:\n", " result += ['']\n", " for c in kenken.cols:\n", " s = r + c\n", " result += [cell(kenken, s, values.get(s, set(['X'])))]\n", " result += ['
']\n", " return display(HTML(''.join(result)))\n", "\n", "def cell(kenken, s, val) -> str:\n", " \"\"\"HTML for a single table cell; if it is the first cell in a cage, show target/op.\n", " Make a thick border with every neighboring cell that is not in the same cage.\"\"\"\n", " cage = kenken.cagefor[s]\n", " T, R, B, L = [1 if same_cage(kenken, s, s2) else 8 for s2 in neighbors(s)]\n", " borders = f\"border: solid black; border-width: {T}px {R}px {B}px {L}px;\"\n", " header = f'{cage.target}{cage.op}' if s == min(cage.squares) else ''\n", " nums = ''.join(map(str, val))\n", " return f'{header}
{nums}' \n", "\n", "def neighbors(s: Square) -> List[Square]: \n", " \"\"\"The neighboring squares in [top, right, bottom, left] directions.\"\"\"\n", " r, c = map(ord, s)\n", " return [chr(r + Δr) + chr(c + Δc)\n", " for (Δr, Δc) in ((-1, 0), (0, 1), (1, 0), (0, -1))]\n", "\n", "def same_cage(kenken, s1, s2) -> bool:\n", " \"\"\"Are squares s1 and s2 in the same cage in kenken?\"\"\"\n", " return kenken.cagefor.get(s1) == kenken.cagefor.get(s2)" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "
11+
5
2/
6

3
20×
4

1

2

6
3-
1

4

5
3/
2

3
240×
4

5

2

3

6

1

3

4

1
7+
2
30×
5

6

2

3

6

1

4
9+
5
8+
1

2

5
2/
6

3

4
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "show(kk6)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "That looks much better!\n", "\n", "# More Puzzles\n", "\n", "We can solve more puzzles. Here is a list of ten puzzles of various sizes:" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [], "source": [ "kenkens = [\n", " KenKen(4, \"7 + A1 B1; 2 / C1 D1; 1 - A2 A3; 3 - B2 B3; 2 / A4 B4; 3 = C2; 12 × C3 C4 D4; 2 / D2 D3\"),\n", " KenKen(4, \"1 - a1 a2; 2 / a3 b3; 5 + a4 b4; 2 / b1 c1; 5 + b2 c2; 1 - c3 d3; 6 x c4 d4; 4 x d1 d2\"),\n", " KenKen(4, \"48 x a1 a2 a3 b1; 5 + a4 b4; 2 / b2 c2; 12 x b3 c3; 5 + c1 d1; 2 - d2 d3; 1 - c4 d4\"),\n", " KenKen(5, \"\"\"12 × a1 b1; 2 - a2 b2; 4 - a3 a4; 2 / a5 b5; 2 / b3 b4; 2 / c1 c2; 15 + c3 c4 c5 d3;\n", " 3 - d1 e1; 16 × d2 e2 e3; 1 - d4 e4; 2 - d5 e5\"\"\"),\n", " KenKen(5, \"\"\"8 + a1 b1 c1; 2 - a2 a3; 4 × a4 a5 b5; 2 / b2 b3; 1 - b4 c4; 3 = c5;\n", " 4 - c2 d2; 10 + c3 d3 d4; 48 × d1 e1 e2; 2 / e3 e4; 3 - d5 e5\"\"\"),\n", " KenKen(5, \"\"\"3 - a1 a2; 1 - a3 a4; 15 × a5 b5 b4; 12 × b1 b2 c2; 10 + b3 c3 d3;\n", " 1 = c1; 2 / c4 c5; 2 / d1 d2; 20 × d4 d5 e5; 8 + e1 e2; 3 + e3 e4\"\"\"),\n", " kk6,\n", " KenKen(7, \"\"\"3 - a1 b1; 108 × a2 a3 b3; 13 + a4 b4 b5; 2 / a5 a6; 13 + a7 b6 b7;\n", " 3 - b2 c2; 70 × c1 d1 e1; 5 = d2; 504 × c3 c4 d3 e3 e4; 60 × c5 d4 d5 e5;\n", " 4 - c6 c7; 1 - d6 d7; 6 - e6 e7; 2 / f1 g1; 2 / g2 g3; 30 × e2 f2 f3; \n", " 140 × f4 f5 g4; 1 - g5 g6; 14 + f6 f7 g7\"\"\"),\n", " KenKen(7, \"\"\"3 = a1; 15 + a2 a3 b3; 12 + a4 a5 a6; 1 - a7 b7;\n", " 6 / b1 b2; 7 + b4 b5; 7 = b6; 8 × c1 c2; 2 / c3 c4; 35 × c5 c6 c7;\n", " 11 + d1 e1 e2; 5 - d2 d3; 3 / d4 e4; 30 × d5 d6; 9 + d7 e7; 2 - e5 e6;\n", " 8 + e3 f3 g3; 24 × f1 f2; 35 × g1 g2; 10 + f4 f5; 2 × f6 f7; 6 + g4 g5; 3 - g6 g7\"\"\"),\n", " KenKen(8, \"\"\"3 / a1 a2; 9 + a3 a4; 210 × a5 b5 c5 b4 c4; 19 + a6 b6 c6; 90 × a7 a8 b7;\n", " 10 × b1 b2; 8 - b3; 4 / b8 c8;\n", " 7 + c1 d1; 7 = c2; 1 - c3 d3; 4 = c7; 2 / d2 e2; 17 + d4 e4 f4; 6 / d5 d6; 6 × d7 d8;\n", " 16 + e1 f1 f2 g2; 7 = e3; 5 - e5 e6; 12 × e7 e8 f8; 7 + f3 g3; 80 × f5 f6 f7;\n", " 8 / g1 h1; 4 - h2 h3; 1 - g4 h4; 15 + g5 h5 h6; 1 = g6; 12 + g7 h7; 1 - g8 h8\"\"\")\n", "]" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "
7+
4
1-
2

3
2/
1

3
3-
1

4

2
2/
2
3=
3
12×
1

4

1
2/
4

2

3
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "\n", "
1-
3

4
2/
2
5+
1
2/
2
5+
3

1

4

1

2
1-
4
6X
3
4X
4

1

3

2
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "\n", "
48X
3

4

2
5+
1

2
2/
1
12X
3

4
5+
1

2

4
1-
3

4
2-
3

1

2
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "\n", "
12×
4
2-
3
4-
1

5
2/
2

3

5
2/
2

4

1
2/
1

2
15+
5

3

4
3-
2
16×
4

3
1-
1
2-
5

5

1

4

2

3
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "\n", "
8+
2
2-
3

5

1

4

5
2/
2

4
1-
3

1

1
4-
5
10+
2

4
3=
3
48×
4

1

3

5
3-
2

3

4
2/
1

2

5
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "\n", "
3-
5

2
1-
4

3
15×
1
12×
4

1
10+
2

5

3
1=
1

3

5
2/
4

2
2/
2

4

3
20×
1

5
8+
3

5
3+
1

2

4
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "\n", "
11+
5
2/
6

3
20×
4

1

2

6
3-
1

4

5
3/
2

3
240×
4

5

2

3

6

1

3

4

1
7+
2
30×
5

6

2

3

6

1

4
9+
5
8+
1

2

5
2/
6

3

4
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "\n", "
3-
4
108×
6

3
13+
7
2/
1

2
13+
5

1
3-
7

6

2

4

5

3
70×
7

4
504×
1

3
60×
5
4-
6

2

2
5=
5

7

1

6
1-
3

4

5
30×
3

4

6

2
6-
7

1
2/
3

2

5
140×
4

7
14+
1

6

6
2/
1

2

5
1-
3

4

7
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "\n", "
3=
3
15+
5

6
12+
4

7

1
1-
2
6/
6

1

4
7+
5

2
7=
7

3

2

4
2/
3

6
35×
1

5

7
11+
1
5-
2

7
3/
3
30×
5

6
9+
4

7

3
8+
2

1
2-
6

4

5
24×
4

6

5
10+
7

3

2

1
35×
5

7

1
6+
2

4
3-
3

6
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "\n", "
3/
6

2
9+
1

8
210×
7
19+
4
90×
3

5
10×
2

5
8-
8

1

3

7

6
4/
4
7+
3
7=
7
1-
6

5

2

8
4=
4

1

4
2/
8

5
17+
7
6/
1

6

2

3
16+
5

4
7=
7

6
5-
8

3
12×
1

2

7

1
7+
3

4
80×
5

2

8

6
8/
8

3

4
1-
2
15+
6
1=
1
12+
5
1-
7

1
4-
6

2

3

4

5

7

8
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "CPU times: user 240 ms, sys: 4.26 ms, total: 244 ms\n", "Wall time: 243 ms\n" ] } ], "source": [ "%%time\n", "for kk in kenkens:\n", " show(kk)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "It took only about 1/4 second to correctly solve (and print) all 10 puzzles. That's not bad, but it is a lot slower than the [Sudoku solver](SudokuJava.ipynb). I think part of the problem is that the code to handle cages could be more efficient; it could cache results instead of recomputing them, and it could consider the possible values for all the squares in a cage together, rather than just consider the values for each square independently. For example, in a 7×7 grid with a four-square \"105 ×\" cage, `possible_cage_values` computes that each square must be one of `{1, 2, 3, 5, 6, 7}`, but it would be better to make use of the fact that the four squares must be either `{1, 5, 6, 7}` (in some order) or `{2, 3, 5, 7}`.\n", "\n", "# Tests\n", "\n", "Here are some unit tests to give partial code coverage, and to show how the subfunctions work:" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def tests():\n", " \"\"\"Unit tests.\"\"\"\n", " for kk in kenkens:\n", " assert len(kk.units) == 2 * kk.N\n", " assert len(kk.peers) -- kk.N ** 2\n", " assert all(len(kk.peers[s]) == 2 * kk.N - 2 for s in kk.squares)\n", " \n", " kk4 = kenkens[0]\n", " soln = {'A1': {4}, 'A2': {2}, 'A3': {3}, 'A4': {1}, \n", " 'B1': {3}, 'B2': {1}, 'B3': {4}, 'B4': {2}, \n", " 'C1': {2}, 'C2': {3}, 'C3': {1}, 'C4': {4}, \n", " 'D1': {1}, 'D2': {4}, 'D3': {2}, 'D4': {3}}\n", " assert solve(kk4) == soln\n", " assert is_solution(soln, kk4)\n", " \n", " cage = kk6.cagefor['D4']\n", " assert cage == Cage(target=7, op='+', squares=['D4', 'E4', 'E5'])\n", " assert cage == make_cage('7 + d4 e4 e5')\n", " assert cage_solved(cage, [2, 1, 4])\n", " assert not cage_solved(cage, [2, 1, 3])\n", " \n", " assert orderings(add, [2, 1, 4]) == [[2, 1, 4]]\n", " assert orderings(sub, [2, 1, 4]) == [[2, 1, 4], [1, 4, 2], [4, 2, 1]]\n", " \n", " vals = {'D4': kk6.digits, 'E4': kk6.digits, 'E5': kk6.digits}\n", " assert possible_cage_values(vals, cage) == {\n", " 'D4': {1, 2, 3, 4, 5},\n", " 'E4': {1, 2, 3, 4, 5},\n", " 'E5': {1, 2, 3, 4, 5}}\n", " \n", " assert first([0, 1, 2, 3]) == 0\n", " assert first({3}) == 3\n", " \n", " assert first_true([0, '', False, 4]) == 4\n", " assert first_true([0, '', False]) == None\n", " \n", " assert union([{1, 2, 3}, {3, 4, 5}]) == {1, 2, 3, 4, 5}\n", " \n", " assert neighbors('B2') == ['A2', 'B3', 'C2', 'B1']\n", " \n", " assert same_cage(kk6, 'A1', 'B1')\n", " assert not same_cage(kk6, 'A1', 'A2')\n", "\n", " return True\n", "\n", "tests()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.6" } }, "nbformat": 4, "nbformat_minor": 4 }