├── .github └── workflows │ └── semgrep.yml ├── .gitignore ├── LICENSE ├── README.md ├── button.jpg ├── ep.py ├── logo.png ├── mazes.py ├── output.jpg ├── printing.gif ├── requirements.txt ├── sudoku-src.png └── sudoku.py /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | 2 | on: 3 | pull_request: {} 4 | workflow_dispatch: {} 5 | push: 6 | branches: 7 | - main 8 | - master 9 | schedule: 10 | - cron: '0 0 * * *' 11 | name: Semgrep config 12 | jobs: 13 | semgrep: 14 | name: semgrep/ci 15 | runs-on: ubuntu-20.04 16 | env: 17 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 18 | SEMGREP_URL: https://cloudflare.semgrep.dev 19 | SEMGREP_APP_URL: https://cloudflare.semgrep.dev 20 | SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version 21 | container: 22 | image: returntocorp/semgrep 23 | steps: 24 | - uses: actions/checkout@v3 25 | - run: semgrep ci 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | # Generated temporary files 104 | maze.png 105 | sudoku.png 106 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Cloudflare 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cloudflare Randomness Printer 2 | ----------------------------- 3 | 4 | The Cloudflare Randomness Printer prints on demand a 'receipt' 5 | containing the following: 6 | 7 | 1. A random number less than 1,000,000 8 | 9 | 2. A six word [diceware](https://en.wikipedia.org/wiki/Diceware) password 10 | 11 | 3. Three passwords that have 128-bits of entropy. One has just 12 | hexadecimal digits, one has alphanumerics and another printable ASCII. 13 | 14 | 4. A random response from the [Magic 8 Ball](https://en.wikipedia.org/wiki/Magic_8-Ball). 15 | 16 | 5. A QR code containing the information from 1 to 4. 17 | 18 | 6. A maze generated using [Prim's 19 | Algorithm](https://en.wikipedia.org/wiki/Prim%27s_algorithm) using a 20 | modified version of [this 21 | code](http://www.brian-gordon.name/portfolio/maze.html) to make it 22 | more legible on the printer. 23 | 24 | 7. A random Sudoku generated using a modified version of [this 25 | code](http://davidbau.com/downloads/sudoku.py). 26 | 27 | 8. The current UTC date and time in ISO8601 format. 28 | 29 | ![](https://github.com/cloudflare/receipt-printer/raw/master/printing.gif) 30 | 31 | The Printer 32 | ----------- 33 | 34 | The specific printer used is a [GSAN 35 | 5870W](http://www.gsan.cn/En/prodShow.asp?vid=144) Thermal Receipt 36 | Printer but the code will work with other printers that handle the 37 | [ESC/POS](https://en.wikipedia.org/wiki/ESC/P) format (which is very 38 | common). This specific printer was only used because we had one lying 39 | around. 40 | 41 | ![](https://github.com/cloudflare/receipt-printer/raw/master/output.jpg) 42 | 43 | Random Source 44 | ------------- 45 | 46 | The program uses /dev/urandom which is fed with entropy from 47 | Cloudflare's internal randomness source (such as [lava 48 | lamps](https://twitter.com/swiftonsecurity/status/728603357665857537?lang=en)) 49 | 50 | Button 51 | ------ 52 | 53 | The code above runs on a Raspberry Pi Model B with an 54 | [LED](https://thepihut.com/blogs/raspberry-pi-tutorials/27968772-turning-on-an-led-with-your-raspberry-pis-gpio-pins) 55 | and a [button](http://razzpisampler.oreilly.com/ch07.html) connected 56 | to two GPIO ports. Pressing the button simply executes `ep.py`. It 57 | uses code similar to this: 58 | 59 | ```python 60 | import RPi.GPIO as GPIO 61 | import time, os 62 | 63 | GPIO.setmode(GPIO.BCM) 64 | GPIO.setwarnings(False) 65 | GPIO.setup(18, GPIO.IN, pull_up_down=GPIO.PUD_UP) 66 | GPIO.setup(25, GPIO.OUT) 67 | 68 | while True: 69 | input_state = GPIO.input(18) 70 | if input_state == False: 71 | GPIO.output(25, GPIO.HIGH) 72 | os.system("python ep.py") 73 | GPIO.output(25, GPIO.HIGH) 74 | time.sleep(0.01) 75 | ``` 76 | 77 | The button is connected between GND and GPIO18. The LED is connected 78 | between GND and GPIO25 with a 330 Ohm resistor to GND. 79 | 80 | ![](https://github.com/cloudflare/receipt-printer/raw/master/button.jpg) 81 | -------------------------------------------------------------------------------- /button.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/receipt-printer/5e3fadedc3d10998171642e318cd4e83269571cc/button.jpg -------------------------------------------------------------------------------- /ep.py: -------------------------------------------------------------------------------- 1 | # ep.py - prints some random data to an ESC POS printer connected via 2 | # USB 3 | # 4 | # Copyright (c) 2017 Cloudflare, Inc. 5 | 6 | from escpos.printer import Usb 7 | from datetime import datetime 8 | 9 | import random, string, os, qrcode 10 | 11 | # Use the system random number generator (which is os.urandom() which 12 | # will be /dev/urandom). 13 | rnd = random.SystemRandom() 14 | 15 | # text outputs string s to printer p accumuating the printed text in 16 | # qrl 17 | def text(p, s): 18 | qrl.append(s) 19 | s += "\n" 20 | p.text(s) 21 | 22 | # get_printer returns the printer connected via USB. To use find the 23 | # Product ID and Vendor ID and fill them in here. 24 | def get_printer(): 25 | return Usb(0x067b, 0x2305, 0) 26 | 27 | # image prints image f (either a file or an image) to printer p 28 | def image(p, f): 29 | p.image(f, True, True, "bitImageColumn") 30 | 31 | # rand_string returns a random string of length n drawn from alphabet 32 | # a 33 | def rand_string(a, n): 34 | return "".join(rnd.choice(a) for _ in range(n)) 35 | 36 | magic8 = ["It is certain", "It is decidedly so", "Without a doubt", "Yes, definitely", 37 | "You may rely on it", "As I see it, yes", "Most likely", "Outlook good", 38 | "Yes", "Signs point to yes", "Reply hazy, try again", "Ask again later", 39 | "Better not tell you now", "Cannot predict now", "Concentrate and ask again", 40 | "Don't count on it", "My reply is no", "My sources say no", "Outlook not so good", 41 | "Very doubtful"] 42 | 43 | qrl = ["Cloudflare"] 44 | p = get_printer() 45 | 46 | p.set("center", "a", "b") 47 | image(p, "logo.png") 48 | text(p, "FREE RANDOMNESS") 49 | text(p, "FROM LAVA LAMPS") 50 | text(p, "& OTHER SOURCES") 51 | 52 | p.set("center") 53 | text(p, "\nRANDOM NUMBER < 1,000,000") 54 | text(p, str(rnd.randint(0, 999999))) 55 | 56 | # The diceware output requires the diceware program (pip install 57 | # diceware) That program uses random.SystemRandom by default 58 | text(p, "\nSIX WORD DICEWARE PASSWORD") 59 | dice = os.popen("diceware -d' '").read() 60 | qrl.append(dice) 61 | p.block_text(dice, 32) 62 | text(p, "") 63 | 64 | text(p, "\n128-BIT ENTROPY PASSWORDS") 65 | text(p, rand_string("0123456789ABCDEF", 32)) 66 | text(p, rand_string(string.ascii_letters+string.digits, 22)) 67 | text(p, rand_string(string.ascii_letters+string.digits+string.punctuation, 20)) 68 | 69 | text(p, "\nMAGIC 8 BALL SAYS") 70 | text(p, rnd.choice(magic8)) 71 | 72 | text(p, "\nSCAN ME") 73 | qr = qrcode.QRCode(version=None, box_size=4, border=1, error_correction=qrcode.constants.ERROR_CORRECT_L) 74 | qrl.append("cloudflare.com") 75 | qr.add_data("\n".join(qrl)) 76 | qr.make(fit=True) 77 | img = qr.make_image() 78 | image(p, img._img.convert("RGB")) 79 | 80 | text(p, "\nRANDOM PUZZLES") 81 | os.system("python mazes.py --prims -s 30 30") 82 | image(p, "maze.png") 83 | os.system("python sudoku.py") 84 | image(p, "sudoku.png") 85 | 86 | dt = datetime.utcnow() 87 | text(p, "\n" + dt.isoformat("Z")) 88 | text(p, "cloudflare.com") 89 | 90 | p.cut() 91 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/receipt-printer/5e3fadedc3d10998171642e318cd4e83269571cc/logo.png -------------------------------------------------------------------------------- /mazes.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011 Brian Gordon 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | import png, random, heapq, argparse 22 | rnd = random.SystemRandom() 23 | 24 | 25 | class undirected_graph(dict): 26 | """A dictionary of unordered pairs.""" 27 | def __setitem__(self, key, value): 28 | super(undirected_graph, self).__setitem__(tuple(sorted(key)), value) 29 | 30 | def __getitem__(self, key): 31 | return super(undirected_graph, self).__getitem__(tuple(sorted(key))) 32 | 33 | def __has_key__(self, key): 34 | return super(undirected_graph, self).__has_key__(tuple(sorted(key))) 35 | 36 | def grid_adjacent(vertex): 37 | """Return all grid vertices adjacent to the given point.""" 38 | x, y = vertex 39 | adj = [] 40 | 41 | if x > 0: 42 | adj.append((x-1, y)) 43 | if x < GRID_WIDTH-1: 44 | adj.append((x+1, y)) 45 | if y > 0: 46 | adj.append((x, y-1)) 47 | if y < GRID_HEIGHT-1: 48 | adj.append((x, y+1)) 49 | 50 | return adj 51 | 52 | def make_grid(): 53 | weights = undirected_graph() 54 | for x in range(GRID_WIDTH): 55 | for y in range(GRID_HEIGHT): 56 | vertex = (x,y) 57 | for neighbor in grid_adjacent(vertex): 58 | weights[(vertex,neighbor)] = rnd.random() 59 | 60 | return weights 61 | 62 | def MCST(): 63 | spanning = undirected_graph() 64 | weights = make_grid() 65 | 66 | closed = set([(0,0)]) 67 | heap = [] 68 | for neighbor in grid_adjacent((0,0)): 69 | cost = weights[(0,0),neighbor] 70 | heapq.heappush(heap, (cost, (0,0), neighbor)) 71 | 72 | while heap: 73 | cost, v1, v2 = heapq.heappop(heap) 74 | 75 | # v1 is the vertex already in the spanning tree 76 | # it's possible that we've already added v2 to the spanning tree 77 | if v2 in closed: 78 | continue 79 | 80 | # add v2 to the closed set 81 | closed.add(v2) 82 | 83 | # add v2's neighbors to the heap 84 | for neighbor in grid_adjacent(v2): 85 | if neighbor not in closed: 86 | cost = weights[v2, neighbor] 87 | heapq.heappush(heap, (cost, v2, neighbor)) 88 | 89 | # update the spanning tree 90 | spanning[(v1,v2)] = True 91 | 92 | return draw_tree(spanning) 93 | 94 | def RDM(): 95 | spanning = undirected_graph() 96 | 97 | closed = set([(0,0)]) 98 | neighbors = [((0,0), x) for x in grid_adjacent((0,0))] 99 | 100 | while neighbors: 101 | v1, v2 = neighbors.pop(rnd.randrange(len(neighbors))) 102 | 103 | # v1 is the vertex already in the spanning tree 104 | # it's possible that we've already added v2 to the spanning tree 105 | if v2 in closed: 106 | continue 107 | 108 | # add v2 to the closed set 109 | closed.add(v2) 110 | 111 | for neighbor in grid_adjacent(v2): 112 | if neighbor not in closed: 113 | neighbors.append((v2, neighbor)) 114 | 115 | # update the spanning tree 116 | spanning[(v1,v2)] = True 117 | 118 | return draw_tree(spanning) 119 | 120 | bl = 5 121 | 122 | def draw_tree(spanning): 123 | # Create a big array of 0s and 1s for pypng 124 | 125 | pixels = [] 126 | 127 | # Add a row of off pixels for the top 128 | [pixels.append([0]*bl + [1]*bl + ([0] * (img_width-2*bl))) for _ in range(bl)] 129 | 130 | for y in range(GRID_HEIGHT): 131 | # Row containing nodes 132 | row = [0] * bl # First column is off 133 | for x in range(GRID_WIDTH): 134 | [row.append(1) for _ in range(bl)] 135 | if x < GRID_WIDTH-1: 136 | [row.append( int(((x,y),(x+1,y)) in spanning) ) for _ in range(bl)] 137 | [row.append(0) for _ in range(bl)] 138 | [pixels.append(row) for _ in range(bl)] 139 | 140 | if y < GRID_HEIGHT-1: 141 | # Row containing vertical connections between nodes 142 | row = [0] * bl # First column is off 143 | for x in range(GRID_WIDTH): 144 | [row.append( int(((x,y),(x,y+1)) in spanning) ) for _ in range(bl)] 145 | [row.append(0) for _ in range(bl)] 146 | [row.append(0) for _ in range(bl)] 147 | [pixels.append(row) for _ in range(bl)] 148 | 149 | # Add a row of off pixels for the bottom 150 | [pixels.append(([0] * (img_width-2*bl)) + [1] * bl + [0] * bl) for _ in range(bl)] 151 | 152 | return pixels 153 | 154 | # Handle arguments 155 | 156 | parser = argparse.ArgumentParser(description='Generate a maze with one of two algorithms.') 157 | 158 | group = parser.add_mutually_exclusive_group() 159 | group.add_argument('--prims', action='store_true', help='Use Prim\'s algorithm') 160 | group.add_argument('--random', action='store_true', help='Produce a random spanning tree') 161 | 162 | parser.add_argument('-s', dest='size', required=True, type=int, nargs=2, action='store', metavar='size', help='The maze\'s size, width then height in cells') 163 | parser.add_argument('-o', dest='filename', metavar='filename', nargs=1, default='maze.png') 164 | 165 | args = parser.parse_args() 166 | 167 | GRID_WIDTH, GRID_HEIGHT = tuple(args.size) 168 | 169 | img_width = GRID_WIDTH * bl*2 + bl 170 | img_height = GRID_HEIGHT * bl*2 + bl 171 | 172 | if args.random: 173 | pix = RDM() 174 | else: 175 | pix = MCST() 176 | 177 | f = open(args.filename, 'wb') 178 | w = png.Writer(img_width, img_height, greyscale=True, bitdepth=1) 179 | w.write(f, pix) 180 | f.close() 181 | -------------------------------------------------------------------------------- /output.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/receipt-printer/5e3fadedc3d10998171642e318cd4e83269571cc/output.jpg -------------------------------------------------------------------------------- /printing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/receipt-printer/5e3fadedc3d10998171642e318cd4e83269571cc/printing.gif -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | diceware 2 | pypng 3 | python-escpos 4 | -------------------------------------------------------------------------------- /sudoku-src.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/receipt-printer/5e3fadedc3d10998171642e318cd4e83269571cc/sudoku-src.png -------------------------------------------------------------------------------- /sudoku.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Sudoku Generator and Solver in 250 lines of python 4 | # Copyright (c) 2006 David Bau. All rights reserved. 5 | # 6 | # Can be used as either a command-line tool or as a cgi script. 7 | # 8 | # As a cgi-script, generates puzzles and estimates their level of 9 | # difficulty. Uses files sudoku-template.pdf/.ps/.txt/.html 10 | # in which it can fill in 81 underscores with digits for a puzzle. 11 | # The suffix of the request URL determines which template is used. 12 | # 13 | # On a command line without any arguments, prints text for a 14 | # random sudoku puzzle, with an estimate of its difficulty. 15 | # On a command line with a filename, solves the given puzzles 16 | # (files should look like the text generated by the generator). 17 | # 18 | # Adapted for Adafruit_Thermal library by Phil Burgess for Adafruit 19 | # Industries. This version uses bitmaps (in the 'gfx' subdirectory) 20 | # to render the puzzle rather than text symbols. See sudoku-txt 21 | # for a different Sudoku example that's all text-based. 22 | 23 | from __future__ import print_function 24 | import sys, os, random, getopt, re 25 | from PIL import Image 26 | 27 | rnd = random.SystemRandom() 28 | bg = Image.new("1", [384, 426], "white") # Working 'background' image 29 | img = Image.open('sudoku-src.png') # Source bitmaps 30 | xcoord = [ 15, 55, 95, 139, 179, 219, 263, 303, 343 ] 31 | ycoord = [ 56, 96, 136, 180, 220, 260, 304, 344, 384 ] 32 | numbers = [] 33 | 34 | try: 35 | file # Python 2 36 | except NameError: 37 | file = open # Python 3 38 | 39 | def main(): 40 | # Crop number bitmaps out of source image 41 | for i in range(9): 42 | numbers.append(img.crop([384, i*28, 410, (i+1)*28])) 43 | args = sys.argv[1:] 44 | if len(args) > 0: 45 | puzzles = [loadboard(filename) for filename in args] 46 | else: 47 | puzzles = [makepuzzle(solution([None] * 81))] 48 | for puzzle in puzzles: 49 | printboard(puzzle) # Doesn't print, just modifies 'bg' image 50 | bg.save("sudoku.png") 51 | 52 | def makepuzzle(board): 53 | puzzle = []; deduced = [None] * 81 54 | order = rnd.sample(range(81), 81) 55 | for pos in order: 56 | if deduced[pos] is None: 57 | puzzle.append((pos, board[pos])) 58 | deduced[pos] = board[pos] 59 | deduce(deduced) 60 | rnd.shuffle(puzzle) 61 | for i in range(len(puzzle) - 1, -1, -1): 62 | e = puzzle[i]; del puzzle[i] 63 | rating = checkpuzzle(boardforentries(puzzle), board) 64 | if rating == -1: puzzle.append(e) 65 | return boardforentries(puzzle) 66 | 67 | def ratepuzzle(puzzle, samples): 68 | total = 0 69 | for i in range(samples): 70 | state, answer = solveboard(puzzle) 71 | if answer is None: return -1 72 | total += len(state) 73 | return float(total) / samples 74 | 75 | def checkpuzzle(puzzle, board = None): 76 | state, answer = solveboard(puzzle) 77 | if answer is None: return -1 78 | if board is not None and not boardmatches(board, answer): return -1 79 | difficulty = len(state) 80 | state, second = solvenext(state) 81 | if second is not None: return -1 82 | return difficulty 83 | 84 | def solution(board): 85 | return solveboard(board)[1] 86 | 87 | def solveboard(original): 88 | board = list(original) 89 | guesses = deduce(board) 90 | if guesses is None: return ([], board) 91 | track = [(guesses, 0, board)] 92 | return solvenext(track) 93 | 94 | def solvenext(remembered): 95 | while len(remembered) > 0: 96 | guesses, c, board = remembered.pop() 97 | if c >= len(guesses): continue 98 | remembered.append((guesses, c + 1, board)) 99 | workspace = list(board) 100 | pos, n = guesses[c] 101 | workspace[pos] = n 102 | guesses = deduce(workspace) 103 | if guesses is None: return (remembered, workspace) 104 | remembered.append((guesses, 0, workspace)) 105 | return ([], None) 106 | 107 | def deduce(board): 108 | while True: 109 | stuck, guess, count = True, None, 0 110 | # fill in any spots determined by direct conflicts 111 | allowed, needed = figurebits(board) 112 | for pos in range(81): 113 | if None == board[pos]: 114 | numbers = listbits(allowed[pos]) 115 | if len(numbers) == 0: return [] 116 | elif len(numbers) == 1: board[pos] = numbers[0]; stuck = False 117 | elif stuck: 118 | guess, count = pickbetter(guess, count, [(pos, n) for n in numbers]) 119 | if not stuck: allowed, needed = figurebits(board) 120 | # fill in any spots determined by elimination of other locations 121 | for axis in range(3): 122 | for x in range(9): 123 | numbers = listbits(needed[axis * 9 + x]) 124 | for n in numbers: 125 | bit = 1 << n 126 | spots = [] 127 | for y in range(9): 128 | pos = posfor(x, y, axis) 129 | if allowed[pos] & bit: spots.append(pos) 130 | if len(spots) == 0: return [] 131 | elif len(spots) == 1: board[spots[0]] = n; stuck = False 132 | elif stuck: 133 | guess, count = pickbetter(guess, count, [(pos, n) for pos in spots]) 134 | if stuck: 135 | if guess is not None: rnd.shuffle(guess) 136 | return guess 137 | 138 | def figurebits(board): 139 | allowed, needed = [e is None and 511 or 0 for e in board], [] 140 | for axis in range(3): 141 | for x in range(9): 142 | bits = axismissing(board, x, axis) 143 | needed.append(bits) 144 | for y in range(9): 145 | allowed[posfor(x, y, axis)] &= bits 146 | return allowed, needed 147 | 148 | def posfor(x, y, axis = 0): 149 | if axis == 0: return x * 9 + y 150 | elif axis == 1: return y * 9 + x 151 | else: return ((0,3,6,27,30,33,54,57,60)[x] + (0,1,2,9,10,11,18,19,20)[y]) 152 | 153 | def axisfor(pos, axis): 154 | if axis == 0: return pos / 9 155 | elif axis == 1: return pos % 9 156 | else: return (pos / 27) * 3 + (pos / 3) % 3 157 | 158 | def axismissing(board, x, axis): 159 | bits = 0 160 | for y in range(9): 161 | e = board[posfor(x, y, axis)] 162 | if e is not None: bits |= 1 << e 163 | return 511 ^ bits 164 | 165 | def listbits(bits): 166 | return [y for y in range(9) if 0 != bits & 1 << y] 167 | 168 | def allowed(board, pos): 169 | bits = 511 170 | for axis in range(3): 171 | x = axisfor(pos, axis) 172 | bits &= axismissing(board, x, axis) 173 | return bits 174 | 175 | def pickbetter(b, c, t): 176 | if b is None or len(t) < len(b): return (t, 1) 177 | if len(t) > len(b): return (b, c) 178 | if rnd.randint(0, c) == 0: return (t, c + 1) 179 | else: return (b, c + 1) 180 | 181 | def entriesforboard(board): 182 | return [(pos, board[pos]) for pos in range(81) if board[pos] is not None] 183 | 184 | def boardforentries(entries): 185 | board = [None] * 81 186 | for pos, n in entries: board[pos] = n 187 | return board 188 | 189 | def boardmatches(b1, b2): 190 | for i in range(81): 191 | if b1[i] != b2[i]: return False 192 | return True 193 | 194 | def printboard(board): 195 | bg.paste(img, (0, 0)) # Numbers are cropped off right side 196 | for row in range(9): 197 | for col in range(9): 198 | n = board[posfor(row, col)] 199 | if n is not None: 200 | bg.paste(numbers[n], (xcoord[col], ycoord[row])) 201 | 202 | def parseboard(str): 203 | result = [] 204 | for w in str.split(): 205 | for x in w: 206 | if x in '|-=+': continue 207 | if x in '123456789': result.append(int(x) - 1) 208 | else: result.append(None) 209 | if len(result) == 81: return result 210 | 211 | def loadboard(filename): 212 | f = file(filename, 'r') 213 | result = parseboard(f.read()) 214 | f.close() 215 | return result 216 | 217 | def basedir(): 218 | if hasattr(sys.modules[__name__], '__file__'): 219 | return os.path.split(__file__)[0] 220 | elif __name__ == '__main__': 221 | if len(sys.argv) > 0 and sys.argv[0] != '': 222 | return os.path.split(sys.argv[0])[0] 223 | else: 224 | return os.curdir 225 | 226 | def loadsudokutemplate(ext): 227 | f = open(os.path.join(basedir(), 'sudoku-template.%s' % ext), 'r') 228 | result = f.read() 229 | f.close() 230 | return result 231 | 232 | if __name__ == '__main__': 233 | main() 234 | 235 | --------------------------------------------------------------------------------