def test_add_black_cell(self): grid = Grid(9) grid.add_black_cell(1, 5) grid.add_black_cell(4, 9) expected = [(1, 5), (4, 9), (6, 1), (9, 5)] actual = grid.get_black_cells() self.assertEqual(expected, actual)
def test_add_black(self): n = 5 grid = Grid(n) black_cells = [ (1, 1), (1, 3), (2, 3), (2, 4), (3, 1), (3, 2), ] for (r, c) in black_cells: grid.add_black_cell(r, c) puzzle = Puzzle(grid) expected_cells = [ ["*", " ", "*", " ", " "], [" ", " ", "*", "*", " "], ["*", "*", " ", "*", "*"], [" ", "*", "*", " ", " "], [" ", " ", "*", " ", "*"], ] for r in range(1, n + 1): for c in range(1, n + 1): expected = expected_cells[r - 1][c - 1] actual = puzzle.get_cell(r, c) self.assertEqual(expected, actual, f'Mismatch at ({r},{c})')
def create_atlantic_puzzle(): """ Creates a puzzle from the Atlantic puzzle of May 15, 2020 +-----------------+ |D|A|B|*|*|E|F|T|S| |S|L|I|M|*|R|I|O|T| |L|O|C|A|V|O|R|E|S| |R|E|U|N|I|T|E|D|*| |*|*|R|A|P|I|D|*|*| |*|R|I|C|E|C|A|K|E| |C|O|O|L|R|A|N|C|H| |C|L|U|E|*|C|A|L|X| |R|O|S|S|*|*|E|R|E| +-----------------+ """ n = 9 grid = Grid(n) for (r, c) in [ (1, 4), (1, 5), (2, 5), (4, 9), (5, 8), (5, 9), ]: grid.add_black_cell(r, c) puzzle = Puzzle(grid) puzzle.title = 'My Atlantic Theme' return puzzle
def grid_new(): """ Creates a new grid and redirects to grid screen """ # Get the grid size from the form n = int(request.args.get('n')) # Remove any leftover grid name in the session session.pop('gridname', None) # Create the grid grid = Grid(n) jsonstr = grid.to_json() session['grid'] = jsonstr session['grid.initial.sha'] = sha256(jsonstr) return redirect(url_for('uigrid.grid_screen'))
def puzzle_new(): """ Creates a new puzzle and redirects to puzzle screen """ # Get the chosen grid name from the query parameters gridname = request.args.get('gridname') # Open the corresponding file and read its contents as json # and recreate the grid from it userid = 1 # TODO replace hard-coded user ID query = DBGrid.query.filter_by(userid=userid, gridname=gridname) jsonstr = query.first().jsonstr grid = Grid.from_json(jsonstr) # Now pass this grid to the Puzzle() constructor puzzle = Puzzle(grid) # Save puzzle in the session jsonstr = puzzle.to_json() session['puzzle'] = jsonstr session['puzzle.initial.sha'] = sha256(jsonstr) # Remove any leftover puzzle name in the session session.pop('puzzlename', None) return redirect(url_for('uipuzzle.puzzle_screen'))
def get_bad_grid(): jsonstr = """ { "n": 7, "black_cells": [ [ 1, 3 ], [ 2, 3 ], [ 3, 3 ], [ 3, 4 ], [ 3, 5 ], [ 4, 2 ], [ 4, 6 ], [ 5, 3 ], [ 5, 4 ], [ 5, 5 ], [ 6, 5 ], [ 7, 5 ] ], "numbered_cells": [ { "seq": 1, "r": 1, "c": 1, "a": 2, "d": 7 }, { "seq": 2, "r": 1, "c": 2, "a": 0, "d": 3 }, { "seq": 3, "r": 1, "c": 4, "a": 4, "d": 2 }, { "seq": 4, "r": 1, "c": 5, "a": 0, "d": 2 }, { "seq": 5, "r": 1, "c": 6, "a": 0, "d": 3 }, { "seq": 6, "r": 1, "c": 7, "a": 0, "d": 7 }, { "seq": 7, "r": 2, "c": 1, "a": 2, "d": 0 }, { "seq": 8, "r": 2, "c": 4, "a": 4, "d": 0 }, { "seq": 9, "r": 3, "c": 1, "a": 2, "d": 0 }, { "seq": 10, "r": 3, "c": 6, "a": 2, "d": 0 }, { "seq": 11, "r": 4, "c": 3, "a": 3, "d": 0 }, { "seq": 12, "r": 5, "c": 1, "a": 2, "d": 0 }, { "seq": 13, "r": 5, "c": 2, "a": 0, "d": 3 }, { "seq": 14, "r": 5, "c": 6, "a": 2, "d": 3 }, { "seq": 15, "r": 6, "c": 1, "a": 4, "d": 0 }, { "seq": 16, "r": 6, "c": 3, "a": 0, "d": 2 }, { "seq": 17, "r": 6, "c": 4, "a": 0, "d": 2 }, { "seq": 18, "r": 6, "c": 6, "a": 2, "d": 0 }, { "seq": 19, "r": 7, "c": 1, "a": 4, "d": 0 }, { "seq": 20, "r": 7, "c": 6, "a": 2, "d": 0 } ] } """ grid = Grid.from_json(jsonstr) return grid
def grid_screen(): """ Renders the grid screen """ # Get the existing grid from the session grid = Grid.from_json(session['grid']) gridname = session.get('gridname', None) # Create the SVG svg = GridToSVG(grid) boxsize = svg.boxsize svgstr = svg.generate_xml() # Set the state to editing grid session['uistate'] = UIState.EDITING_GRID enabled = session['uistate'].get_enabled() enabled["grid_undo"] = len(grid.undo_stack) > 0 enabled["grid_redo"] = len(grid.redo_stack) > 0 enabled["grid_delete"] = gridname is not None # Show the grid.html screen return render_template('grid.html', enabled=enabled, n=grid.n, gridname=gridname, boxsize=boxsize, svgstr=svgstr)
def grid_redo(): """ Redoes the last grid action then redirects to grid screen """ jsonstr = session.get('grid', None) grid = Grid.from_json(jsonstr) grid.redo() jsonstr = grid.to_json() session['grid'] = jsonstr return redirect(url_for('uigrid.grid_screen'))
def from_json(jsonstr): image = json.loads(jsonstr) # Create a puzzle of the specified size n = image['n'] grid = Grid(n) # Initialize the black cells black_cells = image['black_cells'] for black_cell in black_cells: grid.add_black_cell(*black_cell) # Create the puzzle puzzle = Puzzle(grid) title = image.get('title', None) puzzle._title = title # Can't use the undo/redo here yet # Reload the "ACROSS" words awlist = image['across_words'] for aw in awlist: seq = aw['seq'] text = aw['text'] clue = aw['clue'] word = puzzle.get_across_word(seq) word.set_text(text) # TODO: Can't do this - undo/redo word.set_clue(clue) # TODO: Can't do this - undo/redo # Reload the "DOWN" words dwlist = image['down_words'] for dw in dwlist: seq = dw['seq'] text = dw['text'] clue = dw['clue'] word = puzzle.get_down_word(seq) word.set_text(text) # TODO: Can't do this - undo/redo word.set_clue(clue) # TODO: Can't do this - undo/redo # Reload the undo/redo stacks puzzle.undo_stack = image.get('undo_stack', []) puzzle.redo_stack = image.get('redo_stack', []) # Done return puzzle
def test_add_undo(self): grid = Grid(5) grid.add_black_cell(1, 2) self.assertEqual(True, grid.is_black_cell(1, 2)) self.assertEqual(True, grid.is_black_cell(5, 4)) self.assertEqual([(1, 2)], grid.undo_stack) self.assertEqual([], grid.redo_stack) grid.undo() self.assertEqual(False, grid.is_black_cell(1, 2)) self.assertEqual(False, grid.is_black_cell(5, 4)) self.assertEqual([], grid.undo_stack) self.assertEqual([(1, 2)], grid.redo_stack)
def __init__(self, data): self.data = data self.offset = 0 # Get the size self.offset = self.OFFSET_WIDTH self.n = n = self.read_byte() self.grid = grid = Grid(n) # Read the solution to get the black cell locations self.offset = self.OFFSET_WORDS for r in range(1, n+1): line = self.read_chunk(n) for c in range(1, n+1): letter = chr(line[c-1]) if letter == self.BLACK_CELL: grid.add_black_cell(r, c) # Create the puzzle, then go back and read the words self.puzzle = puzzle = Puzzle(grid) self.offset = self.OFFSET_WORDS for r in range(1, n+1): line = self.read_chunk(n) for c in range(1, n + 1): letter = chr(line[c-1]) if not grid.is_black_cell(r, c): puzzle.set_cell(r, c, letter) # Skip over the solution work area self.offset += n*n # Read the title and set it in the puzzle title = self.read_string() if title != "": puzzle.title = title # Skip the author and copyright lines s = self.read_string() # author s = self.read_string() # copyright # Read the clues for nc in puzzle.numbered_cells: if nc.a: # Across word clue = self.read_string() puzzle.set_clue(nc.seq, Word.ACROSS, clue) if nc.d: # Down word clue = self.read_string() puzzle.set_clue(nc.seq, Word.DOWN, clue) # Done self.jsonstr = puzzle.to_json()
def create_puzzle(): """ Creates sample puzzle as: +---------+ | | |*| | | | | |*|*| | |*|*| |*|*| | |*|*| | | | | |*| | | +---------+ """ n = 5 grid = Grid(n) for (r, c) in [ (1, 3), (2, 3), (2, 4), (3, 1), (3, 2), ]: grid.add_black_cell(r, c) return Puzzle(grid)
def create_nyt_puzzle(): """ Creates a puzzle from a real New York Times crossword of March 15, 2020 +-----------------------------------------+ |A|B|B|A|*| | | |*| | | | | |*| | | | | | | | | | | |*| | | |*| | | | | |*| | | | | | | | | | | | | | | | | | | | | |*| | | | | | | | | | | | |*| | | | | |*| | | |*| | | | | | | | | |*| | | | | |*|*| | | | | |*|*| | | | |*|*|*| | | | | | | | | | | | | | | | | | | | | | | | | |*|*| | | | | | |*| | | | | | | | | | | |*| | | |*| | | |*|*| | | | | | | | | | | | | | | | | | | | | | | | | | |*|*|*| | | | |*| | | | | |*| | | | | | |*| | | | | | | | | | |*| | | | |*| | | | |*| | | | | | | | | | |*| | | | | | |*| | | | | |*| | | | |*|*|*| | | | | | | | | | | | | | | | | | | | | | | | | | |*|*| | | |*| | | |*| | | | | | | | | | | |*| | | | | | |*|*| | | | | | | | | | | | | | | | | | | | | | | | | |*|*|*| | | | |*|*| | | | | |*|*| | | | | |*| | | | | | | | | |*| | | |*| | | | | |*| | | | | | | | | | | | |*| | | | | | | | | | | | | | | | | | | | | |*| | | | | |*| | | |*| | | | | | | | | | | |*| | | | | |*| | | |*| | | | | +-----------------------------------------+ """ n = 21 grid = Grid(n) for (r, c) in [(1, 5), (1, 9), (1, 15), (2, 5), (2, 9), (2, 15), (3, 15), (4, 6), (4, 12), (4, 16), (5, 4), (5, 10), (5, 11), (5, 17), (5, 18), (6, 1), (6, 2), (6, 3), (7, 7), (7, 8), (7, 15), (8, 5), (8, 9), (8, 13), (8, 14), (9, 19), (9, 20), (9, 21), (10, 4), (10, 10), (11, 6), (11, 11), (12, 5), (13, 1), (13, 2), (13, 3), (14, 8), (15, 7), (17, 4), (17, 5)]: grid.add_black_cell(r, c) return Puzzle(grid)
def test_set_cell(self): n = 5 puzzle = Puzzle(Grid(n)) puzzle.set_cell(2, 3, 'D') cell = puzzle.get_cell(2, 3) self.assertEqual('D', cell) for r in range(1, n + 1): if r == 2: continue for c in range(1, n + 1): if c == 3: continue cell = puzzle.get_cell(r, c) self.assertEqual(Puzzle.WHITE, cell, f'Mismatch at ({r}, {c})')
def grid_save_common(gridname): """ Common method used by both grid_save and grid_save_as """ # Recreate the grid from the JSON in the session # and validate it jsonstr = session.get('grid', None) grid = Grid.from_json(jsonstr) ok, messages = grid.validate() if not ok: flash("Grid not saved") for message_type in messages: message_list = messages[message_type] if len(message_list) > 0: flash(f"*** {message_type} ***") for message in message_list: flash(" " + message) else: # Save the file userid = 1 # TODO Replace hard coded user id query = DBGrid.query.filter_by(userid=userid, gridname=gridname) if not query.all(): # No grid in the database. This is an insert logging.debug(f"Inserting grid '{gridname}' into grids table") created = modified = datetime.now().isoformat() newgrid = DBGrid(userid=userid, gridname=gridname, created=created, modified=modified, jsonstr=jsonstr) db.session.add(newgrid) db.session.commit() else: # There is a grid. This is an update logging.debug(f"Updating grid '{gridname}' in grids table") oldgrid = query.first() oldgrid.modified = datetime.now().isoformat() oldgrid.jsonstr = jsonstr db.session.commit() # Send message about save flash(f"Grid saved as {gridname}") # Store the sha256 of the saved version of the grid # in the session as 'grid.initial.sha' so that we can detect # whether it has been changed since it was last saved session['grid.initial.sha'] = sha256(jsonstr) # Show the grid screen return redirect(url_for('uigrid.grid_screen'))
def grid_rotate(): """ Rotates the grid 90 degrees left then returns the new SVG """ # Rotate the grid jsonstr = session.get('grid', None) grid = Grid.from_json(jsonstr) grid.rotate() # Save the updated grid in the session session['grid'] = grid.to_json() # Send the new SVG data to the client svg = GridToSVG(grid) svgstr = svg.generate_xml() response = make_response(svgstr, HTTPStatus.OK) return response
def grid_statistics(): """ Return the grid statistics in a JSON string """ # Get the grid from the session grid = Grid.from_json(session['grid']) gridname = session.get('gridname', None) stats = grid.get_statistics() enabled = {} svgstr = GridToSVG(grid).generate_xml() # Render with grid statistics template return render_template("grid-statistics.html", enabled=enabled, gridname=gridname, svgstr=svgstr, stats=stats)
def grid_new_from_puzzle(): """ Creates a new grid from the specified puzzle """ puzzlename = request.args.get('puzzlename') userid = 1 # TODO replace hard-coded user ID query = DBPuzzle.query.filter_by(userid=userid, puzzlename=puzzlename) jsonstr = query.first().jsonstr grid = Grid.from_json(jsonstr) grid.undo_stack = [] grid.redo_stack = [] jsonstr = grid.to_json() # Save grid in the session session['grid'] = jsonstr session['grid.initial.sha'] = sha256(jsonstr) # Remove any leftover grid name in the session session.pop('gridname', None) return redirect(url_for('uigrid.grid_screen'))
def grid_preview(): """ Creates a grid preview and returns it to ??? """ userid = 1 # TODO Replace hard coded user id # Get the chosen grid name from the query parameters gridname = request.args.get('gridname') # Open the corresponding file and read its contents as json # and recreate the grid from it jsonstr = grid_load_common(userid, gridname) grid = Grid.from_json(jsonstr) # Get the top two word lengths heading_list = [f"{grid.get_word_count()} words"] wlens = grid.get_word_lengths() wlenkeys = sorted(wlens.keys(), reverse=True) wlenkeys = wlenkeys[:min(2, len(wlenkeys))] for wlen in wlenkeys: entry = wlens[wlen] total = 0 if entry["alist"]: total += len(entry["alist"]) if entry["dlist"]: total += len(entry["dlist"]) heading_list.append(f"{wlen}-letter: {total}") heading = f'Grid {gridname}({", ".join(heading_list)})' scale = 0.75 svgobj = GridToSVG(grid, scale=scale) width = (svgobj.boxsize * grid.n + 32) * scale svgstr = svgobj.generate_xml() obj = { "gridname": gridname, "heading": heading, "width": width, "svgstr": svgstr } resp = make_response(json.dumps(obj), HTTPStatus.OK) resp.headers['Content-Type'] = "application/json" return resp
def grid_click(): """ Adds or removes a black cell then returns the new SVG """ # Get the row and column clicked from the query parms r = int(request.args.get('r')) c = int(request.args.get('c')) # Get the existing grid from the session jsonstr = session['grid'] grid = Grid.from_json(jsonstr) # Toggle the black cell status if grid.is_black_cell(r, c): grid.remove_black_cell(r, c) else: grid.add_black_cell(r, c) # Save the updated grid in the session session['grid'] = grid.to_json() return redirect(url_for('uigrid.grid_screen'))
def puzzle_replace_grid(): # Get the chosen grid name from the query parameters gridname = request.args.get('gridname') if not gridname: return redirect(url_for('uipuzzle.puzzle_screen')) # Create the grid userid = 1 # TODO replace hard-coded user ID query = DBGrid.query.filter_by(userid=userid, gridname=gridname) jsonstr = query.first().jsonstr grid = Grid.from_json(jsonstr) # Get the current puzzle puzzle = Puzzle.from_json(session['puzzle']) # and replace its grid puzzle.replace_grid(grid) # Save in the session session['puzzle'] = puzzle.to_json() return redirect(url_for('uipuzzle.puzzle_screen'))
def __init__(self, grid: Grid, *args, **kwargs): super().__init__(grid.n, *args, **kwargs) self.grid = grid self.black_cells = grid.get_black_cells() self.numbered_cells = grid.get_numbered_cells()
def test_symmetric_point_on_edge(self): grid = Grid(9) expected = (9, 1) actual = grid.symmetric_point(1, 9) self.assertEqual(expected, actual)
def test_symmetric_point(self): grid = Grid(9) expected = (8, 5) actual = grid.symmetric_point(2, 5) self.assertEqual(expected, actual)
def test_str(self): grid = Grid(3) grid_string = str(grid) self.assertTrue("+-----+" in grid_string) self.assertTrue("| | | |" in grid_string)
def test_bad_symmetric_point(self): grid = Grid(9) actual = grid.symmetric_point(-3, 45) self.assertIsNone(actual)
def test_to_json(self): grid = Grid(9) grid.add_black_cell(1, 5) grid.add_black_cell(4, 9) jsonstr = grid.to_json() self.assertIsNotNone(jsonstr)
def test_same_grid(self): oldpuzzle = TestPuzzle.create_solved_atlantic_puzzle() grid = Grid.from_json(oldpuzzle.to_json()) newpuzzle = Puzzle.from_json(oldpuzzle.to_json()) newpuzzle.replace_grid(grid) self.assertEqual(oldpuzzle, newpuzzle)
def test_new_grid(self): puzzle = TestPuzzle.create_solved_atlantic_puzzle() oldjson = puzzle.to_json() grid = Grid.from_json(puzzle.to_json()) grid.add_black_cell(4, 4) puzzle.replace_grid(grid) newjson = puzzle.to_json() import json old = json.loads(oldjson) new = json.loads(newjson) # Compare black cells self.assertIn([4, 4], new['black_cells']) self.assertIn([6, 6], new['black_cells']) new['black_cells'].remove([4, 4]) new['black_cells'].remove([6, 6]) self.assertListEqual(old['black_cells'], new['black_cells']) # Compare numbered cells expected = [ NumberedCell(seq=1, r=1, c=1, a=3, d=4), NumberedCell(seq=2, r=1, c=2, a=0, d=4), NumberedCell(seq=3, r=1, c=3, a=0, d=9), NumberedCell(seq=4, r=1, c=6, a=4, d=5), NumberedCell(seq=5, r=1, c=7, a=0, d=9), NumberedCell(seq=6, r=1, c=8, a=0, d=4), NumberedCell(seq=7, r=1, c=9, a=0, d=3), NumberedCell(seq=8, r=2, c=1, a=4, d=0), NumberedCell(seq=9, r=2, c=4, a=0, d=2), NumberedCell(seq=10, r=2, c=6, a=4, d=0), NumberedCell(seq=11, r=3, c=1, a=9, d=0), NumberedCell(seq=12, r=3, c=5, a=0, d=5), NumberedCell(seq=13, r=4, c=1, a=3, d=0), NumberedCell(seq=14, r=4, c=5, a=4, d=0), NumberedCell(seq=15, r=5, c=3, a=5, d=0), NumberedCell(seq=16, r=5, c=4, a=0, d=5), NumberedCell(seq=17, r=6, c=2, a=4, d=4), NumberedCell(seq=18, r=6, c=7, a=3, d=0), NumberedCell(seq=19, r=6, c=8, a=0, d=4), NumberedCell(seq=20, r=6, c=9, a=0, d=4), NumberedCell(seq=21, r=7, c=1, a=9, d=3), NumberedCell(seq=22, r=7, c=6, a=0, d=2), NumberedCell(seq=23, r=8, c=1, a=4, d=0), NumberedCell(seq=24, r=8, c=6, a=4, d=0), NumberedCell(seq=25, r=9, c=1, a=4, d=0), NumberedCell(seq=26, r=9, c=7, a=3, d=0), ] actual = [] for x in new['numbered_cells']: jsonstr = json.dumps(x) nc = NumberedCell.from_json(jsonstr) actual.append(nc) self.assertListEqual(expected, actual) # Compare clues oldclues = { x['text']: x['clue'] for x in old['across_words'] + old['down_words'] } newclues = { x['text']: x['clue'] for x in new['across_words'] + new['down_words'] } for k, v in newclues.items(): if k in oldclues: oldclue = oldclues[k] self.assertEqual(oldclue, newclues[k])
def test_wrong_size(self): puzzle = TestPuzzle.create_solved_atlantic_puzzle() grid = Grid(5) with self.assertRaises(ValueError): puzzle.replace_grid(grid)
def _get_minion_string(self, grid, wordlist): across, down = grid.get_words() nums = grid.get_numbered_cells(across, down) formed_across = Grid.format_words(across, nums) formed_down = Grid.format_words(down, nums) outfile = cStringIO.StringIO() outfile.write("MINION 3\n") outfile.write("**VARIABLES**\n") allwords = [] for word in formed_across: key = "across%d" % word[0] allwords.append((key, word[1])) outfile.write("DISCRETE %s[%d] {97..122}\n" % (key, len(word[1]))) for word in formed_down: key = "down%d" % word[0] allwords.append((key, word[1])) outfile.write("DISCRETE %s[%d] {97..122}\n" % (key, len(word[1]))) for word in formed_across: key = "h%d" % word[0] words = wordlist.words_matching(word[1]) outfile.write("**TUPLELIST**\n%s %s %s\n" % (key, len(words), len(words[0]))) outfile.write("\n".join([" ".join([str(ord(c)) for c in word]) for word in words])) outfile.write("\n") for word in formed_down: key = "v%d" % word[0] words = wordlist.words_matching(word[1]) outfile.write("**TUPLELIST**\n%s %s %s\n" % (key, len(words), len(words[0]))) outfile.write("\n".join([" ".join([str(ord(c)) for c in word]) for word in words])) outfile.write("\n") outfile.write("**CONSTRAINTS**\n") for word, itword in allwords: if len(itword) > 1: outfile.write("\n".join([ ("watchvecneq(%s,%s)" % (w, word)) for w, wcon in allwords if w != word and len(wcon) == len(itword)])) outfile.write("\n") for start, word in across.items(): rownum = nums[word[0]] for hidx, cell in enumerate(word): column = grid.col_at(cell.x, cell.y) colnum = nums[column[0]] vidx = column.index(cell) outfile.write("eq(across%d[%d],down%d[%d])" % (rownum, hidx, colnum, vidx)) outfile.write("\n") self.diffs = [] for word in Grid.format_words(across, nums): key = "across%d" % word[0] self.diffs.append("h%d" % word[0]) outfile.write("table(across%d, h%d)\n" % (word[0], word[0])) for word in Grid.format_words(down, nums): key = "down%d" % word[0] self.diffs.append("v%d" % word[0]) outfile.write("table(down%d, v%d)\n" % (word[0], word[0])) outfile.write("**EOF**") return outfile.getvalue()